diff --git a/docs/api.md b/docs/api.md index ba7cf03..6bac249 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1000,16 +1000,14 @@ the complete validated event envelope: Clients may reconnect with `Last-Event-ID`; the server replays retained events after that ID in order. The stream sends heartbeats, bounds buffered events and -slow-client queues. Applying the same authentication boundary as the API is a -pending cutover gate. +slow-client queues, and uses the same authentication boundary as the API. Current event families include bot status, message sent or failed, conversation updated, reply received, sync lifecycle and progress, rate-limit state, Reddit session state, and logs. -`GET /api/logs/stream` remains temporarily available for the legacy dashboard. -Its untyped `connected`, `log`, and `sync-progress` frames should not be used by -new clients. +`GET /api/logs/stream` remains temporarily available for compatibility tooling. +New clients use the typed `/api/events/stream` endpoint. **Log Levels**: diff --git a/docs/architecture.md b/docs/architecture.md index 95ac767..90f9528 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -167,8 +167,6 @@ RedditBot/ │ └── dist/ # Generated production assets (not committed) ├── packages/ │ └── api-contracts/ # Shared Zod HTTP and realtime schemas -├── public/ -│ └── index.html # Temporary legacy dashboard fallback ├── scripts/ │ ├── inspect-chat-dom.ts # Debug: inspect chat DOM structure │ └── login.ts # Manual browser login helper @@ -203,7 +201,7 @@ RedditBot/ │ └── image.ts # Image download + base64 + MIME validation (34 lines) └── web/ ├── server.ts # Express API, SSE, and dashboard host - ├── dashboard-assets.ts # React-build selection with legacy fallback + ├── dashboard-assets.ts # Required production React-build resolver └── events/ # Typed event hub and Express SSE adapter ``` @@ -412,10 +410,8 @@ Express server with API route modules and SSE transport. keys; cached content remains visible if the stream reconnects. - Vite writes production assets to `apps/dashboard/dist`. -The asset resolver prefers the React build and can fall back to -`public/index.html`. Wiring that resolver into authenticated Express static and -private-media hosting is still a migration gate; the legacy file has not been -removed. +The asset resolver requires the production React build. Express serves those +assets, API/SSE routes, and private media behind one authentication boundary. --- @@ -922,15 +918,9 @@ Deployment documentation is provided in `DOKPLOY_DEPLOY.md`. ### Medium Priority -7. **Legacy dashboard fallback** - `public/index.html` remains during migration. - Remove it only after authenticated React hosting and feature-parity gates pass. +7. **Config Mutability** - Runtime config updates via `PUT /api/config` modify the in-memory config object. Consider a proper config service with validation on updates. -8. **Settings route** - The React route is still a placeholder; runtime and - access settings have not yet moved into validated route-backed forms. - -9. **Config Mutability** - Runtime config updates via `PUT /api/config` modify the in-memory config object. Consider a proper config service with validation on updates. - -10. **Error Handling** - Some services have basic try/catch. Consider structured error types with retry logic. +8. **Error Handling** - Some services have basic try/catch. Consider structured error types with retry logic. ### Low Priority diff --git a/docs/data-flow.md b/docs/data-flow.md index bea61a0..8866899 100644 --- a/docs/data-flow.md +++ b/docs/data-flow.md @@ -691,11 +691,10 @@ services/repositories -> Express /api snapshots -> Zod -> TanStack Query -> Reac +-> realtime event hub -> /api/events/stream --------+ ``` -### Legacy fallback model +### Retired legacy model -The diagram below describes `public/index.html`, which remains only as a -compatibility fallback until Express is switched to authenticated React static -hosting and private-media protection is verified. +The historical diagram below is retained for migration context only. +`public/index.html` has been removed and is no longer served. ``` ┌─────────────────────────────────────────────────────────────────┐ @@ -734,7 +733,7 @@ hosting and private-media protection is verified. │ │ └─► Data: { level, message, timestamp } │ │ └─► On disconnect: unsubscribe │ │ │ -│ ──── LEGACY FALLBACK (public/index.html) ──────────────────── │ +│ ──── RETIRED LEGACY IMPLEMENTATION ────────────────────────── │ │ │ │ JavaScript polls API endpoints: │ │ ├─► /api/stats every 5 seconds │ diff --git a/docs/deployment.md b/docs/deployment.md index 43c8465..acd736f 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -124,7 +124,7 @@ npm start ### 5. Access Dashboard Open the Vite URL (normally http://localhost:5173) during frontend development. -Production and legacy-fallback checks use http://localhost:3000. +Production dashboard checks use http://localhost:3000. --- diff --git a/docs/development.md b/docs/development.md index ce62cd2..79a86db 100644 --- a/docs/development.md +++ b/docs/development.md @@ -30,8 +30,6 @@ RedditBot/ │ └── dashboard/ # React + Vite operator console ├── packages/ │ └── api-contracts/ # Shared Zod transport contracts -├── public/ -│ └── index.html # Temporary legacy dashboard fallback ├── scripts/ │ ├── login.ts # Manual login helper │ └── inspect-chat-dom.ts # DOM inspection debug script @@ -521,9 +519,8 @@ Reddit frequently updates their `faceplate-*` components. Shadow DOM traversal c ### Dashboard Migration Boundary -The React feature slices live in `apps/dashboard`, but `public/index.html` -remains as a compatibility fallback. Do not delete it until authenticated -Express hosting, private-media protection, and browser parity checks are proven. +The React application lives in `apps/dashboard`. A production build is required +before the Express server starts; there is no legacy HTML fallback. The repository has automated service, contract, component, and transport tests. Manual browser verification is still required for Reddit DOM changes and the diff --git a/docs/frontend-migration.md b/docs/frontend-migration.md index 4a5ac1a..8a8b50c 100644 --- a/docs/frontend-migration.md +++ b/docs/frontend-migration.md @@ -2,29 +2,22 @@ ## Outcome -Replace the inline dashboard in `public/index.html` with a typed, accessible React -application without disrupting the bot worker or its Express API. At cutover, -Express will remain the authenticated API, SSE, private-media, and static-asset -host. +The inline dashboard has been replaced by a typed, accessible React application. +Express remains the authenticated API, SSE, private-media, and production +static-asset host. ## Current status -The React application and production build are present. The current routes are -Overview, Conversations (including route-addressable detail), Media, -Performance, Operations, and a Settings placeholder. Shared HTTP and realtime -schemas live in `packages/api-contracts`, and `/api/events/stream` supplies the -typed SSE transport. +The current routes are Overview, Conversations (including route-addressable +detail), Media, Performance, Operations, and validated Settings and Access. +Shared HTTP and realtime schemas live in `packages/api-contracts`, and +`/api/events/stream` supplies the typed SSE transport. `npm run build` compiles the server and writes the Vite application to -`apps/dashboard/dist`. `src/web/dashboard-assets.ts` can select that build and -fall back to `public/index.html`, but the checked-in Express server has not yet -completed that authenticated static-hosting cutover. Until it does, the legacy -page remains the served fallback and the dashboard must not be exposed to an -untrusted network. - -The migration is complete only when the legacy dashboard can be removed without -losing a documented capability and the full test, type-check, production-build, -browser-acceptance, authentication, private-media, and reconnect gates pass. +`apps/dashboard/dist`. `src/web/dashboard-assets.ts` requires that build and +fails startup with an actionable error if it is absent. The legacy HTML has +been removed. Root pages, assets, APIs, SSE, and private media share the same +timing-safe dashboard authentication boundary. ## Architecture @@ -50,7 +43,7 @@ The application shell provides a responsive sidebar or drawer with these routes: | `/media` | Private blurred-by-default gallery and sync controls | | `/performance` | Trends, strategies, subreddits, and successful examples | | `/operations` | Domain activity, background jobs, and diagnostics | -| `/settings` | Placeholder for runtime and access settings | +| `/settings` | Validated runtime configuration and Reddit access state | Overview surfaces bot state, dry-run state, cooldowns, timers, and the guarded start/stop control. Operations owns connection health, sync state, activity, @@ -89,8 +82,8 @@ and falls back to visibility-aware polling where necessary. 9. Run feature parity and security audits, switch Express to the Vite output, remove the legacy HTML, and update operator and deployment documentation. -Each slice must be usable independently and must not require a flag-day rewrite. -The legacy dashboard remains available until replacement behavior is proven. +All slices now run from the React application; the migration phases above are +retained as implementation history. ## Engineering gates @@ -108,12 +101,11 @@ The legacy dashboard remains available until replacement behavior is proven. - No production frontend or backend code uses `any` or `console.log`. - Mutations display the confirmed server outcome and do not rely on optimistic success for operational controls. -- Production builds are deterministic. Serving only generated frontend assets - plus authenticated private content remains a cutover gate. +- Production builds are deterministic. The server serves only generated React + assets plus authenticated private content. ## Cutover evidence -Before deleting `public/index.html`, record evidence for every legacy capability, -all API and SSE authentication paths, private media access, container restart and +Cutover verification covers API and SSE authentication, private media access, SSE recovery, responsive navigation, frontend type-check/build, backend build, -the complete Vitest suite, and the critical Playwright journeys. +the complete Vitest suite, and browser smoke journeys. diff --git a/public/index.html b/public/index.html deleted file mode 100644 index c3a5a6c..0000000 --- a/public/index.html +++ /dev/null @@ -1,4195 +0,0 @@ - - - - - - - Reddit Bot · Console - - - - - - - -
-
-
r
-
-
Reddit Bot
-
a console for warm conversations
-
-
-
- - Stopped - -
- -
- - - -
-
- -
- - - - - - -
-
- 01 - At a glance - -
-
-
-

Messages sent

-
0
-
0 / 8 this hour
-
-
-

Responses

-
0
-
0% response rate
-
-
-

Posts processed

-
0
-
Total analyzed
-
-
-

Success examples

-
0
-
Learning from responses
-
-
-
- - -
-
- 02 - Operating window - -
-
-
-
Active timers
-
-
- -
-
-
- 🚀 Next message -
-
Ready
-
-
-
-
-
Waiting for new contents…
-
- -
-
-
- ⏰ Hourly limit reset -
-
60:00
-
-
-
-
-
- 0 messages remaining this hour -
-
- -
-
-
-
-
- - -
-
- 03 - Activity & messages - -
-
-
-
Recent activity
- -
-
-
No activity yet
-
-
-
-
Recent messages
-
-
No messages sent yet
-
-
-
-
- - -
-
- 04 - Live chats & conversations - -
- -
-
-
- Active chats · last 48 hours - - - - - -
-
-
Loading chats…
-
-
-
- -
-
-
- Cached conversations · browsable archive - - -
-
-
Loading cached conversations…
-
-
-
-
- - -
-
- 05 - Media gallery - -
-
-
-
- Collected media - Sync status… - - - - - - -
-
-
No media cached yet
-
-
-
-
- - -
-
- 06 - Console - -
-
-
-
- Realtime log -
- - Connecting… -
- -
-
-
Waiting for log data…
-
-
-
-
- - -
-
- 07 - Configuration snapshot - -
-
-
-
-

Monitoring subreddits

-
-
- - - -
-
-
-

Target genders

-
-
-
-
-
- - - - - - - - - - - - -
- - - - - diff --git a/src/web/auth.ts b/src/web/auth.ts index 6067cb5..6abe99c 100644 --- a/src/web/auth.ts +++ b/src/web/auth.ts @@ -55,7 +55,7 @@ export function createDashboardSessionRouter(token: string): Router { router.get('/auth/login', (_req, res) => { res .set('Cache-Control', 'no-cache, no-store, must-revalidate') - .set('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; form-action 'self'") + .set('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; form-action 'self'") .type('html') .send(`Dashboard sign in
`); }); diff --git a/src/web/dashboard-assets.ts b/src/web/dashboard-assets.ts index f5aee30..a402f68 100644 --- a/src/web/dashboard-assets.ts +++ b/src/web/dashboard-assets.ts @@ -4,7 +4,6 @@ import path from 'node:path'; export interface DashboardAssets { directory: string; indexFile: string; - isLegacy: boolean; } export type FileExists = (filePath: string) => boolean; @@ -20,15 +19,10 @@ export function resolveDashboardAssets( return { directory: reactDirectory, indexFile: reactIndexFile, - isLegacy: false, }; } - const legacyDirectory = path.join(workspaceDirectory, 'public'); - - return { - directory: legacyDirectory, - indexFile: path.join(legacyDirectory, 'index.html'), - isLegacy: true, - }; + throw new Error( + `React dashboard build is missing at ${reactIndexFile}. Run npm run dashboard:build before starting the web server.`, + ); } diff --git a/tests/dashboard-assets.test.ts b/tests/dashboard-assets.test.ts index 1dbd370..845a21a 100644 --- a/tests/dashboard-assets.test.ts +++ b/tests/dashboard-assets.test.ts @@ -11,17 +11,14 @@ describe('resolveDashboardAssets', () => { expect(resolveDashboardAssets(workspace, fileExists)).toEqual({ directory: path.dirname(reactIndex), indexFile: reactIndex, - isLegacy: false, }); }); - it('falls back to the legacy dashboard during incremental migration', () => { + it('fails clearly when the production React build is missing', () => { const workspace = path.resolve('workspace'); - expect(resolveDashboardAssets(workspace, () => false)).toEqual({ - directory: path.join(workspace, 'public'), - indexFile: path.join(workspace, 'public', 'index.html'), - isLegacy: true, - }); + expect(() => resolveDashboardAssets(workspace, () => false)).toThrow( + 'React dashboard build is missing', + ); }); }); diff --git a/tests/dashboard-login-banner.test.ts b/tests/dashboard-login-banner.test.ts deleted file mode 100644 index 6383bd4..0000000 --- a/tests/dashboard-login-banner.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { describe, expect, it } from 'vitest'; - -describe('dashboard Reddit session banner', () => { - it('renders a persistent recovery banner with a Reddit login action', () => { - const html = fs.readFileSync(path.resolve('public/index.html'), 'utf8'); - - expect(html).toContain('id="reddit-session-banner"'); - expect(html).toContain('id="reddit-session-banner-title"'); - expect(html).toContain('Open Reddit Login'); - expect(html).toContain("data.status === 'unreachable'"); - expect(html).toContain("data.status === 'login-required'"); - expect(html).toContain('info.publicUrl'); - }); -}); diff --git a/tests/web-auth-cutover.test.ts b/tests/web-auth-cutover.test.ts index 7bcda64..9c59df0 100644 --- a/tests/web-auth-cutover.test.ts +++ b/tests/web-auth-cutover.test.ts @@ -110,10 +110,4 @@ describe('authenticated React dashboard cutover', () => { expect(upload.status).toBe(404); }); - it('uses the legacy dashboard only when a React build is absent', async () => { - const running = await startServer({ includeReactBuild: false }); - runningServers.push(running); - const response = await fetch(running.baseUrl, { headers: { Authorization: `Bearer ${AUTH_TOKEN}` } }); - expect(await response.text()).toContain('Legacy dashboard'); - }); }); diff --git a/tests/web-auth.test.ts b/tests/web-auth.test.ts index f354223..3508968 100644 --- a/tests/web-auth.test.ts +++ b/tests/web-auth.test.ts @@ -70,6 +70,15 @@ describe('dashboard authentication', () => { expect(valid.headers.get('set-cookie')).toMatch(/SameSite=Strict/); }); + it('allows the login page to create a same-origin session under its CSP', async () => { + const app = express(); + app.use(createDashboardSessionRouter('dashboard-secret')); + + const response = await request(app, '/auth/login'); + + expect(response.headers.get('content-security-policy')).toContain("connect-src 'self'"); + }); + it('does not mount debug endpoints unless explicitly enabled', async () => { const config = ConfigSchema.parse({ reddit: { subreddits: ['test'] },