Fastify + single-page frontend on shared @nexusai/core. Reads the same SQLite journal and media dir. Features: usage stats (by provider/capability/day), content gallery (image/audio/video with local file serving + path-traversal guard), provider CRUD with encrypted keys (masked in UI), enable/disable, set-default, and real key validation via zero-cost /media/models call. Adds Journal.getById and NordRouterMediaProvider.checkKey to core.
149 lines
7.5 KiB
HTML
149 lines
7.5 KiB
HTML
<!doctype html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>NexusAI Admin</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0f1115; --panel: #171a21; --panel2: #1f242e; --line: #2a2f3a;
|
|
--text: #e6e9ef; --muted: #9aa4b2; --accent: #4f8cff; --ok: #35c46a;
|
|
--warn: #f0b429; --err: #f26d6d;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; background: var(--bg); color: var(--text);
|
|
font: 14px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
|
|
header { padding: 14px 20px; border-bottom: 1px solid var(--line);
|
|
display: flex; align-items: center; gap: 16px; }
|
|
header h1 { font-size: 16px; margin: 0; font-weight: 600; }
|
|
.badge { font-size: 12px; color: var(--muted); }
|
|
nav { display: flex; gap: 6px; padding: 12px 20px 0; }
|
|
nav button { background: var(--panel); color: var(--muted); border: 1px solid var(--line);
|
|
padding: 8px 14px; border-radius: 8px 8px 0 0; cursor: pointer; }
|
|
nav button.active { background: var(--panel2); color: var(--text); border-bottom-color: var(--panel2); }
|
|
main { padding: 20px; }
|
|
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
|
|
padding: 16px; margin-bottom: 16px; }
|
|
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
|
|
.stat { background: var(--panel2); border: 1px solid var(--line); border-radius: 10px; padding: 14px; }
|
|
.stat .n { font-size: 22px; font-weight: 700; }
|
|
.stat .l { color: var(--muted); font-size: 12px; }
|
|
table { width: 100%; border-collapse: collapse; }
|
|
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line); font-size: 13px; }
|
|
th { color: var(--muted); font-weight: 500; }
|
|
input, select { background: var(--panel2); color: var(--text); border: 1px solid var(--line);
|
|
border-radius: 8px; padding: 8px 10px; font: inherit; width: 100%; }
|
|
label { display: block; font-size: 12px; color: var(--muted); margin: 8px 0 4px; }
|
|
button.act { background: var(--accent); color: #fff; border: none; border-radius: 8px;
|
|
padding: 8px 14px; cursor: pointer; font: inherit; }
|
|
button.ghost { background: transparent; color: var(--text); border: 1px solid var(--line);
|
|
border-radius: 8px; padding: 6px 10px; cursor: pointer; }
|
|
button.danger { color: var(--err); border-color: var(--err); }
|
|
.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: end; }
|
|
.row > div { flex: 1; min-width: 140px; }
|
|
.pill { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; }
|
|
.pill.on { background: rgba(53,196,106,.15); color: var(--ok); }
|
|
.pill.off { background: rgba(242,109,109,.15); color: var(--err); }
|
|
.pill.def { background: rgba(79,140,255,.15); color: var(--accent); }
|
|
.gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 14px; }
|
|
.tile { background: var(--panel2); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
|
|
.tile img { width: 100%; height: 150px; object-fit: cover; display: block; background: #000; }
|
|
.tile .meta { padding: 8px 10px; font-size: 12px; }
|
|
.tile .meta .m { color: var(--muted); }
|
|
.muted { color: var(--muted); }
|
|
.hidden { display: none; }
|
|
.toast { position: fixed; right: 16px; bottom: 16px; background: var(--panel2);
|
|
border: 1px solid var(--line); padding: 10px 14px; border-radius: 8px; }
|
|
audio { width: 100%; }
|
|
code { background: var(--panel2); padding: 1px 5px; border-radius: 4px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>NexusAI Admin</h1>
|
|
<span class="badge" id="cryptoBadge"></span>
|
|
</header>
|
|
<nav>
|
|
<button data-tab="usage" class="active">Потребление</button>
|
|
<button data-tab="gallery">Галерея</button>
|
|
<button data-tab="providers">Провайдеры</button>
|
|
</nav>
|
|
<main>
|
|
<section id="tab-usage">
|
|
<div class="card">
|
|
<div class="row" style="margin-bottom:12px">
|
|
<div style="max-width:200px">
|
|
<label>Период</label>
|
|
<select id="statsRange">
|
|
<option value="">Всё время</option>
|
|
<option value="1">1 день</option>
|
|
<option value="7" selected>7 дней</option>
|
|
<option value="30">30 дней</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="grid" id="statCards"></div>
|
|
</div>
|
|
<div class="card">
|
|
<h3 style="margin-top:0">По провайдерам</h3>
|
|
<table id="byProvider"><thead><tr><th>Провайдер</th><th>Кол-во</th><th>Стоимость, $</th></tr></thead><tbody></tbody></table>
|
|
</div>
|
|
<div class="card">
|
|
<h3 style="margin-top:0">По возможностям</h3>
|
|
<table id="byCapability"><thead><tr><th>Возможность</th><th>Кол-во</th><th>Стоимость, $</th></tr></thead><tbody></tbody></table>
|
|
</div>
|
|
<div class="card">
|
|
<h3 style="margin-top:0">По дням</h3>
|
|
<table id="byDay"><thead><tr><th>День</th><th>Кол-во</th><th>Стоимость, $</th></tr></thead><tbody></tbody></table>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="tab-gallery" class="hidden">
|
|
<div class="card">
|
|
<div class="row">
|
|
<div style="max-width:220px">
|
|
<label>Фильтр по возможности</label>
|
|
<select id="galleryFilter">
|
|
<option value="">Все</option>
|
|
<option value="image">image</option>
|
|
<option value="tts">tts</option>
|
|
<option value="music">music</option>
|
|
<option value="video">video</option>
|
|
<option value="stt">stt</option>
|
|
<option value="search">search</option>
|
|
</select>
|
|
</div>
|
|
<div style="max-width:120px"><label> </label><button class="ghost" id="galleryReload">Обновить</button></div>
|
|
</div>
|
|
</div>
|
|
<div class="gallery" id="gallery"></div>
|
|
</section>
|
|
|
|
<section id="tab-providers" class="hidden">
|
|
<div class="card">
|
|
<h3 style="margin-top:0">Добавить / обновить провайдера</h3>
|
|
<div class="row">
|
|
<div><label>Имя</label><input id="pName" placeholder="nordrouter" /></div>
|
|
<div><label>Тип</label><select id="pType"></select></div>
|
|
<div><label>Base URL (опц.)</label><input id="pBaseUrl" placeholder="https://nordrouter.com" /></div>
|
|
</div>
|
|
<div class="row">
|
|
<div><label>API ключ (опц., шифруется)</label><input id="pKey" type="password" placeholder="sk-..." /></div>
|
|
<div style="max-width:140px"><label>По умолчанию</label><select id="pDefault"><option value="false">Нет</option><option value="true">Да</option></select></div>
|
|
<div style="max-width:140px"><label>Включён</label><select id="pEnabled"><option value="true">Да</option><option value="false">Нет</option></select></div>
|
|
</div>
|
|
<div style="margin-top:12px"><button class="act" id="pSave">Сохранить</button></div>
|
|
</div>
|
|
<div class="card">
|
|
<h3 style="margin-top:0">Провайдеры</h3>
|
|
<table id="providersTable">
|
|
<thead><tr><th>Имя</th><th>Тип</th><th>Ключ</th><th>Статус</th><th>Действия</th></tr></thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
<script src="app.js"></script>
|
|
</body>
|
|
</html>
|