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

+80
View File
@@ -0,0 +1,80 @@
<script lang="ts">
import Sidebar from './components/Sidebar.svelte';
import Toasts from './components/Toasts.svelte';
import Login from './pages/Login.svelte';
import Dashboard from './pages/Dashboard.svelte';
import Instances from './pages/Instances.svelte';
import InstanceEditor from './pages/InstanceEditor.svelte';
import Users from './pages/Users.svelte';
import Branding from './pages/Branding.svelte';
import Settings from './pages/Settings.svelte';
import { get } from './lib/api';
import { route } from './lib/router.svelte';
import { logout, session, type Me } from './lib/session.svelte';
import { toast } from './lib/toast.svelte';
let brand = $state<{ name: string; logo_url: string | null }>({ name: 'ScopeNet', logo_url: null });
let checking = $state(!!session.token);
$effect(() => {
if (!session.token) {
checking = false;
return;
}
get<Me>('/api/v1/auth/me')
.then((me) => {
if (me.role !== 'admin') {
toast('That account is not an admin.', 'error');
logout();
} else session.user = me;
})
.catch(() => logout())
.finally(() => (checking = false));
});
$effect(() => {
get('/api/v1/launcher/manifest')
.then((m) => {
brand = m.branding;
document.title = `${m.branding.name} · Admin`;
})
.catch(() => {});
});
</script>
{#if checking}
<div class="boot"><span class="pulse"></span></div>
{:else if !session.user}
<Login brandName={brand.name} logo={brand.logo_url} />
{:else}
<div class="shell">
<Sidebar brandName={brand.name} logo={brand.logo_url} />
<main>
{#key route.name + route.params.join('/')}
{#if route.name === 'instances' && route.params[0]}
<InstanceEditor id={route.params[0]} />
{:else if route.name === 'instances'}
<Instances />
{:else if route.name === 'users'}
<Users />
{:else if route.name === 'branding'}
<Branding onsaved={(b) => (brand = b)} />
{:else if route.name === 'settings'}
<Settings />
{:else}
<Dashboard />
{/if}
{/key}
</main>
</div>
{/if}
<Toasts />
<style>
.shell { display: flex; min-height: 100vh; }
main { flex: 1; min-width: 0; }
.boot { height: 100vh; display: grid; place-items: center; }
.pulse { width: 42px; height: 42px; border-radius: 12px; background: linear-gradient(135deg, var(--accent), var(--accent-2)); animation: pulse 1s ease-in-out infinite alternate; }
@keyframes pulse { from { transform: scale(0.85); opacity: 0.6; } to { transform: scale(1); opacity: 1; } }
</style>