335 lines
35 KiB
Markdown
335 lines
35 KiB
Markdown
# 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 8–29; 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 old→new 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.
|