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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user