Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b93a44d
docs: add frontend monorepo integration design spec
jack482653 Aug 15, 2026
41f9a6b
docs: add frontend monorepo integration implementation plan
jack482653 Aug 15, 2026
7bc1b71
feat: fold argus-dashboard into frontend/
jack482653 Aug 15, 2026
577c57d
feat: multi-stage Docker build for the frontend static export
jack482653 Aug 15, 2026
d77f6bd
ci: add frontend lint/build job
jack482653 Aug 15, 2026
9a9cdf1
feat: add GET /dashboard/api/me for frontend session bootstrap
jack482653 Aug 15, 2026
c9ce6e3
feat: serve the built frontend at /dashboard, retire two Jinja2 routes
jack482653 Aug 15, 2026
19420a2
feat(frontend): add API client and session auth-check hook
jack482653 Aug 15, 2026
3bb160d
feat(frontend): build the real event-list home page
jack482653 Aug 15, 2026
c28f882
feat(frontend): add event detail page with Recharts-based chart
jack482653 Aug 15, 2026
7235f8d
feat(frontend): add webhook logs page
jack482653 Aug 15, 2026
17b985f
fix(frontend): await reload() in webhook-logs handlers
jack482653 Aug 15, 2026
d80480a
feat(frontend): proxy API calls to the backend during next dev
jack482653 Aug 15, 2026
ee6d885
chore: fix ruff format drift in main.py
jack482653 Aug 15, 2026
ab8553c
fix(dashboard): serve Next's static events/* payload files before the…
jack482653 Aug 15, 2026
8cae131
feat(frontend): nav/logout, report+delete actions, error handling, sa…
jack482653 Aug 15, 2026
4fc82bc
chore: run frontend unit tests in CI, refresh docs, ignore built fron…
jack482653 Aug 15, 2026
61ea4c9
fix(frontend): target the backend origin for auth navigations in dev
jack482653 Aug 15, 2026
d897715
feat(frontend): dark OLED theme with indigo accent, card-based layout
jack482653 Aug 15, 2026
9ca7e7e
refactor(frontend): hoist the shared <main> wrapper into the root layout
jack482653 Aug 16, 2026
182904b
feat(frontend): add a reusable EmptyState component
jack482653 Aug 16, 2026
a4ccae6
feat(frontend): add expandable header/body view to webhook logs
jack482653 Aug 16, 2026
425462a
feat(frontend): show webhook log timestamps in Taipei time next to ID
jack482653 Aug 16, 2026
c91aaf6
fix(frontend): move webhook log Delete button back to the row
jack482653 Aug 16, 2026
739ea7d
style(frontend): pin webhook log pagination footer left/right
jack482653 Aug 16, 2026
3bd03ca
refactor(frontend): extract pagination footer into a shared component
jack482653 Aug 22, 2026
254c697
refactor(dashboard): remove the legacy /dashboard/events/{slug} route
jack482653 Aug 27, 2026
8de4c36
fix(frontend): stack webhook log rows into cards below tablet width
jack482653 Aug 27, 2026
52e05b7
fix(frontend): wrap webhook log Body summary instead of truncating
jack482653 Aug 27, 2026
eac02a7
fix(frontend): stack label above value in the mobile webhook log card
jack482653 Aug 27, 2026
32ad956
ci: add frontend LCOV coverage, uploaded to Codecov with a frontend flag
jack482653 Sep 5, 2026
2f81ac2
ci: run corepack enable after actions/setup-node
jack482653 Sep 5, 2026
46b126c
chore: drop docs/superpowers and the finished migrate-radix-to-base s…
jack482653 Sep 5, 2026
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
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ __pycache__
*.py[cod]
argus.db
*.db
frontend/node_modules
frontend/.next
frontend/out
41 changes: 41 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,44 @@ jobs:
uses: codecov/codecov-action@v7
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).

name: Frontend
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v7

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

- name: Enable corepack
run: corepack enable

- name: Install dependencies
working-directory: frontend
run: pnpm install --frozen-lockfile

- name: Lint
working-directory: frontend
run: pnpm lint

# Scoped to the "unit" vitest project only — the Storybook/Playwright
# browser-based component tests require installing a browser and are
# deliberately left as a local-only check for now.
- name: Test
working-directory: frontend
run: pnpm test:coverage

- name: Upload coverage report
uses: codecov/codecov-action@v7
with:
files: frontend/coverage/lcov.info
flags: frontend

- name: Build
working-directory: frontend
run: pnpm build
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,13 @@ dist/
.claude/
CLAUDE.md
AGENTS.md

# Built frontend, copied into the backend package at build time
src/argus/dashboard/frontend/

# Superpowers scratch workspaces (SDD ledgers, brainstorm mockups) — local only
.superpowers/

# Superpowers specs/plans — useful during active design/implementation,
# not worth keeping once the work they describe has landed
docs/superpowers/
13 changes: 13 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
FROM node:22-slim AS frontend-build

WORKDIR /frontend
RUN corepack enable && corepack prepare pnpm@10.33.0 --activate

COPY frontend/package.json frontend/pnpm-lock.yaml frontend/pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile

COPY frontend ./
RUN pnpm build


FROM python:3.12-slim-bookworm

LABEL org.opencontainers.image.source="https://github.com/sciwork/argus"
Expand All @@ -9,6 +21,7 @@ WORKDIR /app

COPY pyproject.toml README.md ./
COPY src ./src
COPY --from=frontend-build /frontend/out ./src/argus/dashboard/frontend

RUN pip install --no-cache-dir .

Expand Down
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ URL: `https://your-domain/webhook/kktix/sprint`, env var: `DISCORD_WEBHOOK_SPRIN
A Google-OAuth-protected web UI for viewing per-event registration time series.

- **Event list:** `/dashboard`
- **Per-event chart:** `/dashboard/events/{slug}` — line chart of Total + each ticket type, with capacity (horizontal dashed) and event start (vertical dashed) reference lines.
- **Per-event chart:** `/dashboard/events?slug=<slug>` — line chart of Total + each ticket type, with capacity (horizontal dashed) and event start (vertical dashed) reference lines.

### One-time Google OAuth setup

Expand All @@ -87,8 +87,11 @@ A Google-OAuth-protected web UI for viewing per-event registration time series.

### Try it locally

The dashboard UI is a Next.js app (`frontend/`) built to static files and served same-origin by the backend. `uvicorn` alone won't serve it in a fresh checkout — build the frontend first (or use the `next dev` proxy workflow), see [Development](#development) below.

```bash
set -a && source .env && set +a
cd frontend && pnpm install && pnpm build && cd .. # one-time (or after frontend changes)
uv run uvicorn argus.main:app --host 0.0.0.0 --port 8000
# open http://localhost:8000/dashboard
```
Expand All @@ -100,6 +103,7 @@ You will be redirected to Google to sign in. Only emails in `ALLOWED_EMAILS` are
When deploying (e.g. to Railway):

- **Railway builds the Dockerfile** using `python:3.12-slim-bookworm`, installs the package with `pip install .`, and starts uvicorn via `railway.json` `startCommand`. Railway injects `$PORT` and the start command binds to it.
- **The frontend build is automatic** — the Dockerfile's first stage builds `frontend/` (`pnpm install && pnpm build`) and copies its static export into `src/argus/dashboard/frontend/` before the Python stage installs the package. No manual frontend build step is needed for Docker/Railway deploys.
- **For SQLite, mount a persistent volume** at `/data` and set `DATABASE_URL=sqlite:////data/argus.db`. SQLite written to the container's local filesystem will be wiped on every redeploy.
- **`SESSION_SECRET` is required** — the app refuses to boot without it. Generate with `python -c "import secrets; print(secrets.token_hex(32))"`.
- **Port:** the Dockerfile's `CMD` binds to a fixed port 8000. Railway overrides this via `railway.json`'s `startCommand`, which substitutes its injected `$PORT`. To change the port in non-Railway environments, override the container command (e.g. `docker run … argus-image uvicorn argus.main:app --host 0.0.0.0 --port 9000`).
Expand All @@ -124,3 +128,27 @@ uv run ruff format src tests # format
# Visual inspection of Discord report (sends a real webhook):
ARGUS_MANUAL_TEST=1 uv run pytest tests/test_discord_format_manual.py -v -s
```

### Frontend (`frontend/`)

The dashboard UI lives in `frontend/` — a Next.js app using `pnpm` (not `npm`), statically exported (`next build`) into `src/argus/dashboard/frontend/`, and served same-origin by the backend under `/dashboard` (no separate frontend server or CORS setup in production). This copy step happens automatically in Docker's multi-stage build; locally you have two options:

1. **Build once, run `uvicorn` normally** — full same-origin experience, matches production:
```bash
cd frontend && pnpm install && pnpm build && cd ..
uv run uvicorn argus.main:app --host 0.0.0.0 --port 8000
# open http://localhost:8000/dashboard
```
Re-run `pnpm build` after frontend changes to see them.

2. **`next dev` + dev proxy** — for active frontend development with hot reload, run both processes side by side:
```bash
# terminal 1
uv run uvicorn argus.main:app --host 0.0.0.0 --port 8000
# terminal 2
cd frontend && pnpm install && pnpm dev
# open http://localhost:3000/dashboard
```
`frontend/next.config.ts` proxies `/dashboard/api/*` calls from the `next dev` server (port 3000) to `uvicorn` (port 8000), so no CORS configuration is needed.

Other frontend commands (run from `frontend/`): `pnpm lint`, `pnpm test` (vitest unit/component tests), `pnpm exec tsc --noEmit`.
23 changes: 13 additions & 10 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ Argus uses a **vertical slice** layout: each feature owns its full stack (HTTP r
| `GET` | `/dashboard/login` | — | Start Google OAuth flow | [Dashboard](#dashboard) |
| `GET` | `/dashboard/oauth/callback` | — | OAuth redirect target | [Dashboard](#dashboard) |
| `GET` | `/dashboard/logout` | — | Clear session, redirect to login | [Dashboard](#dashboard) |
| `GET` | `/dashboard` | session (HTML) | Event list page | [Dashboard](#dashboard) |
| `GET` | `/dashboard/events/{slug}` | session (HTML) | Per-event chart page | [Dashboard](#dashboard) |
| `GET` | `/dashboard/webhook-logs` | session (HTML) | Webhook log viewer page | [Dashboard](#dashboard) |
| `GET` | `/dashboard` | public (shell) | Event list page (static Next.js export) | [Dashboard](#dashboard) |
| `GET` | `/dashboard/events` | public (shell) | Per-event chart page, `?slug=<slug>` (static Next.js export) | [Dashboard](#dashboard) |
| `GET` | `/dashboard/webhook-logs` | public (shell) | Webhook log viewer page (static Next.js export) | [Dashboard](#dashboard) |
| `GET` | `/dashboard/api/me` | session (401) | JSON: currently authenticated user | [Dashboard](#dashboard) |
| `GET` | `/dashboard/api/events` | session (401) | JSON: event list | [Dashboard](#dashboard) |
| `GET` | `/dashboard/api/events/{slug}/timeseries` | session (401) | JSON: per-event time series | [Dashboard](#dashboard) |
| `DELETE` | `/dashboard/api/events/{slug}` | session (401) | Permanently delete event + its tickets | [Dashboard](#dashboard) |
Expand All @@ -66,6 +67,7 @@ Argus uses a **vertical slice** layout: each feature owns its full stack (HTTP r

**Auth column legend:**
- `x-kktix-secret header` — request must include header matching `WEBHOOK_SECRET` (constant-time compared)
- `public (shell)` — served with no server-side session check at all; the page itself is a public static shell, and it protects its own data by calling `/dashboard/api/*` routes, which each require the session cookie client-side (a request without a valid session gets a 401 from those API calls, not from the page)
- `session (HTML)` — protected by signed session cookie; missing/invalid → 302 to `/dashboard/login`
- `session (401)` — same protection but JSON routes return 401 instead of redirecting

Expand All @@ -91,10 +93,9 @@ argus/
│ │ ├── __init__.py
│ │ ├── router.py # /dashboard/* routes
│ │ ├── queries.py # time series queries
│ │ └── templates/
│ │ ├── _base.html # shared layout
│ │ ├── index.html # event list
│ │ └── event.html # per-event chart
│ │ └── frontend/ # built Next.js static export, copied in at
│ │ # build time (gitignored); served by main.py's
│ │ # StaticFiles mount at /dashboard
│ │
│ │ # ── shared infrastructure ──
│ ├── auth.py # OAuth client + require_login dependency (reusable)
Expand All @@ -113,6 +114,8 @@ argus/
│ ├── conftest.py
│ ├── test_*.py # automated tests
│ └── test_discord_format_manual.py # opt-in test that sends real Discord webhooks
├── frontend/ # Next.js dashboard UI (source); builds into
│ # src/argus/dashboard/frontend/ (see above)
├── .env.example
├── pyproject.toml
├── railway.json
Expand Down Expand Up @@ -391,19 +394,19 @@ A web UI that visualizes registration trends per event over time. Implemented as

### Routes

See [API Reference](#api-reference) for the canonical list. All routes under `/dashboard/*` (except `login` and `oauth/callback`) require an authenticated session. HTML routes redirect to `/dashboard/login` on failure; JSON API routes return `401`.
See [API Reference](#api-reference) for the canonical list. The dashboard UI itself is a statically-exported Next.js app served same-origin under `/dashboard`, `/dashboard/events`, and `/dashboard/webhook-logs` — these three pages are public shells with no server-side session check; each gates its own content client-side by calling `/dashboard/api/me` (and other `/dashboard/api/*` routes) and redirecting to `/dashboard/login` in the browser if that call 401s. Every `/dashboard/api/*` route requires the session cookie and returns `401` if it's missing or invalid.

### Authentication

Server-side OAuth 2.0 with Google as the identity provider. After successful OAuth, the user's email is checked against `ALLOWED_EMAILS`. If allowed, a signed session cookie is set.

**Flow:**

1. Visit `/dashboard` (or any protected route) without session → 302 to `/dashboard/login`
1. Visit `/dashboard` without a session → the static shell loads, its client-side `/dashboard/api/me` call 401s, and the browser is redirected to `/dashboard/login`
2. `/dashboard/login` → 302 to Google consent screen
3. Google → `/dashboard/oauth/callback?code=...`
4. Backend exchanges code for `id_token`, verifies email is in `ALLOWED_EMAILS`
5. On success: session cookie written, 302 to original destination (or `/dashboard`)
5. On success: session cookie written, 302 to `/dashboard`
6. On rejection: 403 page

Session is signed using `SESSION_SECRET` via Starlette's `SessionMiddleware`.
Expand Down
Loading