feat: fold frontend dashboard into the monorepo - #18
Conversation
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 Report❌ Patch coverage is 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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>
| with: | ||
| files: coverage.xml | ||
|
|
||
| frontend: |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
Suggest to remove the directory below:
docs/superpowers: since monorepo migration is done, these are good to removefrontend/.agents/skills/migrate-radix-to-base: migration was done
Also a comment for the workflow.
| - name: Enable corepack | ||
| run: corepack enable | ||
|
|
||
| - name: Set up Node | ||
| uses: actions/setup-node@v5 | ||
| with: | ||
| node-version: "22" |
There was a problem hiding this comment.
For me it's kinda weird, corepack enable should be execute after node installation?
There was a problem hiding this comment.
Good catch — fixed in 2f81ac2 (Set up Node now runs before Enable corepack). CI passes with the new order (Frontend + Test both green).
chestercheng
left a comment
There was a problem hiding this comment.
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.
|
|
||
| const handleTriggerReport = () => { | ||
| startTransition(async () => { | ||
| await triggerReport(); |
There was a problem hiding this comment.
Should we add error handling here and surface the failure through the existing error state?
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>
|
Addressed in 46b126c:
|
Summary
Folds the previously-separate
argus-dashboardfrontend into this repo asfrontend/and serves it same-origin from FastAPI, replacing the oldJinja2-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"), pureclient-side rendering — no SSR, avoiding the RCE surface SSR previously
exposed.
StaticFiles, served from thesame origin/port as the API — no CORS, no separate token-based auth; the
frontend reuses the existing signed session cookie.
GET /dashboard/api/meadded for the frontend's session bootstrap; twolegacy Jinja2 dashboard routes retired in favor of the static pages.
the backend image; CI gets a frontend lint/build job and frontend unit
tests.
Frontend pages
webhook logs, each backed by the existing dashboard JSON API.
expand via
Collapsibleshowing pretty-printed, syntax-highlightedrequest 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).
PaginationFootercomponent (offset/limit + Previous/Next) reusedby webhook logs, ready for
eventsonce that endpoint gets pagination.EmptyStatecomponent for empty list states.single
<main>wrapper hoisted into the root layout instead of each pageduplicating it.
Fixes along the way
explicitly instead of the frontend dev server's own origin.
events/*client-navigation payload filesbefore the legacy dynamic route would otherwise swallow them.
Verification
uv run --locked --group dev ruff check src testsuv run --locked --group dev ruff format --check src testsuv run --locked --group dev pytest tests/ -W error --cov=argus --cov-report=term-missing --cov-report=xml(35 passed)pnpm lintpnpm exec tsc --noEmitpnpm exec vitest run(26 passed, unit + Storybook projects)pnpm buildlogin flow, webhook log expand/collapse, pagination, dark theme).
🤖 Generated with Claude Code