feat: fold frontend dashboard into the monorepo - #18
jack482653 wants to merge 43 commits into
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% 60.85% -14.57%
===========================================
Files 16 34 +18
Lines 720 940 +220
Branches 0 58 +58
===========================================
+ Hits 543 572 +29
- Misses 177 364 +187
- 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?
There was a problem hiding this comment.
Fixed — handleTriggerReport (and every other UI-triggered action that was silently swallowing failures: delete event, delete/clear webhook logs) now shows an error toast on failure. See 9f8ad2a for the full react-hot-toast refactor and 13b842d for a follow-up that swaps the raw err.message for a fixed, friendly toast message (the real error still goes to console.error).
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:
|
Per review feedback on PR sciwork#18: UI-triggered actions (Run report now, delete event, delete/clear webhook logs) had no error feedback at all if their API call failed - only the initial page-load fetch showed anything, via an inline "Failed to load: ..." message. - Mount a single <Toaster /> in the root layout, styled to match the dashboard's dark theme. - Every .then()/.catch() promise chain is rewritten as async/await with try/catch; every catch calls toast.error(...) - both for initial-load failures (previously inline text) and for the previously-silent action handlers. - Each independent async operation gets its own useTransition instead of sharing one, per this repo's existing convention: page.tsx and webhook-logs/page.tsx each now have separate transitions for the initial load, per-item delete, and the page-level action (trigger report / clear all) - sharing one was the actual bug root cause, since clicking "Run report now" would otherwise also flip the events list into its Loading view. - Dropped the local `error` state var + its inline render in all three pages. A failed load now converges on the same EmptyState already used for "genuinely no data" - toast carries the failure detail, the page itself doesn't need a second, redundant error rendering path. events (EventSummary[]) and the webhook-logs page (WebhookLogsPage) both drop their `| null` type in favor of an empty default value, now that "still loading" is tracked by the transition's own isPending instead of a null sentinel. - app/events/page.tsx gains two EmptyState cases it didn't have before (no slug selected; slug given but no data), instead of leaving the page stuck on "Loading..." forever after a failed fetch. Its timeseries stays nullable - no reasonable non-null default exists for this single-object payload, unlike the two list pages. Verified via a rebuilt local Docker image: forced a real network failure (browser set offline) and confirmed the toast fires with the correct message and dark-theme styling in Webhook Logs; forced a real 404 on the event-detail page and confirmed both the toast and the "No data available" EmptyState render together; confirmed the "No events yet" and "No webhook events yet" empty states render immediately (no loading-state flash) after clearing all data via the API. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Toasts were showing err.message directly, which is a raw axios/backend
string ("Network Error", "Request failed with status code 404", ...)
not meant for end users. Each catch now shows a fixed, friendly
message via toast.error and logs the actual error to the console via
console.error for debugging.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Root-caused via a real local test event (5x-works org): tagging an event with the "測試" category removes the application/ld+json block from its KKTIX page, which argus.kktix.scraper relies on for start_at. capacity scraping is unaffected (different HTML source), so this silently manifests as only start_at staying null, easy to miss. Confirmed the parser itself is fine against real production data — sciwork.kktix.cc's scisprint events (untagged as Test) still serve JSON-LD with startDate present. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Events on the same month/day in different years were indistinguishable (both rendered as e.g. "Dec 1"). Extracted the previously-duplicated formatStartDate from app/page.tsx and app/events/page.tsx into a single formatEventStartDate in lib/datetime.ts, now including year: "numeric". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Below tablet width: - The header (title/email + Run report now) stacks into a column, left-aligned, instead of squeezing Run report now to the right of the title on a narrow screen. - Each card's channel badge / capacity / start date stack top-to-bottom instead of wrapping mid-line into an unreadable jumble; the " · " separator between capacity and start only reappears at tablet+ where they're back on one line. Also dropped the ChevronRight icon next to Delete — it read as part of the delete action rather than as a "this card links somewhere" affordance, which was the original intent. The card's existing hover/border treatment already signals it's a link without needing a second, confusingly-placed icon. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The logged-in user's email was only ever rendered on the Events page
(app/page.tsx), so it disappeared on every other page — an
inconsistency the old dashboard didn't have (it kept the email next to
Logout in the persistent nav). Fixing the display properly meant
lifting auth state out of each page.
Previously every page called useRequireAuth() independently: fetching
/dashboard/api/me itself, and repeating the same
loading/error/unauthenticated guard-clause block before rendering its
real content. Nav (in layout.tsx) had no access to that state at all,
since it lived outside any of the pages.
- contexts/auth-context.tsx: AuthProvider calls useRequireAuth() exactly
once and shares the result via context; useAuth() reads it.
- components/nav.tsx: the nav bar, extracted out of layout.tsx, now
showing "{email} · Logout" (always visible, not page-local) using
useAuth().
- components/require-auth.tsx: the loading/error/unauthenticated
handling every page used to repeat, now centralized around
{children} in layout.tsx.
- app/layout.tsx: <AuthProvider><Nav />wrapped in <main>: <RequireAuth>
{children}</RequireAuth></main></AuthProvider> — Nav always renders;
page content only renders once authenticated.
- All three pages (page.tsx, events/page.tsx, webhook-logs/page.tsx)
no longer call useRequireAuth() or handle auth state at all; they can
assume they're authenticated whenever they render.
hooks/use-require-auth.ts itself is unchanged (only its AuthState type
is now exported for the context to reuse) and is called from exactly
one place now instead of three — still just one /dashboard/api/me
fetch per page load.
Verified visually: email now shows in the nav consistently across all
three pages (Events, Webhook Logs, event detail).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The single-row nav (Argus / Webhook Logs / email / Logout) overflowed at phone widths once email was added. Below tablet width it now stacks into two left-aligned rows (brand+links, then email+Logout); tablet: restores the original single row with email/Logout pushed right. Also added truncate on the email span (with min-w-0 on its flex container) as a defensive guard against unusually long addresses, matching this project's existing truncate convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The two-row stacked nav (19e96af) read as awkward. Reverted to the original single-row layout and instead hide the email itself (hidden tablet:inline) below tablet width, keeping only Logout visible on narrow screens. Email still shows at tablet+ alongside Logout, same as before this whole nav investigation started. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tail page
Two things the legacy Jinja2 event-detail page had that this one was
missing:
- components/event-chart.tsx: add <ChartLegend content={<ChartLegendContent />} />
— the shadcn chart wrapper already exports this, it just wasn't wired
up. Colored swatches + names come from the same ChartConfig already
built for the tooltip, so no extra data plumbing needed.
- app/events/page.tsx: a "Delete event" button next to the title
(same confirm-then-delete flow as the events list), redirecting back
to / on success. Reuses the responsive title-row pattern already
established on the events list page (stacks below the title on
mobile instead of squeezing next to it).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Now that pages routinely have enough rows to scroll (webhook logs, events list), the nav needs to stay pinned instead of scrolling away with the content. sticky top-0 plus an explicit bg-background (the nav had none of its own — it relied on body's background showing through, which breaks once content scrolls underneath it) and z-10 so it sits above scrolled rows. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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