diff --git a/AGENTS.md b/AGENTS.md index 69ab718..52d0f48 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,10 @@ src/ 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 + 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/ @@ -40,17 +43,22 @@ src/ WiFiScanOverlay.tsx # WiFi scan results; Connect sends "connect []" 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 + # 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 + # 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 + # 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) @@ -81,9 +89,13 @@ public/ 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/) + 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 ``` @@ -104,7 +116,7 @@ Bottom tab bar + header icons: | (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))"`). +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 @@ -169,6 +181,14 @@ Grid of app cards grouped by category, each navigates to a stub `AppPage` unless 49. **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); `paused` persisted to `localStorage` key `slideshow.paused`; `idx` persisted to `localStorage` key `slideshow.idx:` (per directory, clamped to image count on restore) 50. **MediaSet named entries** — `MediaSetEntry = 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: `general`→`preferences`, `iec`(bus settings)→`settings`, `iec.devices`→`devices.iec`; peripheral config moved into `devices`: `hardware.userport`→`devices.userport`, `hardware.ps2`→`devices.ps2`, `cassette`→`devices.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 8–29; 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 system** — `settings.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` 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 `` 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** — `` 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 action** — `duplicateEntry()` 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` ## Known Issues / Open Work @@ -184,12 +204,24 @@ 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": {...} } } }` | +| `/.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 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 `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. It contains the full merged shape (including `devices.iec`) so the UI renders immediately without waiting. +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 diff --git a/src/app/components/DeviceDetailOverlay.tsx b/src/app/components/DeviceDetailOverlay.tsx index 369a38f..5a5aa62 100644 --- a/src/app/components/DeviceDetailOverlay.tsx +++ b/src/app/components/DeviceDetailOverlay.tsx @@ -94,7 +94,7 @@ export default function DeviceDetailOverlay({ }; const getDevicePath = (): string[] => { - return ['iec', 'devices', device.number]; + return ['devices', 'iec', device.number]; }; const getDeviceData = () => { @@ -298,26 +298,6 @@ export default function DeviceDetailOverlay({
-
- - { - const path = getDevicePath(); - updateDeviceSetting([...path, 'name'], e.target.value); - }} - className="w-full px-3 py-2 border border-neutral-300 rounded-lg" - /> -
- -
- -
- {device.type.charAt(0).toUpperCase() + device.type.slice(1)} -
-
-
- - {deviceData.base_url !== undefined && ( -
- -
- { - const path = getDevicePath(); - updateDeviceSetting([...path, 'base_url'], e.target.value); - }} - className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg" - /> - -
+ +
+ +
+ {device.type.charAt(0).toUpperCase() + device.type.slice(1)}
- )} +
+ +
+ + { + const path = getDevicePath(); + updateDeviceSetting([...path, 'name'], e.target.value); + }} + className="w-full px-3 py-2 border border-neutral-300 rounded-lg" + /> +
+ +
+ +
+ { + const path = getDevicePath(); + updateDeviceSetting([...path, 'base_url'], e.target.value); + }} + className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg" + /> + +
+
@@ -431,15 +429,6 @@ export default function DeviceDetailOverlay({
)} - {deviceData.type && ( -
- -
- {deviceData.type} -
-
- )} - {deviceData.baud !== undefined && (
diff --git a/src/app/components/MediaManager.tsx b/src/app/components/MediaManager.tsx index 0bfd425..ea87f3c 100644 --- a/src/app/components/MediaManager.tsx +++ b/src/app/components/MediaManager.tsx @@ -81,6 +81,7 @@ function defaultViewMode(entry: EntryInfo): ViewMode { if (CODE_EXTS.has(ext)) return 'code'; if (DOC_EXTS.has(ext)) return 'doc'; if (TEXT_EXTS.has(ext)) return 'text'; + if (PLAYLIST_EXTS.has(ext)) return 'text'; return 'hex'; } @@ -231,13 +232,13 @@ function ActionsModal({ entry, onClose, onOpen, onMount, onDownload, onDuplicate Open / View {VIEWER_LABEL[defaultViewMode(entry)]} - {availableViewers(entry).filter(m => m !== defaultViewMode(entry)).map(mode => ( + {/* {availableViewers(entry).filter(m => m !== defaultViewMode(entry)).map(mode => ( - ))} + ))} */}