Files
suno-mcp-server/NexusAI/packages/admin/public/index.html
T
OpenCode 1bc3746594 Admin UX: light/dark theme, gallery pagination, content modal + delete, openai type
- Light/dark theme toggle persisted in localStorage
- Gallery pagination: page size 50/100/150/200 with prev/next
- Fullscreen modal viewer (image/video/audio) with metadata and delete
- Delete content: DELETE /api/generations/:id removes file (media-dir guarded)
  and journal record; added Journal.deleteById
- Provider types now include openai
- HTML output escaping in UI
2026-08-17 18:13:36 +07:00

196 lines
10 KiB
HTML

<!doctype html>
<html lang="ru" data-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>NexusAI Admin</title>
<style>
:root[data-theme="dark"] {
--bg: #0f1115; --panel: #171a21; --panel2: #1f242e; --line: #2a2f3a;
--text: #e6e9ef; --muted: #9aa4b2; --accent: #4f8cff; --ok: #35c46a;
--warn: #f0b429; --err: #f26d6d; --shadow: rgba(0,0,0,.5);
}
:root[data-theme="light"] {
--bg: #f4f6fa; --panel: #ffffff; --panel2: #f0f2f7; --line: #dce1ea;
--text: #1a1f2b; --muted: #5c6675; --accent: #2f6fed; --ok: #1a9e51;
--warn: #b3810f; --err: #d63d3d; --shadow: rgba(0,0,0,.18);
}
* { 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;
transition: background .15s, color .15s; }
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); }
header .spacer { flex: 1; }
#themeToggle { background: var(--panel2); border: 1px solid var(--line); color: var(--text);
border-radius: 8px; padding: 6px 12px; cursor: pointer; font: inherit; }
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); }
button:disabled { opacity: .45; cursor: not-allowed; }
.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; position: relative; }
.tile .thumb { cursor: pointer; }
.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); }
.tile .del { position: absolute; top: 6px; right: 6px; background: var(--panel);
border: 1px solid var(--line); color: var(--err); border-radius: 6px; padding: 2px 8px;
cursor: pointer; font-size: 12px; opacity: .85; }
.tile .del:hover { opacity: 1; }
.pager { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.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; box-shadow: 0 6px 24px var(--shadow); }
audio { width: 100%; }
code { background: var(--panel2); padding: 1px 5px; border-radius: 4px; }
/* Modal */
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.7);
display: flex; align-items: center; justify-content: center; padding: 24px; z-index: 50; }
.modal { background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
max-width: 92vw; max-height: 92vh; overflow: auto; box-shadow: 0 20px 60px var(--shadow); }
.modal-head { display: flex; align-items: center; gap: 12px; padding: 12px 16px;
border-bottom: 1px solid var(--line); position: sticky; top: 0; background: var(--panel); }
.modal-head .spacer { flex: 1; }
.modal-body { padding: 16px; }
.modal-body img, .modal-body video { max-width: 88vw; max-height: 72vh; display: block; margin: 0 auto; }
.modal-body audio { width: 60vw; max-width: 600px; }
.kv { color: var(--muted); font-size: 12px; margin-top: 10px; word-break: break-all; }
</style>
</head>
<body>
<header>
<h1>NexusAI Admin</h1>
<span class="badge" id="cryptoBadge"></span>
<span class="spacer"></span>
<button id="themeToggle" title="Переключить тему">🌙 Тёмная</button>
</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:180px">
<label>Элементов на странице</label>
<select id="pageSize">
<option value="50" selected>50</option>
<option value="100">100</option>
<option value="150">150</option>
<option value="200">200</option>
</select>
</div>
<div style="max-width:120px"><label>&nbsp;</label><button class="ghost" id="galleryReload">Обновить</button></div>
</div>
<div class="pager" style="margin-top:12px">
<button class="ghost" id="prevPage">← Назад</button>
<span class="muted" id="pageInfo">Страница 1</span>
<button class="ghost" id="nextPage">Вперёд →</button>
</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>
<div id="modalRoot"></div>
<script src="app.js"></script>
</body>
</html>