- 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.
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 viaBASE_PATHenv 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)
- Initial commit — project scaffolded with basic pages
- StatusPage enhancements — file info display, loading progress bar
- StatusPage overlays — reset functionality, directory map overlay, disk map overlay
- SearchOverlay polish — improved layout, responsiveness, background opacity
- DeviceDetailOverlay — media button titles improved
- Apps page — added
appsnav item; builtAppCardgrid with category groupings; added stubAppPagefor all individual apps - StatusPage layout — reorganized action buttons; enhanced system status display and activity log
- PWA support — added
manifest.webmanifest, service worker, PWA icons (icon.192.png,icon.512.png) - Vite base path — set
base: process.env.BASE_PATH || '/config/'invite.config.ts - Icon/manifest path fix — updated icon paths in manifest and HTML; adjusted service worker registration
- WebDAV client — replaced
webdav@5npm package with vendoredwebdav-component(browser-nativefetch+DOMParser, no external deps). All pages import fromwebdav.tsabstraction layer. - WebDAV path fixes —
webdav.ts: alwaysdecodeURIComponentpaths; useentry.uri(not brokenentry.path) for servers returning relative hrefs webdav3.pyserver fixes —displaynamenow returns leaf name only (not full path); PROPFIND depth-1 guard prevents crash when called on a file- 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 - Settings persistence —
settings.ts+useSettings()hook: loads/.sys/config.jsonvia WebDAV on mount, auto-saves 3 s after last change, exposessaveStatus/pendingCount/flushNow;beforeunloadflushes viafetch keepalive - Save-status badge —
SaveStatusBadgeinApp.tsxheader shows: idle (hidden), loading spinner, amber "N unsaved + Save button", saving spinner, saved checkmark, red error + retry - File icons by extension — MediaManager shows
Discicon for disk extensions (.d64,.d71,.d81, etc.) andHardDriveicon for HD extensions (.img,.iso, etc.) - Device URL display — StatusPage shows
device.base_url + device.urlconcatenated - WebSocket server —
webdav3.pyupgraded with RFC 6455 WebSocket handler at/ws; echoes received frames to all connected clients; thread-safe broadcast - 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 - RealityOverrideAdminPage — command palette (Image/Audio/Video conversion groups, 30 commands); freeform command input with Enter key support; sent-command flash indicator
- Source map warnings fixed — stripped all
//# sourceMappingURL=comments from 26 vendored webdav-component ESM files - MediaManager: navigate into new folder — after
handleCreateFolder, automatically callsload(newPath)to navigate into the created folder - MediaManager: Configure Folder — "Configure Folder" action in kebab menu creates
.configif absent and opens it in ConfigEditor; saving re-parses content intofolderConfigstate immediately and closes editor - Mount logic: base_url=
.— in.config,base_url: .resolves to the current folder path; mounting a file inside the base setsurlto the relative path; mounting outside clearsbase_url - 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 - Mobile PWA fullscreen —
viewport-fit=cover,apple-mobile-web-app-capable,black-translucentstatus bar style,apple-mobile-web-app-title; bodyoverscroll-behavior: none; safe-area CSS env() insets on header and nav - Fullscreen API button — header Maximize2/Minimize2 button; webkit prefix fallback;
isFullscreenstate tracksfullscreenchangeevent - Logo click → status page — logo wrapped in
<button onClick={() => setCurrentPage('status')}> - Double-tap to toggle vortex —
onPointerDownhandler withisPrimaryguard and 400 ms window;pausedRef+bgVisiblestate; mode persisted inlocalStoragekeyro-bg - Shared WebSocket context —
ws.tsx:WsProvidermanages a singleWebSockettows://<hostname>/ws; subscriber pattern vialistenersref Set; auto-reconnect after 3 s;useWs()exposes{ status, send, subscribe };App.tsxreturn wrapped in<WsProvider> - Components migrated to shared WS —
RealityOverridePageandRealityOverrideAdminPageremoved own WS effects; now useuseWs(); status indicator usesRadioicon (distinct fromWifiused for WiFi) - StatusPage WS status —
Radioicon + livewsStatusshown in System Status grid alongside WiFi - Reset via WebSocket — "Reset Meatloaf" sends
"reset"; "Reset Host" sends"reset hard"via shared WS on confirm - Rescan Bus via WebSocket — DevicesPage "Rescan Bus" button sends
"iec scan"via shared WS - WiFi Scan via WebSocket — NetworkPage "Scan" button sends
"scan"via shared WS before opening overlay - WiFi connect via WebSocket — WiFiScanOverlay "Connect" sends
"connect <ssid>"or"connect <ssid> <password>"; NetworkPage known-network click sends same command, marks networkenabled: 1, moves it to top of list - Split config / devices storage —
settings.tsloads/.sys/config.jsonand/.sys/devices.jsonin parallel; merges with one-level deep merge (sodevices.iecfrom devices.json is merged intoiecfrom config.json); saves with split:devices.iec→devices.json, everything else (including remainingiecbus settings) →config.json;beforeunloadflush also split across both files - SerialConsolePage — xterm.js terminal (
@xterm/xterm+@xterm/addon-fit) over shareduseWs(); line-buffered input: printable chars echoed locally,\rsends buffer,\x7fbackspaces,\x03clears; echo suppression viaechoQueueref; tiled icon background; lazy-loaded - LazyLoader component —
ui/lazy-loader.tsx: animated progress bar with staged percentage steps (30 → 60 → 80 → 92%) for Suspense fallbacks; replaces inlinePageLoaderinApp.tsx - MediaViewerEditor tiled background — icon tile overlay (
z-index: -1) inside az-index: 0stacking context; sub-components (HexEditor, ConfigEditor, CodeEditor) use transparent backgrounds so the tile shows through - HexEditor responsive columns —
ResizeObserveron scroll container switches between 8 columns (< 600px) and 16 columns (≥ 600px); address column brightened totext-neutral-400 - ProfilePage —
ProfilePage.tsxreplaces 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 - AboutMeatloafPage —
AboutMeatloafPage.tsx: logo, project info table (Project, Platform, Website, GitHub), GPL3 license note - MediaEntry hover highlight —
border-l-2 border-l-transparentalways 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 - Toast position —
<Toaster position="bottom-center" offset="calc(4rem + env(safe-area-inset-bottom))">— toasts appear just above the navbar on all devices - StatusPage: Browse active device —
FolderOpenbutton in Active Device panel header opens MediaManager atbase_url(if set) or parent directory ofurl; usesfileManagerInitialPath/fileManagerReturnPagestate inApp.tsx; Apps page card clearsfileManagerInitialPathso normal navigation restores last-visited path from localStorage - MediaManager initialPath priority —
initialPathprop now takes priority overlocalStoragewhen explicitly set (uses??instead of||); default changed from'/'toundefined - 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);pausedpersisted tolocalStoragekeyslideshow.paused;idxpersisted tolocalStoragekeyslideshow.idx:<path>(per directory, clamped to image count on restore) - MediaSet named entries —
MediaSetEntry = string | { url: string; name?: string };MediaSet.tsxexports the type andmediaSetEntryUrl()helper;namefield displayed instead of filename when provided;DeviceDetailOverlayandStatusPageupdated to useMediaSetEntry[] - StatusPage: Active Device cover image — when
activeDevice.urlhas a matching image in the same directory, it is shown (fixedh-48) instead of theDirectorySlideshow; matching is case-insensitive with_/-/space normalization via a singlelistDirectorycall; the media_set entry'snamefield is tried first, URL base name second
Known Issues / Open Work
- Service worker 404:
https://meatloaf.cc/service-worker.jsreturns 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.