Skip to content

Commit 36611b5

Browse files
jonesXYZclaude
andcommitted
feat(admin): web admin dashboard (logs, storage, build download, fxmanifest editor)
Browser dashboard at /admin gated on ATELIER_ADMIN_DISCORD_IDS via a separate Discord web login (HttpOnly session cookie, re-checked per request). Overview (storage + counts), live server logs (SSE) + activity audit, server-build trigger + ZIP download, per-pack fxmanifest/resource-name override (opt-in; default build stays byte-identical), user approve/lock. Security-reviewed: CSP/X-Frame-Options, SameSite+origin CSRF guard, timingSafeEqual nonce. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d17c365 commit 36611b5

14 files changed

Lines changed: 1841 additions & 7 deletions

File tree

README.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,13 +256,37 @@ curl http://127.0.0.1:3095/api/v1/internal/ping -H "x-fg-service-token: <ATELIER
256256
2. Links **OAuth2** öffnen.
257257
3. **Client ID** kopieren → `ATELIER_DISCORD_CLIENT_ID`.
258258
4. **Reset Secret****Client Secret** kopieren → `ATELIER_DISCORD_CLIENT_SECRET`.
259-
5. Unter **Redirects** exakt eintragen:
260-
`{ATELIER_PUBLIC_ORIGIN}/api/v1/auth/discord/callback`
261-
(lokal also `http://127.0.0.1:3095/api/v1/auth/discord/callback`).
259+
5. Unter **Redirects** exakt eintragen (BEIDE):
260+
- `{ATELIER_PUBLIC_ORIGIN}/api/v1/auth/discord/callback` — Desktop-App-Login
261+
- `{ATELIER_PUBLIC_ORIGIN}/admin/callback` — Web-Admin-Dashboard
262+
(lokal also `http://127.0.0.1:3095/api/v1/auth/discord/callback`
263+
bzw. `http://127.0.0.1:3095/admin/callback`).
262264
6. Scope `identify` reicht — wird vom Service automatisch angefragt.
263265
7. `ATELIER_DEV_FAKE_AUTH=0` setzen (sobald echte Creds da sind, deaktiviert
264266
sich der Fake-Mode auch von selbst).
265267

268+
## Admin-Dashboard (Web)
269+
270+
Browser-Dashboard unter **`{ATELIER_PUBLIC_ORIGIN}/admin`** — Login nur fuer
271+
Discord-IDs aus `ATELIER_ADMIN_DISCORD_IDS` (eigener Discord-Web-Login getrennt
272+
vom Desktop-Loopback-Flow; signiertes HttpOnly-Session-Cookie, 12 h, Admin-Check
273+
bei jedem Request). Bietet:
274+
275+
- **Uebersicht** — Speichergroesse (CAS/Builds/tmp) + Kennzahlen (Assets, Packs,
276+
Revisionen, Builds, Nutzer).
277+
- **Logs** — Live-Server-Logs (SSE) + Aktivitaets-Audit (`atelierActivity`).
278+
- **Packs & Builds** — Server-Build pro Revision erzeugen/neu bauen, fertige
279+
Pakete als **ZIP** herunterladen.
280+
- **fxmanifest & Build-Config** — pro Pack ein Resource-Name- und
281+
`fxmanifest.lua`-Template-Override (Platzhalter `{{files}}` / `{{data_files}}`);
282+
betrifft nur Server-Builds und greift beim naechsten Build. Ohne Override bleibt
283+
das Manifest byte-identisch zum Desktop-Build.
284+
- **Nutzer** — freischalten / sperren.
285+
286+
Voraussetzung: echte Discord-Creds + die `/admin/callback`-Redirect-URI (siehe
287+
oben). Lokal mit Fake-Auth loggt `/admin/login` direkt als
288+
`ATELIER_DEV_FAKE_DISCORD_ID` ein (muss in `ATELIER_ADMIN_DISCORD_IDS` stehen).
289+
266290
## Docker & CI
267291

268292
- **Docker:** `docker build -t atelier-api .` — Image auf `oven/bun:1`,

assets/admin/app.css

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
/* atelier-api — Admin-Dashboard. Dark glass UI in the atelier design language
2+
* (#0b0b0b, Blurple #5865F2/#7289DA, Sora). Served as a static asset. */
3+
4+
:root {
5+
--bg: #0b0b0b;
6+
--panel: rgba(255, 255, 255, 0.03);
7+
--panel-2: rgba(0, 0, 0, 0.55);
8+
--border: rgba(255, 255, 255, 0.1);
9+
--border-strong: rgba(255, 255, 255, 0.16);
10+
--blurple: #5865f2;
11+
--blurple-soft: #7289da;
12+
--blurple-hover: #4752c4;
13+
--txt: #fff;
14+
--txt-2: rgba(255, 255, 255, 0.55);
15+
--txt-3: rgba(255, 255, 255, 0.38);
16+
--ok: #4ade80;
17+
--warn: #fbbf24;
18+
--err: #f87171;
19+
--radius: 16px;
20+
}
21+
22+
* { margin: 0; padding: 0; box-sizing: border-box; }
23+
24+
html, body { height: 100%; }
25+
26+
body {
27+
background: var(--bg);
28+
color: var(--txt);
29+
font-family: "Sora", "Segoe UI", system-ui, -apple-system, sans-serif;
30+
-webkit-font-smoothing: antialiased;
31+
font-size: 14px;
32+
}
33+
34+
a { color: var(--blurple-soft); text-decoration: none; }
35+
a:hover { color: #fff; }
36+
37+
::-webkit-scrollbar { width: 8px; height: 8px; }
38+
::-webkit-scrollbar-track { background: transparent; }
39+
::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.12); border-radius: 8px; }
40+
::-webkit-scrollbar-thumb:hover { background: var(--blurple-soft); }
41+
42+
/* --------------------------------------------------------------- layout */
43+
.app {
44+
display: grid;
45+
grid-template-columns: 248px 1fr;
46+
min-height: 100vh;
47+
}
48+
49+
.sidebar {
50+
position: sticky;
51+
top: 0;
52+
align-self: start;
53+
height: 100vh;
54+
display: flex;
55+
flex-direction: column;
56+
gap: 6px;
57+
padding: 22px 16px;
58+
background: rgba(0, 0, 0, 0.45);
59+
backdrop-filter: blur(20px) saturate(180%);
60+
-webkit-backdrop-filter: blur(20px) saturate(180%);
61+
border-right: 1px solid var(--border);
62+
}
63+
64+
.brand { display: flex; align-items: center; gap: 11px; padding: 4px 8px 18px; }
65+
.brand img { width: 30px; height: 30px; }
66+
.brand b { font-size: 17px; font-weight: 600; letter-spacing: -0.02em; }
67+
.brand span { font-size: 11px; color: var(--blurple-soft); font-weight: 500; }
68+
69+
.nav { display: flex; flex-direction: column; gap: 3px; }
70+
.nav-item {
71+
display: flex; align-items: center; gap: 11px;
72+
padding: 9px 12px; border-radius: 11px;
73+
color: var(--txt-2); font-weight: 500; font-size: 13.5px;
74+
cursor: pointer; transition: all 0.15s ease; border: 1px solid transparent;
75+
}
76+
.nav-item:hover { background: rgba(255, 255, 255, 0.04); color: #fff; }
77+
.nav-item.active {
78+
background: rgba(88, 101, 242, 0.14);
79+
color: #fff; border-color: rgba(88, 101, 242, 0.3);
80+
}
81+
.nav-item svg { width: 17px; height: 17px; flex-shrink: 0; opacity: 0.9; }
82+
83+
.sidebar-foot { margin-top: auto; padding-top: 14px; border-top: 1px solid var(--border); }
84+
.who { display: flex; align-items: center; gap: 10px; padding: 6px 8px; }
85+
.who .avatar { width: 30px; height: 30px; border-radius: 50%; background: var(--blurple); object-fit: cover; }
86+
.who .meta { min-width: 0; }
87+
.who .name { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
88+
.who .role { font-size: 10.5px; color: var(--txt-3); }
89+
90+
/* --------------------------------------------------------------- main */
91+
.main { min-width: 0; display: flex; flex-direction: column; }
92+
.topbar {
93+
position: sticky; top: 0; z-index: 5;
94+
display: flex; align-items: center; justify-content: space-between;
95+
padding: 18px 28px;
96+
background: rgba(11, 11, 11, 0.7);
97+
backdrop-filter: blur(12px);
98+
border-bottom: 1px solid var(--border);
99+
}
100+
.topbar h1 { font-size: 18px; font-weight: 600; letter-spacing: -0.01em; }
101+
.topbar .sub { font-size: 12px; color: var(--txt-3); margin-top: 2px; }
102+
.content { padding: 28px; max-width: 1180px; width: 100%; }
103+
104+
/* --------------------------------------------------------------- cards */
105+
.card {
106+
background: var(--panel-2);
107+
border: 1px solid var(--border);
108+
border-radius: var(--radius);
109+
padding: 20px 22px;
110+
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
111+
}
112+
.card + .card { margin-top: 18px; }
113+
.card-h { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
114+
.card-h h2 { font-size: 15px; font-weight: 600; }
115+
.card-h .hint { font-size: 12px; color: var(--txt-3); }
116+
117+
.grid { display: grid; gap: 18px; }
118+
.grid.cols-2 { grid-template-columns: repeat(2, 1fr); }
119+
.grid.cols-4 { grid-template-columns: repeat(4, 1fr); }
120+
121+
/* stat tiles */
122+
.stat {
123+
background: var(--panel);
124+
border: 1px solid var(--border);
125+
border-radius: 14px;
126+
padding: 18px;
127+
position: relative; overflow: hidden;
128+
}
129+
.stat .label { font-size: 11.5px; color: var(--txt-3); text-transform: uppercase; letter-spacing: 0.08em; }
130+
.stat .value { font-size: 28px; font-weight: 600; letter-spacing: -0.02em; margin-top: 6px; }
131+
.stat .unit { font-size: 13px; color: var(--txt-2); font-weight: 500; margin-left: 3px; }
132+
.stat .extra { font-size: 12px; color: var(--txt-3); margin-top: 4px; }
133+
.stat .glow { position: absolute; top: -30px; right: -30px; width: 90px; height: 90px; border-radius: 50%;
134+
background: var(--blurple); opacity: 0.16; filter: blur(28px); pointer-events: none; }
135+
136+
/* --------------------------------------------------------------- buttons */
137+
.btn {
138+
display: inline-flex; align-items: center; gap: 7px;
139+
padding: 9px 15px; border-radius: 10px;
140+
font-family: inherit; font-size: 13px; font-weight: 500;
141+
border: 1px solid var(--border-strong); background: rgba(255, 255, 255, 0.05);
142+
color: #fff; cursor: pointer; transition: all 0.15s ease; white-space: nowrap;
143+
}
144+
.btn:hover { background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.25); }
145+
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
146+
.btn-primary { background: var(--blurple); border-color: var(--blurple); }
147+
.btn-primary:hover { background: var(--blurple-hover); border-color: var(--blurple-hover); }
148+
.btn-sm { padding: 6px 11px; font-size: 12px; border-radius: 8px; }
149+
.btn svg { width: 15px; height: 15px; }
150+
151+
/* --------------------------------------------------------------- table */
152+
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
153+
.table th {
154+
text-align: left; padding: 9px 12px; font-size: 11px; font-weight: 600;
155+
text-transform: uppercase; letter-spacing: 0.07em; color: var(--txt-3);
156+
border-bottom: 1px solid var(--border);
157+
}
158+
.table td { padding: 11px 12px; border-bottom: 1px solid rgba(255, 255, 255, 0.05); vertical-align: middle; }
159+
.table tr:last-child td { border-bottom: 0; }
160+
.table tr:hover td { background: rgba(255, 255, 255, 0.02); }
161+
.mono { font-family: ui-monospace, "Cascadia Code", Consolas, monospace; font-size: 12px; }
162+
.muted { color: var(--txt-3); }
163+
164+
/* badges */
165+
.badge {
166+
display: inline-flex; align-items: center; gap: 5px;
167+
padding: 3px 9px; border-radius: 999px; font-size: 11px; font-weight: 600;
168+
border: 1px solid var(--border);
169+
}
170+
.badge .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
171+
.badge-done { color: var(--ok); background: rgba(74, 222, 128, 0.1); border-color: rgba(74, 222, 128, 0.25); }
172+
.badge-error { color: var(--err); background: rgba(248, 113, 113, 0.1); border-color: rgba(248, 113, 113, 0.25); }
173+
.badge-running { color: var(--blurple-soft); background: rgba(114, 137, 218, 0.12); border-color: rgba(114, 137, 218, 0.3); }
174+
.badge-queued { color: var(--warn); background: rgba(251, 191, 36, 0.1); border-color: rgba(251, 191, 36, 0.25); }
175+
176+
/* --------------------------------------------------------------- forms */
177+
.field { margin-bottom: 16px; }
178+
.field label { display: block; font-size: 12.5px; font-weight: 500; margin-bottom: 7px; color: var(--txt-2); }
179+
.field .desc { font-size: 11.5px; color: var(--txt-3); margin-top: 6px; line-height: 1.5; }
180+
input[type="text"], textarea, select {
181+
width: 100%; padding: 10px 12px; border-radius: 10px;
182+
background: rgba(0, 0, 0, 0.4); border: 1px solid var(--border-strong);
183+
color: #fff; font-family: inherit; font-size: 13px; outline: none;
184+
transition: border-color 0.15s ease;
185+
}
186+
input[type="text"]:focus, textarea:focus, select:focus { border-color: var(--blurple); }
187+
textarea { resize: vertical; min-height: 220px; line-height: 1.55;
188+
font-family: ui-monospace, "Cascadia Code", Consolas, monospace; font-size: 12.5px; }
189+
190+
/* --------------------------------------------------------------- logs */
191+
.logbox {
192+
background: #060606; border: 1px solid var(--border); border-radius: 12px;
193+
padding: 12px 14px; height: 460px; overflow-y: auto;
194+
font-family: ui-monospace, "Cascadia Code", Consolas, monospace; font-size: 12px; line-height: 1.7;
195+
}
196+
.log-line { display: flex; gap: 10px; white-space: pre-wrap; word-break: break-word; }
197+
.log-line .t { color: var(--txt-3); flex-shrink: 0; }
198+
.log-line .s { color: var(--blurple-soft); flex-shrink: 0; }
199+
.log-line.error .m { color: var(--err); }
200+
.log-line.warn .m { color: var(--warn); }
201+
.log-line.info .m { color: rgba(255, 255, 255, 0.8); }
202+
203+
/* tabs */
204+
.tabs { display: flex; gap: 6px; margin-bottom: 16px; }
205+
.tab { padding: 7px 14px; border-radius: 9px; font-size: 13px; font-weight: 500; color: var(--txt-2);
206+
cursor: pointer; border: 1px solid transparent; }
207+
.tab:hover { background: rgba(255, 255, 255, 0.04); color: #fff; }
208+
.tab.active { background: rgba(255, 255, 255, 0.07); color: #fff; border-color: var(--border); }
209+
210+
/* misc */
211+
.row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
212+
.spread { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
213+
.empty { text-align: center; color: var(--txt-3); padding: 40px 20px; font-size: 13px; }
214+
.spinner { width: 16px; height: 16px; border-radius: 50%; border: 2px solid rgba(255, 255, 255, 0.15);
215+
border-top-color: var(--blurple); animation: spin 0.8s linear infinite; display: inline-block; }
216+
@keyframes spin { to { transform: rotate(360deg); } }
217+
218+
/* toast */
219+
.toasts { position: fixed; bottom: 22px; right: 22px; display: flex; flex-direction: column; gap: 10px; z-index: 100; }
220+
.toast {
221+
padding: 12px 16px; border-radius: 11px; font-size: 13px; font-weight: 500;
222+
background: var(--panel-2); border: 1px solid var(--border-strong);
223+
backdrop-filter: blur(20px); box-shadow: 0 14px 40px rgba(0, 0, 0, 0.5);
224+
animation: rise 0.25s ease-out; max-width: 340px;
225+
}
226+
.toast.ok { border-color: rgba(74, 222, 128, 0.35); }
227+
.toast.err { border-color: rgba(248, 113, 113, 0.4); }
228+
@keyframes rise { from { opacity: 0; transform: translateY(10px); } }
229+
230+
.hide { display: none !important; }
231+
232+
/* back link in detail view */
233+
.back { display: inline-flex; align-items: center; gap: 6px; color: var(--txt-2);
234+
font-size: 13px; cursor: pointer; margin-bottom: 16px; }
235+
.back:hover { color: #fff; }
236+
237+
@media (max-width: 900px) {
238+
.app { grid-template-columns: 1fr; }
239+
.sidebar { position: static; height: auto; flex-direction: row; flex-wrap: wrap; align-items: center; }
240+
.sidebar .nav { flex-direction: row; flex-wrap: wrap; }
241+
.sidebar-foot { margin: 0 0 0 auto; padding: 0; border: 0; }
242+
.grid.cols-4 { grid-template-columns: repeat(2, 1fr); }
243+
.grid.cols-2 { grid-template-columns: 1fr; }
244+
.content { padding: 18px; }
245+
}

0 commit comments

Comments
 (0)