'use strict'; const $ = (sel) => document.querySelector(sel); const $$ = (sel) => Array.from(document.querySelectorAll(sel)); function toast(msg) { const el = document.createElement('div'); el.className = 'toast'; el.textContent = msg; document.body.appendChild(el); setTimeout(() => el.remove(), 2600); } async function api(path, opts) { const res = await fetch(path, { headers: { 'Content-Type': 'application/json' }, ...opts, }); const text = await res.text(); let data = null; try { data = text ? JSON.parse(text) : null; } catch { data = { raw: text }; } if (!res.ok) throw new Error((data && data.error) || `HTTP ${res.status}`); return data; } function usd(n) { return (n == null ? 0 : n).toFixed(4); } // --- Tabs --- $$('nav button').forEach((btn) => { btn.addEventListener('click', () => { $$('nav button').forEach((b) => b.classList.remove('active')); btn.classList.add('active'); $$('main section').forEach((s) => s.classList.add('hidden')); $(`#tab-${btn.dataset.tab}`).classList.remove('hidden'); if (btn.dataset.tab === 'usage') loadStats(); if (btn.dataset.tab === 'gallery') loadGallery(); if (btn.dataset.tab === 'providers') loadProviders(); }); }); // --- Usage --- async function loadStats() { const range = $('#statsRange').value; const q = range ? `?sinceDays=${range}` : ''; const s = await api(`/api/stats${q}`); $('#statCards').innerHTML = `
$${usd(s.totalCostUsd)}
Всего потрачено
${s.totalCount}
Всего операций
${s.byProvider.length}
Провайдеров
${s.byCapability.length}
Типов операций
`; const fill = (id, rows, key) => { $(`#${id} tbody`).innerHTML = rows.map((r) => `${r[key]}${r.count}$${usd(r.costUsd)}`).join('') || 'Нет данных'; }; fill('byProvider', s.byProvider, 'provider'); fill('byCapability', s.byCapability, 'capability'); fill('byDay', s.byDay, 'day'); } $('#statsRange').addEventListener('change', loadStats); // --- Gallery --- async function loadGallery() { const cap = $('#galleryFilter').value; const q = cap ? `?capability=${cap}&limit=60` : '?limit=60'; const { items } = await api(`/api/generations${q}`); $('#gallery').innerHTML = items.map(renderTile).join('') || '
Пока нет сгенерированного контента.
'; } function renderTile(it) { const cost = it.costUsd != null ? `$${usd(it.costUsd)}` : ''; let media = ''; if (it.mediaUrl) { if (it.capability === 'image') media = ``; else if (it.capability === 'tts' || it.capability === 'music') media = `
`; else if (it.capability === 'video') media = ``; else media = `Открыть файл`; } else { const status = it.status === 'failed' ? '⚠ ошибка' : 'нет локального файла'; media = `
${status}
`; } return `
${media}
${it.capability} · ${cost}
${it.model}
${new Date(it.createdAt).toLocaleString()}
`; } $('#galleryReload').addEventListener('click', loadGallery); $('#galleryFilter').addEventListener('change', loadGallery); // --- Providers --- let CRYPTO_ENABLED = true; async function loadProviders() { const { providers, knownTypes, cryptoEnabled } = await api('/api/providers'); CRYPTO_ENABLED = cryptoEnabled; $('#cryptoBadge').textContent = cryptoEnabled ? 'Шифрование ключей: включено' : '⚠ NEXUS_MASTER_KEY не задан — ключи из БД недоступны'; const typeSel = $('#pType'); if (!typeSel.options.length) { typeSel.innerHTML = knownTypes.map((t) => ``).join(''); } $('#providersTable tbody').innerHTML = providers.map((p) => { const status = [ p.isDefault ? 'default' : '', p.enabled ? 'включён' : 'выключен', ].join(' '); return ` ${p.name} ${p.type} ${p.hasKey ? `${p.keyHint || '••••'}` : 'нет'} ${status} `; }).join('') || 'Нет провайдеров. Добавьте выше.'; $$('[data-check]').forEach((b) => b.onclick = () => checkProvider(b.dataset.check)); $$('[data-toggle]').forEach((b) => b.onclick = () => toggleProvider(b.dataset.toggle, b.dataset.en !== 'true')); $$('[data-default]').forEach((b) => b.onclick = () => setDefault(b.dataset.default)); $$('[data-del]').forEach((b) => b.onclick = () => delProvider(b.dataset.del)); } $('#pSave').addEventListener('click', async () => { try { const body = { name: $('#pName').value.trim(), type: $('#pType').value, baseUrl: $('#pBaseUrl').value.trim() || undefined, apiKey: $('#pKey').value.trim() || undefined, isDefault: $('#pDefault').value === 'true', enabled: $('#pEnabled').value === 'true', }; if (!body.name) return toast('Укажите имя'); if (body.apiKey && !CRYPTO_ENABLED) return toast('Нельзя сохранить ключ без NEXUS_MASTER_KEY'); await api('/api/providers', { method: 'POST', body: JSON.stringify(body) }); $('#pKey').value = ''; toast('Сохранено'); loadProviders(); } catch (e) { toast('Ошибка: ' + e.message); } }); async function checkProvider(name) { try { toast('Проверяю ключ...'); const r = await api(`/api/providers/${encodeURIComponent(name)}/check`, { method: 'POST', body: '{}' }); toast(r.ok ? `OK · моделей: ${r.modelsCount}` : `Ошибка ключа: ${r.error}`); } catch (e) { toast('Ошибка: ' + e.message); } } async function toggleProvider(name, enabled) { try { await api(`/api/providers/${encodeURIComponent(name)}/enabled`, { method: 'POST', body: JSON.stringify({ enabled }) }); loadProviders(); } catch (e) { toast('Ошибка: ' + e.message); } } async function setDefault(name) { try { await api(`/api/providers/${encodeURIComponent(name)}/default`, { method: 'POST', body: '{}' }); loadProviders(); } catch (e) { toast('Ошибка: ' + e.message); } } async function delProvider(name) { if (!confirm(`Удалить провайдера "${name}"?`)) return; try { await api(`/api/providers/${encodeURIComponent(name)}`, { method: 'DELETE' }); loadProviders(); } catch (e) { toast('Ошибка: ' + e.message); } } // initial loadStats();