meatloaf-config/AGENTS.md

335 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Meatloaf Manipulator — Agent Context
## Project Overview
**Meatloaf Manipulator** is a React/Vite PWA for configuring and managing Meatloaf devices (Commodore 64 retro computing hardware). It is served at `https://meatloaf.cc/config/`.
## Tech Stack
- **Framework**: React 18 + Vite 6
- **Styling**: Tailwind CSS 4 (via `@tailwindcss/vite`)
- **UI components**: Radix UI primitives, Lucide React icons
- **3D / animation**: Three.js r0.160 (`WebGLRenderer`, `EffectComposer`, `UnrealBloomPass`, custom GLSL shaders)
- **Terminal**: xterm.js (`@xterm/xterm`, `@xterm/addon-fit`) — SerialConsolePage only
- **Routing**: React Router 7 (single-page, page state managed in `App.tsx`)
- **Build base path**: `/config/` (overridable via `BASE_PATH` env var)
## Project Structure
```
src/
app/
App.tsx # Root component: routing, nav, layout, WsProvider wrapper
# fileManagerInitialPath / fileManagerReturnPage state for
# deep-linking into MediaManager from StatusPage
settings.ts # useSettings() hook — loads config.json + devices.json, saves split;
# version-based migration: migrateConfig() renames old keys and
# restructures; deepMergeDefaults() fills missing keys from bundled
# defaults; auto-saves migrated config immediately on load
webdav.ts # WebDAV abstraction (listDirectory, stat, put/get, fileExists, etc.);
# FetchProgress type; clearDirectory(path) deletes all files in a dir;
# streamFetch(res, onProgress) streams Response → ArrayBuffer with progress;
# listDirectory accepts optional 4th signal?: AbortSignal param
ws.tsx # WsProvider + useWs() — single shared WebSocket connection context
components/
StatusPage.tsx # System status, activity log, reset (via WS), WS status indicator
# Active Device panel: FolderOpen → MediaManager, DirectorySlideshow
# or per-entry cover image, MediaSet with named entries
DevicesPage.tsx # Device list, Rescan Bus (sends "iec scan" via WS)
DeviceDetailOverlay.tsx
GeneralPage.tsx # General/settings
IECPage.tsx # IEC bus configuration
NetworkPage.tsx # Network settings, WiFi scan/connect (via WS)
OtherPage.tsx # Misc settings
ToolsPage.tsx # Tools
SearchPane.tsx # Swipeable search shell: spring slide-up, 4 tabs (Local | Assembly64 |
# CommoServe | CSDb-ng), swipe dot indicators (pill = active), horizontal
# snap-x scroll container; initialTab prop (0-3); onScroll updates active
# tab; load bar removed (progress now inline in SearchLocal)
SearchLocal.tsx # Local file search panel (panel inside SearchPane, no fixed positioning);
# TOSEC/No-Intro tag parsing (PAL/NTSC, system, ISO 639-1 language);
# wildcard search (* → %, ? → _); MediaEntry rows with badges + path;
# faceted filter chips; module-level _store persists state across unmounts;
# actions dialog: "Mount on virtual drive" + "Open containing folder";
# SD card check on mount: stat('/sd') → if missing show red error, skip
# engine load; fileExists('/sd/.locate') → if missing highlight scan button;
# inline load status: hides search input while loading engine/DB, shows
# progress bar with bytes in its place; scan controls: Stop button aborts
# via AbortController (also auto-aborts on unmount); scan shows dir/file
# counts + elapsed time (Xh Xm Xs) + current path (rtl direction);
# "Search your device" empty state hidden during scan
SearchAssembly64.tsx # Assembly64 Leet API browser panel (panel inside SearchPane);
# fetches /search/categories + /search/aql/presets on mount;
# AQL search with pagination; category filter chips; ContentItem result rows;
# item tap → /search/entries → file list with Download + Mount actions;
# Download saves to /sd/downloads/ via putFileContents;
# Client-Id: Ultimate + User-Agent: Assembly Query headers (identifies
# the 1541 Ultimate cartridge; server returns 464 for unknown Client-Id
# and 463 for missing/foreign User-Agent); module-level _store
SearchCommoServe.tsx # CommoServe Leet API browser (panel inside SearchPane);
# AQL search + preset filter chips + paginated ContentItem rows;
# Assembly64-parity mount: tapping a file entry clears /sd/downloads/commoserve/,
# streams main file via leetFetch+streamFetch, saves to device, downloads
# cover image (matching image entry), then mounts by local dest path;
# isMounting overlay in device picker shows fetching progress bar / saving
# spinner / image spinner; isMounted checks local DOWNLOAD_DIR path;
# LEET_BASE='https://commoserve.files.commodore.net/leet', Client-Id:'Commodore',
# DOWNLOAD_DIR='/sd/downloads/commoserve'; module-level _store
WiFiScanOverlay.tsx # WiFi scan results; Connect sends "connect <ssid> [<pass>]" via WS
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;
# initialPath takes priority over localStorage when set;
# Duplicate action: "name copy.ext" / "name copy N.ext" via copyPath;
# .lst and .vms playlist mount: parses lines into MediaSetEntry[]
MediaBrowser.tsx # Lightweight file picker (used in device mount dialogs)
MediaEntry.tsx # Shared entry row: icon by extension, hover highlight (blue left border),
# leftSlot / nameSlot props; exports EntryIcon + extension sets;
# PLAYLIST_EXTS = {lst, vms} → Layers icon (indigo)
MediaViewerEditor.tsx # Viewer/editor shell: tiled icon background (z-index:-1), toolbar
MediaSet.tsx # Disk-swap button row; MediaSetEntry = string | {url, name?};
# exports mediaSetEntryUrl() helper
DirectorySlideshow.tsx # Auto-advancing image slideshow from a WebDAV directory;
# prev/next arrows, dot indicators, centered pause/play button;
# controls appear on hover or tap; paused + idx persisted to localStorage;
# module-level imgCache preloads all images; dotIdx/showIdx split
# prevents flicker — showIdx only advances once target is loaded
HexEditor.tsx # Hex viewer: responsive 8/16 col via ResizeObserver; bright address col
ConfigEditor.tsx # YAML-style .config editor
CodeEditor.tsx # CodeMirror code editor (transparent background)
SerialConsolePage.tsx # xterm.js terminal over shared WS; line-buffered input with echo
# suppression; tiled background overlay
ProfilePage.tsx # Full-page profile menu (replaced dropdown); iOS-style grouped list
AboutMeatloafPage.tsx # Project info table, GPL3 license
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
ui/
lazy-loader.tsx # Animated progress bar for Suspense fallbacks (staged steps)
marquee-text.tsx # <MarqueeText> — ping-pong scroll when text overflows; per-instance
# @keyframes injected into <head> via ResizeObserver; module-level _seq
confirm-dialog.tsx
# … other shadcn/Radix UI wrappers
vendor/
webdav-component/ # Vendored ESM WebDAV client (no external deps)
esm/index.js
package.json
imports/
logo.svg
config.json # Bundled fallback (full merged config including devices.iec)
main.tsx
styles/
public/
manifest.webmanifest
icon.192.png / icon.512.png
service-worker.js # PWA service worker
files/
.sys/
config.json # Runtime config: version, preferences, host, wifi, network, settings
devices.json # Runtime devices: { "devices": { "iec": {...}, "userport": {...}, ... } }
webdav3.py # Dev Python WebDAV + WebSocket server (port 80, serves files/);
# _log(tag, msg) timestamped logger; ws_broadcast(text) module-level
# broadcast returns sent count; _repl_thread() daemon reads stdin
# and broadcasts each line; log_message() override emits
# [DAV] METHOD /path → status for every request
index.html
vite.config.ts
```
## Navigation
Bottom tab bar + header icons:
| Nav item | Page key | Component |
|-------------|-------------------|------------------|
| Status | `status` | StatusPage |
| Devices | `devices` | DevicesPage |
| IEC | `iec` | IECPage |
| Network | `network` | NetworkPage |
| System | `tools` | ToolsPage |
| (header) | `apps` | Apps grid |
| (header) | `profile` | ProfilePage |
| (profile) | `general` | GeneralPage |
| (profile) | `about-meatloaf` | AboutMeatloafPage |
Header: fullscreen toggle, search, apps grid, profile button (→ ProfilePage). Logo click → status page. Toast messages appear `bottom-center` just above the navbar (`offset="calc(4rem + env(safe-area-inset-bottom))"`) with `richColors`, `closeButton`, and 5 s duration.
## Apps Page
Grid of app cards grouped by category, each navigates to a stub `AppPage` unless implemented:
- **Management**: Media Manager, **Assembly64** (implemented — opens SearchPane at Assembly64 tab), Print Manager, **Serial Console** (implemented), 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
- **Development**: Basic Editor, Assembler, Sprite Editor, Character Set Editor, Petscii Editor
- **Display**: Idle Animation, Loading Animation, **Reality Override** (implemented), **Override Admin** (implemented)
Header search icon opens SearchPane at tab 0 (Local). "Assembly64" AppCard opens SearchPane at tab 1.
## Work to Date (chronological)
1. **Initial commit** — project scaffolded with basic pages
2. **StatusPage enhancements** — file info display, loading progress bar
3. **StatusPage overlays** — reset functionality, directory map overlay, disk map overlay
4. **SearchOverlay polish** — improved layout, responsiveness, background opacity
5. **DeviceDetailOverlay** — media button titles improved
6. **Apps page** — added `apps` nav item; built `AppCard` grid with category groupings; added stub `AppPage` for all individual apps
7. **StatusPage layout** — reorganized action buttons; enhanced system status display and activity log
8. **PWA support** — added `manifest.webmanifest`, service worker, PWA icons (`icon.192.png`, `icon.512.png`)
9. **Vite base path** — set `base: process.env.BASE_PATH || '/config/'` in `vite.config.ts`
10. **Icon/manifest path fix** — updated icon paths in manifest and HTML; adjusted service worker registration
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
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
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
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 `devices.iec` from devices.json is merged into `iec` from config.json); saves with split: `devices.iec``devices.json`, everything else (including remaining `iec` bus settings) → `config.json`; `beforeunload` flush also split across both files
39. **SerialConsolePage** — xterm.js terminal (`@xterm/xterm` + `@xterm/addon-fit`) over shared `useWs()`; line-buffered input: printable chars echoed locally, `\r` sends buffer, `\x7f` backspaces, `\x03` clears; echo suppression via `echoQueue` ref; tiled icon background; lazy-loaded
40. **LazyLoader component**`ui/lazy-loader.tsx`: animated progress bar with staged percentage steps (30 → 60 → 80 → 92%) for Suspense fallbacks; replaces inline `PageLoader` in `App.tsx`
40a. **Pre-React splash loader**`index.html` now ships an inline `#splash` div inside `#root` that shows a dark backdrop, animated blue progress bar, Meatloaf "M" icon, spinner, and "Loading…" label from the moment the HTML parses. Pure CSS + inline SVG, no extra requests, no JS dependency. A tiny inline `<script>` attaches a `MutationObserver` to `#root` and fades the splash out (250 ms) once React replaces it. If JS or the bundle fails, the splash stays visible — never a blank page.
41. **MediaViewerEditor tiled background** — icon tile overlay (`z-index: -1`) inside a `z-index: 0` stacking context; sub-components (HexEditor, ConfigEditor, CodeEditor) use transparent backgrounds so the tile shows through
42. **HexEditor responsive columns**`ResizeObserver` on scroll container switches between 8 columns (< 600px) and 16 columns (≥ 600px); address column brightened to `text-neutral-400`
43. **ProfilePage** `ProfilePage.tsx` replaces the header dropdown; iOS-style grouped list with Preferences GeneralPage, Notifications (stub), Documentation (stub), About Meatloaf AboutMeatloafPage, Log Out; profile button in header navigates to `'profile'` page key
44. **AboutMeatloafPage** `AboutMeatloafPage.tsx`: logo, project info table (Project, Platform, Website, GitHub), GPL3 license note
45. **MediaEntry hover highlight** `border-l-2 border-l-transparent` always reserves space (no layout shift); `hover:bg-blue-50 hover:border-l-blue-400`; selected state mirrors hover; also applied to `..` (navigate-up) rows in MediaBrowser and MediaManager
46. **Toast position** `<Toaster position="bottom-center" offset="calc(4rem + env(safe-area-inset-bottom))">` toasts appear just above the navbar on all devices
47. **StatusPage: Browse active device** `FolderOpen` button in Active Device panel header opens MediaManager at `base_url` (if set) or parent directory of `url`; uses `fileManagerInitialPath` / `fileManagerReturnPage` state in `App.tsx`; Apps page card clears `fileManagerInitialPath` so normal navigation restores last-visited path from localStorage
48. **MediaManager initialPath priority** `initialPath` prop now takes priority over `localStorage` when explicitly set (uses `??` instead of `||`); default changed from `'/'` to `undefined`
49. **DirectorySlideshow component** `DirectorySlideshow.tsx`: lists a WebDAV directory, filters for image files, auto-advances every 4 s; prev/next arrow buttons + dot indicators + centered pause/play button; all controls fade in on hover or tap (3 s auto-hide on touch); `paused` persisted to `localStorage` key `slideshow.paused`; `idx` persisted to `localStorage` key `slideshow.idx:<path>` (per directory, clamped to image count on restore)
50. **MediaSet named entries** `MediaSetEntry = string | { url: string; name?: string }`; `MediaSet.tsx` exports the type and `mediaSetEntryUrl()` helper; `name` field displayed instead of filename when provided; `DeviceDetailOverlay` and `StatusPage` updated to use `MediaSetEntry[]`
51. **StatusPage: Active Device cover image** when `activeDevice.url` has a matching image in the same directory, it is shown (fixed `h-48`) instead of the `DirectorySlideshow`; matching is case-insensitive with `_`/`-`/space normalization via a single `listDirectory` call; the media_set entry's `name` field is tried first, URL base name second
52. **Config structure restructure** top-level keys renamed: `general`→`preferences`, `iec`(bus settings)→`settings`, `iec.devices`→`devices.iec`; peripheral config moved into `devices`: `hardware.userport`→`devices.userport`, `hardware.ps2`→`devices.ps2`, `cassette`→`devices.cassette`; `bluetooth`/`modem` also moved into `devices`; `boip` removed; `version: "0.5.0"` added; all page components updated (`IECPage` uses `config.settings`, `GeneralPage` uses `config.preferences`, etc.)
53. **DevicesPage: physical/virtual device model** IEC devices are virtual by default; "Rescan Bus" simulates discovery of physical devices (`SD2IEC`, `1541`, `1571`, `1581`, `PI1541`, etc.) at random IDs 829; physical device shadows virtual at same address (virtual hidden); physical badge shown; per-device Action Dialog (Radix `Dialog`) with type-specific actions (Initialize, Format, etc.); device number displayed above icon stacked vertically
54. **Config migration system** `settings.ts` `readSettings()` now returns `{ config, migrated } | null`; `migrateConfig()` handles all oldnew key renames and structural moves; `deepMergeDefaults()` recursively fills missing keys from bundled defaults without overwriting existing values; version mismatch triggers migration; migrated config is written back to device immediately (bypasses 3-second debounce); write failure falls back to dirty state
55. **DirectorySlideshow: flicker-free image transitions** module-level `imgCache: Map<url, HTMLImageElement>` persists across re-renders; all images preloaded eagerly when directory is fetched; `dotIdx` (dot indicator, advances immediately) separated from `showIdx` (displayed image, advances only when target is confirmed loaded in browser cache); `key` prop removed from `<img>` so element stays mounted; auto-advance interval based on `showIdxRef` via ref to avoid stale closure
56. **`webdav3.py` REPL + logging** `_log(tag, msg)` timestamped printer used throughout; module-level `ws_broadcast(text) -> int` builds WS frame and sends to all clients (removes dead sockets, returns sent count); `_repl_thread()` daemon reads stdin lines and broadcasts each, printing `> ` prompt and sent count; `log_message` override parses `requestline`/status into `[DAV] METHOD /path → status`; WS connect/recv/disconnect use `_log('WS', ...)`; REPL thread started as daemon before `serve_forever()`
57. **Toast visibility improvements** `<Toaster>` in `App.tsx` gains `richColors` (vivid green/red/amber/blue for success/error/warning/loading), `closeButton` (explicit ✕), `duration={5000}` (up from 4 s default), and `toastOptions` with `fontSize: 0.9rem`, `fontWeight: 500`, `padding: 14px 16px`
58. **MediaManager: Duplicate action** `duplicateEntry()` generates unique `"name copy.ext"` / `"name copy 2.ext"` etc. via sequential `fileExists` checks, then calls `copyPath`; button appears in the file Actions Dialog between Download and Rename; reloads current directory on success
59. **`.vms` Virtual Media Stack format** `.vms` files are like `.lst` but each line is `path,name` (comma-separated); name after comma becomes the `MediaSetEntry.name` display label; `PLAYLIST_EXTS = new Set(['lst', 'vms'])` exported from `MediaEntry.tsx` with `Layers` icon (indigo); `MediaManager` and `DeviceDetailOverlay` both parse `.vms` into `MediaSetEntry[]`; `mediaSetEntryUrl()` used for `fileExists` checks and `dev.url`
60. **MarqueeText component** `ui/marquee-text.tsx`: extracted from inline MediaManager code; module-level `_seq` counter generates unique animation IDs; per-instance `@keyframes` rule injected into `<head>` and cleaned up on unmount; `ResizeObserver` re-measures on resize; ping-pong (alternate) scroll with `ease-in-out` and 0.8 s delay; used by MediaManager, SearchLocal, SearchAssembly64
61. **SearchLocal + SearchAssembly64 + SearchPane** `SearchOverlay.tsx` split into three components: `SearchPane.tsx` (swipeable shell: `fixed inset-0 bg-white/80 backdrop-blur-md`, spring slide-up via `motion/react`, tab bar with underline indicator + X button, horizontal `snap-x snap-mandatory` scroll container, `onScroll` updates active tab); `SearchLocal.tsx` (local file search panel: TOSEC/No-Intro tag parsing for system/video/language facet chips, wildcard search via `toSqlLike()` in `locate-db.ts` converting `*`→`%` and `?`→`_`, `MediaEntry` rows with badges + path, module-level `_store` persists results/scroll/filters across unmounts, actions dialog with MarqueeText + "Mount on virtual drive" + "Open containing folder", mounted-path highlight via `base_url + url` resolution); `SearchAssembly64.tsx` (Assembly64 Leet API: AQL search via `GET /search/aql/{offset}/{limit}`, categories + presets on mount, category filter chips, paginated ContentItem results, item tap fetches file entries, Download saves to `/sd/downloads/` via `putFileContents`, Mount opens device picker dialog, `Client-Id: Ultimate` + `User-Agent: Assembly Query` headers (identifies the 1541 Ultimate cartridge; server returns 464 for wrong Client-Id and 463 for missing/foreign User-Agent), module-level `_store`); header search icon tab 0, Apps "Assembly64" card tab 1
62. **SearchPane expanded to 4 tabs** `SearchCommoServe.tsx` and `SearchCSDbNG.tsx` added as tabs 2 and 3; `initialTab` now accepts `0|1|2|3`; tab indicator changed from underline bar to pill-shaped swipe dots (active dot is wider, blue); load-progress bar removed from SearchPane and relocated inline in SearchLocal
63. **SearchCommoServe: Assembly64-parity mount** entry rows are now a single tappable `<button>` (no separate Download/Mount split); tapping opens the device picker; on confirm: `clearDirectory(DOWNLOAD_DIR)` wipes `/sd/downloads/commoserve/`, main file streamed via `leetFetch`+`streamFetch` with progress bar, saved via `putFileContents`, cover image (same item's image entry) downloaded and saved; device config set to the local dest path; `isMounted` resolved against local path not remote URL; `isMounting` overlay in device picker shows phase-specific UI (progress bar saving spinner image spinner); `LEET_BASE = 'https://commoserve.files.commodore.net/leet'`, `Client-Id: 'Commodore'`, `DOWNLOAD_DIR = '/sd/downloads/commoserve'`
64. **webdav.ts: clearDirectory + streamFetch** `clearDirectory(path)` lists files in a directory and deletes them all (ignores errors, handles missing dir); `streamFetch(res, onProgress)` drains a fetch `Response` body chunk-by-chunk, reporting `{received, total}` progress, returns `ArrayBuffer`; `FetchProgress = { received: number; total: number }` exported type; `listDirectory` accepts new 4th `signal?: AbortSignal` parameter passed to `fetch()`
65. **locate-db.ts: new schema + BFS walk + gzip + stop** Schema changed to: `dirs (id, path, scanned)`, `files (id, dir_id, name, size, mtime, is_dir, UNIQUE(dir_id,name))`, `files_fts (FTS5 contentless, path, content='')`, `status (id, total_dirs, total_files, last_scan, duration, last_folder)`; BFS walk replaces Depth:infinity one `listDirectory(dir, false, ...)` per directory from a queue; directories with a `.config` whose `base_url` has a URL scheme are skipped entirely; FTS rowid alignment via `sqlite3_last_insert_rowid` after each file insert; `buildLocateDb` accepts `signal?: AbortSignal` for cancellation, `onProgress` now receives `(phase, value, path?, counts?)` `ScanCounts = { dirs, files }` exported; `setTimeout(0)` yield every `SCAN_PROGRESS_INTERVAL` (= 50) entries to unblock browser paint; after saving `/sd/.locate`, gzip at level 9 via `fflate` and save to `/sd/.locate.gz`; `searchLocate` JOINs dirs+files reconstructing full path as `d.path || '/' || f.name`; `dirPath()` helper extracts parent directory of a path
66. **SearchLocal: SD card checks + inline load status + scan UX** on mount: `stat('/sd')` if missing show red `AlertCircle` error and skip engine load; `fileExists('/sd/.locate')` if missing highlight scan button (blue ring); search input slot is a 4-branch conditional: SD missing error, checking spinner, loading engine/DB progress bar with bytes, else search input + controls; scan Stop button aborts via `scanAbortRef` (`AbortController`); `useEffect` cleanup auto-aborts on unmount so navigating away stops a scan; scan body shows phase label, `N folders · N files · Xh Xm Xs` elapsed time, current path (rtl text direction for long paths), Stop button; `formatDuration(seconds)` helper formats as `Xh Xm Xs` / `Xm Xs` / `Xs`; "Search your device" empty state suppressed while scanning; `subscribeLoadProgress` drives the inline load bar
67. **DeviceDetailOverlay: close button moved left** X button is now the first element in the header row so it is never obscured by a phone camera cutout; device counter moved to the right edge as a standalone flex item
## 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.
- **App pages**: Most individual app pages are stubs. Reality Override, Override Admin, and Serial Console 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.
- **StatusPage mock data**: Last File Access, Size, Transfer Speed, Uptime, IP/MAC, WiFi status, and memory stats are all hardcoded mock values.
## Configuration Shape
Config is split across two files on the device:
| File | Contents |
|---|---|
| `/.sys/config.json` | `version`, `preferences`, `host`, `wifi`, `network`, `settings` (IEC bus no `devices`) |
| `/.sys/devices.json` | `{ "devices": { "iec": {...}, "ps2": 0, "userport": {...}, "cassette": {...}, "bluetooth": {...}, "modem": {...} } }` |
The `useSettings()` hook in `settings.ts` loads both files on mount, merges `devices.json`'s top-level keys one level deep into the config object, then splits them back on save (`devices` key `devices.json`, everything else `config.json`). 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. Its `version` field is compared against the loaded config: a mismatch triggers `migrateConfig()` which renames old keys, restructures peripheral config into `devices`, fills missing keys from bundled defaults, and immediately saves the migrated result.
### Top-level config keys
| Key | Component | Notes |
|---|---|---|
| `version` | | `"0.5.0"`; migration guard |
| `preferences` | GeneralPage | Was `general` |
| `host` | | Model, video, kernal |
| `wifi` | NetworkPage | Array of `{ssid, passphrase, enabled}` |
| `network` | NetworkPage | Hostname, SNTP, services |
| `settings` | IECPage | IEC bus settings; was `iec_config` in old firmware |
| `devices` | DevicesPage | `iec` flat device map + `userport`, `ps2`, `cassette`, `bluetooth`, `modem` |
### media_set format
`media_set` entries can be plain strings (URL) or named objects:
```json
"media_set": [
{ "url": "/sd/game/1.d64", "name": "Boot Disk" },
{ "url": "/sd/game/2.d64", "name": "Disk 1 - Side A" },
"/sd/game/3.d64"
]
```
When a `name` is present, it is shown in the MediaSet UI instead of the filename, and is used as the primary candidate when looking for a matching cover image.
## 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
All WebDAV I/O goes through `src/app/webdav.ts`:
| Export | Purpose |
|---|---|
| `getWebDAVClient()` | Returns (or creates) the singleton `WebDAVManager` pointed at `window.location.hostname` |
| `listDirectory(path)` | PROPFIND depth-1, returns `EntryInfo[]` (filters self-entry + non-direct-children) |
| `stat(path)` | PROPFIND depth-0, returns `EntryInfo \| null` |
| `putFileContents(path, data)` | PUT |
| `getFileContents(path)` | GET `Blob` |
| `createFolder(path)` | MKCOL |
| `deletePath(path)` | DELETE |
| `movePath(from, to)` | MOVE |
| `copyPath(from, to)` | COPY |
| `fileExists(path)` | HEAD `boolean` |
| `clearDirectory(path)` | Deletes all files inside a directory (non-fatal if dir missing) |
| `streamFetch(res, onProgress)` | Drains a Response body with progress, returns `ArrayBuffer` |
| `FetchProgress` | `{ received: number; total: number }` |
| `humanFileSize(bytes)` | Formatting helper |
| `normalizePath` / `splitPath` / `joinPath` / `basename` | Path utilities |
`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.