chore: initial project scaffold
This commit is contained in:
311
docs/features/TRUEREF-0015.md
Normal file
311
docs/features/TRUEREF-0015.md
Normal file
@@ -0,0 +1,311 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user