318 lines
8.3 KiB
Markdown
318 lines
8.3 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
|