meatloaf-config/AGENTS.md
Jaime Idolpx faa2e41be4 Refactor config structure: rename 'iec.devices' to 'devices.iec' across the codebase
- Updated references in AGENTS.md, README.md, and various component files (DevicesPage, GeneralPage, IECPage, MediaManager, SearchOverlay, StatusPage) to reflect the new config structure.
- Adjusted settings handling in settings.ts to accommodate the new structure.
- Modified initial config in config.json to match the updated schema.
2026-06-11 17:08:27 -04:00

20 KiB

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
    webdav.ts             # WebDAV abstraction (listDirectory, stat, put/get, fileExists, etc.)
    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
      SearchOverlay.tsx
      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
      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
      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
      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)
        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 on device (no devices.iec)
    devices.json           # Runtime device list: { "iec": { "devices": { ... } } }
webdav3.py                 # Dev Python WebDAV + WebSocket server (port 80, serves files/)
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))").

Apps Page

Grid of app cards grouped by category, each navigates to a stub AppPage unless implemented:

  • Management: Media Manager, 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)

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 fixeswebdav.ts: always decodeURIComponent paths; use entry.uri (not broken entry.path) for servers returning relative hrefs
  13. webdav3.py server fixesdisplayname 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 persistencesettings.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 badgeSaveStatusBadge 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 serverwebdav3.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 checkPromise.all(fileExists) filter applied before building media set in both MediaManager and DeviceDetailOverlay; toast warning shown if some files are missing
  27. Mobile PWA fullscreenviewport-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 vortexonPointerDown handler with isPrimary guard and 400 ms window; pausedRef + bgVisible state; mode persisted in localStorage key ro-bg
  31. Shared WebSocket contextws.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 WSRealityOverridePage and RealityOverrideAdminPage removed own WS effects; now use useWs(); status indicator uses Radio icon (distinct from Wifi used for WiFi)
  33. StatusPage WS statusRadio 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 storagesettings.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.iecdevices.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 componentui/lazy-loader.tsx: animated progress bar with staged percentage steps (30 → 60 → 80 → 92%) for Suspense fallbacks; replaces inline PageLoader in App.tsx
  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 columnsResizeObserver on scroll container switches between 8 columns (< 600px) and 16 columns (≥ 600px); address column brightened to text-neutral-400
  43. ProfilePageProfilePage.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. AboutMeatloafPageAboutMeatloafPage.tsx: logo, project info table (Project, Platform, Website, GitHub), GPL3 license note
  45. MediaEntry hover highlightborder-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 deviceFolderOpen 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 priorityinitialPath prop now takes priority over localStorage when explicitly set (uses ?? instead of ||); default changed from '/' to undefined
  49. DirectorySlideshow componentDirectorySlideshow.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 entriesMediaSetEntry = 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

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 general, host, hardware, wifi, network, bluetooth, modem, cassette, boip, iec (bus settings only — no devices.iec)
/.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 devices.iec) so the UI renders immediately without waiting.

media_set format

media_set entries can be plain strings (URL) or named objects:

"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
fileExists(path) HEAD → boolean
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.