28 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;
# 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.)
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, tab bar (Local | Assembly64),
# X close button, horizontal snap-x scroll container; initialTab prop
# (0=Local, 1=Assembly64); onScroll updates active tab indicator
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"
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: meatloaf-config header; 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)
- 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 - Config structure restructure — top-level keys renamed:
general→preferences,iec(bus settings)→settings,iec.devices→devices.iec; peripheral config moved intodevices:hardware.userport→devices.userport,hardware.ps2→devices.ps2,cassette→devices.cassette;bluetooth/modemalso moved intodevices;boipremoved;version: "0.5.0"added; all page components updated (IECPageusesconfig.settings,GeneralPageusesconfig.preferences, etc.) - 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 (RadixDialog) with type-specific actions (Initialize, Format, etc.); device number displayed above icon stacked vertically - Config migration system —
settings.tsreadSettings()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 - 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 fromshowIdx(displayed image, advances only when target is confirmed loaded in browser cache);keyprop removed from<img>so element stays mounted; auto-advance interval based onshowIdxRefvia ref to avoid stale closure webdav3.pyREPL + logging —_log(tag, msg)timestamped printer used throughout; module-levelws_broadcast(text) -> intbuilds 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_messageoverride parsesrequestline/status into[DAV] METHOD /path → status; WS connect/recv/disconnect use_log('WS', ...); REPL thread started as daemon beforeserve_forever()- Toast visibility improvements —
<Toaster>inApp.tsxgainsrichColors(vivid green/red/amber/blue for success/error/warning/loading),closeButton(explicit ✕),duration={5000}(up from 4 s default), andtoastOptionswithfontSize: 0.9rem,fontWeight: 500,padding: 14px 16px - MediaManager: Duplicate action —
duplicateEntry()generates unique"name copy.ext"/"name copy 2.ext"etc. via sequentialfileExistschecks, then callscopyPath; button appears in the file Actions Dialog between Download and Rename; reloads current directory on success .vmsVirtual Media Stack format —.vmsfiles are like.lstbut each line ispath,name(comma-separated); name after comma becomes theMediaSetEntry.namedisplay label;PLAYLIST_EXTS = new Set(['lst', 'vms'])exported fromMediaEntry.tsxwithLayersicon (indigo);MediaManagerandDeviceDetailOverlayboth parse.vmsintoMediaSetEntry[];mediaSetEntryUrl()used forfileExistschecks anddev.url- MarqueeText component —
ui/marquee-text.tsx: extracted from inline MediaManager code; module-level_seqcounter generates unique animation IDs; per-instance@keyframesrule injected into<head>and cleaned up on unmount;ResizeObserverre-measures on resize; ping-pong (alternate) scroll withease-in-outand 0.8 s delay; used by MediaManager, SearchLocal, SearchAssembly64 - SearchLocal + SearchAssembly64 + SearchPane —
SearchOverlay.tsxsplit into three components:SearchPane.tsx(swipeable shell:fixed inset-0 bg-white/80 backdrop-blur-md, spring slide-up viamotion/react, tab bar with underline indicator + X button, horizontalsnap-x snap-mandatoryscroll container,onScrollupdates active tab);SearchLocal.tsx(local file search panel: TOSEC/No-Intro tag parsing for system/video/language facet chips, wildcard search viatoSqlLike()inlocate-db.tsconverting*→%and?→_,MediaEntryrows with badges + path, module-level_storepersists results/scroll/filters across unmounts, actions dialog with MarqueeText + "Mount on virtual drive" + "Open containing folder", mounted-path highlight viabase_url + urlresolution);SearchAssembly64.tsx(Assembly64 Leet API: AQL search viaGET /search/aql/{offset}/{limit}, categories + presets on mount, category filter chips, paginated ContentItem results, item tap fetches file entries, Download saves to/sd/downloads/viaputFileContents, Mount opens device picker dialog,client-id: meatloaf-configheader, module-level_store); header search icon → tab 0, Apps "Assembly64" card → tab 1
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 |
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:
"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 |
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.