feat: add admin panel with project and tag management

- Full CRUD interface for projects with GitHub integration and tagging
- Real-time event log with expandable metadata viewer
- Reusable component library (Badge, Button, Input, Modal, Table,
TagPicker)
- Server-side API client with Unix socket and HTTP support
- JWT-based authentication with Svelte 5 reactive stores
- Settings management for social links and site identity
- Remove /admin path from tarpit to allow legitimate access
This commit is contained in:
2026-01-06 10:07:30 -06:00
parent 045781f7a5
commit 16bf2b76f3
32 changed files with 3260 additions and 60 deletions
@@ -0,0 +1,86 @@
<script lang="ts">
import { page } from "$app/stores";
import { goto } from "$app/navigation";
import ProjectForm from "$lib/components/admin/ProjectForm.svelte";
import { getAdminProject, getAdminTags, updateAdminProject } from "$lib/api";
import type { AdminProject, AdminTag, AdminTagWithCount, CreateProjectData, UpdateProjectData } from "$lib/admin-types";
const projectId = $derived(($page.params as { id: string }).id);
let project = $state<AdminProject | null>(null);
let tags = $state<AdminTag[]>([]);
let loading = $state(true);
async function loadData() {
try {
const [projectData, tagsWithCounts] = await Promise.all([
getAdminProject(projectId),
getAdminTags(),
]);
project = projectData;
tags = tagsWithCounts.map((t: AdminTagWithCount): AdminTag => ({
id: t.id,
slug: t.slug,
name: t.name,
createdAt: t.createdAt
}));
} catch (error) {
console.error("Failed to load data:", error);
alert("Failed to load project");
goto("/admin/projects");
} finally {
loading = false;
}
}
$effect(() => {
loadData();
});
async function handleSubmit(data: CreateProjectData) {
const updateData: UpdateProjectData = {
...data,
id: projectId,
};
await updateAdminProject(updateData);
goto("/admin/projects");
}
</script>
<svelte:head>
<title>Edit Project | Admin</title>
</svelte:head>
<div class="max-w-3xl space-y-6">
<!-- Header -->
<div>
<h1 class="text-2xl font-bold text-admin-text">Edit Project</h1>
<p class="mt-1 text-sm text-admin-text-muted">
Update project details and settings
</p>
</div>
<!-- Form -->
{#if loading}
<div class="text-center py-12 text-admin-text-muted">
Loading...
</div>
{:else if !project}
<div class="text-center py-12">
<p class="text-admin-text-muted mb-4">Project not found</p>
<a href="/admin/projects" class="text-blue-400 hover:text-blue-300">
← Back to projects
</a>
</div>
{:else}
<div class="rounded-lg border border-admin-border bg-admin-panel p-6">
<ProjectForm
{project}
availableTags={tags}
onsubmit={handleSubmit}
submitLabel="Update Project"
/>
</div>
{/if}
</div>