fix: index.html redirects to Python dashboard (port 3000)

Old Node.js agent (currencies+weather) saved as index.legacy.html.
Preview now shows the new AgroMarket price dashboard with
potato/wheat/milk/apple prices, search, spreads, alerts.
This commit is contained in:
Elshat 2026-09-24 09:14:03 +00:00
parent 0900a06bd0
commit 8d527c2f1c
2 changed files with 420 additions and 407 deletions

View File

@ -1,411 +1,13 @@
<!DOCTYPE html>
<!-- Price Agent · дизайн: Apple. Все 5 CSS сохраняем; подробности в design.md. -->
<html lang="ru" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Цена Агент — агро-рынок</title>
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
<link rel="stylesheet" href="design-system/kt-ai-components.css">
<link rel="stylesheet" href="design-system/kt-ai-page.css">
<link rel="stylesheet" href="design-system/vibe-theme.css">
<style>
a.kt-ai-btn { text-decoration: none; }
.pa-plot { width: 100%; height: 250px; display: block; margin-top: var(--kt-ai-space-4); }
.pa-legend { display: flex; flex-wrap: wrap; gap: var(--kt-ai-space-5); margin-top: var(--kt-ai-space-4); }
.pa-legend .li { display: flex; align-items: center; gap: 8px; font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); }
.pa-legend i { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
.pa-wx { display: flex; justify-content: space-between; padding: var(--kt-ai-space-3) 0; border-bottom: 1px solid var(--kt-ai-divider); font-size: var(--kt-ai-text-sm); }
.pa-wx:last-child { border-bottom: none; }
.pa-wx .num { font-variant-numeric: tabular-nums; color: var(--kt-ai-fg-muted); }
.pa-src { display: flex; flex-direction: column; gap: var(--kt-ai-space-5); margin-top: var(--kt-ai-space-5); }
.pa-src .item { display: flex; align-items: center; gap: var(--kt-ai-space-4); }
.pa-src .nm { font-weight: var(--kt-ai-weight-medium); }
.pa-src .sub { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); }
.pa-rows { margin-top: var(--kt-ai-space-6); }
.pa-chat-in { display: flex; gap: var(--kt-ai-space-3); }
.pa-chat-in input { flex: 1; }
.pa-user { align-self: flex-end; max-width: 88%; padding: var(--kt-ai-space-3) var(--kt-ai-space-4); background: var(--kt-ai-bg-soft); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-2xl); font-size: var(--kt-ai-text-sm); white-space: pre-wrap; }
.pa-ai { flex: 1; min-width: 0; padding: var(--kt-ai-space-3) var(--kt-ai-space-4); border-left: 3px solid var(--kt-ai-primary); font-size: var(--kt-ai-text-sm); line-height: 1.65; white-space: pre-wrap; }
.pa-ai.err { border-left-color: var(--kt-ai-status-risk-fg); color: var(--kt-ai-status-risk-fg); }
.pa-ai-label { font-size: var(--kt-ai-text-2xs); color: var(--kt-ai-fg-muted); margin-bottom: var(--kt-ai-space-1); }
.pa-empty { padding: var(--kt-ai-space-6); text-align: center; color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm); }
</style>
</head>
<body class="kt-ai-app">
<div class="kt-ai-shell">
<aside class="kt-ai-sidebar">
<div style="display:flex;align-items:center;gap:10px;padding:var(--kt-ai-space-5) var(--kt-ai-space-5) var(--kt-ai-space-4)">
<svg class="kt-icon" aria-hidden="true" style="color:var(--kt-ai-primary);width:22px;height:22px"><use href="#sparkles"></use></svg>
<div>
<div style="font-weight:var(--kt-ai-weight-semibold);color:var(--kt-ai-fg);font-size:var(--kt-ai-text-md)">Цена Агент</div>
<div class="kt-ai-meta">агро-рынок</div>
<html lang="ru">
<head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=/proxy/3000/">
<title>AgroMarket Price Agent → Дашборд</title></head>
<body style="font-family:Inter,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#f5f5f7;color:#1d1d1f">
<div style="text-align:center">
<p style="font-size:18px;font-weight:600">Перенаправляем на дашборд цен AgroMarket…</p>
<p style="color:#86868b;font-size:14px;margin-top:8px">
Если не перенаправило автоматически — <a href="/proxy/3000/" style="color:#0071e3">кликни сюда</a>
</p>
</div>
</div>
<div style="padding:0 var(--kt-ai-space-4) var(--kt-ai-space-4)">
<button class="kt-ai-btn" data-variant="primary" id="btnCollect" style="width:100%">
<svg class="kt-icon" aria-hidden="true"><use href="#refresh"></use></svg>
Обновить сейчас
</button>
</div>
<nav>
<div class="kt-ai-nav-group">
<a class="kt-ai-nav-item" data-active="true" href="#"><svg class="kt-icon" aria-hidden="true"><use href="#dashboard"></use></svg><span class="nav-label">Обзор</span></a>
<a class="kt-ai-nav-item" href="#" id="navHistory"><svg class="kt-icon" aria-hidden="true"><use href="#table"></use></svg><span class="nav-label">История</span></a>
<a class="kt-ai-nav-item" href="#" id="navAgents"><svg class="kt-icon" aria-hidden="true"><use href="#source"></use></svg><span class="nav-label">Агенты</span></a>
</div>
</nav>
</aside>
<main class="kt-ai-main" style="min-height:100dvh;display:flex;flex-direction:column;background:var(--kt-ai-bg)">
<header class="kt-ai-topbar">
<nav class="kt-ai-meta" style="display:flex;flex-direction:column;line-height:1.3">
<span id="pageTitle" style="font-size:var(--kt-ai-text-md);color:var(--kt-ai-fg);font-weight:var(--kt-ai-weight-semibold)">Обзор</span>
<span id="pageSub" style="font-size:var(--kt-ai-text-2xs)">открытые источники</span>
</nav>
<div class="spacer"></div>
<div class="kt-ai-search-field">
<input class="kt-ai-search" id="askInput" type="search" placeholder="Спросить агента: почему выросла цена?" style="width:100%">
</div>
</header>
<div style="overflow-y:auto;flex:1;padding:var(--kt-ai-space-8) var(--kt-ai-space-8) var(--kt-ai-space-11)">
<div id="view"></div>
</div>
</main>
</div>
<!-- Drawer: чат с агентом -->
<div class="kt-ai-drawer" id="drawer" role="dialog" aria-modal="false" aria-label="Ассистент" style="display:none">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--kt-ai-space-5)">
<strong style="font-size:var(--kt-ai-text-md)">Ассистент агента</strong>
<button class="kt-ai-btn" data-variant="ghost" id="drawerClose" style="padding:4px 8px"><svg class="kt-icon" aria-hidden="true"><use href="#x"></use></svg></button>
</div>
<div class="kt-ai-chat" data-fill="true" id="chatLog"></div>
<div class="kt-ai-drawer-actions" style="margin-top:var(--kt-ai-space-5)">
<div class="pa-chat-in">
<input class="kt-ai-input" id="chatInput" placeholder="Вопрос по ценам…" />
<button class="kt-ai-btn" data-variant="primary" id="chatSend"><svg class="kt-icon" aria-hidden="true"><use href="#send"></use></svg></button>
</div>
</div>
</div>
<div class="kt-ai-toast-stack" id="toasts"></div>
<script src="design-system/icons/kt-ai-icons.js"></script>
<script>
const $ = (s) => document.querySelector(s);
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
const fmtDay = (iso) => new Date(iso).toLocaleDateString("ru-RU", { day: "numeric", month: "short" });
const fmtTs = (iso) => new Date(iso).toLocaleDateString("ru-RU", { day: "2-digit", month: "2-digit" }) + ", " + new Date(iso).toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" });
const CODE_NAME = { USD: "Доллар США", EUR: "Евро", RUB: "Рубль РФ", CNY: "Юань КНР" };
const C = {
primary: "var(--kt-ai-primary)",
muted: "var(--kt-ai-fg-muted)",
grid: "var(--kt-ai-divider)",
warn: "var(--kt-ai-status-warn-fg)",
info: "var(--kt-ai-primary)",
line: "var(--kt-ai-primary)"
};
let DATA = null;
let VIEW = "overview";
function toast(msg) {
const t = document.createElement("div");
t.className = "kt-ai-toast";
t.textContent = msg;
$("#toasts").appendChild(t);
setTimeout(() => t.remove(), 4500);
}
function pill(txt, st) { return `<span class="kt-ai-status-pill" data-status="${st}">${txt}</span>`; }
// ---------- Графики (чистый SVG) ----------
function linePlot(series) {
const W = 640, H = 250, P = { t: 18, r: 14, b: 28, l: 44 };
if (!series || !series.length) return '';
const single = series.length === 1;
const vals = series.map((s) => s.avg);
const minV = Math.min(0, ...vals) - 0.15;
const maxV = Math.max(0.3, ...vals) + 0.15;
const x = (i) => P.l + (i * (W - P.l - P.r)) / (series.length - 1);
const y = (v) => P.t + (1 - (v - minV) / (maxV - minV)) * (H - P.t - P.b);
let out = "";
for (let g = Math.ceil(minV); g <= Math.floor(maxV); g++) {
out += `<line x1="${P.l}" y1="${y(g)}" x2="${W - P.r}" y2="${y(g)}" stroke="${C.grid}" stroke-width="1" ${g === 0 ? "" : 'stroke-dasharray="3 4"'} />`;
out += `<text x="${P.l - 8}" y="${y(g) + 4}" text-anchor="end" font-size="11" fill="${C.muted}">${g > 0 ? "+" : ""}${g}%</text>`;
}
series.forEach((s, i) => {
if (s.rain > 0.5) {
const h = Math.min(46, s.rain * 1.8);
out += `<rect x="${x(i) - 5}" y="${H - P.b - h}" width="10" height="${h}" rx="2" fill="${C.primary}" opacity="0.13" />`;
}
});
if (single) {
// Одна точка — рисуем вертикальный маркер
out += `<circle cx="${P.l + (W - P.l - P.r) / 2}" cy="${y(series[0].avg)}" r="5" fill="${C.primary}" />`;
out += `<text x="${W / 2}" y="${y(series[0].avg) - 12}" text-anchor="middle" font-size="12" font-weight="600" fill="${C.line}">${series[0].avg > 0 ? "+" : ""}${series[0].avg}%</text>`;
} else {
const d = series.map((s, i) => (i ? "L" : "M") + x(i).toFixed(1) + " " + y(s.avg).toFixed(1)).join(" ");
out += `<path d="${d}" fill="none" stroke="${C.primary}" stroke-width="2.5" stroke-linejoin="round" />`;
series.forEach((s, i) => { if (i % Math.ceil(series.length / 6) === 0 || i === series.length - 1) out += `<circle cx="${x(i)}" cy="${y(s.avg)}" r="3.2" fill="${C.primary}" />`; });
series.forEach((s, i) => {
if (i % Math.ceil(series.length / 7) === 0 || i === series.length - 1)
out += `<text x="${x(i)}" y="${H - 8}" text-anchor="middle" font-size="11" fill="${C.muted}">${fmtDay(s.date)}</text>`;
});
}
return `<svg class="pa-plot" viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet">${out}</svg>`;
}
function barsPlot(items, unit) {
// items: [{label, value, sub}]
const W = 640, H = 250, P = { t: 18, r: 14, b: 34, l: 44 };
const maxV = Math.max(...items.map((i) => Math.abs(i.value))) * 1.1 || 1;
const y = (v) => P.t + (1 - (v + maxV) / (2 * maxV)) * (H - P.t - P.b);
const zero = y(0);
const bw = Math.min(56, (W - P.l - P.r) / items.length * 0.55);
const x = (i) => P.l + i * ((W - P.l - P.r) / items.length) + ((W - P.l - P.r) / items.length - bw) / 2;
let out = `<line x1="${P.l}" y1="${zero}" x2="${W - P.r}" y2="${zero}" stroke="${C.grid}" />`;
items.forEach((it, i) => {
const h = Math.abs((it.value / maxV) * ((H - P.t - P.b) / 2));
const top = it.value >= 0 ? zero - h : zero;
const col = it.value >= 0 ? C.primary : C.warn;
out += `<rect x="${x(i)}" y="${top}" width="${bw}" height="${Math.max(2, h)}" rx="4" fill="${col}" opacity="${it.dim ? 0.45 : 0.9}" />`;
out += `<text x="${x(i) + bw / 2}" y="${(it.value >= 0 ? zero - h - 5 : zero + h + 13)}" text-anchor="middle" font-size="11" fill="${C.muted}" ${it.dim ? 'opacity="0.6"' : ""}>${it.dim ? "нет данных" : (it.value >= 0 ? "+" : "") + it.value.toFixed(unit === "mm" ? 1 : 2) + (unit === "%" ? "%" : "")}</text>`;
const lbl = it.label.length > 9 ? it.label.slice(0, 8) + "…" : it.label;
out += `<text x="${x(i) + bw / 2}" y="${H - 10}" text-anchor="middle" font-size="11" fill="${C.muted}">${esc(lbl)}</text>`;
});
return `<svg class="pa-plot" viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet">${out}</svg>`;
}
// ---------- Views ----------
function vOverview() {
const d = DATA;
const last = (d.meta && d.meta.lastRun) ? fmtTs(d.meta.lastRun) : "ещё не запускался";
const kpis = d.kpis.map((k) =>
`<div class="kt-ai-kpi"><span class="label">${esc(k.label)}</span><span class="value">${esc(k.value)}</span><span class="hint">${esc(k.hint)}</span></div>`
).join("");
const banner = d.banner
? `<div class="kt-ai-banner" data-tone="${d.banner.tone}" style="display:flex;align-items:center;justify-content:space-between;gap:var(--kt-ai-space-4)">
<span>${esc(d.banner.text)}</span>
<button class="kt-ai-btn" data-variant="ghost" data-action="collect" style="flex-shrink:0">Обновить</button>
</div>`
: "";
const series = d.series || [];
const legend = ` <div class="pa-legend">
<span class="li"><i style="background:${C.primary}"></i> среднее изменение курсов, %</span>
<span class="li"><i style="background:var(--kt-ai-primary);opacity:.6"></i> средняя температура, °C</span>
<span class="li"><i style="background:var(--kt-ai-status-info-bg)"></i> осадки, мм (столбцы)</span>
</div>`;
const wx = (d.weatherNow || []).map((w) =>
`<div class="pa-wx"><span>${esc(w.city)}</span><span class="num">${w.min}…${w.max}°C · ${w.rain} мм</span></div>`
).join("") || `<div class="pa-empty">Нет данных за сегодня</div>`;
const table = `<div class="kt-ai-table-wrap" data-framed="true">
<table class="kt-ai-table">
<thead><tr><th>Валюта</th><th style="text-align:right">Курс, ₸</th><th style="text-align:right">Изменение</th></tr></thead>
<tbody>${d.latest.map((q) => {
const st = q.changePct > 0.12 ? "ok" : q.changePct < -0.12 ? "risk" : "muted";
return `<tr><td><strong>${esc(q.code)}</strong><div class="meta" style="font-size:var(--kt-ai-text-2xs);color:var(--kt-ai-fg-muted)">${esc(CODE_NAME[q.code] || q.code)}</div></td>
<td class="num" style="text-align:right">${Number(q.value).toLocaleString("ru-RU")}</td>
<td class="num" style="text-align:right">${pill((q.changePct > 0 ? "+" : "") + q.changePct.toFixed(2) + "%", st)}</td></tr>`;
}).join("")}</tbody>
</table></div>`;
const summary = (d.meta && d.meta.summary) ? esc(d.meta.summary) : "Сводка формируется…";
const chips = ['Итог за день', "Что двигалось сильнее всего?", "Риски погоды"].map((s) =>
`<button class="kt-ai-btn" data-size="sm" style="border-color:var(--kt-ai-border-strong)" data-action="ask" data-q="${esc(s)}">${esc(s)}</button>`
).join("");
const alerts = (d.alerts || []).map((a) => `<div class="kt-ai-banner" data-tone="${a.tone}" style="margin-bottom:var(--kt-ai-space-2)">${esc(a.text)}</div>`).join("");
return `
<div style="max-width:1200px;margin:0 auto">
${banner}
${alerts}
<div class="kt-ai-kpi-strip" style="margin-top:var(--kt-ai-space-5)">${kpis}</div>
<div class="kt-ai-card">
<div style="display:flex;justify-content:space-between;align-items:baseline">
<h3 class="kt-ai-h3">Динамика: курсов валют и осадки</h3>
<span class="kt-ai-meta" id="seriesRange">${series.length ? fmtDay(series[0].date) + " — " + fmtDay(series[series.length - 1].date) : ""}</span>
</div>
${linePlot(series)}
${legend}
</div>
<div class="kt-ai-card" style="margin-top:var(--kt-ai-space-5)">
<h3 class="kt-ai-h3">Погода сегодня · 4 региона</h3>
<div style="margin-top:var(--kt-ai-space-4)">${wx}</div>
</div>
<div class="kt-ai-card" style="margin-top:var(--kt-ai-space-5)">
<div style="display:flex;justify-content:space-between;align-items:baseline">
<h3 class="kt-ai-h3">Сводка ИИ-аналитика</h3>
<span class="kt-ai-meta">обновлено ${last}</span>
</div>
<p style="margin:var(--kt-ai-space-4) 0 0;line-height:1.7">${summary}</p>
<div style="display:flex;gap:var(--kt-ai-space-3);flex-wrap:wrap;margin-top:var(--kt-ai-space-5)">${chips}</div>
</div>
<h3 class="kt-ai-h3" style="margin:var(--kt-ai-space-7) 0 var(--kt-ai-space-3)">Последние данные</h3>
${table}
</div>`;
}
function vHistory() {
const d = DATA;
const quotes = (d && d.quotes) || [];
const byDate = {};
for (const q of quotes) (byDate[q.date] = byDate[q.date] || []).push(q);
const dates = Object.keys(byDate).sort().reverse().slice(0, 10);
const uniq = [...new Set(quotes.map((q) => q.code))].sort();
const rows = dates.map((date) => {
const cells = uniq.map((code) => {
const q = byDate[date].find((x) => x.code === code);
if (!q) return `<td class="num" style="text-align:right;color:var(--kt-ai-fg-faint)">—</td>`;
const st = q.changePct > 0.12 ? "ok" : q.changePct < -0.12 ? "risk" : "muted";
return `<td class="num" style="text-align:right;white-space:nowrap">${Number(q.value).toLocaleString("ru-RU")} ${pill((q.changePct > 0 ? "+" : "") + (q.changePct || 0).toFixed(1) + "%", st)}</td>`;
}).join("");
return `<tr><td><strong>${fmtDay(date)}</strong></td>${cells}</tr>`;
}).join("");
return `
<div style="max-width:1200px;margin:0 auto">
<div class="kt-ai-card">
<h3 class="kt-ai-h3">История по дням · последние 10</h3>
<div style="margin-top:var(--kt-ai-space-4)"></div>
<div class="kt-ai-table-wrap" data-framed="true">
<table class="kt-ai-table">
<thead><tr><th>Дата</th>${uniq.map((c) => `<th style="text-align:right">${c}, ₸</th>`).join("")}</tr></thead>
<tbody>${rows || `<tr><td colspan="5"><div class="pa-empty">Дат нет — нажмите «Обновить сейчас»</div></td></tr>`}</tbody>
</table>
</div>
</div>
</div>`;
}
function vAgents() {
const d = DATA;
const agents = d && d.meta && d.meta.lastRun ? [
{ name: "Сбор данных", role: "Сканирует открытые источники: курсы валют и погоду по регионам", lastRun: d.meta.lastRun },
{ name: "ИИ-аналитик", role: "Формирует ежедневную сводку и отвечает на вопросы", lastRun: d.meta.lastRun }
] : [];
const sources = d && d.sources || [];
return `
<div style="max-width:1200px;margin:0 auto">
<div class="kt-ai-card">
<h3 class="kt-ai-h3">Агенты</h3>
<div class="pa-src">${agents.map((a) => `
<div class="item">
<svg class="kt-icon" aria-hidden="true" style="color:var(--kt-ai-primary)"><use href="#sparkles"></use></svg>
<div><div class="nm">${esc(a.name)}</div><div class="sub">${esc(a.role)}</div></div>
<span class="kt-ai-meta" style="margin-left:auto">запущен: ${esc(a.lastRun)}</span>
</div>`).join("") || `<div class="pa-empty">Запустите агента кнопкой «Обновить сейчас».</div>`}</div>
</div>
<div class="kt-ai-card" style="margin-top:var(--kt-ai-space-5)">
<h3 class="kt-ai-h3">Источники данных</h3>
<div class="pa-src">${sources.map((s) => `
<div class="item">
<svg class="kt-icon" aria-hidden="true" style="color:${s.ok ? "var(--kt-ai-status-ok-fg)" : "var(--kt-ai-status-warn-fg)"}"><use href="${s.ok ? "check-circle" : "warning"}"></use></svg>
<div><div class="nm">${esc(s.name)}</div><div class="sub">${s.ok ? esc(s.count + " записей сегодня") : esc(s.note || s.error || "нет данных")}</div></div>
</div>`).join("") || `<div class="pa-empty">Источники появятся после первого сбора.</div>`}</div>
</div>
</div>`;
}
const VIEWS = { overview: vOverview, history: vHistory, agents: vAgents };
const TITLES = { overview: ["Обзор", "открытые источники"], history: ["История", "еженедельно"], agents: ["Агенты", "инфраструктура"] };
function setView(v) {
VIEW = v;
$("#pageTitle").textContent = TITLES[v][0];
$("#pageSub").textContent = TITLES[v][1];
if (!DATA) return;
$("#view").innerHTML = VIEWS[v]();
document.querySelectorAll(".kt-ai-nav-item").forEach((n, i) => n.dataset.active = ["overview", "history", "agents"][i] === v ? "true" : "false");
bindActions();
}
function bindActions() {
document.querySelectorAll("[data-action='collect']").forEach((b) => (b.onclick = runCollect));
document.querySelectorAll("[data-action='ask']").forEach((b) => (b.onclick = () => ask(b.dataset.q)));
}
// Fallback-данные: страница работает и без сервера (Предпросмотр)
const FALLBACK = { kpis:[
{label:"Среднее движение курсов",value:"+0.12%",hint:"за сегодня"},
{label:"Валют за отслеживание",value:"4",hint:"USD · EUR · RUB · CNY"},
{label:"Регионы погоды",value:"4",hint:"Астана · Алматы · Шымкент · Павлодар"},
{label:"Дней истории",value:"30",hint:"с 25 авг 2026"}
],banner:{tone:"info",text:"Данные 24.09.2026 · запустите сервер для живых данных",action:"Обновить сейчас"},latest:[
{code:"USD",value:445.6,changePct:0,unit:"тенге за 1 доллар"},
{code:"EUR",value:508.1,changePct:0,unit:"тенге за 1 евро"},
{code:"RUB",value:5.294,changePct:0,unit:"тенге за 1 рубль"},
{code:"CNY",value:66.7,changePct:0,unit:"тенге за 1 юань"}
],series:[{date:"2026-08-25",avg:-0.05,rain:1.2,tmin:11,tmax:26},{date:"2026-08-26",avg:+0.02,rain:0.1,tmin:13,tmax:27},{date:"2026-08-27",avg:-0.03,rain:0,tmin:14,tmax:31},{date:"2026-08-28",avg:+0.08,rain:0.7,tmin:15,tmax:30},{date:"2026-08-29",avg:-0.01,rain:2,tmin:10,tmax:27},{date:"2026-08-30",avg:+0.04,rain:0.5,tmin:9,tmax:28},{date:"2026-09-24",avg:0,rain:0,tmin:14,tmax:29}],weatherNow:[
{city:"Астана",max:18,min:9,rain:0},
{city:"Алматы",max:29,min:17,rain:0},
{city:"Шымкент",max:34,min:19,rain:0},
{city:"Павлодар",max:20,min:10,rain:0}
],alerts:[],meta:{lastRun:new Date().toISOString(),summary:"Агент собирает курсы валют (USD/EUR/RUB/CNY в тенге) и погоду по 4 регионам Казахстана. Данные обновляются ежедневно. Для анализа агро-цен подключите источники vetlex.kz или ALAPI."},quotes:[],sources:[{id:"fx",name:"Forex open.er-api.com",ok:true,count:4},{id:"weather",name:"open-meteo.com (4 региона)",ok:true,count:4},{id:"agrop",name:"Vetlex/ALAPI (agricultural prices)",ok:false,note:"Planned — container has no access to KZ sources"}]};
async function load() {
try { DATA = await fetch("api/overview").then((r) => r.json()); }
catch (e) { DATA = FALLBACK; }
if (!DATA || !DATA.kpis) DATA = FALLBACK;
setView(VIEW);
}
let collecting = false;
async function runCollect() {
if (collecting) return;
collecting = true;
const b = $("#btnCollect");
if (b) { b.disabled = true; b.innerHTML = '<svg class="kt-icon" aria-hidden="true"><use href="#sync"></use></svg> Собираю…'; }
try {
const j = await fetch("api/collect", { method: "POST" }).then((r) => r.json());
if (j.error) toast("Ошибка сбора: " + j.error);
else toast("Источники обновлены, ИИ пишет сводку…");
} catch (e) { toast("Сбор не удался: " + e.message); }
await load();
if (b) { b.disabled = false; b.innerHTML = '<svg class="kt-icon" aria-hidden="true"><use href="#refresh"></use></svg> Обновить сейчас'; }
collecting = false;
}
// ---------- Чат ----------
function addChat(role, text) {
const log = $("#chatLog");
if (role === "user") log.insertAdjacentHTML("beforeend", `<div class="pa-user">${esc(text)}</div>`);
else {
const id = "m" + Date.now();
log.insertAdjacentHTML("beforeend", `<div><div class="pa-ai-label">Ассистент</div><div class="pa-ai" id="${id}">${esc(text || "Думаю…")}</div></div>`);
}
log.scrollTop = log.scrollHeight;
}
function setChat(id, text, err) {
const el = document.querySelector(id);
if (el) { el.textContent = text; if (err) el.classList.add("err"); }
}
async function ask(q) {
if (!q) return;
$("#drawer").style.display = "";
addChat("user", q);
const log = $("#chatLog");
const id = "m" + Date.now();
log.insertAdjacentHTML("beforeend", `<div><div class="pa-ai-label">Ассистент</div><div class="pa-ai" id="${id}">Думаю…</div></div>`);
log.scrollTop = log.scrollHeight;
try {
const j = await fetch("api/ask", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ q }) }).then((r) => r.json());
setChat("#" + id, j.answer || j.error || "Нет ответа", !!j.error);
} catch (e) {
setChat("#" + id, "ИИ временно недоступен: " + e.message, true);
}
log.scrollTop = log.scrollHeight;
}
// ---------- Инициализация ----------
$("#btnCollect").onclick = runCollect;
$("#drawerClose").onclick = () => ($("#drawer").style.display = "none");
$("#chatSend").onclick = () => { const v = $("#chatInput").value.trim(); if (v) { $("#chatInput").value = ""; ask(v); } };
$("#chatInput").addEventListener("keydown", (e) => { if (e.key === "Enter") { const v = e.target.value.trim(); if (v) { e.target.value = ""; ask(v); } } });
$("#askInput").addEventListener("keydown", (e) => { if (e.key === "Enter") { const v = e.target.value.trim(); if (v) { e.target.value = ""; ask(v); } } });
document.querySelectorAll(".kt-ai-nav-item").forEach((n, i) => (n.onclick = (e) => { e.preventDefault(); setView(["overview", "history", "agents"][i]); }));
load();
</script>
</body>
</html>

411
index.legacy.html Normal file
View File

@ -0,0 +1,411 @@
<!DOCTYPE html>
<!-- Price Agent · дизайн: Apple. Все 5 CSS сохраняем; подробности в design.md. -->
<html lang="ru" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Цена Агент — агро-рынок</title>
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
<link rel="stylesheet" href="design-system/kt-ai-components.css">
<link rel="stylesheet" href="design-system/kt-ai-page.css">
<link rel="stylesheet" href="design-system/vibe-theme.css">
<style>
a.kt-ai-btn { text-decoration: none; }
.pa-plot { width: 100%; height: 250px; display: block; margin-top: var(--kt-ai-space-4); }
.pa-legend { display: flex; flex-wrap: wrap; gap: var(--kt-ai-space-5); margin-top: var(--kt-ai-space-4); }
.pa-legend .li { display: flex; align-items: center; gap: 8px; font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); }
.pa-legend i { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
.pa-wx { display: flex; justify-content: space-between; padding: var(--kt-ai-space-3) 0; border-bottom: 1px solid var(--kt-ai-divider); font-size: var(--kt-ai-text-sm); }
.pa-wx:last-child { border-bottom: none; }
.pa-wx .num { font-variant-numeric: tabular-nums; color: var(--kt-ai-fg-muted); }
.pa-src { display: flex; flex-direction: column; gap: var(--kt-ai-space-5); margin-top: var(--kt-ai-space-5); }
.pa-src .item { display: flex; align-items: center; gap: var(--kt-ai-space-4); }
.pa-src .nm { font-weight: var(--kt-ai-weight-medium); }
.pa-src .sub { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); }
.pa-rows { margin-top: var(--kt-ai-space-6); }
.pa-chat-in { display: flex; gap: var(--kt-ai-space-3); }
.pa-chat-in input { flex: 1; }
.pa-user { align-self: flex-end; max-width: 88%; padding: var(--kt-ai-space-3) var(--kt-ai-space-4); background: var(--kt-ai-bg-soft); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-2xl); font-size: var(--kt-ai-text-sm); white-space: pre-wrap; }
.pa-ai { flex: 1; min-width: 0; padding: var(--kt-ai-space-3) var(--kt-ai-space-4); border-left: 3px solid var(--kt-ai-primary); font-size: var(--kt-ai-text-sm); line-height: 1.65; white-space: pre-wrap; }
.pa-ai.err { border-left-color: var(--kt-ai-status-risk-fg); color: var(--kt-ai-status-risk-fg); }
.pa-ai-label { font-size: var(--kt-ai-text-2xs); color: var(--kt-ai-fg-muted); margin-bottom: var(--kt-ai-space-1); }
.pa-empty { padding: var(--kt-ai-space-6); text-align: center; color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm); }
</style>
</head>
<body class="kt-ai-app">
<div class="kt-ai-shell">
<aside class="kt-ai-sidebar">
<div style="display:flex;align-items:center;gap:10px;padding:var(--kt-ai-space-5) var(--kt-ai-space-5) var(--kt-ai-space-4)">
<svg class="kt-icon" aria-hidden="true" style="color:var(--kt-ai-primary);width:22px;height:22px"><use href="#sparkles"></use></svg>
<div>
<div style="font-weight:var(--kt-ai-weight-semibold);color:var(--kt-ai-fg);font-size:var(--kt-ai-text-md)">Цена Агент</div>
<div class="kt-ai-meta">агро-рынок</div>
</div>
</div>
<div style="padding:0 var(--kt-ai-space-4) var(--kt-ai-space-4)">
<button class="kt-ai-btn" data-variant="primary" id="btnCollect" style="width:100%">
<svg class="kt-icon" aria-hidden="true"><use href="#refresh"></use></svg>
Обновить сейчас
</button>
</div>
<nav>
<div class="kt-ai-nav-group">
<a class="kt-ai-nav-item" data-active="true" href="#"><svg class="kt-icon" aria-hidden="true"><use href="#dashboard"></use></svg><span class="nav-label">Обзор</span></a>
<a class="kt-ai-nav-item" href="#" id="navHistory"><svg class="kt-icon" aria-hidden="true"><use href="#table"></use></svg><span class="nav-label">История</span></a>
<a class="kt-ai-nav-item" href="#" id="navAgents"><svg class="kt-icon" aria-hidden="true"><use href="#source"></use></svg><span class="nav-label">Агенты</span></a>
</div>
</nav>
</aside>
<main class="kt-ai-main" style="min-height:100dvh;display:flex;flex-direction:column;background:var(--kt-ai-bg)">
<header class="kt-ai-topbar">
<nav class="kt-ai-meta" style="display:flex;flex-direction:column;line-height:1.3">
<span id="pageTitle" style="font-size:var(--kt-ai-text-md);color:var(--kt-ai-fg);font-weight:var(--kt-ai-weight-semibold)">Обзор</span>
<span id="pageSub" style="font-size:var(--kt-ai-text-2xs)">открытые источники</span>
</nav>
<div class="spacer"></div>
<div class="kt-ai-search-field">
<input class="kt-ai-search" id="askInput" type="search" placeholder="Спросить агента: почему выросла цена?" style="width:100%">
</div>
</header>
<div style="overflow-y:auto;flex:1;padding:var(--kt-ai-space-8) var(--kt-ai-space-8) var(--kt-ai-space-11)">
<div id="view"></div>
</div>
</main>
</div>
<!-- Drawer: чат с агентом -->
<div class="kt-ai-drawer" id="drawer" role="dialog" aria-modal="false" aria-label="Ассистент" style="display:none">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--kt-ai-space-5)">
<strong style="font-size:var(--kt-ai-text-md)">Ассистент агента</strong>
<button class="kt-ai-btn" data-variant="ghost" id="drawerClose" style="padding:4px 8px"><svg class="kt-icon" aria-hidden="true"><use href="#x"></use></svg></button>
</div>
<div class="kt-ai-chat" data-fill="true" id="chatLog"></div>
<div class="kt-ai-drawer-actions" style="margin-top:var(--kt-ai-space-5)">
<div class="pa-chat-in">
<input class="kt-ai-input" id="chatInput" placeholder="Вопрос по ценам…" />
<button class="kt-ai-btn" data-variant="primary" id="chatSend"><svg class="kt-icon" aria-hidden="true"><use href="#send"></use></svg></button>
</div>
</div>
</div>
<div class="kt-ai-toast-stack" id="toasts"></div>
<script src="design-system/icons/kt-ai-icons.js"></script>
<script>
const $ = (s) => document.querySelector(s);
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
const fmtDay = (iso) => new Date(iso).toLocaleDateString("ru-RU", { day: "numeric", month: "short" });
const fmtTs = (iso) => new Date(iso).toLocaleDateString("ru-RU", { day: "2-digit", month: "2-digit" }) + ", " + new Date(iso).toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" });
const CODE_NAME = { USD: "Доллар США", EUR: "Евро", RUB: "Рубль РФ", CNY: "Юань КНР" };
const C = {
primary: "var(--kt-ai-primary)",
muted: "var(--kt-ai-fg-muted)",
grid: "var(--kt-ai-divider)",
warn: "var(--kt-ai-status-warn-fg)",
info: "var(--kt-ai-primary)",
line: "var(--kt-ai-primary)"
};
let DATA = null;
let VIEW = "overview";
function toast(msg) {
const t = document.createElement("div");
t.className = "kt-ai-toast";
t.textContent = msg;
$("#toasts").appendChild(t);
setTimeout(() => t.remove(), 4500);
}
function pill(txt, st) { return `<span class="kt-ai-status-pill" data-status="${st}">${txt}</span>`; }
// ---------- Графики (чистый SVG) ----------
function linePlot(series) {
const W = 640, H = 250, P = { t: 18, r: 14, b: 28, l: 44 };
if (!series || !series.length) return '';
const single = series.length === 1;
const vals = series.map((s) => s.avg);
const minV = Math.min(0, ...vals) - 0.15;
const maxV = Math.max(0.3, ...vals) + 0.15;
const x = (i) => P.l + (i * (W - P.l - P.r)) / (series.length - 1);
const y = (v) => P.t + (1 - (v - minV) / (maxV - minV)) * (H - P.t - P.b);
let out = "";
for (let g = Math.ceil(minV); g <= Math.floor(maxV); g++) {
out += `<line x1="${P.l}" y1="${y(g)}" x2="${W - P.r}" y2="${y(g)}" stroke="${C.grid}" stroke-width="1" ${g === 0 ? "" : 'stroke-dasharray="3 4"'} />`;
out += `<text x="${P.l - 8}" y="${y(g) + 4}" text-anchor="end" font-size="11" fill="${C.muted}">${g > 0 ? "+" : ""}${g}%</text>`;
}
series.forEach((s, i) => {
if (s.rain > 0.5) {
const h = Math.min(46, s.rain * 1.8);
out += `<rect x="${x(i) - 5}" y="${H - P.b - h}" width="10" height="${h}" rx="2" fill="${C.primary}" opacity="0.13" />`;
}
});
if (single) {
// Одна точка — рисуем вертикальный маркер
out += `<circle cx="${P.l + (W - P.l - P.r) / 2}" cy="${y(series[0].avg)}" r="5" fill="${C.primary}" />`;
out += `<text x="${W / 2}" y="${y(series[0].avg) - 12}" text-anchor="middle" font-size="12" font-weight="600" fill="${C.line}">${series[0].avg > 0 ? "+" : ""}${series[0].avg}%</text>`;
} else {
const d = series.map((s, i) => (i ? "L" : "M") + x(i).toFixed(1) + " " + y(s.avg).toFixed(1)).join(" ");
out += `<path d="${d}" fill="none" stroke="${C.primary}" stroke-width="2.5" stroke-linejoin="round" />`;
series.forEach((s, i) => { if (i % Math.ceil(series.length / 6) === 0 || i === series.length - 1) out += `<circle cx="${x(i)}" cy="${y(s.avg)}" r="3.2" fill="${C.primary}" />`; });
series.forEach((s, i) => {
if (i % Math.ceil(series.length / 7) === 0 || i === series.length - 1)
out += `<text x="${x(i)}" y="${H - 8}" text-anchor="middle" font-size="11" fill="${C.muted}">${fmtDay(s.date)}</text>`;
});
}
return `<svg class="pa-plot" viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet">${out}</svg>`;
}
function barsPlot(items, unit) {
// items: [{label, value, sub}]
const W = 640, H = 250, P = { t: 18, r: 14, b: 34, l: 44 };
const maxV = Math.max(...items.map((i) => Math.abs(i.value))) * 1.1 || 1;
const y = (v) => P.t + (1 - (v + maxV) / (2 * maxV)) * (H - P.t - P.b);
const zero = y(0);
const bw = Math.min(56, (W - P.l - P.r) / items.length * 0.55);
const x = (i) => P.l + i * ((W - P.l - P.r) / items.length) + ((W - P.l - P.r) / items.length - bw) / 2;
let out = `<line x1="${P.l}" y1="${zero}" x2="${W - P.r}" y2="${zero}" stroke="${C.grid}" />`;
items.forEach((it, i) => {
const h = Math.abs((it.value / maxV) * ((H - P.t - P.b) / 2));
const top = it.value >= 0 ? zero - h : zero;
const col = it.value >= 0 ? C.primary : C.warn;
out += `<rect x="${x(i)}" y="${top}" width="${bw}" height="${Math.max(2, h)}" rx="4" fill="${col}" opacity="${it.dim ? 0.45 : 0.9}" />`;
out += `<text x="${x(i) + bw / 2}" y="${(it.value >= 0 ? zero - h - 5 : zero + h + 13)}" text-anchor="middle" font-size="11" fill="${C.muted}" ${it.dim ? 'opacity="0.6"' : ""}>${it.dim ? "нет данных" : (it.value >= 0 ? "+" : "") + it.value.toFixed(unit === "mm" ? 1 : 2) + (unit === "%" ? "%" : "")}</text>`;
const lbl = it.label.length > 9 ? it.label.slice(0, 8) + "…" : it.label;
out += `<text x="${x(i) + bw / 2}" y="${H - 10}" text-anchor="middle" font-size="11" fill="${C.muted}">${esc(lbl)}</text>`;
});
return `<svg class="pa-plot" viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet">${out}</svg>`;
}
// ---------- Views ----------
function vOverview() {
const d = DATA;
const last = (d.meta && d.meta.lastRun) ? fmtTs(d.meta.lastRun) : "ещё не запускался";
const kpis = d.kpis.map((k) =>
`<div class="kt-ai-kpi"><span class="label">${esc(k.label)}</span><span class="value">${esc(k.value)}</span><span class="hint">${esc(k.hint)}</span></div>`
).join("");
const banner = d.banner
? `<div class="kt-ai-banner" data-tone="${d.banner.tone}" style="display:flex;align-items:center;justify-content:space-between;gap:var(--kt-ai-space-4)">
<span>${esc(d.banner.text)}</span>
<button class="kt-ai-btn" data-variant="ghost" data-action="collect" style="flex-shrink:0">Обновить</button>
</div>`
: "";
const series = d.series || [];
const legend = ` <div class="pa-legend">
<span class="li"><i style="background:${C.primary}"></i> среднее изменение курсов, %</span>
<span class="li"><i style="background:var(--kt-ai-primary);opacity:.6"></i> средняя температура, °C</span>
<span class="li"><i style="background:var(--kt-ai-status-info-bg)"></i> осадки, мм (столбцы)</span>
</div>`;
const wx = (d.weatherNow || []).map((w) =>
`<div class="pa-wx"><span>${esc(w.city)}</span><span class="num">${w.min}…${w.max}°C · ${w.rain} мм</span></div>`
).join("") || `<div class="pa-empty">Нет данных за сегодня</div>`;
const table = `<div class="kt-ai-table-wrap" data-framed="true">
<table class="kt-ai-table">
<thead><tr><th>Валюта</th><th style="text-align:right">Курс, ₸</th><th style="text-align:right">Изменение</th></tr></thead>
<tbody>${d.latest.map((q) => {
const st = q.changePct > 0.12 ? "ok" : q.changePct < -0.12 ? "risk" : "muted";
return `<tr><td><strong>${esc(q.code)}</strong><div class="meta" style="font-size:var(--kt-ai-text-2xs);color:var(--kt-ai-fg-muted)">${esc(CODE_NAME[q.code] || q.code)}</div></td>
<td class="num" style="text-align:right">${Number(q.value).toLocaleString("ru-RU")}</td>
<td class="num" style="text-align:right">${pill((q.changePct > 0 ? "+" : "") + q.changePct.toFixed(2) + "%", st)}</td></tr>`;
}).join("")}</tbody>
</table></div>`;
const summary = (d.meta && d.meta.summary) ? esc(d.meta.summary) : "Сводка формируется…";
const chips = ['Итог за день', "Что двигалось сильнее всего?", "Риски погоды"].map((s) =>
`<button class="kt-ai-btn" data-size="sm" style="border-color:var(--kt-ai-border-strong)" data-action="ask" data-q="${esc(s)}">${esc(s)}</button>`
).join("");
const alerts = (d.alerts || []).map((a) => `<div class="kt-ai-banner" data-tone="${a.tone}" style="margin-bottom:var(--kt-ai-space-2)">${esc(a.text)}</div>`).join("");
return `
<div style="max-width:1200px;margin:0 auto">
${banner}
${alerts}
<div class="kt-ai-kpi-strip" style="margin-top:var(--kt-ai-space-5)">${kpis}</div>
<div class="kt-ai-card">
<div style="display:flex;justify-content:space-between;align-items:baseline">
<h3 class="kt-ai-h3">Динамика: курсов валют и осадки</h3>
<span class="kt-ai-meta" id="seriesRange">${series.length ? fmtDay(series[0].date) + " — " + fmtDay(series[series.length - 1].date) : ""}</span>
</div>
${linePlot(series)}
${legend}
</div>
<div class="kt-ai-card" style="margin-top:var(--kt-ai-space-5)">
<h3 class="kt-ai-h3">Погода сегодня · 4 региона</h3>
<div style="margin-top:var(--kt-ai-space-4)">${wx}</div>
</div>
<div class="kt-ai-card" style="margin-top:var(--kt-ai-space-5)">
<div style="display:flex;justify-content:space-between;align-items:baseline">
<h3 class="kt-ai-h3">Сводка ИИ-аналитика</h3>
<span class="kt-ai-meta">обновлено ${last}</span>
</div>
<p style="margin:var(--kt-ai-space-4) 0 0;line-height:1.7">${summary}</p>
<div style="display:flex;gap:var(--kt-ai-space-3);flex-wrap:wrap;margin-top:var(--kt-ai-space-5)">${chips}</div>
</div>
<h3 class="kt-ai-h3" style="margin:var(--kt-ai-space-7) 0 var(--kt-ai-space-3)">Последние данные</h3>
${table}
</div>`;
}
function vHistory() {
const d = DATA;
const quotes = (d && d.quotes) || [];
const byDate = {};
for (const q of quotes) (byDate[q.date] = byDate[q.date] || []).push(q);
const dates = Object.keys(byDate).sort().reverse().slice(0, 10);
const uniq = [...new Set(quotes.map((q) => q.code))].sort();
const rows = dates.map((date) => {
const cells = uniq.map((code) => {
const q = byDate[date].find((x) => x.code === code);
if (!q) return `<td class="num" style="text-align:right;color:var(--kt-ai-fg-faint)">—</td>`;
const st = q.changePct > 0.12 ? "ok" : q.changePct < -0.12 ? "risk" : "muted";
return `<td class="num" style="text-align:right;white-space:nowrap">${Number(q.value).toLocaleString("ru-RU")} ${pill((q.changePct > 0 ? "+" : "") + (q.changePct || 0).toFixed(1) + "%", st)}</td>`;
}).join("");
return `<tr><td><strong>${fmtDay(date)}</strong></td>${cells}</tr>`;
}).join("");
return `
<div style="max-width:1200px;margin:0 auto">
<div class="kt-ai-card">
<h3 class="kt-ai-h3">История по дням · последние 10</h3>
<div style="margin-top:var(--kt-ai-space-4)"></div>
<div class="kt-ai-table-wrap" data-framed="true">
<table class="kt-ai-table">
<thead><tr><th>Дата</th>${uniq.map((c) => `<th style="text-align:right">${c}, ₸</th>`).join("")}</tr></thead>
<tbody>${rows || `<tr><td colspan="5"><div class="pa-empty">Дат нет — нажмите «Обновить сейчас»</div></td></tr>`}</tbody>
</table>
</div>
</div>
</div>`;
}
function vAgents() {
const d = DATA;
const agents = d && d.meta && d.meta.lastRun ? [
{ name: "Сбор данных", role: "Сканирует открытые источники: курсы валют и погоду по регионам", lastRun: d.meta.lastRun },
{ name: "ИИ-аналитик", role: "Формирует ежедневную сводку и отвечает на вопросы", lastRun: d.meta.lastRun }
] : [];
const sources = d && d.sources || [];
return `
<div style="max-width:1200px;margin:0 auto">
<div class="kt-ai-card">
<h3 class="kt-ai-h3">Агенты</h3>
<div class="pa-src">${agents.map((a) => `
<div class="item">
<svg class="kt-icon" aria-hidden="true" style="color:var(--kt-ai-primary)"><use href="#sparkles"></use></svg>
<div><div class="nm">${esc(a.name)}</div><div class="sub">${esc(a.role)}</div></div>
<span class="kt-ai-meta" style="margin-left:auto">запущен: ${esc(a.lastRun)}</span>
</div>`).join("") || `<div class="pa-empty">Запустите агента кнопкой «Обновить сейчас».</div>`}</div>
</div>
<div class="kt-ai-card" style="margin-top:var(--kt-ai-space-5)">
<h3 class="kt-ai-h3">Источники данных</h3>
<div class="pa-src">${sources.map((s) => `
<div class="item">
<svg class="kt-icon" aria-hidden="true" style="color:${s.ok ? "var(--kt-ai-status-ok-fg)" : "var(--kt-ai-status-warn-fg)"}"><use href="${s.ok ? "check-circle" : "warning"}"></use></svg>
<div><div class="nm">${esc(s.name)}</div><div class="sub">${s.ok ? esc(s.count + " записей сегодня") : esc(s.note || s.error || "нет данных")}</div></div>
</div>`).join("") || `<div class="pa-empty">Источники появятся после первого сбора.</div>`}</div>
</div>
</div>`;
}
const VIEWS = { overview: vOverview, history: vHistory, agents: vAgents };
const TITLES = { overview: ["Обзор", "открытые источники"], history: ["История", "еженедельно"], agents: ["Агенты", "инфраструктура"] };
function setView(v) {
VIEW = v;
$("#pageTitle").textContent = TITLES[v][0];
$("#pageSub").textContent = TITLES[v][1];
if (!DATA) return;
$("#view").innerHTML = VIEWS[v]();
document.querySelectorAll(".kt-ai-nav-item").forEach((n, i) => n.dataset.active = ["overview", "history", "agents"][i] === v ? "true" : "false");
bindActions();
}
function bindActions() {
document.querySelectorAll("[data-action='collect']").forEach((b) => (b.onclick = runCollect));
document.querySelectorAll("[data-action='ask']").forEach((b) => (b.onclick = () => ask(b.dataset.q)));
}
// Fallback-данные: страница работает и без сервера (Предпросмотр)
const FALLBACK = { kpis:[
{label:"Среднее движение курсов",value:"+0.12%",hint:"за сегодня"},
{label:"Валют за отслеживание",value:"4",hint:"USD · EUR · RUB · CNY"},
{label:"Регионы погоды",value:"4",hint:"Астана · Алматы · Шымкент · Павлодар"},
{label:"Дней истории",value:"30",hint:"с 25 авг 2026"}
],banner:{tone:"info",text:"Данные 24.09.2026 · запустите сервер для живых данных",action:"Обновить сейчас"},latest:[
{code:"USD",value:445.6,changePct:0,unit:"тенге за 1 доллар"},
{code:"EUR",value:508.1,changePct:0,unit:"тенге за 1 евро"},
{code:"RUB",value:5.294,changePct:0,unit:"тенге за 1 рубль"},
{code:"CNY",value:66.7,changePct:0,unit:"тенге за 1 юань"}
],series:[{date:"2026-08-25",avg:-0.05,rain:1.2,tmin:11,tmax:26},{date:"2026-08-26",avg:+0.02,rain:0.1,tmin:13,tmax:27},{date:"2026-08-27",avg:-0.03,rain:0,tmin:14,tmax:31},{date:"2026-08-28",avg:+0.08,rain:0.7,tmin:15,tmax:30},{date:"2026-08-29",avg:-0.01,rain:2,tmin:10,tmax:27},{date:"2026-08-30",avg:+0.04,rain:0.5,tmin:9,tmax:28},{date:"2026-09-24",avg:0,rain:0,tmin:14,tmax:29}],weatherNow:[
{city:"Астана",max:18,min:9,rain:0},
{city:"Алматы",max:29,min:17,rain:0},
{city:"Шымкент",max:34,min:19,rain:0},
{city:"Павлодар",max:20,min:10,rain:0}
],alerts:[],meta:{lastRun:new Date().toISOString(),summary:"Агент собирает курсы валют (USD/EUR/RUB/CNY в тенге) и погоду по 4 регионам Казахстана. Данные обновляются ежедневно. Для анализа агро-цен подключите источники vetlex.kz или ALAPI."},quotes:[],sources:[{id:"fx",name:"Forex open.er-api.com",ok:true,count:4},{id:"weather",name:"open-meteo.com (4 региона)",ok:true,count:4},{id:"agrop",name:"Vetlex/ALAPI (agricultural prices)",ok:false,note:"Planned — container has no access to KZ sources"}]};
async function load() {
try { DATA = await fetch("api/overview").then((r) => r.json()); }
catch (e) { DATA = FALLBACK; }
if (!DATA || !DATA.kpis) DATA = FALLBACK;
setView(VIEW);
}
let collecting = false;
async function runCollect() {
if (collecting) return;
collecting = true;
const b = $("#btnCollect");
if (b) { b.disabled = true; b.innerHTML = '<svg class="kt-icon" aria-hidden="true"><use href="#sync"></use></svg> Собираю…'; }
try {
const j = await fetch("api/collect", { method: "POST" }).then((r) => r.json());
if (j.error) toast("Ошибка сбора: " + j.error);
else toast("Источники обновлены, ИИ пишет сводку…");
} catch (e) { toast("Сбор не удался: " + e.message); }
await load();
if (b) { b.disabled = false; b.innerHTML = '<svg class="kt-icon" aria-hidden="true"><use href="#refresh"></use></svg> Обновить сейчас'; }
collecting = false;
}
// ---------- Чат ----------
function addChat(role, text) {
const log = $("#chatLog");
if (role === "user") log.insertAdjacentHTML("beforeend", `<div class="pa-user">${esc(text)}</div>`);
else {
const id = "m" + Date.now();
log.insertAdjacentHTML("beforeend", `<div><div class="pa-ai-label">Ассистент</div><div class="pa-ai" id="${id}">${esc(text || "Думаю…")}</div></div>`);
}
log.scrollTop = log.scrollHeight;
}
function setChat(id, text, err) {
const el = document.querySelector(id);
if (el) { el.textContent = text; if (err) el.classList.add("err"); }
}
async function ask(q) {
if (!q) return;
$("#drawer").style.display = "";
addChat("user", q);
const log = $("#chatLog");
const id = "m" + Date.now();
log.insertAdjacentHTML("beforeend", `<div><div class="pa-ai-label">Ассистент</div><div class="pa-ai" id="${id}">Думаю…</div></div>`);
log.scrollTop = log.scrollHeight;
try {
const j = await fetch("api/ask", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ q }) }).then((r) => r.json());
setChat("#" + id, j.answer || j.error || "Нет ответа", !!j.error);
} catch (e) {
setChat("#" + id, "ИИ временно недоступен: " + e.message, true);
}
log.scrollTop = log.scrollHeight;
}
// ---------- Инициализация ----------
$("#btnCollect").onclick = runCollect;
$("#drawerClose").onclick = () => ($("#drawer").style.display = "none");
$("#chatSend").onclick = () => { const v = $("#chatInput").value.trim(); if (v) { $("#chatInput").value = ""; ask(v); } };
$("#chatInput").addEventListener("keydown", (e) => { if (e.key === "Enter") { const v = e.target.value.trim(); if (v) { e.target.value = ""; ask(v); } } });
$("#askInput").addEventListener("keydown", (e) => { if (e.key === "Enter") { const v = e.target.value.trim(); if (v) { e.target.value = ""; ask(v); } } });
document.querySelectorAll(".kt-ai-nav-item").forEach((n, i) => (n.onclick = (e) => { e.preventDefault(); setView(["overview", "history", "agents"][i]); }));
load();
</script>
</body>
</html>