feat: update documentation to reflect new features and architecture changes, including WebSocket integration and split config storage

This commit is contained in:
Jaime Idolpx 2026-06-09 02:35:21 -04:00
parent 4fe0adccc6
commit d0d513b502
2 changed files with 126 additions and 30 deletions

111
AGENTS.md
View File

@ -8,7 +8,8 @@
- **Framework**: React 18 + Vite 6 - **Framework**: React 18 + Vite 6
- **Styling**: Tailwind CSS 4 (via `@tailwindcss/vite`) - **Styling**: Tailwind CSS 4 (via `@tailwindcss/vite`)
- **UI components**: Radix UI primitives, MUI icons (`@mui/icons-material`), Lucide React icons - **UI components**: Radix UI primitives, Lucide React icons
- **3D / animation**: Three.js r0.160 (`WebGLRenderer`, `EffectComposer`, `UnrealBloomPass`, custom GLSL shaders)
- **Routing**: React Router 7 (single-page, page state managed in `App.tsx`) - **Routing**: React Router 7 (single-page, page state managed in `App.tsx`)
- **Build base path**: `/config/` (overridable via `BASE_PATH` env var) - **Build base path**: `/config/` (overridable via `BASE_PATH` env var)
@ -17,21 +18,27 @@
``` ```
src/ src/
app/ app/
App.tsx # Root component: routing, nav, layout, SaveStatusBadge App.tsx # Root component: routing, nav, layout, SaveStatusBadge, WsProvider wrapper
settings.ts # useSettings() hook — load/save config.json via WebDAV settings.ts # useSettings() hook — loads config.json + devices.json, saves split
webdav.ts # WebDAV abstraction (listDirectory, stat, put/get, etc.) webdav.ts # WebDAV abstraction (listDirectory, stat, put/get, fileExists, etc.)
ws.tsx # WsProvider + useWs() — single shared WebSocket connection context
components/ components/
StatusPage.tsx # System status, activity log, file info, progress, overlays StatusPage.tsx # System status, activity log, reset (via WS), WS status indicator
DevicesPage.tsx # Device list DevicesPage.tsx # Device list, Rescan Bus (sends "iec scan" via WS)
DeviceDetailOverlay.tsx DeviceDetailOverlay.tsx
GeneralPage.tsx # General/settings GeneralPage.tsx # General/settings
IECPage.tsx # IEC bus configuration IECPage.tsx # IEC bus configuration
NetworkPage.tsx # Network settings NetworkPage.tsx # Network settings, WiFi scan/connect (via WS)
OtherPage.tsx # Misc settings OtherPage.tsx # Misc settings
ToolsPage.tsx # Tools ToolsPage.tsx # Tools
SearchOverlay.tsx SearchOverlay.tsx
WiFiScanOverlay.tsx WiFiScanOverlay.tsx # WiFi scan results; Connect sends "connect <ssid> [<pass>]" via WS
MediaBrowser.tsx # WebDAV file browser (file click = select, kebab menu) MediaManager.tsx # WebDAV file browser: file icons by type, kebab actions,
# Configure Folder (.config editor), base_url mount logic,
# media set existence check, navigate into new folder
RealityOverridePage.tsx # Full-screen WS command display; Three.js Digital Tokamak
# vortex + star field; double-tap to toggle background
RealityOverrideAdminPage.tsx # Command palette (Image/Audio/Video); freeform input; WS send
figma/ # Figma-generated components figma/ # Figma-generated components
ui/ # shadcn/Radix UI wrappers ui/ # shadcn/Radix UI wrappers
vendor/ vendor/
@ -40,14 +47,18 @@ src/
package.json package.json
imports/ imports/
logo.svg logo.svg
# config.json removed — config now loaded at runtime from /.sys/config.json config.json # Bundled fallback (full merged config including iec.devices)
main.tsx main.tsx
styles/ styles/
public/ public/
manifest.webmanifest manifest.webmanifest
icon.192.png / icon.512.png icon.192.png / icon.512.png
service-worker.js # PWA service worker service-worker.js # PWA service worker
webdav3.py # Dev Python WebDAV server (port 80, serves files/) files/
.sys/
config.json # Runtime config on device (no iec.devices)
devices.json # Runtime device list: { "iec": { "devices": { ... } } }
webdav3.py # Dev Python WebDAV + WebSocket server (port 80, serves files/)
index.html index.html
vite.config.ts vite.config.ts
``` ```
@ -67,16 +78,17 @@ Bottom tab bar + header icons:
| (profile) | `general` | GeneralPage | | (profile) | `general` | GeneralPage |
| (profile) | `tools` | ToolsPage | | (profile) | `tools` | ToolsPage |
Header also has: fullscreen toggle (Maximize2/Minimize2), search, apps grid, save-status badge, profile menu. Logo click → status page.
## Apps Page ## Apps Page
Grid of app cards grouped by category, each navigates to a stub `AppPage`: Grid of app cards grouped by category, each navigates to a stub `AppPage` unless implemented:
- **Disk**: Directory Editor, Sector Editor, BAM Editor, Disk Visualizer, RAM/ROM Explorer, Dump Disk Image, Write Disk Image - **Management**: Media Manager, Print Manager, Serial Console, Short Codes
- **Disk**: RAM/ROM Explorer, BAM Editor, Directory Editor, Sector Editor, Disk Visualizer, Dump/Write Disk Image
- **Cartridge**: PRG to CRT, Magic Desk Cart Builder, Easy Flash Cart Builder - **Cartridge**: PRG to CRT, Magic Desk Cart Builder, Easy Flash Cart Builder
- **Development**: Basic Editor, Assembler, Sprite Editor, Character Set Editor, Petscii Editor - **Development**: Basic Editor, Assembler, Sprite Editor, Character Set Editor, Petscii Editor
- **Display**: Idle Animation, Loading Animation - **Display**: Idle Animation, Loading Animation, **Reality Override** (implemented), **Override Admin** (implemented)
All individual app pages are currently stubs (`AppPage` component with "coming soon" message).
## Work to Date (chronological) ## Work to Date (chronological)
@ -93,18 +105,73 @@ All individual app pages are currently stubs (`AppPage` component with "coming s
11. **WebDAV client** — replaced `webdav@5` npm package with vendored `webdav-component` (browser-native `fetch` + `DOMParser`, no external deps). All pages import from `webdav.ts` abstraction layer. 11. **WebDAV client** — replaced `webdav@5` npm package with vendored `webdav-component` (browser-native `fetch` + `DOMParser`, no external deps). All pages import from `webdav.ts` abstraction layer.
12. **WebDAV path fixes**`webdav.ts`: always `decodeURIComponent` paths; use `entry.uri` (not broken `entry.path`) for servers returning relative hrefs 12. **WebDAV path fixes**`webdav.ts`: always `decodeURIComponent` paths; use `entry.uri` (not broken `entry.path`) for servers returning relative hrefs
13. **`webdav3.py` server fixes** — `displayname` now returns leaf name only (not full path); PROPFIND depth-1 guard prevents crash when called on a file 13. **`webdav3.py` server fixes** — `displayname` now returns leaf name only (not full path); PROPFIND depth-1 guard prevents crash when called on a file
14. **MediaBrowser redesign** — file click = `onSelect` + close; folder click = navigate; per-row kebab (`MoreVert`) opens a Dialog with contextual actions; permanent "Select Folder" button in footer; no mode-toggle buttons 14. **MediaBrowser redesign** — file click = `onSelect` + close; folder click = navigate; per-row kebab (`MoreVert`) opens a Dialog with contextual actions; permanent "Select Folder" button in footer
15. **Settings persistence**`settings.ts` + `useSettings()` hook: loads `/.sys/config.json` via WebDAV on mount, auto-saves 3 s after last change, exposes `saveStatus` / `pendingCount` / `flushNow`; `beforeunload` flushes via `fetch keepalive` 15. **Settings persistence**`settings.ts` + `useSettings()` hook: loads `/.sys/config.json` via WebDAV on mount, auto-saves 3 s after last change, exposes `saveStatus` / `pendingCount` / `flushNow`; `beforeunload` flushes via `fetch keepalive`
16. **Save-status badge**`SaveStatusBadge` in `App.tsx` header shows: idle (hidden), loading spinner, amber "N unsaved + Save button", saving spinner, saved checkmark, red error + retry 16. **Save-status badge**`SaveStatusBadge` in `App.tsx` header shows: idle (hidden), loading spinner, amber "N unsaved + Save button", saving spinner, saved checkmark, red error + retry
17. **File icons by extension** — MediaManager shows `Disc` icon for disk extensions (`.d64`, `.d71`, `.d81`, etc.) and `HardDrive` icon for HD extensions (`.img`, `.iso`, etc.)
18. **Device URL display** — StatusPage shows `device.base_url + device.url` concatenated
19. **WebSocket server**`webdav3.py` upgraded with RFC 6455 WebSocket handler at `/ws`; echoes received frames to all connected clients; thread-safe broadcast
20. **RealityOverridePage** — full-screen page that subscribes to WS commands and displays them centered on screen; Three.js Digital Tokamak vortex background with `UnrealBloomPass`; twinkling star field canvas always running beneath
21. **RealityOverrideAdminPage** — command palette (Image/Audio/Video conversion groups, 30 commands); freeform command input with Enter key support; sent-command flash indicator
22. **Source map warnings fixed** — stripped all `//# sourceMappingURL=` comments from 26 vendored webdav-component ESM files
23. **MediaManager: navigate into new folder** — after `handleCreateFolder`, automatically calls `load(newPath)` to navigate into the created folder
24. **MediaManager: Configure Folder** — "Configure Folder" action in kebab menu creates `.config` if absent and opens it in ConfigEditor; saving re-parses content into `folderConfig` state immediately and closes editor
25. **Mount logic: base_url=`.`** — in `.config`, `base_url: .` resolves to the current folder path; mounting a file inside the base sets `url` to the relative path; mounting outside clears `base_url`
26. **Media Set existence check**`Promise.all(fileExists)` filter applied before building media set in both MediaManager and DeviceDetailOverlay; toast warning shown if some files are missing
27. **Mobile PWA fullscreen**`viewport-fit=cover`, `apple-mobile-web-app-capable`, `black-translucent` status bar style, `apple-mobile-web-app-title`; body `overscroll-behavior: none`; safe-area CSS env() insets on header and nav
28. **Fullscreen API button** — header Maximize2/Minimize2 button; webkit prefix fallback; `isFullscreen` state tracks `fullscreenchange` event
29. **Logo click → status page** — logo wrapped in `<button onClick={() => setCurrentPage('status')}>`
30. **Double-tap to toggle vortex**`onPointerDown` handler with `isPrimary` guard and 400 ms window; `pausedRef` + `bgVisible` state; mode persisted in `localStorage` key `ro-bg`
31. **Shared WebSocket context**`ws.tsx`: `WsProvider` manages a single `WebSocket` to `ws://<hostname>/ws`; subscriber pattern via `listeners` ref Set; auto-reconnect after 3 s; `useWs()` exposes `{ status, send, subscribe }`; `App.tsx` return wrapped in `<WsProvider>`
32. **Components migrated to shared WS**`RealityOverridePage` and `RealityOverrideAdminPage` removed own WS effects; now use `useWs()`; status indicator uses `Radio` icon (distinct from `Wifi` used for WiFi)
33. **StatusPage WS status**`Radio` icon + live `wsStatus` shown in System Status grid alongside WiFi
34. **Reset via WebSocket** — "Reset Meatloaf" sends `"reset"`; "Reset Host" sends `"reset hard"` via shared WS on confirm
35. **Rescan Bus via WebSocket** — DevicesPage "Rescan Bus" button sends `"iec scan"` via shared WS
36. **WiFi Scan via WebSocket** — NetworkPage "Scan" button sends `"scan"` via shared WS before opening overlay
37. **WiFi connect via WebSocket** — WiFiScanOverlay "Connect" sends `"connect <ssid>"` or `"connect <ssid> <password>"`; NetworkPage known-network click sends same command, marks network `enabled: 1`, moves it to top of list
38. **Split config / devices storage**`settings.ts` loads `/.sys/config.json` and `/.sys/devices.json` in parallel; merges with one-level deep merge (so `iec.devices` from devices.json is merged into `iec` from config.json); saves with split: `iec.devices``devices.json`, everything else (including remaining `iec` bus settings) → `config.json`; `beforeunload` flush also split across both files
## Known Issues / Open Work ## Known Issues / Open Work
- **Service worker 404**: `https://meatloaf.cc/service-worker.js` returns 404. The SW is not being served at the correct path relative to the `/config/` base. - **Service worker 404**: `https://meatloaf.cc/service-worker.js` returns 404. The SW is not being served at the correct path relative to the `/config/` base.
- **App pages**: All individual app pages (Directory Editor, Sector Editor, etc.) are stubs. Actual implementations are pending. - **App pages**: Most individual app pages are stubs. Reality Override and Override Admin are implemented; all others show "coming soon".
- **WiFi scan results**: Currently mock data. Needs real scan results delivered over WebSocket from the device.
- **Reset confirmation**: The "in-progress" and "done" states are simulated with `setTimeout`. Needs real completion signal from the device over WebSocket.
## Configuration Shape ## Configuration Shape
Config is stored at `files/.sys/config.json` on the device and loaded at runtime via WebDAV. Top-level keys: `general`, `host`, `hardware`, `wifi`, `network`, `bluetooth`, `modem`, `cassette`, `iec`, `boip`. The `useSettings()` hook in `settings.ts` provides `config` / `setConfig` to all page components (passed as props from `App.tsx`). Config is split across two files on the device:
| File | Contents |
|---|---|
| `/.sys/config.json` | `general`, `host`, `hardware`, `wifi`, `network`, `bluetooth`, `modem`, `cassette`, `boip`, `iec` (bus settings only — no `iec.devices`) |
| `/.sys/devices.json` | `{ "iec": { "devices": { "printer": {...}, "drive": {...}, "network": {...}, "other": {...}, "meatloaf": {...} } } }` |
The `useSettings()` hook in `settings.ts` loads both files on mount, merges them into a single `config` object, and splits them back on save. All page components receive the unified `config` / `setConfig` props from `App.tsx`.
The bundled `src/imports/config.json` is used as the initial UI state while the server fetch is in flight. It contains the full merged shape (including `iec.devices`) so the UI renders immediately without waiting.
## WebSocket Layer
A single `WebSocket` connection to `ws://<hostname>/ws` is managed by `WsProvider` in `ws.tsx` and shared app-wide via React Context.
| Export | Purpose |
|---|---|
| `WsProvider` | Mounts in `App.tsx`; owns connection lifecycle + auto-reconnect |
| `useWs()` | Returns `{ status: WsStatus, send, subscribe }` |
| `WsStatus` | `'connecting' \| 'connected' \| 'disconnected'` |
| `send(msg)` | Sends a string frame if socket is open |
| `subscribe(handler)` | Adds a message listener; returns unsubscribe function |
Commands sent to the device:
| Command | Sent from | Trigger |
|---|---|---|
| `reset` | StatusPage | Reset Meatloaf confirmed |
| `reset hard` | StatusPage | Reset Host confirmed |
| `iec scan` | DevicesPage | Rescan Bus button |
| `scan` | NetworkPage | WiFi Scan button |
| `connect <ssid> [<pass>]` | NetworkPage / WiFiScanOverlay | Network tap / Connect button |
## WebDAV Layer ## WebDAV Layer
@ -116,14 +183,14 @@ All WebDAV I/O goes through `src/app/webdav.ts`:
| `listDirectory(path)` | PROPFIND depth-1, returns `EntryInfo[]` (filters self-entry + non-direct-children) | | `listDirectory(path)` | PROPFIND depth-1, returns `EntryInfo[]` (filters self-entry + non-direct-children) |
| `stat(path)` | PROPFIND depth-0, returns `EntryInfo \| null` | | `stat(path)` | PROPFIND depth-0, returns `EntryInfo \| null` |
| `putFileContents(path, data)` | PUT | | `putFileContents(path, data)` | PUT |
| `getFileContents(path)` | GET → `string` | | `getFileContents(path)` | GET → `Blob` |
| `createFolder(path)` | MKCOL | | `createFolder(path)` | MKCOL |
| `deletePath(path)` | DELETE | | `deletePath(path)` | DELETE |
| `movePath(from, to)` | MOVE | | `movePath(from, to)` | MOVE |
| `fileExists(path)` | HEAD | | `fileExists(path)` | HEAD `boolean` |
| `humanFileSize(bytes)` | Formatting helper | | `humanFileSize(bytes)` | Formatting helper |
| `normalizePath` / `splitPath` / `joinPath` / `basename` | Path utilities | | `normalizePath` / `splitPath` / `joinPath` / `basename` | Path utilities |
`EntryInfo`: `{ name, path, type: 'folder'\|'file', size, lastModified, contentType }` `EntryInfo`: `{ name, path, type: 'folder'|'file', size, lastModified, contentType }`
The vendored `webdav-component` lives at `src/app/vendor/webdav-component/esm/index.js`. Known quirk: `entry.path` is broken for servers that return relative hrefs — always use `entry.uri` in the wrapper. The vendored `webdav-component` lives at `src/app/vendor/webdav-component/esm/index.js`. Known quirk: `entry.path` is broken for servers that return relative hrefs — always use `entry.uri` in the wrapper.

View File

@ -5,17 +5,38 @@ A Progressive Web App (PWA) for configuring and managing [Meatloaf](https://meat
## Features ## Features
- **Status** — live system status, activity log, file info, loading progress, directory and disk map overlays - **Status** — live system status, activity log, file info, loading progress, directory and disk map overlays; WebSocket connection status; reset device or host (sends command via WebSocket)
- **Devices** — browse and manage attached storage devices with detailed device overlays - **Devices** — browse and manage attached IEC devices; Rescan Bus sends `iec scan` via WebSocket
- **IEC** — configure the IEC serial bus - **IEC** — configure the IEC serial bus (fastloaders, chainloaders, directory options)
- **Network** — manage network settings and scan for Wi-Fi networks - **Network** — manage WiFi networks (scan, connect, remove); clicking a known network reconnects and moves it to the top; all connect/scan actions send commands via WebSocket
- **Media Manager** — WebDAV file browser with per-row action menus, folder configuration (`.config` editor), media set detection with existence checks, and smart `base_url` mount logic
- **Reality Override** — full-screen WebSocket command display over a Three.js Digital Tokamak vortex; double-tap to toggle background; twinkling star field when hidden
- **Reality Override Admin** — command palette for Image/Audio/Video conversion commands plus freeform input; broadcasts to all connected clients via WebSocket
- **Apps** — built-in tools grouped by category: - **Apps** — built-in tools grouped by category:
- **Disk**: Directory Editor, Sector Editor, BAM Editor, Disk Visualizer, RAM/ROM Explorer, Dump/Write Disk Image - **Management**: Media Manager, Print Manager, Serial Console, Short Codes
- **Disk**: RAM/ROM Explorer, BAM Editor, Directory Editor, Sector Editor, Disk Visualizer, Dump/Write Disk Image
- **Cartridge**: PRG to CRT, Magic Desk Cart Builder, Easy Flash Cart Builder - **Cartridge**: PRG to CRT, Magic Desk Cart Builder, Easy Flash Cart Builder
- **Development**: Basic Editor, Assembler, Sprite Editor, Character Set Editor, PETSCII Editor - **Development**: Basic Editor, Assembler, Sprite Editor, Character Set Editor, PETSCII Editor
- **Display**: Idle Animation, Loading Animation - **Display**: Idle Animation, Loading Animation, Reality Override, Override Admin
- **Tools & Settings** — general configuration and utility tools - **PWA** — installable on desktop and mobile; runs offline via service worker; fullscreen-capable with safe-area insets for notched phones
- **PWA** — installable on desktop and mobile; runs offline via service worker
## Architecture Highlights
### Shared WebSocket
A single `WebSocket` connection to `ws://<device-hostname>/ws` is managed by `WsProvider` (`src/app/ws.tsx`) and shared app-wide via React Context. Any component can send commands or subscribe to incoming frames via `useWs()`. The connection auto-reconnects every 3 s on drop.
### Split Config Storage
Device settings are stored in two separate files on the device (LittleFS erase-block friendly, ≤ 4 096 bytes each):
| File | Contents |
|---|---|
| `/.sys/config.json` | General, host, hardware, WiFi, network, Bluetooth, modem, cassette, BOIP, IEC bus settings |
| `/.sys/devices.json` | IEC device list (`iec.devices`) |
`useSettings()` loads both files in parallel and merges them; saves split them back automatically. A bundled fallback (`src/imports/config.json`) is used as the initial UI state while the device is being contacted.
### WebDAV Layer
All file I/O goes through `src/app/webdav.ts`, which wraps the vendored `webdav-component` (browser-native `fetch` + `DOMParser`, no npm dependencies).
## Requirements ## Requirements
@ -38,6 +59,14 @@ npm run dev
The app is served at `http://localhost:5173/config/` by default. The app is served at `http://localhost:5173/config/` by default.
Start the local WebDAV + WebSocket dev server (requires Python 3):
```bash
python webdav3.py
```
This serves `files/` over WebDAV and provides a `/ws` WebSocket endpoint on port 80.
To use a different base path: To use a different base path:
```bash ```bash