diff --git a/ai-cache.json b/ai-cache.json
new file mode 100644
index 0000000..c8c6166
--- /dev/null
+++ b/ai-cache.json
@@ -0,0 +1,5 @@
+{
+ "generated_at": null,
+ "period": null,
+ "response": null
+}
diff --git a/dashboard.html b/dashboard.html
index a4de98f..14fb0bc 100644
--- a/dashboard.html
+++ b/dashboard.html
@@ -159,6 +159,15 @@ body { font-family: var(--font-base); background: var(--color-bg); color: var(--
.ai-divider { height: 1px; background: var(--color-border); margin: 12px 0; }
.ai-hint { font-size: 11px; color: var(--color-text-secondary); text-align: center; line-height: 1.5; }
.ai-key-note { font-size: 11px; color: var(--color-green); text-align: center; margin-bottom: 8px; font-weight: 500; }
+.ai-cache-meta { display: flex; align-items: center; gap: 4px; margin-bottom: 6px; }
+.ai-update-details { margin-top: 4px; }
+.ai-update-details[open] .ai-update-summary { color: var(--color-brand); }
+.ai-update-summary {
+ font-size: 13px; font-weight: 600; color: var(--color-text-secondary);
+ cursor: pointer; list-style: none; padding: 6px 0; user-select: none;
+}
+.ai-update-summary::-webkit-details-marker { display: none; }
+.ai-update-summary:hover { color: var(--color-brand); }
/* ── RESPONSIVE ── */
@media (max-width: 1200px) { .summary-bar { grid-template-columns: repeat(3,1fr); } }
@@ -271,16 +280,30 @@ body { font-family: var(--font-base); background: var(--color-bg); color: var(--
🤖 AI-анализ KPI
-
-
✓ Токен загружен из config.js
-
-
+
+
+
+
+
+ Загрузка...
+ · общий для всех
+
+
+
-
-
+
+
-
DeepSeek API · Ключ не сохраняется за пределами сессии браузера
+
+ 🔄 Обновить анализ
+
+
✓ DeepSeek токен загружен из config.js
+
+
+
+ Новый ответ сохранится в репо и сразу станет доступен всем пользователям. Обновляйте раз в месяц при новых данных.
+
@@ -444,7 +467,12 @@ const EMBEDDED_CSV = `"report_period_id";"entry_date";"abons";"registered_total"
// ─────────────────────────────────────────────────────────────────────────────
const MONTH_NAMES = ['','Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'];
-const CSV_FILENAME = 'drb_iliyas_kpi_2026.csv';
+const CSV_FILENAME = 'drb_iliyas_kpi_2026.csv';
+const GITEA_API = 'https://git.telecom.quest/api/v1';
+const GITEA_OWNER = 'Kyrykbaev-I';
+const GITEA_REPO = 'kpi-dashboard';
+const AI_CACHE_FILE = 'ai-cache.json';
+const AI_CACHE_RAW = `https://git.telecom.quest/${GITEA_OWNER}/${GITEA_REPO}/raw/branch/main/${AI_CACHE_FILE}`;
const AppState = {
rawRows: [], snapshots: [], dailySeries: {},
@@ -696,7 +724,7 @@ function showDashboard() {
document.getElementById('dashboard').style.display = 'block';
renderHeader(); renderSummaryBar(); renderToolbar();
destroyAllCharts(); renderAllCharts();
- restoreAiCache(); initAiKey();
+ loadAiCache(); initAiKey();
}
function renderHeader() {
@@ -1120,6 +1148,7 @@ async function runAiAnalysis() {
AppState.lastAiResponse=text;
sessionStorage.setItem('lastAiResponse',text);
showAiResult(text);
+ await saveAiCache(text); // save to git repo for all users (requires GITEA_TOKEN in config.js)
} catch(e) {
clearTimeout(t);
showAiError(e.name==='AbortError'?'Превышено время ожидания. Попробуйте снова.':'Ошибка: '+e.message);
@@ -1131,6 +1160,85 @@ function showAiResult(text){document.getElementById('ai-text').textContent=text;
function showAiError(msg){const el=document.getElementById('ai-error');if(msg){el.textContent=msg;el.classList.remove('hidden');}else el.classList.add('hidden');}
function restoreAiCache(){const c=sessionStorage.getItem('lastAiResponse');if(c)showAiResult(c);}
+// ═══════════════════ AI CACHE (git-based, shared for all users) ═══════════════════
+
+/** Load AI response from ai-cache.json stored in the git repo */
+async function loadAiCache() {
+ const dateEl = document.getElementById('ai-cache-date');
+ try {
+ const resp = await fetch(AI_CACHE_RAW, {cache:'no-cache'});
+ if (!resp.ok) { if(dateEl) dateEl.textContent = 'Кэш не найден'; return; }
+ const data = await resp.json();
+ if (!data.response) return;
+ AppState.lastAiResponse = data.response;
+ showAiResult(data.response);
+ if (dateEl && data.generated_at) {
+ const d = new Date(data.generated_at);
+ dateEl.textContent = 'Обновлено: ' + d.toLocaleDateString('ru-RU',{day:'numeric',month:'long',year:'numeric'});
+ }
+ } catch(e) {
+ if(dateEl) dateEl.textContent = 'Кэш недоступен';
+ console.info('AI cache not loaded:', e.message);
+ }
+}
+
+/** Save AI response to ai-cache.json in git via Gitea API.
+ * Requires window.GITEA_TOKEN (from config.js). */
+async function saveAiCache(text) {
+ const token = window.GITEA_TOKEN;
+ if (!token) return; // no write access — cache stays local only
+
+ const lastSnap = AppState.snapshots[AppState.snapshots.length-1];
+ const payload = {
+ generated_at: new Date().toISOString(),
+ period: periodLabel(lastSnap.report_period_id),
+ response: text,
+ };
+
+ // base64-encode the JSON (Gitea API requires base64 content)
+ const content = btoa(unescape(encodeURIComponent(JSON.stringify(payload, null, 2))));
+
+ // Fetch current file SHA (needed for update; absent for first create)
+ let sha = null;
+ try {
+ const fileResp = await fetch(
+ `${GITEA_API}/repos/${GITEA_OWNER}/${GITEA_REPO}/contents/${AI_CACHE_FILE}`,
+ {headers:{'Authorization':`token ${token}`}}
+ );
+ if (fileResp.ok) { sha = (await fileResp.json()).sha; }
+ } catch {}
+
+ const body = JSON.stringify({
+ message: `Update AI analysis cache — ${payload.period}`,
+ content,
+ ...(sha ? {sha} : {}),
+ });
+
+ try {
+ const saveResp = await fetch(
+ `${GITEA_API}/repos/${GITEA_OWNER}/${GITEA_REPO}/contents/${AI_CACHE_FILE}`,
+ {
+ method: 'PUT',
+ headers: {
+ 'Authorization': `token ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ body,
+ }
+ );
+ if (saveResp.ok) {
+ const dateEl = document.getElementById('ai-cache-date');
+ if (dateEl) dateEl.textContent = 'Обновлено: ' +
+ new Date().toLocaleDateString('ru-RU',{day:'numeric',month:'long',year:'numeric'});
+ console.info('AI cache saved to git ✓');
+ } else {
+ console.warn('AI cache save failed:', saveResp.status);
+ }
+ } catch(e) {
+ console.warn('AI cache save error:', e.message);
+ }
+}
+
// ═══════════════════ EVENTS ═══════════════════
document.getElementById('file-input').addEventListener('change',function(e){
const file=e.target.files[0]; if(!file)return;