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
This commit is contained in:
OpenCode
2026-08-17 18:13:36 +07:00
parent 6ff2b27556
commit 1bc3746594
5 changed files with 216 additions and 31 deletions
+53 -6
View File
@@ -1,22 +1,31 @@
<!doctype html>
<html lang="ru">
<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 {
:root[data-theme="dark"] {
--bg: #0f1115; --panel: #171a21; --panel2: #1f242e; --line: #2a2f3a;
--text: #e6e9ef; --muted: #9aa4b2; --accent: #4f8cff; --ok: #35c46a;
--warn: #f0b429; --err: #f26d6d;
--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; }
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; }
@@ -39,6 +48,7 @@
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; }
@@ -46,22 +56,43 @@
.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 { 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; }
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>
@@ -113,8 +144,22 @@
<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>
@@ -143,6 +188,8 @@
</div>
</section>
</main>
<div id="modalRoot"></div>
<script src="app.js"></script>
</body>
</html>