feat(web): redesign API token modal with show/hide and inline retry#117
Conversation
Replaces the page-reload-on-save flow with a shadcn Dialog that retries the parked /api/* requests instead, surfaces an inline 401 error when the typed token still doesn't work, and links to a new #api-token section in the setup docs explaining where the value comes from. Closes #64 https://claude.ai/code/session_01WjsbdUtxDJZL3GaAvhyzav
The save flow no longer reloads — the dialog dismisses inline after the parked /api/* requests succeed on retry. Update the existing spec to match the new placeholder/button labels and 16-char minimum, and add two new specs covering the validation error and the second-401 inline error paths added in this PR. https://claude.ai/code/session_01WjsbdUtxDJZL3GaAvhyzav
# Conflicts: # app/packages/web/e2e/specs/auth-gate.spec.ts
There was a problem hiding this comment.
Pull request overview
This PR updates the web dashboard’s API-token auth flow to use an in-app shadcn Dialog (with show/hide + inline validation/errors) and replaces the previous “save + reload” behavior with an inline retry mechanism that replays requests that previously failed with 401.
Changes:
- Redesign
ApiTokenModalinto a blocking shadcnDialogwith show/hide toggle, inline validation, doc link, and inline 401 error messaging. - Refactor
api.tsso 401s enqueue “pending retries” that can be replayed after a token is saved, instead of returning a never-resolving promise or forcing a full reload. - Update docs and Playwright e2e coverage to reflect the new inline-retry flow (including validation + second-401 paths).
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| docs/docs/setup.md | Adds an ### API token section and updates Docker instructions to reflect “Save” (no reload). |
| app/packages/web/src/components/ApiTokenModal.tsx | Replaces bespoke modal with shadcn Dialog, adds show/hide, validation, inline retry + error states, and docs link. |
| app/packages/web/src/App.tsx | Switches from conditional rendering to always rendering the dialog overlay controlled by needsToken. |
| app/packages/web/src/api.ts | Introduces pendingRetries queue + retryPendingAfterAuth() to replay requests after auth succeeds. |
| app/packages/web/e2e/specs/auth-gate.spec.ts | Updates existing auth-gate test for inline dismissal and adds new validation + “second 401” coverage. |
| app/packages/web/e2e/pages/AuthGatePage.ts | Updates page object locators/semantics for the new dialog UI. |
| 1. **`API_TOKEN` environment variable** — wins, even when set to empty (use | ||
| this to deliberately disable auth in scripts/CI). Required when running | ||
| under `NODE_ENV=production`; the server refuses to boot otherwise. | ||
| 2. **`api_token` field in `app/server_config.json`** — the persisted file | ||
| bind-mounted by `docker-compose.yml`. Edit it directly or let the | ||
| server write it via `/api/config`. |
There was a problem hiding this comment.
Fixed in e0304fe. The bullet now reads:
API_TOKENenvironment variable — takes precedence overserver_config.jsonwhen set, including when set to empty. An empty value is normalized to "no token configured" and prevents the config file from being consulted, but it is not a supported way to disable auth —NODE_ENV=productionstartup fails when neither source supplies a non-empty token.
The second bullet also notes "Used when API_TOKEN is absent" to make the two-source resolution unambiguous.
Generated by Claude Code
There was a problem hiding this comment.
Added in e0304fe — ApiTokenModal.test.tsx with 14 specs covering:
- Dialog visibility (
opentrue/false) - Password show/hide toggle
- Inline validation: whitespace token, short token, live re-validate on input change
- No network call when client validation fails
setStoredApiToken+retryPendingAfterAuthcalled on valid submitonSuccesscalled when retry resolvestrue- Inline server error shown when retry resolves
false; cleared on next input change onSuccessnot called when retry resolvesfalse
Generated by Claude Code
- Add 14 Vitest+RTL specs covering: dialog visibility, password show/hide toggle, inline validation (whitespace / short token / live re-validate), submission → setStoredApiToken + retryPendingAfterAuth, onSuccess on accepted token, inline server error on 401 retry, and server-error cleared on input change. - Correct the API_TOKEN bullet in docs/docs/setup.md: empty env var prevents the config file from being consulted but is not a supported way to disable auth (production startup fails either way); both sources satisfy the production non-empty-token requirement. https://claude.ai/code/session_01WjsbdUtxDJZL3GaAvhyzav
| a non-empty token. | ||
| 2. **`api_token` field in `app/server_config.json`** — the persisted file | ||
| bind-mounted by `docker-compose.yml`. Used when `API_TOKEN` is absent. | ||
| Edit it directly or let the server write it via `/api/config`. |
There was a problem hiding this comment.
Good catch — ConfigService.saveConfig() writes only the WatchdogConfig shape, so calling /api/config with an existing api_token in the file would actually clobber it. Fixed in c2e8b09:
Edit the file directly; the dashboard's
/api/configendpoint only manages watchdog settings and does not write the token.
Generated by Claude Code
The previous wording suggested operators could let the server write the token via /api/config, but ConfigService.saveConfig() only writes watchdog fields and would actually clobber an existing api_token in server_config.json. Spell out that the file must be edited directly. https://claude.ai/code/session_01WjsbdUtxDJZL3GaAvhyzav
Resolves App.tsx/DashboardPage/CostsPage/DiscordPage/LogsPage conflicts between the polling indicator work (#65) and the page-redesign series that landed on main (#77, #79, #115, #116, #117). - App.tsx now wraps the redesigned ApiTokenModal (rendered inline as a Dialog overlay rather than swapping the tree) inside PollingProvider + GameStatusProvider so polling persists across the auth-token flow. - Each redesigned page (Dashboard, Costs, Discord, Logs) now hosts a PollingIndicator next to its existing header chrome (search input, range selector, ServerlessBadge, LIVE badge). - LogsPage.test.tsx mounts the page through a PollingProvider wrapper so the new indicator can read the registry under jsdom. - polling.spec.ts updated to use main's page-object fixtures (DashboardPage / AppLayout) and the redesigned STOPPED status label.
Closes #68 ## Summary - **Dashboard**: replaced bare "No games configured" text with a shadcn `Card` showing a Server icon, "No games deployed" heading, a one-paragraph Terraform explanation, and two CTA links — "Open setup guide" (docs site) and "Edit `terraform.tfvars`" (example file on GitHub). - **Discord wizard**: enhanced `SetupWizard` to accept `cfg` and show live `CheckCircle2` checkmarks as each precondition is met (client ID set → step 1, both secrets set → step 2, interactions URL present → step 3, guild added → step 4). Wizard now shows whenever `allowedGuilds` is empty, not only when both guilds and token are absent. - **Watchdog**: added a `tooltip` prop to the `Field` helper and wrapped each label with a `HelpCircle` icon + shadcn `Tooltip` carrying plain-language help text for all three inputs. - **API token modal**: already had the "Where do I find this?" link from #117 — no changes needed. ## Test plan - [ ] Unit tests: 339 passing (`npm run app:test`) - [ ] E2e tests: 56 passing (`npm run app:test:e2e`) - [ ] Navigate to `/` with no games configured — card with "No games deployed", setup guide link, and `terraform.tfvars` link renders. - [ ] Navigate to `/discord` with `allowedGuilds: []` — "Get started" wizard renders with numbered steps. - [ ] Configure client ID only — step 1 shows a green check, steps 2–4 remain numbered. - [ ] Add bot token + public key — step 2 gains a green check. - [ ] Navigate to `/settings` — hover each watchdog label to confirm tooltip text appears. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes #64
Summary
Dialog(focus trap, scroll-lock, ESC/overlay dismissal disabled), Eye/EyeOff show-hide toggle, and an external "Where do I find this?" link to a new#api-tokenanchor indocs/docs/setup.md.api.tsso 401s park their request in apendingRetriesqueue instead of returning a hanging promise;retryPendingAfterAuth()re-issues the queue with the new bearer when the operator saves a token. The dialog dismisses inline on success and surfaces an inlineInvalid token — check 'app/server_config.json' or 'API_TOKEN'error if the retry comes back 401 again. No morewindow.location.reload().< 16-char tokens before any network call (server still has the final say via 401).AuthGatePagepage object now points at the new placeholder/button labels; the existing save spec is rewritten to assert inline dismissal (no reload), and two new specs cover the validation-error and second-401 paths.### API tokensubsection (anchor#api-token) todocs/docs/setup.mddocumenting precedence, generation, storage, and rotation; trims the now-stale "Save & reload" reference under the Docker section.Test plan
npm run app:lintfrom repo root passes.npm run app:buildproduces a working production bundle.npm run app:test— full unit/integration suite stays green (274/274 locally).npm run app:test:e2e— auth-gate suite passes against the production build, including the two new specs.localStorage.apiToken, hit a protected route, paste a valid token — modal dismisses without a page reload and the dashboard renders. Repeat with a wrong token — inline 401 error appears and the modal stays open.docs/docs/setup.md#api-tokenresolves on the published Docusaurus site.https://claude.ai/code/session_01WjsbdUtxDJZL3GaAvhyzav
Generated by Claude Code