Signet — Frontend Design (Next.js Portal, v0.1)
The web portal (web/): route map, rendering strategy, component architecture, data access, auth, and testing. Scaffolded at milestone M2. Related: architecture.md §6, api-reference.md1. Stack
| Concern | Choice | Rationale |
|---|---|---|
| Framework | Next.js (App Router), TypeScript strict | architecture §2 — SSR/ISR keyed to data mutability, SEO for audit pages |
| Styling | Tailwind CSS + a small set of shared primitives | boring, greppable; no runtime CSS-in-JS cost on server components |
| Data | native fetch in server components + a typed API client generated from the OpenAPI spec | one source of truth for shapes; no client state library until something actually needs it |
| Auth | Auth.js with GitHub OAuth | portal sessions only — signing never happens in the browser (§5) |
| Charts (score history) | server-rendered SVG sparklines | no client charting dependency for read-only trend lines |
2. Route Map & Rendering Strategy
app/
├── page.tsx # home: search + registry stats [SSR, 60s cache]
├── records/[id]/page.tsx # record detail [ISR, content-addressed → effectively static]
├── repo/[host]/[owner]/[name]/
│ ├── page.tsx # repo overview: audited commits [SSR, 60s]
│ └── [commit]/page.tsx # coverage view for one commit [SSR, 30s]
├── auditors/[id]/page.tsx # identity profile: tier, R, history [SSR, 300s]
├── disputes/page.tsx # dispute queue (reviewer workflow) [dynamic, no cache]
├── search/page.tsx # search results [dynamic]
├── docs/[...slug]/page.tsx # rendered design/user docs [static]
└── api/auth/[...nextauth]/ # Auth.js routes (the only portal API routes)Rendering rules (architecture §6):
- Immutable data → ISR.
/records/[id]content never changes; only the
registry annotation block (verifications, dispute status) does. The page splits accordingly: bundle content rendered at ISR time, the annotation panel in a Suspense boundary fetched per-request with a short TTL.
- **Mutable data → SSR with
fetchrevalidate TTLs**, no client-side
polling. The portal is a reader, not a dashboard.
- No portal-private API. Every page renders from the public REST API —
if a page needs data, tools get the same endpoint (backend.md rule). The only exception is Auth.js session routes.
3. Component Architecture
components/
├── record/ BundleViewer, PredicateView (per-type: Audit/Verification/Flag/Vouch),
│ SignatureChain, VerificationList, DisputeBanner
├── coverage/ CoverageTable, ConfidenceBadge, CweChips, FreshnessIndicator
├── identity/ TierBadge, ScoreCard (E×G breakdown), ScoreSparkline, SlashHistory
├── shared/ UrnLink, ShaChip (truncate + copy + monospace), TimeAgo,
│ SearchBox, EmptyState, ProblemError (renders problem+json)
└── layout/ Header, Footer, ThemeToggleConventions:
- Server components by default;
"use client"only for interactivity
(copy buttons, search box, theme toggle, dispute forms).
- Trust-sensitive rendering rules live in exactly one place each:
ConfidenceBadge owns the confidence→color mapping, DisputeBanner owns "disputed = treat as uncovered" messaging, TierBadge owns tier labels. These encode trust-model semantics; scattering them invites inconsistency.
- Everything renders from API types generated off OpenAPI — no hand-written
response interfaces.
4. Key Page Specs
- Record detail — the page a CVE triager lands on. Above the fold:
status, subjects (repo @ commit), signer + tier + reputation, dispute banner if any. Then: claims table (status × CWEs), scope file list with hashes, false-positive entries (rule ID + justification — this is the FP registry's human UI), methodology, verification list with depths, raw bundle (collapsed, copyable).
- Commit coverage — the "should I trust the skip-scan" page: per-file
table mirroring GET /v1/coverage exactly (same numbers the pipeline saw), with a confidence-threshold slider that filters client-side.
- Auditor profile — tier, R score with E×G breakdown and params version,
score sparkline, attestation history, slash events with cause_ref links. Full transparency of the reputation math is a trust feature, not vanity UI.
- Dispute queue — the one write-flow in the portal: reviewers (session +
reputation gate checked server-side by the API on submission) browse open flags with evidence and submit resolutions. Resolution submissions are signed records too — the portal hands the reviewer a prefilled signet flag resolve … CLI command rather than signing in the browser.
5. Auth & Key Boundary
- GitHub OAuth session = *browsing* identity: linking your profile page,
prefilled CLI commands, dispute-queue visibility.
- Private keys never touch the portal (cli-spec.md §9). Any action
requiring a signature renders as a copyable CLI command with context filled in. If a browser signing flow ever lands, it's WebAuthn-backed and a separate design review.
6. Quality & Testing
- Playwright E2E against a seeded docker-compose stack (fixture records from
examples/generate-sample): record page renders and verifies, coverage table matches API response, dispute flow gates correctly.
- Vitest for pure logic (confidence formatting, URN parsing).
- Accessibility: badges/banners never encode meaning by color alone
(confidence and dispute states carry text labels); keyboard-first tables.
- Web vitals budget: record page LCP < 1.5s on cold ISR miss; zero
client-side JS on the docs routes.
7. Self-Hosting Note (mirrors)
The portal is optional per mirror (architecture §10-Q4). It ships as a standalone Next.js Docker target with SIGNET_API_URL as its only required config; ISR uses the default filesystem cache handler — no external cache service. Minimal mirrors stay API-only.