14 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) - 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, SaveStatusBadge, WsProvider wrapper
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
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
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/ # 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 iec.devices)
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 iec.devices)
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 |
| More | other |
OtherPage |
| (header) | apps |
Apps grid |
| (profile) | general |
GeneralPage |
| (profile) | tools |
ToolsPage |
Header also has: fullscreen toggle (Maximize2/Minimize2), search, apps grid, save-status badge, profile menu. Logo click → status page.
Apps Page
Grid of app cards grouped by category, each navigates to a stub AppPage unless implemented:
- Management: Media Manager, Print Manager, Serial Console, Short Codes
- Disk: RAM/ROM Explorer, BAM Editor, Directory Editor, Sector Editor, Disk Visualizer, Dump/Write Disk Image
- Cartridge: PRG to CRT, Magic Desk Cart Builder, Easy Flash Cart Builder
- 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 (soiec.devicesfrom devices.json is merged intoiecfrom config.json); saves with split:iec.devices→devices.json, everything else (including remainingiecbus settings) →config.json;beforeunloadflush also split across both files
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 and Override Admin are implemented; all others show "coming soon".
- WiFi scan results: Currently mock data. Needs real scan results delivered over WebSocket from the device.
- Reset confirmation: The "in-progress" and "done" states are simulated with
setTimeout. Needs real completion signal from the device over WebSocket.
Configuration Shape
Config is split across two files on the device:
| File | Contents |
|---|---|
/.sys/config.json |
general, host, hardware, wifi, network, bluetooth, modem, cassette, boip, iec (bus settings only — no iec.devices) |
/.sys/devices.json |
{ "iec": { "devices": { "printer": {...}, "drive": {...}, "network": {...}, "other": {...}, "meatloaf": {...} } } } |
The useSettings() hook in settings.ts loads both files on mount, merges them into a single config object, and splits them back on save. All page components receive the unified config / setConfig props from App.tsx.
The bundled src/imports/config.json is used as the initial UI state while the server fetch is in flight. It contains the full merged shape (including iec.devices) so the UI renders immediately without waiting.
WebSocket Layer
A single WebSocket connection to ws://<hostname>/ws is managed by WsProvider in ws.tsx and shared app-wide via React Context.
| Export | Purpose |
|---|---|
WsProvider |
Mounts in App.tsx; owns connection lifecycle + auto-reconnect |
useWs() |
Returns { status: WsStatus, send, subscribe } |
WsStatus |
'connecting' | 'connected' | 'disconnected' |
send(msg) |
Sends a string frame if socket is open |
subscribe(handler) |
Adds a message listener; returns unsubscribe function |
Commands sent to the device:
| Command | Sent from | Trigger |
|---|---|---|
reset |
StatusPage | Reset Meatloaf confirmed |
reset hard |
StatusPage | Reset Host confirmed |
iec scan |
DevicesPage | Rescan Bus button |
scan |
NetworkPage | WiFi Scan button |
connect <ssid> [<pass>] |
NetworkPage / WiFiScanOverlay | Network tap / Connect button |
WebDAV Layer
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.