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

8.7 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