Compare commits
No commits in common. "3f7cc16ce1b8adfa8c902231443f111fe4955691" and "5ac6b6ce956cacf7dcf778372f02660a8597b6f2" have entirely different histories.
3f7cc16ce1
...
5ac6b6ce95
46
AGENTS.md
46
AGENTS.md
|
|
@ -17,9 +17,7 @@
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
app/
|
app/
|
||||||
App.tsx # Root component: routing, nav, layout, SaveStatusBadge
|
App.tsx # Root component: routing, nav, layout
|
||||||
settings.ts # useSettings() hook — load/save config.json via WebDAV
|
|
||||||
webdav.ts # WebDAV abstraction (listDirectory, stat, put/get, etc.)
|
|
||||||
components/
|
components/
|
||||||
StatusPage.tsx # System status, activity log, file info, progress, overlays
|
StatusPage.tsx # System status, activity log, file info, progress, overlays
|
||||||
DevicesPage.tsx # Device list
|
DevicesPage.tsx # Device list
|
||||||
|
|
@ -31,23 +29,18 @@ src/
|
||||||
ToolsPage.tsx # Tools
|
ToolsPage.tsx # Tools
|
||||||
SearchOverlay.tsx
|
SearchOverlay.tsx
|
||||||
WiFiScanOverlay.tsx
|
WiFiScanOverlay.tsx
|
||||||
FileBrowser.tsx # WebDAV file browser (file click = select, kebab menu)
|
FileBrowser.tsx
|
||||||
figma/ # Figma-generated components
|
figma/ # Figma-generated components
|
||||||
ui/ # shadcn/Radix UI wrappers
|
ui/ # shadcn/Radix UI wrappers
|
||||||
vendor/
|
|
||||||
webdav-component/ # Vendored ESM WebDAV client (no external deps)
|
|
||||||
esm/index.js
|
|
||||||
package.json
|
|
||||||
imports/
|
imports/
|
||||||
logo.svg
|
logo.svg
|
||||||
# config.json removed — config now loaded at runtime from /.sys/config.json
|
config.json # Device config data shape
|
||||||
main.tsx
|
main.tsx
|
||||||
styles/
|
styles/
|
||||||
public/
|
public/
|
||||||
manifest.webmanifest
|
manifest.webmanifest
|
||||||
icon.192.png / icon.512.png
|
icon.192.png / icon.512.png
|
||||||
service-worker.js # PWA service worker
|
service-worker.js # PWA service worker
|
||||||
webdav3.py # Dev Python WebDAV server (port 80, serves files/)
|
|
||||||
index.html
|
index.html
|
||||||
vite.config.ts
|
vite.config.ts
|
||||||
```
|
```
|
||||||
|
|
@ -90,40 +83,13 @@ All individual app pages are currently stubs (`AppPage` component with "coming s
|
||||||
8. **PWA support** — added `manifest.webmanifest`, service worker, PWA icons (`icon.192.png`, `icon.512.png`)
|
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`
|
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
|
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 fixes** — `webdav.ts`: always `decodeURIComponent` paths; use `entry.uri` (not broken `entry.path`) for servers returning relative hrefs
|
|
||||||
13. **`webdav3.py` server fixes** — `displayname` now returns leaf name only (not full path); PROPFIND depth-1 guard prevents crash when called on a file
|
|
||||||
14. **FileBrowser redesign** — file click = `onSelect` + close; folder click = navigate; per-row kebab (`MoreVert`) opens a Dialog with contextual actions; permanent "Select Folder" button in footer; no mode-toggle buttons
|
|
||||||
15. **Settings persistence** — `settings.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 badge** — `SaveStatusBadge` in `App.tsx` header shows: idle (hidden), loading spinner, amber "N unsaved + Save button", saving spinner, saved checkmark, red error + retry
|
|
||||||
|
|
||||||
## Known Issues / Open Work
|
## 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.
|
- **Service worker 404**: `https://meatloaf.cc/service-worker.js` returns 404. The SW is likely not being copied to the dist output at the correct path, or the registration URL does not account for the `/config/` base path.
|
||||||
- **App pages**: All individual app pages (Directory Editor, Sector Editor, etc.) are stubs. Actual implementations are pending.
|
- **App pages**: All individual app pages (Directory Editor, Sector Editor, etc.) are stubs. Actual implementations are pending.
|
||||||
|
- **`apple-mobile-web-app-capable` deprecation**: Already resolved in `index.html` (uses `mobile-web-app-capable` instead).
|
||||||
|
|
||||||
## Configuration Shape
|
## Configuration Shape
|
||||||
|
|
||||||
Config is stored at `files/.sys/config.json` on the device and loaded at runtime via WebDAV. Top-level keys: `general`, `host`, `hardware`, `wifi`, `network`, `bluetooth`, `modem`, `cassette`, `iec`, `boip`. The `useSettings()` hook in `settings.ts` provides `config` / `setConfig` to all page components (passed as props from `App.tsx`).
|
Config is loaded from `src/imports/config.json` and passed as props (`config`, `setConfig`) to all page components.
|
||||||
|
|
||||||
## 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 → `string` |
|
|
||||||
| `createFolder(path)` | MKCOL |
|
|
||||||
| `deletePath(path)` | DELETE |
|
|
||||||
| `movePath(from, to)` | MOVE |
|
|
||||||
| `fileExists(path)` | HEAD |
|
|
||||||
| `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.
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ type AppId =
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [currentPage, setCurrentPage] = useState<Page>('status');
|
const [currentPage, setCurrentPage] = useState<Page>('status');
|
||||||
const { config, setConfig, saveStatus, pendingCount, flushNow, reload } = useSettings();
|
const { config, setConfig, saveStatus, reload } = useSettings();
|
||||||
const [showSearch, setShowSearch] = useState(false);
|
const [showSearch, setShowSearch] = useState(false);
|
||||||
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
||||||
|
|
||||||
|
|
@ -168,7 +168,7 @@ function AppPage({ title, onBack }: { title: string; onBack: () => void }) {
|
||||||
>
|
>
|
||||||
<AppWindow className="w-5 h-5 text-white" />
|
<AppWindow className="w-5 h-5 text-white" />
|
||||||
</button>
|
</button>
|
||||||
<SaveStatusBadge status={saveStatus} pendingCount={pendingCount} onSave={flushNow} onReload={reload} />
|
<SaveStatusBadge status={saveStatus} onReload={reload} />
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowProfileMenu(!showProfileMenu)}
|
onClick={() => setShowProfileMenu(!showProfileMenu)}
|
||||||
|
|
@ -285,61 +285,46 @@ function AppPage({ title, onBack }: { title: string; onBack: () => void }) {
|
||||||
/**
|
/**
|
||||||
* Tiny indicator that reflects the current save status of the settings
|
* Tiny indicator that reflects the current save status of the settings
|
||||||
* file. Renders nothing when idle (so it doesn't clutter the header).
|
* file. Renders nothing when idle (so it doesn't clutter the header).
|
||||||
|
* Clicking the error badge re-attempts the load.
|
||||||
*/
|
*/
|
||||||
function SaveStatusBadge({
|
function SaveStatusBadge({
|
||||||
status,
|
status,
|
||||||
pendingCount,
|
|
||||||
onSave,
|
|
||||||
onReload,
|
onReload,
|
||||||
}: {
|
}: {
|
||||||
status: 'idle' | 'loading' | 'unsaved' | 'saving' | 'saved' | 'error';
|
status: 'idle' | 'loading' | 'saving' | 'saved' | 'error';
|
||||||
pendingCount: number;
|
|
||||||
onSave: () => void;
|
|
||||||
onReload: () => void;
|
onReload: () => void;
|
||||||
}) {
|
}) {
|
||||||
if (status === 'idle') return null;
|
if (status === 'idle') return null;
|
||||||
|
|
||||||
if (status === 'loading') {
|
if (status === 'loading') {
|
||||||
return (
|
return (
|
||||||
<span className="text-xs text-white/80 inline-flex items-center gap-1" title="Loading settings from /.sys/config.json">
|
<span
|
||||||
|
className="text-xs text-white/80 inline-flex items-center gap-1"
|
||||||
|
title="Loading settings from /.sys/config.json"
|
||||||
|
>
|
||||||
<Loader2 className="w-3 h-3 animate-spin" /> Loading
|
<Loader2 className="w-3 h-3 animate-spin" /> Loading
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'unsaved') {
|
|
||||||
return (
|
|
||||||
<span className="inline-flex items-center gap-1">
|
|
||||||
<span className="text-xs text-amber-300" title={`${pendingCount} unsaved change${pendingCount === 1 ? '' : 's'}`}>
|
|
||||||
{pendingCount} unsaved
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={onSave}
|
|
||||||
className="text-xs bg-amber-500 hover:bg-amber-400 text-white px-2 py-0.5 rounded"
|
|
||||||
title="Save now"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'saving') {
|
if (status === 'saving') {
|
||||||
return (
|
return (
|
||||||
<span className="text-xs text-white/80 inline-flex items-center gap-1" title="Saving to /.sys/config.json">
|
<span
|
||||||
|
className="text-xs text-white/80 inline-flex items-center gap-1"
|
||||||
|
title="Saving to /.sys/config.json"
|
||||||
|
>
|
||||||
<Loader2 className="w-3 h-3 animate-spin" /> Saving
|
<Loader2 className="w-3 h-3 animate-spin" /> Saving
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'saved') {
|
if (status === 'saved') {
|
||||||
return (
|
return (
|
||||||
<span className="text-xs text-white/80 inline-flex items-center gap-1" title="Saved to /.sys/config.json">
|
<span
|
||||||
|
className="text-xs text-white/80 inline-flex items-center gap-1"
|
||||||
|
title="Saved to /.sys/config.json"
|
||||||
|
>
|
||||||
<Check className="w-3 h-3" /> Saved
|
<Check className="w-3 h-3" /> Saved
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// error
|
// error
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user