Skip to content

feat: fold frontend dashboard into the monorepo - #18

Open
jack482653 wants to merge 33 commits into
sciwork:mainfrom
jack482653:worktree-frontend-monorepo-integration
Open

feat: fold frontend dashboard into the monorepo#18
jack482653 wants to merge 33 commits into
sciwork:mainfrom
jack482653:worktree-frontend-monorepo-integration

Conversation

@jack482653

Copy link
Copy Markdown
Collaborator

Summary

Folds the previously-separate argus-dashboard frontend into this repo as
frontend/ and serves it same-origin from FastAPI, replacing the old
Jinja2-rendered dashboard pages. Design/rationale is in
docs/superpowers/specs/2026-08-15-frontend-monorepo-integration-design.md.

Changes

Architecture

  • frontend/: a Next.js 16 app, static export (output: "export"), pure
    client-side rendering — no SSR, avoiding the RCE surface SSR previously
    exposed.
  • FastAPI mounts the built static export via StaticFiles, served from the
    same origin/port as the API — no CORS, no separate token-based auth; the
    frontend reuses the existing signed session cookie.
  • GET /dashboard/api/me added for the frontend's session bootstrap; two
    legacy Jinja2 dashboard routes retired in favor of the static pages.
  • Multi-stage Docker build produces the frontend export and copies it into
    the backend image; CI gets a frontend lint/build job and frontend unit
    tests.

Frontend pages

  • Event list (home), event detail (Recharts-based registration chart), and
    webhook logs, each backed by the existing dashboard JSON API.
  • Webhook logs: ID/Created/Method/Channel/Body-summary columns, per-row
    expand via Collapsible showing pretty-printed, syntax-highlighted
    request headers/body (hand-rolled highlighter — no shiki/prismjs, to keep
    the static-export bundle small); timestamps converted to Taipei time
    (dayjs, fixed +8 offset — Taiwan has no DST).
  • Shared PaginationFooter component (offset/limit + Previous/Next) reused
    by webhook logs, ready for events once that endpoint gets pagination.
  • Reusable EmptyState component for empty list states.
  • Dark OLED theme (indigo accent) via shadcn/ui on Base UI primitives; a
    single <main> wrapper hoisted into the root layout instead of each page
    duplicating it.

Fixes along the way

  • Dev-mode auth navigations (login/logout) now target the backend origin
    explicitly instead of the frontend dev server's own origin.
  • FastAPI serves Next's static events/* client-navigation payload files
    before the legacy dynamic route would otherwise swallow them.

Verification

  • uv run --locked --group dev ruff check src tests
  • uv run --locked --group dev ruff format --check src tests
  • uv run --locked --group dev pytest tests/ -W error --cov=argus --cov-report=term-missing --cov-report=xml (35 passed)
  • pnpm lint
  • pnpm exec tsc --noEmit
  • pnpm exec vitest run (26 passed, unit + Storybook projects)
  • pnpm build
  • Manual verification via browser automation against real dev data (OAuth
    login flow, webhook log expand/collapse, pagination, dark theme).

🤖 Generated with Claude Code

jack482653 and others added 26 commits August 15, 2026 21:47
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
isPending must stay true until the refetched page has landed in state,
not just until the mutation call returns, to actually prevent races
between overlapping delete/clear-all + reload cycles sharing one
useTransition.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… legacy route

Next's static export emits internal client-navigation payload files
(index.txt, __next._tree.txt, __next.events.__PAGE__.txt, ...) under
/dashboard/events/*, the same URL prefix as the legacy FastAPI route
GET /dashboard/events/{slug}. Since that route is registered before the
StaticFiles mount, it swallowed these payload requests as spurious
event-slug DB lookups, degrading Next's client-side prefetch/navigation
to a full page reload.

dashboard_event() now checks whether the requested slug corresponds to
a real file under the built frontend's events/ directory and serves it
directly via FileResponse before any session/DB logic. Falls through
unchanged (and safely, via Path.is_file() on a possibly-missing
directory) when it doesn't.

Adds real test coverage for this route for the first time: the fixed
collision, the legacy route still rendering end-to-end for a real
event slug, and the existing unauthenticated-redirect behavior.

Also corrects two test_docker_integration.py docstrings that
attributed the /dashboard/events and /dashboard/webhook-logs
no-trailing-slash 307 to Starlette Mount's redirect_slashes -- it's
actually StaticFiles' own directory-index redirect, a related but
distinct mechanism (redirect_slashes only applies to the bare mount
path, i.e. plain /dashboard).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…fe chart keys

- Add a nav bar (layout.tsx) with links home/webhook-logs and a raw
  /dashboard/logout link, since the new frontend previously had no way
  to navigate between pages or sign out.
- Wire up the previously-dead triggerReport()/deleteEvent() API calls:
  add a "Run report now" button and a per-event "Delete" (with
  confirm) button to the home page, using the same
  useTransition+confirm pattern as the webhook-logs page.
- Add an "error" AuthState to useRequireAuth() so a failed
  getCurrentUser() call surfaces a visible message instead of leaving
  every page stuck in an infinite unresolved "loading" state; add a
  matching error branch plus .catch() on every data-fetch call
  (listEvents, getEventTimeseries, listWebhookLogs) across all three
  pages.
- Add a confirm dialog to "Clear all" on the webhook-logs page (the
  bulk, irreversible action), fetching page 0 directly after clearing
  rather than via the stale-offset reload() closure.
- Key EventChart's per-line CSS custom properties, Recharts dataKeys,
  and chart config by series index instead of the raw ticket-type name
  -- KKTIX ticket names can contain spaces/parens, which produced
  invalid var() references and silently dropped that line's color.
  dataset.name is still used for the tooltip label and the "Total"
  strokeWidth check.
- Remove components/ui/card.tsx (confirmed unused, dead shadcn
  scaffolding).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tend

- Add "pnpm test" (vitest --project unit) to frontend/package.json and
  wire it into the CI frontend job, so the 6+ frontend tests actually
  run on every push/PR. Deliberately scoped to the unit project only;
  Storybook/Playwright-browser component tests stay a local-only check
  for now (avoids adding browser installation to CI in this pass).
- Ignore src/argus/dashboard/frontend/, the build output copied in by
  the Docker multi-stage build / local `pnpm build`, which was
  previously untracked-but-not-ignored.
- Update SPEC.md's API reference and auth-model description: /dashboard,
  /dashboard/webhook-logs, and the new /dashboard/events are public
  static shells gated client-side via /dashboard/api/me, not
  session-checked HTML routes; only the legacy /dashboard/events/{slug}
  route still redirects server-side. Also refreshes the repo-structure
  diagram to include frontend/ and dashboard/frontend/.
- Update README.md's dev instructions: a fresh checkout needs
  `cd frontend && pnpm install && pnpm build` before `/dashboard` will
  serve anything (the StaticFiles mount is skipped when nothing's been
  built), document the next-dev-plus-proxy workflow as an alternative,
  and note that Docker/Railway builds the frontend automatically.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
next dev (:3000) and uvicorn (:8000) are different origins in local
development, even though production serves both from the same FastAPI
process at the same origin. The dev-only proxy in next.config.ts only
rewrites /dashboard/api/* data calls, not full-page navigations — so
window.location.href = "/dashboard/login" (and the logout link) resolved
against the wrong origin (:3000, which has no such route) and 404'd.

Add configurations/backend.ts (BACKEND_ORIGIN, inlined by Next.js at build
time via process.env.NODE_ENV — empty string in production, unchanged
behavior there) and use it for both the login redirect and logout link.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Redesign following a UI/UX brainstorming pass (visual companion mockups,
iterated on color palette, layout density, chart label rendering, type
scale, and interactive states):

- Replace the hueless grayscale 'mist' preset with a permanent dark theme
  (near-black #0a0a0a background, indigo #818cf8 accent) — force via a
  'dark' class on <html> rather than a light/dark toggle, per request.
- Card-based event list (badge for channel, capacity/start metadata) in
  place of bare underlined links; consistent border/card treatment across
  all three pages and the nav.
- Larger, more legible type scale (28px page titles, 16px body, 13px
  meta/nav) — badges stay at text-xs (12px) by design.
- Interactive elements (Run report, Delete, Clear all, pagination) now use
  the existing shadcn Button component instead of hand-styled <button>s,
  inheriting its hover/active/disabled treatment for free; plain nav/back
  links get an explicit hover color transition.
- Add the shadcn Badge component via the CLI (matching the project's
  established 'let the CLI generate real component code' convention).
- Add .superpowers/ to .gitignore (local-only scratch workspaces).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each page independently declared its own <main className="mx-auto max-w-Nxl p-8">
wrapper, with inconsistent max-widths (3xl vs 4xl) across pages — and,
combined with body's flex-col layout, content wasn't reliably filling the
available width. Move the wrapper into layout.tsx (one consistent max-w-4xl
for all three pages), leaving each page to render only its own content.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the bare 'No events yet.' text and the sparse empty-table-with-
pagination look on Webhook Logs with a shared, centered empty state (icon
in a muted circle, title, optional description) — matches the project's
convention of a reusable component + Storybook story for anything used
more than once. Webhook Logs now shows this in place of the whole
table+pagination block when there are zero logs, rather than an empty
table with '1-0 of 0' pagination controls that have nothing to page
through.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Restore the ability to inspect a webhook's raw headers and body, dropped
during the dashboard redesign. Each row expands inline via Collapsible
(chosen over Dialog so multiple entries can be compared side-by-side,
like a devtools network inspector) to reveal pretty-printed, syntax
highlighted JSON via a new JsonViewer component.

Also adds back the ID column and a Body Summary column (notification
type · event slug) that the table lost in the same redesign.

JsonViewer is hand-rolled rather than pulling in shiki/prismjs — JSON's
grammar is simple enough for a single regex pass, and this ships in a
static-exported bundle where every dependency counts. HTML-escaping
happens before span-wrapping since webhook bodies contain arbitrary
attacker-influenceable strings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Move the Created column to sit right after ID (was previously the
rightmost column), and convert it from the raw UTC DB value to
Asia/Taipei local time — the dashboard's users are all in that
timezone.

Adds a formatTaipeiDateTime() helper (lib/datetime.ts) using dayjs's
utc plugin. Taiwan never observes DST, so a fixed +8 offset is used
instead of the timezone plugin (which needs Intl tz-data support) to
keep the static-export bundle smaller. The DB timestamp format isn't
fully pinned down across backends (SQLite returns a naive
"YYYY-MM-DD HH:mm:ss" string, Postgres may include an offset), so the
helper normalizes both shapes before parsing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Delete now lives in the table row itself (right side), not inside the
expanded Collapsible panel. This also fixes an implicit issue in the
prior layout: the whole row was one CollapsibleTrigger <button>, which
would have made Delete an invalid button-inside-a-button if placed
there. The trigger is now just the chevron icon, sized as its own
small hit target; the rest of the row (id, created, method, channel,
body, delete) are plain siblings in the same CSS grid row.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Result count on the far left, Previous/Next grouped on the far right,
instead of all three left-aligned as one group.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New PaginationFooter (components/pagination-footer.tsx): a controlled,
list-agnostic offset/limit pagination bar (result count + Previous/Next).
It only renders the controls — how the paginated items themselves are
displayed stays entirely with the caller, since webhook logs and future
paginated views (e.g. events) don't share a rendering shape.

webhook-logs/page.tsx now uses it in place of its inline pagination
JSX; behavior is unchanged. Adds a Storybook story covering the middle,
first, and last page states.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 16.80672% with 198 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.87%. Comparing base (2530b1e) to head (46b126c).

Files with missing lines Patch % Lines
frontend/app/webhook-logs/page.tsx 0.00% 64 Missing ⚠️
frontend/app/page.tsx 0.00% 44 Missing ⚠️
frontend/app/events/page.tsx 0.00% 36 Missing ⚠️
frontend/components/json-viewer.tsx 0.00% 15 Missing ⚠️
frontend/apis/auth.ts 0.00% 6 Missing ⚠️
frontend/apis/events.ts 0.00% 6 Missing ⚠️
frontend/app/layout.tsx 0.00% 6 Missing ⚠️
frontend/components/pagination-footer.tsx 0.00% 5 Missing ⚠️
src/argus/main.py 0.00% 5 Missing ⚠️
frontend/apis/webhook-logs.ts 0.00% 4 Missing ⚠️
... and 3 more
Additional details and impacted files
@@             Coverage Diff             @@
##             main      #18       +/-   ##
===========================================
- Coverage   75.41%   61.87%   -13.55%     
===========================================
  Files          16       32       +16     
  Lines         720      918      +198     
  Branches        0       70       +70     
===========================================
+ Hits          543      568       +25     
- Misses        177      346      +169     
- Partials        0        4        +4     
Flag Coverage Δ
frontend 15.72% <15.72%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

jack482653 and others added 3 commits August 27, 2026 21:44
No live caller reaches the Jinja2-render branch anymore: the frontend
links to event details via /dashboard/events?slug=<slug> (query
string), not the old path-segment shape, and nothing else in the repo
generates that legacy URL. The route's other job — keeping Next's
static-export client-navigation payload files (index.txt, __next.*)
from being swallowed as spurious slug lookups, since this dynamic
route was registered before the StaticFiles mount — is now handled
correctly by StaticFiles itself once the intercepting route is gone.

- Drop dashboard_event and its now-unused helpers
  (_session_email_or_redirect, _format_start_at_local) from
  src/argus/dashboard/router.py, along with the imports/module-level
  state (Jinja2Templates, _TEMPLATES_DIR, _EVENTS_STATIC_DIR, FileResponse)
  that only existed to support it.
- Delete the now-orphaned Jinja2 templates (event.html, _base.html)
  and their pyproject.toml package-data entry.
- Remove the three tests in tests/test_auth.py that covered this
  route's behavior; the static-file passthrough it used to hand-roll
  is now just Starlette's StaticFiles, which doesn't need its own
  test here.
- Update SPEC.md and README.md to drop references to the retired
  route and its "legacy bookmark" carve-out.

Verified: full backend + docker-integration test suite (32 passed),
ruff check/format, uv lock --check, and a rebuilt local Docker image —
confirmed the old path-style URL now 404s, Next's static payload files
under /dashboard/events/* still serve correctly via StaticFiles alone,
and the real /dashboard/events?slug=... page still renders end-to-end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The grid row (ID/Created/Method/Channel/Body/Delete) doesn't fit a
narrow viewport — Channel, Body, and Delete were getting clipped by
the container's overflow-hidden. Below the tablet breakpoint (640px),
each row now renders as a labeled, vertically-stacked card instead;
the column header row is hidden at that width since it no longer
applies. The existing grid layout is unchanged from tablet: up.

Both layouts render in the DOM simultaneously, toggled via
tablet:hidden / hidden tablet:grid — the hidden one is display:none,
so it's excluded from the accessibility tree and tab order, and the
shared Collapsible state keeps them in sync regardless of which is
visible.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces truncate (single-line ellipsis) with break-words so the full
summary text ("<type> · <slug>") is visible across multiple lines
instead of getting cut off with "...". Applies to both the tablet+
grid row and the mobile stacked card. min-w-0 is required alongside
break-words on both — without it, the flex/grid item's default
min-width: auto would let the intrinsic content width push past its
track/container before wrapping ever kicks in.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the justify-between label/value rows (Created, Channel, Body)
with a stacked flex-col layout — smaller label on top, value below.
justify-between read as visually noisy with a right-aligned value
next to a left-aligned label, especially once Body started wrapping
to multiple lines. The ID/Method header line is unchanged, since it
reads as a card title rather than a labeled field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread .github/workflows/ci.yml
with:
files: coverage.xml

frontend:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frontend tests are run without coverage, so the current Codecov result only represents the backend. Since this PR adds most of its executable code under frontend/, could we generate an LCOV report here and upload it with a frontend flag?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've generate an LCOV report here and upload it with a frontend flag (see commit 32ad956). CI confirms it: the upload log shows --flag frontend finding and uploading frontend/coverage/lcov.info, and the Codecov PR comment now lists frontend files under "Files with missing lines" (e.g. frontend/app/webhook-logs/page.tsx).

@rockleona rockleona left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest to remove the directory below:

  • docs/superpowers: since monorepo migration is done, these are good to remove
  • frontend/.agents/skills/migrate-radix-to-base: migration was done

Also a comment for the workflow.

Comment thread .github/workflows/ci.yml Outdated
Comment on lines +49 to +55
- name: Enable corepack
run: corepack enable

- name: Set up Node
uses: actions/setup-node@v5
with:
node-version: "22"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me it's kinda weird, corepack enable should be execute after node installation?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 2f81ac2 (Set up Node now runs before Enable corepack). CI passes with the new order (Frontend + Test both green).

@chestercheng chestercheng left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not sure we need all of the frontend/.agents/skills files, but I’m okay with merging them first and tuning things afterward.

Comment thread frontend/app/page.tsx

const handleTriggerReport = () => {
startTransition(async () => {
await triggerReport();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add error handling here and surface the failure through the existing error state?

jack482653 and others added 3 commits September 5, 2026 18:33
Codecov's project-coverage number only ever reflected the backend
(coverage.xml) — the Frontend CI job ran vitest without coverage at
all, so a PR adding a lot of untested frontend code (as this one does)
wouldn't show it.

- frontend/vitest.config.ts: enable v8 coverage (text + lcov
  reporters), scoped via `include` to the real source directories
  (apis/, app/, components/, configurations/, hooks/, lib/) and
  excluding components/ui/** (shadcn-generated primitives, not
  hand-authored) and *.d.ts. Vitest 4's v8 provider reports every
  included file — even ones no test imports — as 0%, so an untested
  page or component shows up in the report instead of being silently
  absent from it, matching how the backend's `pytest --cov=argus`
  already surfaces untested modules.
- frontend/package.json: new `test:coverage` script
  (`vitest run --project unit --coverage`); `test` is left as-is for
  fast local iteration.
- .github/workflows/ci.yml: the Frontend job now runs `test:coverage`
  and uploads frontend/coverage/lcov.info to Codecov with the
  `frontend` flag, alongside the existing unflagged backend upload.
- frontend/eslint.config.mjs: ignore the generated coverage/ directory
  (was already gitignored, but lint was walking it directly).

Verified locally: `pnpm test:coverage` produces
frontend/coverage/lcov.info covering all 16 real source files (event-chart.tsx,
lib/datetime.ts, and hooks/use-require-auth.ts show real per-line hits from
their existing tests; untested pages/components correctly show 0%, not
absent). Full lint/tsc/vitest(unit+storybook)/build suite still green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
corepack writes its pnpm/yarn shims next to whichever node is first on
PATH at the time it runs. Enabling it before setup-node targeted
whatever Node the runner image happened to have preinstalled, not the
version setup-node switches PATH to — it only worked here because the
runner's default Node already matched the requested version 22.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…kill

Per review feedback on PR sciwork#18:
- rockleona: docs/superpowers (the design spec + implementation plan
  for this monorepo migration) is done serving its purpose now that
  the migration has landed — remove it, and gitignore the directory
  so future superpowers-driven work doesn't recommit specs/plans here
  by default once they've shipped.
- rockleona: frontend/.agents/skills/migrate-radix-to-base was only
  ever needed for this repo's one-time Radix→Base UI migration, which
  is done. Removed and gitignored.

frontend/.agents/skills/shadcn stays tracked — chestercheng's comment
on it was "not sure we need all of it, but okay to tune afterward",
not a removal request, and it's still actively useful for any future
shadcn/ui component work in this repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@jack482653

Copy link
Copy Markdown
Collaborator Author

Addressed in 46b126c:

  • Removed docs/superpowers/ (design spec + implementation plan) — its job is done now that the migration has landed, and the directory is now gitignored so it doesn't come back by default.
  • Removed frontend/.agents/skills/migrate-radix-to-base — one-time-use skill for this repo's Radix→Base UI migration, which is done. Also gitignored.
  • Kept frontend/.agents/skills/shadcn tracked — it's still actively useful for future shadcn/ui component work here, and @chestercheng's comment on it wasn't a removal request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants