Files
trueref/docs/features/TRUEREF-0015.md
2026-03-22 17:08:15 +01:00

312 lines
8.7 KiB
Markdown

# 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
```svelte
<!-- 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
```svelte
<!-- 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
```svelte
<!-- 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
```typescript
// 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