Skip to content

Add a Server-owned web page

PowerContext serves a small multi-page web UI from the same FastAPI process as its HTTP API. Use this structure for Server-owned pages that read PowerContext APIs. Do not introduce a separate frontend build or client-side router unless the product requires an independently built application.

Directory layout

The web UI is organized by responsibility:

src/powercontext/server/
├── web.py
├── static/
│   ├── auth.js
│   ├── dashboard.js
│   └── site.css
└── templates/
    ├── base.html
    ├── components/
    └── pages/
        └── dashboard.html

web.py owns the Jinja environment, page router, static mount, and UI support endpoints. base.html owns the document head, global header and footer, and asset slots. auth.js owns bearer-token session storage and authenticated requests. Page templates provide page content. Components contain complete, reusable fragments such as the login form, activity heatmap, and recall trend.

Templates and static files are package resources. Keep them below powercontext.server so both editable installs and built wheels expose the same files.

Add a page

Create a template below templates/pages/ and extend the shared layout:

{% extends "base.html" %}

{% block title %}Page title{% endblock %}

{% block content %}
<section>
  <h1>Page heading</h1>
</section>
{% endblock %}

Register an explicit FastAPI route in mount_web_ui(). Pass the incoming Request to TemplateResponse so Jinja can generate application URLs correctly:

async def page(request: Request) -> Response:
    return _templates().TemplateResponse(
        request=request,
        name="pages/page.html",
        headers=_PAGE_HEADERS,
    )


router.add_api_route(
    "/page",
    page,
    methods=["GET"],
    response_class=HTMLResponse,
    name="page",
)

The root path is the Dashboard entry point. Add later pages at explicit paths and keep API routes under their existing versioned prefixes.

Understand Dashboard data

The browser authenticates against /dashboard/scopes, then requests /v1/stats with the selected scope_id and a 30d period. The Server reads one scoped snapshot and returns inventory, model usage, and recall statistics.

Dashboard value Source
Sources Current scoped Source journal position
Memory entries Entries in the current Memory Artifact
Artifacts Current Artifact heads grouped by family
Pending review Current Candidate heads grouped by family and status
Model usage Persisted daily generation and embedding usage
Recall hits, token reduction, and savings trend Persisted daily recall measurements for the configured estimator

The Runtime performs these reads in one database transaction and calculates totals, pending Sources, family counts, daily buckets, and token reduction on the Server. The browser presents ready_preparations as recall hits and plots the signed daily token_reduction as the savings trend. Each heatmap cell combines those two fields for its date. Its fixed bands are no hit, hit without a positive reduction, 1–255, 256–1023, and 1024 or more estimated tokens reduced. The fixed thresholds keep sparse activity and outliers from changing the meaning of every other cell.

Share only stable page structure

Put document-level structure in base.html. Put a fragment in templates/components/ when it is reused or represents a self-contained UI unit. Import auth.js instead of implementing token storage or bearer headers in each page. Keep page-specific sign-in errors, data loading, and rendering in that page's static script.

Do not create a generic chart abstraction from one chart type. Share markup and styles first. Extract a JavaScript data or rendering contract only after a second page needs the same behavior.

Add the Handoff Report page

When both the Dashboard and Handoff Report are enabled, the Server hosts the project handoff page at /handoff-reports. The existing scoped-statistics Dashboard remains at /. The pages share only base.html, the header and footer, auth.js, theme state, and locale state; their statistics and report calculations remain independent.

The Handoff Report page obtains Projects from POST /v1/handoff-reports/projects/list and uses immutable project_id values for its tabs. Each tab shows both the Project title and full project_id so duplicate titles remain distinguishable. Selecting a tab requests canonical JSON for that Project through POST /v1/handoff-reports/get. The browser formats and displays the returned summary, coverage, Workstream state, Activity count, objective, current state, next action, known omissions, and digests without recalculating report semantics.

The page requests the current day in the Project timezone by default and provides current-day, ISO-week, calendar-month, and custom date-range filters. The custom end date is inclusive in the UI and is converted to the exclusive start of the next day for the API. Every request enables comparison with the preceding period of equal length, and the Markdown download uses the same period request. Because Handoff has no authoritative commit timestamp, the page must explain that Handoff status comes from the current exact selection when handoff_boundary_coverage=unavailable; the selected period precisely filters Activity but must not present current Handoff state as a historical period-end state.

The overview request may disable evidence checks for lower latency. A Markdown download makes a separate request with format=markdown, download=true, and evidence checks enabled by default. The browser never reconstructs Markdown from rendered DOM or canonical JSON. Disabling Handoff Report removes the /handoff-reports page and its API while leaving the original Dashboard route, scope selection, and statistics request unchanged.

Preserve the security boundary

The Dashboard shell and static assets are public so a browser can render the sign-in form. They must not contain bearer tokens, configured scope names, statistics, or other private data. UI support endpoints and /v1/ data endpoints remain behind StaticBearerMiddleware.

Return Server-owned pages with the shared Content Security Policy and Cache-Control: no-store. Prefer external CSS and JavaScript. The short inline script in base.html exists only to apply the saved theme before first paint.

Validate behavior

Test through the public HTTP surface. Cover page routing, protected data requests, scope isolation, and data obtained from a real database-backed Server. Assert user-visible behavior or preserve a concrete regression. Do not assert DOM IDs, static asset paths, JavaScript source text, Jinja internals, or private function call order.

Run:

uv run pytest tests/test_dashboard.py -q
make check
make test
make build

After building, confirm the wheel contains powercontext/server/templates/ and powercontext/server/static/.