Add admin panel web UI (Svelte 5)

Dashboard with launch stats, instance manager with Modrinth/CurseForge
search and zip import, player and group management, launcher design
editor with live preview, and sign-in settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ARcGWxLx21FwXJ3yfGriS
This commit is contained in:
Claude committed 2026-09-28 04:32:15 +00:00
1 parent 8cdf616a94
commit af9147ec75
32 files changed
+4084

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
<script lang="ts">
import { LogIn, LoaderCircle } from '@lucide/svelte';
import { post } from '../lib/api';
import { setToken } from '../lib/session.svelte';
let { brandName, logo }: { brandName: string; logo: string | null } = $props();
let username = $state('');
let password = $state('');
let error = $state('');
let busy = $state(false);
async function submit(e: SubmitEvent) {
e.preventDefault();
busy = true;
error = '';
try {
const r = await post<{ token: string; user: { role: string } }>('/api/v1/auth/login', { username, password });
if (r.user.role !== 'admin') throw new Error('This account is not an administrator.');
setToken(r.token);
} catch (err) {
error = err instanceof Error ? err.message : String(err);
} finally {
busy = false;
}
}
</script>
<div class="wrap">
<div class="orb a"></div>
<div class="orb b"></div>
<form class="card login" onsubmit={submit}>
<img src={logo ?? '/favicon.svg'} alt="" class="logo" />
<h1>{brandName}</h1>
<p class="muted">Sign in to manage your launcher</p>
<label class="field">Username<input bind:value={username} autocomplete="username" required /></label>
<label class="field">Password<input type="password" bind:value={password} autocomplete="current-password" required /></label>
{#if error}<div class="error">{error}</div>{/if}
<button class="primary big" disabled={busy}>
{#if busy}<LoaderCircle class="spin" size={18} />{:else}<LogIn size={18} />{/if}
Sign in
</button>
<p class="tiny muted hint">First start? The admin password is in the container logs (or set <code>ADMIN_PASSWORD</code>).</p>
</form>
</div>
<style>
.wrap { min-height: 100vh; display: grid; place-items: center; padding: 20px; position: relative; overflow: hidden; }
.orb { position: absolute; width: 520px; height: 520px; border-radius: 50%; filter: blur(90px); opacity: 0.35; animation: drift 14s ease-in-out infinite alternate; }
.orb.a { background: var(--accent); top: -160px; right: -120px; }
.orb.b { background: var(--accent-2); bottom: -200px; left: -160px; opacity: 0.2; animation-delay: -7s; }
@keyframes drift { to { transform: translate(-60px, 40px) scale(1.1); } }
.login { width: 100%; max-width: 400px; display: flex; flex-direction: column; gap: 16px; padding: 36px; position: relative; backdrop-filter: blur(20px); background: rgba(18, 20, 31, 0.8); box-shadow: var(--shadow); animation: fade-in 0.4s ease; }
.logo { width: 56px; height: 56px; border-radius: 16px; margin-bottom: 4px; object-fit: cover; }
h1 { font-size: 1.6rem; }
.muted { margin-top: -10px; }
.big { padding: 12px; margin-top: 6px; }
.error { background: rgba(244, 63, 94, 0.1); border: 1px solid rgba(244, 63, 94, 0.3); color: #fda4af; padding: 10px 12px; border-radius: 10px; font-size: 0.88rem; }
.hint { text-align: center; margin-top: 0; line-height: 1.5; }
</style>