meatloaf-config/AGENTS.md

28 KiB
Raw Blame History

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.)
    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: Ultimate header (identifies the 1541 Ultimate
                           #   cartridge; the server returns HTTP 464 for unknown client ids);
                           #   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 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
  52. Config structure restructure — top-level keys renamed: generalpreferences, iec(bus settings)→settings, iec.devicesdevices.iec; peripheral config moved into devices: hardware.userportdevices.userport, hardware.ps2devices.ps2, cassettedevices.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 829; 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 systemsettings.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 actionduplicateEntry() 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 componentui/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 + SearchPaneSearchOverlay.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 header (identifies the 1541 Ultimate cartridge; the server returns HTTP 464 for unknown client ids), 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.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:

"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.