Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ CODEQL_RESULT := $(BUILD_DIR)/codeql-results.sarif
integration codeql require-docker require-codeql require-semgrep require-jq require-node \
verify tools-check tools-install \
dev dev-anon dev-up dev-wait dev-ui-if-needed dev-down dev-logs \
docker docs docs-serve run version
docker docs docs-serve screenshots run version

## all: Build, test, lint
all: build test lint
Expand Down Expand Up @@ -447,6 +447,25 @@ DOCS_PORT ?= 8001
docs-serve:
mkdocs serve -a $(DOCS_HOST):$(DOCS_PORT)

## screenshots: Capture portal screenshots (light + dark) for the docs site.
## Seeds mock audit data via the configured database, then drives
## Playwright through every portal page. Requires the binary to
## be running on $(SHOTS_BASE_URL) (default http://localhost:8080).
## Re-run after any portal UI change.
##
## Sources .env.dev for APITEST_DEV_KEY so the captured-portal API
## key matches the running binary's accepted file key. Override
## SHOTS_API_KEY explicitly to point at a different deployment
## (e.g. staging); that wins over .env.dev.
SHOTS_BASE_URL ?= http://localhost:8080
screenshots: dev-secrets require-node
@. ./.env.dev && \
KEY="$${SHOTS_API_KEY:-$$APITEST_DEV_KEY}" && \
if [ -z "$$KEY" ]; then echo "no API key: set SHOTS_API_KEY or run make dev-secrets"; exit 1; fi && \
cd scripts/screenshots && \
(test -d node_modules || npm install) && \
APITEST_BASE_URL=$(SHOTS_BASE_URL) APITEST_DEV_KEY="$$KEY" node screenshots.mjs

## clean: Remove build artifacts (binary, coverage, codeql db/sarif)
clean:
rm -rf $(BUILD_DIR) coverage.out coverage.html
Expand Down
Binary file added docs/images/portal/about-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/portal/about-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/portal/audit-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/portal/audit-detail-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/portal/audit-detail-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/portal/audit-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/portal/config-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/portal/config-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/portal/dashboard-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/portal/dashboard-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/portal/endpoints-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/portal/endpoints-detail-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/portal/endpoints-detail-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/portal/endpoints-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/portal/keys-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/portal/keys-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/portal/login-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/portal/login-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
63 changes: 57 additions & 6 deletions docs/operations/portal.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,8 @@ binary via `go:embed`. It mounts at `/portal/` when `portal.enabled` is
true; the portal API lives at `/api/v1/portal/*` and is gated by an
operator session cookie established via OIDC PKCE.

!!! note "Lands in M3"
The portal binary support is in place (the Go side mounts
`internal/ui/dist` if it has an `index.html`); the SPA itself
arrives in M3. Until then, point `portal.enabled` to `false` and
use the curl examples in [Quickstart](../getting-started/quickstart.md)
or hit Postgres directly for audit queries.
![api-test portal Dashboard showing 1-hour totals and recent activity, light theme](../images/portal/dashboard-light.png#only-light)
![api-test portal Dashboard showing 1-hour totals and recent activity, dark theme](../images/portal/dashboard-dark.png#only-dark)

## Pages

Expand All @@ -40,10 +36,65 @@ Two paths reach portal data:
for headless operators (CI dashboards, kiosks). The portal API
accepts both schemes.

![Portal sign-in screen with OIDC button and API key form, light theme](../images/portal/login-light.png#only-light)
![Portal sign-in screen with OIDC button and API key form, dark theme](../images/portal/login-dark.png#only-dark)

The portal session is *separate* from the inbound auth chain that
gates `/v1/*`. An operator can have a portal session without any of
the gateway's connection credentials.

## Endpoints

The Endpoints page is a catalog of every registered route, grouped by
behavior (identity, deterministic data, echo, controlled failure modes).
Click any row to see method, path, group, auth requirement, description,
and a curl hint for invoking the route directly.

![Endpoints catalog with the right-pane detail card for a selected route, light theme](../images/portal/endpoints-detail-light.png#only-light)
![Endpoints catalog with the right-pane detail card for a selected route, dark theme](../images/portal/endpoints-detail-dark.png#only-dark)

## Audit log

The Audit page is the filterable, paginated event view. Filters cover
HTTP method, path-contains, and success / error; the table auto-refreshes
on a 5-second interval.

![Audit page with the events table and filter row, light theme](../images/portal/audit-light.png#only-light)
![Audit page with the events table and filter row, dark theme](../images/portal/audit-dark.png#only-dark)

Clicking any row opens a detail panel on the right showing the timestamp,
duration, request id, identity, remote address, byte counts, plus the
full request and response trees (headers, query, body) when the
`audit_payloads` row is present.

![Audit detail panel open over the events table, showing identity / timing fields and request / response trees, light theme](../images/portal/audit-detail-light.png#only-light)
![Audit detail panel open over the events table, showing identity / timing fields and request / response trees, dark theme](../images/portal/audit-detail-dark.png#only-dark)

## API keys

Create or revoke Postgres-backed bcrypt keys. The plaintext is shown
exactly once at creation time and never stored.

![API keys page with the create form and key listing, light theme](../images/portal/keys-light.png#only-light)
![API keys page with the create form and key listing, dark theme](../images/portal/keys-dark.png#only-dark)

## Config

A read-only view of the running server config, with secrets masked.
Useful for sanity-checking what's actually loaded when you suspect an
env-var or override didn't land.

![Config page rendering the loaded YAML with secrets masked, light theme](../images/portal/config-light.png#only-light)
![Config page rendering the loaded YAML with secrets masked, dark theme](../images/portal/config-dark.png#only-dark)

## About

Build info plus the same well-known metadata an MCP / API client sees
(api endpoint, OIDC issuer URL, audience).

![About page with build info and well-known metadata, light theme](../images/portal/about-light.png#only-light)
![About page with build info and well-known metadata, dark theme](../images/portal/about-dark.png#only-dark)

## Try-It

Click any endpoint in the catalog to open a per-route Try-It panel:
Expand Down
103 changes: 98 additions & 5 deletions docs/overrides/home.html
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
{#
Home page template. Replaces Material's article surface with a Plexara-
flavored composition: hero (woven-pattern + hero-glow + LogoMark),
feature grid (cards), reasons rail, and a CTA strip.
portal-screenshots carousel, feature grid (cards), reasons rail.

Triggered via `template: home.html` in the index.md front matter. We
still extend `main.html` so the global header, footer, search, and
announce bar are unchanged. The Markdown content of index.md is
available as `page.content` and rendered below the hero so existing
prose stays editable in source.

Note: the portal-screenshots carousel that mcp-test ships in this
template is omitted here. The api-test portal lands in M3; once
screenshots exist under docs/images/portal/, copy the .plex-shots
section back from mcp-test verbatim.
Screenshots live under docs/images/portal/<slug>-{light,dark}.png and
are produced by `make screenshots` (see scripts/screenshots/README.md).
#}
{% extends "main.html" %}

Expand Down Expand Up @@ -66,6 +64,101 @@ <h1 class="plex-hero__title">
<div class="plex-rule-bottom" aria-hidden="true"></div>
</section>

<section class="plex-shots" aria-label="Portal screenshots">
<div class="plex-shots__inner">
<div class="plex-shots__head">
<span class="plex-section__eyebrow">Portal preview</span>
<h2>Inspect every request from the browser</h2>
<p class="plex-shots__lede">Click any frame to open it full-size. Use the side rails or the arrow keys to step through.</p>
</div>

<div class="plex-shots__stage">
<button class="plex-shots__rail plex-shots__rail--prev" data-shots-prev type="button" aria-label="Previous screenshot">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
</button>

<div class="plex-shots__viewport">
<div class="plex-shots__track" data-shots-track>
{% set shots = [
["dashboard", "Dashboard", "1-hour totals, success / error counts, error rate, and the most recent calls across every endpoint group."],
["audit", "Audit log", "Every request, filterable by method, path, and success / error. Auto-refreshing on a 5-second interval; click a row to inspect the full request and response."],
["audit-detail", "Inspection panel", "Side-pane detail card with timestamp, duration, request id, identity, remote address, byte counts, and the full headers / query / body trees for both sides of the call."],
["endpoints", "Endpoints", "Catalog of every registered route, grouped by behavior — identity, deterministic data, echo, controlled failure modes."],
["endpoints-detail", "Endpoint detail", "Method, path, group, auth requirement, and an inline curl hint per route. Try-It panel arrives with the OpenAPI generator in M4."],
["keys", "API keys", "Create or revoke Postgres-backed bcrypt keys. Plaintext is shown once, then never again."],
["config", "Config", "Read-only view of the running server config, with secrets masked. Useful for sanity-checking what's actually loaded."],
["about", "About", "Build info plus the same well-known metadata an MCP / API client sees: api endpoint, OIDC issuer, audience."]
] %}
{% for slug, title, body in shots %}
<figure class="plex-shots__slide">
<button
class="plex-shots__frame"
type="button"
data-shots-zoom
data-zoom-slug="{{ slug }}"
data-zoom-title="{{ title }}"
data-zoom-body="{{ body }}"
data-zoom-light="{{ ('images/portal/' ~ slug ~ '-light.png') | url }}"
data-zoom-dark="{{ ('images/portal/' ~ slug ~ '-dark.png') | url }}"
aria-label="Open {{ title }} screenshot full size"
>
<img src="{{ ('images/portal/' ~ slug ~ '-light.png') | url }}" alt="Portal {{ title }} screen, light theme" data-theme="light" loading="lazy" decoding="async" />
<img src="{{ ('images/portal/' ~ slug ~ '-dark.png') | url }}" alt="Portal {{ title }} screen, dark theme" data-theme="dark" loading="lazy" decoding="async" />
<span class="plex-shots__zoomhint" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 8V3h5"/><path d="M21 8V3h-5"/><path d="M3 16v5h5"/><path d="M21 16v5h-5"/>
</svg>
</span>
</button>
<figcaption class="plex-shots__caption">
<span class="eyebrow">Portal</span>
<h3>{{ title }}</h3>
<p>{{ body }}</p>
</figcaption>
</figure>
{% endfor %}
</div>
</div>

<button class="plex-shots__rail plex-shots__rail--next" data-shots-next type="button" aria-label="Next screenshot">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
</button>
</div>

<div class="plex-shots__counter" data-shots-counter aria-live="polite"></div>
</div>

{# Lightbox modal. Hidden by default; shots.js shows it on slide click. #}
<div class="plex-lightbox" data-shots-lightbox hidden role="dialog" aria-modal="true" aria-labelledby="plex-lightbox-title">
<div class="plex-lightbox__backdrop" data-shots-close></div>
<div class="plex-lightbox__shell">
<header class="plex-lightbox__bar">
<div class="plex-lightbox__meta">
<span class="plex-lightbox__eyebrow">Portal</span>
<h3 class="plex-lightbox__title" id="plex-lightbox-title"></h3>
</div>
<div class="plex-lightbox__controls">
<span class="plex-lightbox__count" data-lightbox-count aria-live="polite"></span>
<button class="plex-lightbox__navbtn" data-lightbox-prev type="button" aria-label="Previous screenshot">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
</button>
<button class="plex-lightbox__navbtn" data-lightbox-next type="button" aria-label="Next screenshot">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
</button>
<button class="plex-lightbox__close" data-shots-close type="button" aria-label="Close screenshot">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</div>
</header>
<figure class="plex-lightbox__stage">
<img class="plex-lightbox__img" data-lightbox-img-light data-theme="light" alt="" />
<img class="plex-lightbox__img" data-lightbox-img-dark data-theme="dark" alt="" />
<figcaption class="plex-lightbox__caption" data-lightbox-body></figcaption>
</figure>
</div>
</div>
</section>

<section class="plex-section" aria-label="Highlights">
<div class="plex-section__inner">
<span class="plex-section__eyebrow">What's inside</span>
Expand Down
3 changes: 3 additions & 0 deletions scripts/screenshots/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
package-lock.json
pnpm-lock.yaml
70 changes: 70 additions & 0 deletions scripts/screenshots/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Portal screenshots

Generates the `docs/images/portal/*-{light,dark}.png` set the documentation site embeds (homepage carousel + inline captures in `docs/operations/portal.md`). Idempotent: the script truncates `audit_events` / `audit_payloads` / `api_keys` and re-seeds deterministic mock data each run.

## Prerequisites

The dev stack must be running. Open a separate terminal:

```sh
make dev
```

This brings up Postgres, Keycloak, and the api-test binary on `http://localhost:8080`. The script connects to the same Postgres to seed mock data and points a headless Chromium at the same binary to capture frames.

## Capture

From the repo root:

```sh
make screenshots
```

This runs `node scripts/screenshots/screenshots.mjs`. On first run it `npm install`s the script's `package.json` (Playwright + `pg`).

Override host / API key for non-default deployments:

```sh
SHOTS_BASE_URL=https://staging.example.com \
SHOTS_API_KEY=$REAL_KEY \
make screenshots
```

## What gets captured

Nine screens × two themes = 18 PNGs at 1440×900 @ 2x DPR. The homepage carousel embeds eight of these (login is captured but kept out of the rotation).

| slug | shows |
| --- | --- |
| `login` | Sign-in screen (no auth required for capture). |
| `dashboard` | 1-hour stats + recent activity table. |
| `endpoints` | Endpoint catalog grouped by group. |
| `endpoints-detail` | Right-pane endpoint detail card with method/path/group/auth/curl hint. |
| `audit` | Filterable, paginated event view populated with seeded data. |
| `audit-detail` | Detail card open over the events table; first row clicked so request/response headers and body trees render. |
| `keys` | API key listing. |
| `config` | Read-only config viewer. |
| `about` | Project info + well-known endpoint data. |

## Preview

After capturing, two preview paths:

```sh
open docs/images/portal/ # raw PNG view in Finder / Preview
make docs-serve # http://127.0.0.1:8001 (full site context)
```

`make docs-serve` is the closer-to-production view: the homepage carousel cycles through the screenshots (theme-paired with `data-theme` attributes; the page footer toggle swaps which one is visible), and `portal.md` embeds use the mkdocs-material `#only-light` / `#only-dark` URL fragments to switch per the reader's selected theme.

## When to re-run

Any portal UI change that would shift pixels: layout tweaks, copy edits, new components, theme adjustments. The seed step is deterministic (a fixed PRNG seed produces the same audit events across runs), so re-running on an unchanged binary gives byte-stable PNGs, meaning git diffs only show real visual changes.

## Troubleshooting

**`Postgres connection failed`**: `make dev` isn't running or the stack hasn't finished starting. `make dev-wait` blocks until both Postgres and Keycloak are reachable.

**`Detail panel empty / "No event selected"`**: `audit_payloads` seed didn't run; check the Postgres logs for `relation "audit_payloads" does not exist`. Run migrations: `make migrate` (or restart the binary, which auto-migrates on startup).

**`Timeout waiting for selector "Timestamp"`**: the prep step couldn't find a row matching the seeded target path. The script picks the most-recent successful payload event; if the random seed produces zero successful payloads (very unlikely with 100 events), the script aborts before capture with a clear message.
14 changes: 14 additions & 0 deletions scripts/screenshots/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "api-test-screenshots",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Playwright-driven screenshot capture for the api-test portal. Run via `make screenshots`.",
"scripts": {
"capture": "node screenshots.mjs"
},
"dependencies": {
"pg": "^8.13.1",
"playwright": "^1.49.1"
}
}
Loading
Loading