Files
haos-hacs-immich-album-watcher/frontend/src/routes/+layout.svelte
alexei.dolgolyov 87ce1bc5ec
Some checks failed
Validate / Hassfest (push) Has been cancelled
Add SvelteKit frontend with Tailwind CSS (Phase 4)
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>
2026-03-19 13:46:55 +03:00

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}