diff --git a/firmware/website/AGENTS.md b/firmware/website/AGENTS.md new file mode 100644 index 0000000..9b32779 --- /dev/null +++ b/firmware/website/AGENTS.md @@ -0,0 +1,67 @@ +# AGENTS.md + +## Project Overview +This is a Svelte-based web frontend for a model railway system control, using Vite for build tooling and SPA routing via `svelte-spa-router`. The app is structured around two main tabs: Control and Config, with a captive portal route for special cases. + +## Architecture & Data Flow +- **Entry Point:** `src/main.js` mounts the root Svelte component (`app.svelte`) to the DOM. +- **Routing:** Defined in `app.svelte` using `svelte-spa-router`. Routes are: + - `/` → redirects to `/control` + - `/control` → ControlTab + - `/config` → ConfigTab + - `/captive` → Captive Portal +- **State Management:** + - Centralized via Svelte stores, especially `controlStore` in `src/stores/controlStore.ts`. + - `controlStore` manages system state, communicates with backend via REST and WebSocket, and exposes methods for light, mode, and schema changes. + - WebSocket auto-reconnects and is only active when there are subscribers. +- **Internationalization:** + - Language state managed in `src/i18n/store.ts`. + - Translations loaded from `src/i18n/de.json` and `src/i18n/en.json`. + - Language switching persists in `localStorage`. + +## Developer Workflows +- **Build:** `npm run build` (outputs to `../storage/website`) +- **Dev Server:** `npm run dev` (port 5173, strict) +- **Preview:** `npm run preview` +- **Testing:** `npm run test` (uses Vitest, tests in `src/stores/controlStore.test.ts`) +- **Versioning:** + - App version and commit hash injected via `vite.config.js` (from `../version.txt` and `git rev-parse`). + - Displayed in footer. + +## Project-Specific Patterns +- **Tab Navigation:** + - Implemented via `TabBar` and `TabButton` components. + - Active tab is derived from route. +- **Control Logic:** + - All control actions (light, mode, schema) use latest-only sender pattern to avoid race conditions. + - REST endpoints: `/api/light/status`, `/api/light/power`, `/api/light/mode`, `/api/light/schema`. + - WebSocket endpoint: `/ws`. +- **Styling:** + - TailwindCSS and PicoCSS used for styling. + - Custom themes and responsive layouts. +- **Component Structure:** + - Common UI elements in `src/components/common/`. + - Control-specific UI in `src/components/control/`. + - Config-specific UI in `src/components/config/`. + +## Integration Points +- **Backend:** + - Communicates via REST and WebSocket (see endpoints above). + - Host resolution adapts for dev/prod. +- **External Libraries:** + - GSAP for animations. + - TailwindCSS, PicoCSS, Fontsource for UI. + +## Examples +- To add a new control action, extend `controlStore` and update relevant Svelte components. +- To add a new language, add a JSON file in `src/i18n/` and update `translations` in `index.ts`. +- To debug state, use browser devtools and subscribe to Svelte stores. + +## Key Files +- `src/app.svelte` (routing, layout) +- `src/stores/controlStore.ts` (state, backend communication) +- `src/i18n/store.ts` (language state) +- `vite.config.js` (build, versioning) +- `src/components/` (UI) + +_Last updated: 2026-03-17_ diff --git a/firmware/website/src/stores/controlStore.test.ts b/firmware/website/src/stores/controlStore.test.ts index f7d794d..0971c88 100644 --- a/firmware/website/src/stores/controlStore.test.ts +++ b/firmware/website/src/stores/controlStore.test.ts @@ -55,26 +55,6 @@ describe('controlStore', () => { }); }); - it('update values posts updates and applies returned state', async () => { - const { fetchMock, controlStore } = await setupStore( - vi.fn().mockResolvedValueOnce(okResponse({})) - ); - await controlStore.setMode({ mode: 'night' }); - - expect(fetchMock).toHaveBeenCalledWith(expect.stringMatching(/\/api\/light\/mode$/), { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ mode: 'night' }) - }); - expect(get(controlStore)).toEqual({ - on: false, - mode: 'night', - schema: 'schema_01.csv', - color: { r: 0, g: 0, b: 0 }, - clock: '00:00' - }); - }); - it('fetchState throws on non-ok response', async () => { const { controlStore } = await setupStore( vi.fn().mockResolvedValue({ @@ -112,6 +92,17 @@ describe('controlStore', () => { }); }); + it('setSchema posts to /api/light/schema', async () => { + const { fetchMock, controlStore } = await setupStore(); + await controlStore.setSchema({ schema: 'schema_01.csv' }); + + expect(fetchMock).toHaveBeenCalledWith(expect.stringMatching(/\/api\/light\/schema$/), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ schema: 'schema_01.csv' }) + }); + }); + it('setLight coalesces pending updates while request is in flight', async () => { const first = deferred(); const { fetchMock, controlStore } = await setupStore( diff --git a/firmware/website/src/stores/controlStore.ts b/firmware/website/src/stores/controlStore.ts index 9f98780..365bed9 100644 --- a/firmware/website/src/stores/controlStore.ts +++ b/firmware/website/src/stores/controlStore.ts @@ -1,6 +1,6 @@ import { writable } from 'svelte/store'; -import { createLogger } from './logger'; -import { createLatestOnlySender } from './common'; +import { createLogger } from '../utils/logger'; +import { createLatestOnlySender } from './utils'; // Types for state and REST/WebSocket messages export interface ControlState { @@ -131,7 +131,6 @@ const createControlStore = () => { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); - applyState(payload); }, (current, incoming) => ({ ...(current ?? {}), ...incoming }) ); @@ -143,7 +142,6 @@ const createControlStore = () => { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); - applyState(payload); }, (current, incoming) => ({ ...(current ?? {}), ...incoming }) ); @@ -155,7 +153,6 @@ const createControlStore = () => { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); - applyState(payload); }, (current, incoming) => ({ ...(current ?? {}), ...incoming }) ); diff --git a/firmware/website/src/stores/common.ts b/firmware/website/src/stores/utils.ts similarity index 100% rename from firmware/website/src/stores/common.ts rename to firmware/website/src/stores/utils.ts diff --git a/firmware/website/src/stores/logger.ts b/firmware/website/src/utils/logger.ts similarity index 100% rename from firmware/website/src/stores/logger.ts rename to firmware/website/src/utils/logger.ts diff --git a/firmware/website/vite.config.js b/firmware/website/vite.config.js index cd0f517..dfb18d0 100644 --- a/firmware/website/vite.config.js +++ b/firmware/website/vite.config.js @@ -14,6 +14,11 @@ export default defineConfig({ __COMMIT_HASH__: JSON.stringify(commitHash), __APP_VERSION__: JSON.stringify(version), }, + test: { + sequence: { + shuffle: true, + } + }, plugins: [ svelte({ configFile: 'svelte.config.js'