Files
trueref/docs/features/TRUEREF-0015.md
2026-03-27 02:23:01 +01:00

8.3 KiB

TRUEREF-0015 — Web UI: Repository Dashboard

Priority: P1 Status: Pending Depends On: TRUEREF-0002, TRUEREF-0009 Blocks: TRUEREF-0016


Overview

Implement the main web interface for managing repositories. Built with SvelteKit and TailwindCSS v4. The dashboard lets users add repositories, view indexing status with live progress, trigger re-indexing, remove repositories, and view basic statistics.


Acceptance Criteria

  • Repository list page at / showing all repositories with status, snippet count, last indexed date
  • Add repository modal/form (GitHub URL or local path input)
  • Per-repository card with: title, description, state badge, stats, action buttons
  • Live indexing progress bar (polls GET /api/v1/jobs/:id every 2s while running)
  • Trigger re-index button
  • Delete repository (with confirmation dialog)
  • View indexed versions per repository
  • Error state display (show error message when state = error)
  • Empty state (no repositories yet) with clear call-to-action
  • Responsive layout (mobile + desktop)
  • No page reloads — all interactions via fetch with SvelteKit load functions

Page Structure

/ (root)
├── Layout: navbar with TrueRef logo + nav links
├── /
│   └── Repository list + add button
├── /repos/[id]
│   └── Repository detail: versions, recent jobs, config
└── /settings
    └── Embedding provider configuration

Repository Card Component

<!-- src/lib/components/RepositoryCard.svelte -->
<script lang="ts">
	import type { Repository } from '$lib/types';

	let { repo, onReindex, onDelete } = $props<{
		repo: Repository;
		onReindex: (id: string) => void;
		onDelete: (id: string) => void;
	}>();

	const stateColors = {
		pending: 'bg-gray-100 text-gray-600',
		indexing: 'bg-blue-100 text-blue-700',
		indexed: 'bg-green-100 text-green-700',
		error: 'bg-red-100 text-red-700'
	};

	const stateLabels = {
		pending: 'Pending',
		indexing: 'Indexing...',
		indexed: 'Indexed',
		error: 'Error'
	};
</script>

<div class="rounded-xl border border-gray-200 bg-white p-5 shadow-sm">
	<div class="flex items-start justify-between">
		<div>
			<h3 class="font-semibold text-gray-900">{repo.title}</h3>
			<p class="mt-0.5 font-mono text-sm text-gray-500">{repo.id}</p>
		</div>
		<span class="rounded-full px-2.5 py-0.5 text-xs font-medium {stateColors[repo.state]}">
			{stateLabels[repo.state]}
		</span>
	</div>

	{#if repo.description}
		<p class="mt-2 line-clamp-2 text-sm text-gray-600">{repo.description}</p>
	{/if}

	<div class="mt-4 flex gap-4 text-sm text-gray-500">
		<span>{repo.totalSnippets.toLocaleString()} snippets</span>
		<span>·</span>
		<span>Trust: {repo.trustScore.toFixed(1)}/10</span>
		{#if repo.stars}
			<span>·</span>
			<span>{repo.stars.toLocaleString()}</span>
		{/if}
	</div>

	{#if repo.state === 'error'}
		<p class="mt-2 text-xs text-red-600">Indexing failed. Check jobs for details.</p>
	{/if}

	<div class="mt-4 flex gap-2">
		<button
			onclick={() => onReindex(repo.id)}
			class="rounded-lg bg-blue-600 px-3 py-1.5 text-sm text-white hover:bg-blue-700"
			disabled={repo.state === 'indexing'}
		>
			{repo.state === 'indexing' ? 'Indexing...' : 'Re-index'}
		</button>
		<a
			href="/repos/{encodeURIComponent(repo.id)}"
			class="rounded-lg border border-gray-200 px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50"
		>
			Details
		</a>
		<button
			onclick={() => onDelete(repo.id)}
			class="ml-auto rounded-lg px-3 py-1.5 text-sm text-red-600 hover:bg-red-50"
		>
			Delete
		</button>
	</div>
</div>

Add Repository Modal

<!-- src/lib/components/AddRepositoryModal.svelte -->
<script lang="ts">
	let { onClose, onAdded } = $props<{
		onClose: () => void;
		onAdded: () => void;
	}>();

	let source = $state<'github' | 'local'>('github');
	let sourceUrl = $state('');
	let githubToken = $state('');
	let loading = $state(false);
	let error = $state<string | null>(null);

	async function handleSubmit() {
		loading = true;
		error = null;
		try {
			const res = await fetch('/api/v1/libs', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ source, sourceUrl, githubToken: githubToken || undefined })
			});
			if (!res.ok) {
				const data = await res.json();
				throw new Error(data.error ?? 'Failed to add repository');
			}
			onAdded();
			onClose();
		} catch (e) {
			error = (e as Error).message;
		} finally {
			loading = false;
		}
	}
</script>

<dialog class="modal" open>
	<div class="modal-box max-w-md">
		<h2 class="mb-4 text-lg font-semibold">Add Repository</h2>

		<div class="mb-4 flex gap-2">
			<button
				class="flex-1 rounded-lg py-2 text-sm {source === 'github'
					? 'bg-blue-600 text-white'
					: 'border border-gray-200 text-gray-700'}"
				onclick={() => (source = 'github')}>GitHub</button
			>
			<button
				class="flex-1 rounded-lg py-2 text-sm {source === 'local'
					? 'bg-blue-600 text-white'
					: 'border border-gray-200 text-gray-700'}"
				onclick={() => (source = 'local')}>Local Path</button
			>
		</div>

		<label class="block">
			<span class="text-sm font-medium text-gray-700">
				{source === 'github' ? 'GitHub URL' : 'Absolute Path'}
			</span>
			<input
				type="text"
				bind:value={sourceUrl}
				placeholder={source === 'github'
					? 'https://github.com/facebook/react'
					: '/home/user/projects/my-sdk'}
				class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
			/>
		</label>

		{#if source === 'github'}
			<label class="mt-3 block">
				<span class="text-sm font-medium text-gray-700"
					>GitHub Token (optional, for private repos)</span
				>
				<input
					type="password"
					bind:value={githubToken}
					placeholder="ghp_..."
					class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
				/>
			</label>
		{/if}

		{#if error}
			<p class="mt-3 text-sm text-red-600">{error}</p>
		{/if}

		<div class="mt-6 flex justify-end gap-3">
			<button onclick={onClose} class="rounded-lg border border-gray-200 px-4 py-2 text-sm">
				Cancel
			</button>
			<button
				onclick={handleSubmit}
				disabled={loading || !sourceUrl}
				class="rounded-lg bg-blue-600 px-4 py-2 text-sm text-white disabled:opacity-50"
			>
				{loading ? 'Adding...' : 'Add & Index'}
			</button>
		</div>
	</div>
</dialog>

Live Progress Component

<!-- src/lib/components/IndexingProgress.svelte -->
<script lang="ts">
	import { onMount, onDestroy } from 'svelte';
	import type { IndexingJob } from '$lib/types';

	let { jobId } = $props<{ jobId: string }>();
	let job = $state<IndexingJob | null>(null);
	let interval: ReturnType<typeof setInterval>;

	async function pollJob() {
		const res = await fetch(`/api/v1/jobs/${jobId}`);
		if (res.ok) {
			const data = await res.json();
			job = data.job;
			if (job?.status === 'done' || job?.status === 'failed') {
				clearInterval(interval);
			}
		}
	}

	onMount(() => {
		pollJob();
		interval = setInterval(pollJob, 2000);
	});

	onDestroy(() => clearInterval(interval));
</script>

{#if job}
	<div class="mt-2">
		<div class="flex justify-between text-xs text-gray-500">
			<span>{job.processedFiles} / {job.totalFiles} files</span>
			<span>{job.progress}%</span>
		</div>
		<div class="mt-1 h-1.5 w-full rounded-full bg-gray-200">
			<div
				class="h-1.5 rounded-full bg-blue-600 transition-all"
				style="width: {job.progress}%"
			></div>
		</div>
		{#if job.status === 'failed'}
			<p class="mt-1 text-xs text-red-600">{job.error}</p>
		{/if}
	</div>
{/if}

Main Page Data Loading

// src/routes/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ fetch }) => {
	const res = await fetch('/api/v1/libs');
	const data = await res.json();
	return { repositories: data.libraries };
};

Files to Create

  • src/routes/+page.svelte — repository list
  • src/routes/+page.server.ts — load function
  • src/routes/repos/[id]/+page.svelte — repository detail
  • src/routes/repos/[id]/+page.server.ts — load function
  • src/routes/settings/+page.svelte — settings page
  • src/lib/components/RepositoryCard.svelte
  • src/lib/components/AddRepositoryModal.svelte
  • src/lib/components/IndexingProgress.svelte
  • src/lib/components/ConfirmDialog.svelte
  • src/lib/components/StatBadge.svelte
  • src/routes/+layout.svelte — nav + global layout