Some checks failed
Validate / Hassfest (push) Has been cancelled
Build a modern, calm web UI using SvelteKit 5 + Tailwind CSS v4. Pages: - Setup wizard (first-run admin account creation) - Login with JWT token management and auto-refresh - Dashboard with stats cards and recent events timeline - Servers: add/delete Immich server connections with validation - Trackers: create album trackers with album picker, event type selection, target assignment, and scan interval config - Templates: Jinja2 message template editor with live preview - Targets: Telegram and webhook notification targets with test - Users: admin-only user management (create/delete) Architecture: - Reactive auth state with Svelte 5 runes - API client with JWT auth, auto-refresh on 401 - Static adapter builds to 153KB for embedding in FastAPI - Vite proxy config for dev server -> backend API - Sidebar layout with navigation and user info Also adds Rule 2 to primary plan: perform detailed code review after completing each phase. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
91 lines
2.8 KiB
Svelte
91 lines
2.8 KiB
Svelte
<script lang="ts">
|
|
import '../app.css';
|
|
import { page } from '$app/state';
|
|
import { goto } from '$app/navigation';
|
|
import { onMount } from 'svelte';
|
|
import { getAuth, loadUser, logout } from '$lib/auth.svelte';
|
|
|
|
let { children } = $props();
|
|
const auth = getAuth();
|
|
|
|
const navItems = [
|
|
{ href: '/', label: 'Dashboard', icon: '⊞' },
|
|
{ href: '/servers', label: 'Servers', icon: '⬡' },
|
|
{ href: '/trackers', label: 'Trackers', icon: '◎' },
|
|
{ href: '/templates', label: 'Templates', icon: '⎘' },
|
|
{ href: '/targets', label: 'Targets', icon: '◇' },
|
|
];
|
|
|
|
const isAuthPage = $derived(
|
|
page.url.pathname === '/login' || page.url.pathname === '/setup'
|
|
);
|
|
|
|
onMount(async () => {
|
|
await loadUser();
|
|
if (!auth.user && !isAuthPage) {
|
|
goto('/login');
|
|
}
|
|
});
|
|
</script>
|
|
|
|
{#if isAuthPage || auth.loading}
|
|
{@render children()}
|
|
{:else if auth.user}
|
|
<div class="flex h-screen">
|
|
<!-- Sidebar -->
|
|
<aside class="w-56 border-r border-[var(--color-border)] bg-[var(--color-card)] flex flex-col">
|
|
<div class="p-4 border-b border-[var(--color-border)]">
|
|
<h1 class="text-base font-semibold tracking-tight">Immich Watcher</h1>
|
|
<p class="text-xs text-[var(--color-muted-foreground)] mt-0.5">Album notifications</p>
|
|
</div>
|
|
<nav class="flex-1 p-2 space-y-0.5">
|
|
{#each navItems as item}
|
|
<a
|
|
href={item.href}
|
|
class="flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors
|
|
{page.url.pathname === item.href
|
|
? 'bg-[var(--color-accent)] text-[var(--color-accent-foreground)] font-medium'
|
|
: 'text-[var(--color-muted-foreground)] hover:bg-[var(--color-accent)] hover:text-[var(--color-accent-foreground)]'}"
|
|
>
|
|
<span class="text-base">{item.icon}</span>
|
|
{item.label}
|
|
</a>
|
|
{/each}
|
|
{#if auth.isAdmin}
|
|
<a
|
|
href="/users"
|
|
class="flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors
|
|
{page.url.pathname === '/users'
|
|
? 'bg-[var(--color-accent)] text-[var(--color-accent-foreground)] font-medium'
|
|
: 'text-[var(--color-muted-foreground)] hover:bg-[var(--color-accent)] hover:text-[var(--color-accent-foreground)]'}"
|
|
>
|
|
<span class="text-base">⊕</span>
|
|
Users
|
|
</a>
|
|
{/if}
|
|
</nav>
|
|
<div class="p-3 border-t border-[var(--color-border)]">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm font-medium">{auth.user.username}</p>
|
|
<p class="text-xs text-[var(--color-muted-foreground)]">{auth.user.role}</p>
|
|
</div>
|
|
<button
|
|
onclick={logout}
|
|
class="text-xs text-[var(--color-muted-foreground)] hover:text-[var(--color-foreground)] transition-colors"
|
|
>
|
|
Logout
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main content -->
|
|
<main class="flex-1 overflow-auto">
|
|
<div class="max-w-5xl mx-auto p-6">
|
|
{@render children()}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
{/if}
|