Iteration 7.5 Spec — Datastar UI¶
Goal¶
Server-rendered Datastar UI covering all implemented backend functionality (iterations 1–7). Jinja2 templates, SSE for live updates, no JS build step. Hypermedia-first with reactive signals for interactive elements.
Dependencies to add¶
jinja2 is bundled with fastapi[standard] but pin explicitly. sse-starlette provides EventSourceResponse for Datastar SSE endpoints.
Architecture¶
Template structure¶
src/trevor/
templates/
base.html # Shell: <head>, nav, Datastar script, flash area
components/
nav.html # Top nav: logo, project switcher, user menu, role badge
flash.html # Flash message partial
pagination.html # Reusable paginator
status_badge.html # Request/object status pill
file_preview.html # Preview panel (markdown, CSV, code, image, PDF)
researcher/
request_list.html # My requests table (filtered by project)
request_create.html # Create request form
request_detail.html # Request detail: status, objects, reviews, audit timeline
object_upload.html # Upload form (multipart, output_type, statbarn select)
object_metadata.html # Metadata form (description, justification, suppression)
object_replace.html # Replace object form
revision_feedback.html # Checker feedback display + replace/resubmit actions
checker/
review_queue.html # Requests awaiting review (HUMAN_REVIEW status)
review_form.html # Review form: agent report, per-object decisions
admin/
request_overview.html # All-projects request table with filters
metrics_dashboard.html # Pipeline metrics cards + stuck request list
audit_log.html # Filterable audit log table
membership_manage.html # Project membership CRUD
static/
style.css # Minimal custom CSS (utility-first, no framework)
favicon.ico
Router: src/trevor/routers/ui.py¶
Single router for all HTML views. Every route returns TemplateResponse or EventSourceResponse.
GET /ui/ → redirect to /ui/requests
GET /ui/requests → researcher request list
GET /ui/requests/new → create request form
GET /ui/requests/{id} → request detail
GET /ui/requests/{id}/upload → upload object form
GET /ui/requests/{id}/objects/{oid}/metadata → metadata form
GET /ui/requests/{id}/objects/{oid}/replace → replace form
GET /ui/requests/{id}/objects/{oid}/preview → file preview (SSE partial)
GET /ui/review → checker review queue
GET /ui/review/{id} → review form
GET /ui/admin → admin request overview
GET /ui/admin/metrics → metrics dashboard
GET /ui/admin/audit → audit log
GET /ui/admin/memberships/{project_id} → membership management
SSE endpoints (Datastar data-on-load targets)¶
GET /ui/sse/request-status/{id} → stream request status updates
GET /ui/sse/review-queue → stream new items arriving in review queue
Form action endpoints (POST, return HTML fragments)¶
POST /ui/requests → create request, redirect to detail
POST /ui/requests/{id}/upload → upload object, return updated object list
POST /ui/requests/{id}/submit → submit request, return status update
POST /ui/requests/{id}/objects/{oid}/metadata → save metadata, return confirmation
POST /ui/requests/{id}/objects/{oid}/replace → upload replacement, return updated list
POST /ui/requests/{id}/resubmit → resubmit, return status update
POST /ui/review/{id} → submit review, redirect to queue
POST /ui/admin/memberships → create membership, return updated list
DELETE /ui/admin/memberships/{id} → delete membership, return updated list
Datastar patterns¶
Signal-driven state¶
<!-- Project switcher -->
<div data-signals="{projectId: '{{current_project_id}}'}">
<select data-bind="projectId" data-on-change="$$get('/ui/requests?project_id=' + projectId)">
{% for p in projects %}
<option value="{{p.id}}">{{p.display_name}}</option>
{% endfor %}
</select>
</div>
SSE live updates¶
<!-- Request detail: live status -->
<div id="status-area"
data-on-load="$$get('/ui/sse/request-status/{{request.id}}')">
{% include 'components/status_badge.html' %}
</div>
Server sends SSE fragments:
async def sse_request_status(request_id: uuid.UUID):
async def event_generator():
last_status = None
while True:
req = await get_request(request_id)
if req.status != last_status:
last_status = req.status
html = render_template("components/status_badge.html", request=req)
yield {"event": "datastar-merge-fragments", "data": f'<div id="status-area">{html}</div>'}
await asyncio.sleep(2)
return EventSourceResponse(event_generator())
File preview¶
<!-- Preview panel: renders server-side based on output_type -->
<div id="preview-{{obj.id}}" data-on-click="$$get('/ui/requests/{{req.id}}/objects/{{obj.id}}/preview')">
Preview
</div>
Server renders preview based on output_type:
- tabular: polars.read_csv() → first 500 rows → HTML table
- figure/image: <img src="../presigned-url">
- report/markdown: mistune.html(content)
- code: pygments.highlight(content)
- model/other: raw text in <pre> block
UI views detail¶
1. Base shell (base.html)¶
- Top nav: trevor logo, project dropdown (Datastar signal-bound), user name + role badge, logout
- Flash message area (Datastar
data-showwith auto-dismiss) - Main content area (
{% block content %}) - Datastar CDN script (pinned version)
- Minimal CSS: system font stack, CSS custom properties for status colors, responsive grid
2. Researcher: Request list¶
- Table: title, status (color-coded badge), object count, updated_at, age
- Filter bar: status dropdown, direction dropdown (Datastar signals → SSE reload)
- "New Request" button
- Empty state message
3. Researcher: Create request¶
- Form: project (pre-selected), direction (egress default), title, description
- POST → redirect to detail page
4. Researcher: Request detail¶
- Header: title, status badge (SSE live), project name, submitter
- Tab panel (Datastar signals): Objects | Reviews | Audit
- Objects tab: card per object — filename, size, statbarn, status badge, preview button, metadata link. If CHANGES_REQUESTED: checker feedback inline, replace button.
- Reviews tab: agent review summary + findings, human review cards
- Audit tab: timeline of events
- Action bar (conditional on status):
- DRAFT: Upload object button, Submit button
- CHANGES_REQUESTED: Replace/Resubmit buttons
- APPROVED: "Awaiting release" message (admin sees Release button)
- RELEASED: Download link (pre-signed URL)
5. Researcher: Upload object¶
- Multipart form: file input, output_type select, statbarn select
- POST returns updated object list fragment (Datastar merge)
6. Researcher: Metadata form¶
- Fields: description, researcher_justification, suppression_notes
- PATCH via POST (form method override), returns confirmation fragment
7. Checker: Review queue¶
- Table: requests in HUMAN_REVIEW, sorted by age descending
- Columns: title, project, submitter, object count, agent decision, age
- SSE: new items appear live via
$$get('/ui/sse/review-queue') - Click → review form
8. Checker: Review form¶
- Left panel: object list with expandable preview + agent findings per object
- Right panel: review form
- Overall decision: approve / changes_requested / reject (radio)
- Per-object decisions: approve / changes_requested / reject + feedback textarea
- Summary textarea
- Submit → POST, redirect to queue
9. Admin: Request overview¶
- Full table from
GET /admin/requests - Filter bar: status, project, direction (Datastar signals)
- Pagination
- Click → request detail
10. Admin: Metrics dashboard¶
- Cards: total requests, approval rate, median review hours, rejection rate
- Stuck requests alert list (highlighted if > 0)
- Requests per reviewer table
- By-status breakdown (horizontal bar or counts)
11. Admin: Audit log¶
- Table: timestamp, event_type, actor, request link, payload (expandable)
- Filter bar: event_type prefix, actor, date range
- Pagination
- CSV export button (links to
/admin/audit/export)
12. Admin: Membership management¶
- Per-project table: user, role, assigned_by, actions (delete)
- Add membership form: user select, role select
- Inline feedback on role conflict errors
Auth in UI routes¶
- All UI routes require auth (reuse
CurrentAuthdep) - Researcher views: filter by user's project memberships
- Checker views: filter by projects where user is
output_checkerorsenior_checker - Admin views: require
tre_adminorsenior_checker - Unauthenticated → redirect to Keycloak login (dev mode: auto-login as dev-bypass-user)
CSS approach¶
Minimal custom CSS. No Tailwind, no Bootstrap (no build step constraint). Use: - CSS custom properties for colors/spacing - System font stack - CSS Grid for layout - Status colors: green (approved/released), yellow (draft/pending), blue (in review), red (rejected), orange (changes requested) - Responsive: single-column on mobile, sidebar nav on desktop
New files¶
src/trevor/routers/ui.py
src/trevor/templates/base.html
src/trevor/templates/components/{nav,flash,pagination,status_badge,file_preview}.html
src/trevor/templates/researcher/{request_list,request_create,request_detail,object_upload,object_metadata,object_replace,revision_feedback}.html
src/trevor/templates/checker/{review_queue,review_form}.html
src/trevor/templates/admin/{request_overview,metrics_dashboard,audit_log,membership_manage}.html
src/trevor/static/style.css
Test plan¶
UI tests are lightweight — verify template rendering and redirects, not pixel-level:
GET /ui/requestsreturns 200 with HTML content-typeGET /ui/requests/{id}returns request detail HTMLPOST /ui/requestscreates request and redirectsGET /ui/reviewreturns review queue HTMLGET /ui/adminreturns admin overview HTMLGET /ui/admin/metricsreturns metrics dashboard HTML- Auth redirect: unauthenticated request → 302 or 403
- File preview endpoint returns rendered HTML fragment
Implementation order¶
- Dependencies (
jinja2pin,sse-starlette) - Base template + static CSS + nav component
- Researcher views (request list → create → detail → upload → metadata)
- File preview component
- Checker views (queue → review form)
- Admin views (overview → metrics → audit → memberships)
- SSE endpoints (request status, review queue)
- Tests