feat(TRUEREF-0015): implement web UI repository dashboard

- Repository list with state badges, stats, and action buttons
- Add repository modal for GitHub URLs and local paths
- Live indexing progress bar polling every 2s
- Confirm dialog for destructive actions
- Repository detail page with versions and recent jobs
- Settings page placeholder

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Giancarmine Salucci
2026-03-23 09:07:06 +01:00
parent 542f4ce66c
commit 90d93786a8
11 changed files with 1254 additions and 4 deletions

View File

@@ -1,2 +1,179 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
<script lang="ts">
import type { PageData } from './$types';
import type { Repository } from '$lib/types';
import RepositoryCard from '$lib/components/RepositoryCard.svelte';
import AddRepositoryModal from '$lib/components/AddRepositoryModal.svelte';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import IndexingProgress from '$lib/components/IndexingProgress.svelte';
let { data }: { data: PageData } = $props();
// Local mutable copy; refreshRepositories() keeps it up to date after mutations.
// Intentionally captures initial value from server load — mutations happen via fetch.
let repositories = $state<Repository[]>(data.repositories ?? []); // svelte-disable state_referenced_locally
let showAddModal = $state(false);
let confirmDeleteId = $state<string | null>(null);
let activeJobIds = $state<Record<string, string>>({});
let errorMessage = $state<string | null>(null);
const confirmDeleteRepo = $derived(
confirmDeleteId ? repositories.find((r) => r.id === confirmDeleteId) : null
);
async function refreshRepositories() {
try {
const res = await fetch('/api/v1/libs');
const fetched = await res.json();
repositories = fetched.libraries ?? [];
} catch {
// keep existing list on network error
}
}
async function handleReindex(id: string) {
errorMessage = null;
try {
const res = await fetch(`/api/v1/libs/${encodeURIComponent(id)}/index`, {
method: 'POST'
});
if (!res.ok) {
const fetched = await res.json();
throw new Error(fetched.error ?? 'Failed to trigger re-indexing');
}
const fetched = await res.json();
if (fetched.job?.id) {
activeJobIds = { ...activeJobIds, [id]: fetched.job.id };
}
await refreshRepositories();
} catch (e) {
errorMessage = (e as Error).message;
}
}
function handleDeleteRequest(id: string) {
confirmDeleteId = id;
}
async function handleDeleteConfirm() {
if (!confirmDeleteId) return;
const id = confirmDeleteId;
confirmDeleteId = null;
errorMessage = null;
try {
const res = await fetch(`/api/v1/libs/${encodeURIComponent(id)}`, {
method: 'DELETE'
});
if (!res.ok && res.status !== 204) {
const fetched = await res.json();
throw new Error(fetched.error ?? 'Failed to delete repository');
}
repositories = repositories.filter((r) => r.id !== id);
const updated = { ...activeJobIds };
delete updated[id];
activeJobIds = updated;
} catch (e) {
errorMessage = (e as Error).message;
}
}
function handleDeleteCancel() {
confirmDeleteId = null;
}
async function handleRepoAdded() {
await refreshRepositories();
}
</script>
<svelte:head>
<title>Repositories — TrueRef</title>
</svelte:head>
<div class="flex items-center justify-between">
<div>
<h1 class="text-xl font-semibold text-gray-900">Repositories</h1>
<p class="mt-0.5 text-sm text-gray-500">
{repositories.length}
{repositories.length === 1 ? 'repository' : 'repositories'} indexed
</p>
</div>
<button
onclick={() => (showAddModal = true)}
class="flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
/>
</svg>
Add Repository
</button>
</div>
{#if errorMessage}
<div class="mt-4 rounded-lg bg-red-50 px-4 py-3">
<p class="text-sm text-red-700">{errorMessage}</p>
</div>
{/if}
{#if repositories.length === 0}
<div class="mt-16 flex flex-col items-center text-center">
<svg
class="h-16 w-16 text-gray-300"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
<h2 class="mt-4 text-lg font-medium text-gray-700">No repositories yet</h2>
<p class="mt-2 max-w-sm text-sm text-gray-500">
Add your first GitHub or local repository to start indexing documentation for AI-powered
retrieval.
</p>
<button
onclick={() => (showAddModal = true)}
class="mt-6 flex items-center gap-1.5 rounded-lg bg-blue-600 px-5 py-2.5 text-sm font-medium text-white hover:bg-blue-700"
>
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
/>
</svg>
Add Repository
</button>
</div>
{:else}
<div class="mt-6 grid gap-4 sm:grid-cols-1 lg:grid-cols-2">
{#each repositories as repo (repo.id)}
<div>
<RepositoryCard {repo} onReindex={handleReindex} onDelete={handleDeleteRequest} />
{#if activeJobIds[repo.id]}
<div class="px-5">
<IndexingProgress jobId={activeJobIds[repo.id]} />
</div>
{/if}
</div>
{/each}
</div>
{/if}
{#if showAddModal}
<AddRepositoryModal onClose={() => (showAddModal = false)} onAdded={handleRepoAdded} />
{/if}
{#if confirmDeleteId && confirmDeleteRepo}
<ConfirmDialog
title="Delete Repository"
message="Are you sure you want to delete {confirmDeleteRepo.title}? This will remove all indexed data and cannot be undone."
confirmLabel="Delete"
danger={true}
onConfirm={handleDeleteConfirm}
onCancel={handleDeleteCancel}
/>
{/if}