Refactor config structure: rename 'iec.devices' to 'devices.iec' across the codebase

- Updated references in AGENTS.md, README.md, and various component files (DevicesPage, GeneralPage, IECPage, MediaManager, SearchOverlay, StatusPage) to reflect the new config structure.
- Adjusted settings handling in settings.ts to accommodate the new structure.
- Modified initial config in config.json to match the updated schema.
This commit is contained in:
Jaime Idolpx 2026-06-11 17:08:27 -04:00
parent 89d093e159
commit faa2e41be4
10 changed files with 149 additions and 169 deletions

View File

@ -72,7 +72,7 @@ src/
package.json package.json
imports/ imports/
logo.svg logo.svg
config.json # Bundled fallback (full merged config including iec.devices) config.json # Bundled fallback (full merged config including devices.iec)
main.tsx main.tsx
styles/ styles/
public/ public/
@ -81,7 +81,7 @@ public/
service-worker.js # PWA service worker service-worker.js # PWA service worker
files/ files/
.sys/ .sys/
config.json # Runtime config on device (no iec.devices) config.json # Runtime config on device (no devices.iec)
devices.json # Runtime device list: { "iec": { "devices": { ... } } } devices.json # Runtime device list: { "iec": { "devices": { ... } } }
webdav3.py # Dev Python WebDAV + WebSocket server (port 80, serves files/) webdav3.py # Dev Python WebDAV + WebSocket server (port 80, serves files/)
index.html index.html
@ -155,7 +155,7 @@ Grid of app cards grouped by category, each navigates to a stub `AppPage` unless
35. **Rescan Bus via WebSocket** — DevicesPage "Rescan Bus" button sends `"iec scan"` via shared WS 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 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 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 storage**`settings.ts` loads `/.sys/config.json` and `/.sys/devices.json` in parallel; merges with one-level deep merge (so `iec.devices` from devices.json is merged into `iec` from config.json); saves with split: `iec.devices` → `devices.json`, everything else (including remaining `iec` bus settings) → `config.json`; `beforeunload` flush also split across both files 38. **Split config / devices storage**`settings.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.iec` → `devices.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 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 component**`ui/lazy-loader.tsx`: animated progress bar with staged percentage steps (30 → 60 → 80 → 92%) for Suspense fallbacks; replaces inline `PageLoader` in `App.tsx` 40. **LazyLoader component**`ui/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 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
@ -184,12 +184,12 @@ Config is split across two files on the device:
| File | Contents | | File | Contents |
|---|---| |---|---|
| `/.sys/config.json` | `general`, `host`, `hardware`, `wifi`, `network`, `bluetooth`, `modem`, `cassette`, `boip`, `iec` (bus settings only — no `iec.devices`) | | `/.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/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 `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. 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.
### media_set format ### media_set format

View File

@ -31,7 +31,7 @@ Device settings are stored in two separate files on the device (LittleFS erase-b
| File | Contents | | File | Contents |
|---|---| |---|---|
| `/.sys/config.json` | General, host, hardware, WiFi, network, Bluetooth, modem, cassette, BOIP, IEC bus settings | | `/.sys/config.json` | General, host, hardware, WiFi, network, Bluetooth, modem, cassette, BOIP, IEC bus settings |
| `/.sys/devices.json` | IEC device list (`iec.devices`) | | `/.sys/devices.json` | IEC device list (`devices.iec`) |
`useSettings()` loads both files in parallel and merges them; saves split them back automatically. A bundled fallback (`src/imports/config.json`) is used as the initial UI state while the device is being contacted. `useSettings()` loads both files in parallel and merges them; saves split them back automatically. A bundled fallback (`src/imports/config.json`) is used as the initial UI state while the device is being contacted.

View File

@ -106,13 +106,13 @@ export default function DevicesPage({ config, setConfig, openDeviceId, onClearOp
const [physicalDevices, setPhysicalDevices] = useState<PhysicalDevice[]>([]); const [physicalDevices, setPhysicalDevices] = useState<PhysicalDevice[]>([]);
const [actionDevice, setActionDevice] = useState<DisplayDevice | null>(null); const [actionDevice, setActionDevice] = useState<DisplayDevice | null>(null);
const hardware = config.hardware || {}; const cassette = config.devices?.cassette || {};
const cassette = config.cassette || {}; const userport = config.devices?.userport || {};
// Virtual devices from config (used for detail overlay navigation) // Virtual devices from config (used for detail overlay navigation)
const devices: Device[] = []; const devices: Device[] = [];
if (config.iec?.devices) { if (config.devices?.iec) {
Object.entries(config.iec.devices).forEach(([num, device]: [string, any]) => { Object.entries(config.devices.iec).forEach(([num, device]: [string, any]) => {
const type = device.type as Device['type']; const type = device.type as Device['type'];
const name = device.name || ( const name = device.name || (
type === 'drive' ? `Drive ${num}` : type === 'drive' ? `Drive ${num}` :
@ -249,7 +249,7 @@ export default function DevicesPage({ config, setConfig, openDeviceId, onClearOp
const toggleDeviceEnabled = (device: DisplayDevice, e: React.MouseEvent) => { const toggleDeviceEnabled = (device: DisplayDevice, e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
const newConfig = JSON.parse(JSON.stringify(config)); const newConfig = JSON.parse(JSON.stringify(config));
newConfig.iec.devices[device.number].enabled = device.enabled ? 0 : 1; newConfig.devices.iec[device.number].enabled = device.enabled ? 0 : 1;
setConfig(newConfig); setConfig(newConfig);
toast.success(`Device #${device.number} ${device.enabled ? 'disabled' : 'enabled'}`); toast.success(`Device #${device.number} ${device.enabled ? 'disabled' : 'enabled'}`);
}; };
@ -457,7 +457,7 @@ export default function DevicesPage({ config, setConfig, openDeviceId, onClearOp
<div className="p-4 flex items-center justify-between"> <div className="p-4 flex items-center justify-between">
<label className="text-sm text-neutral-500">Enabled</label> <label className="text-sm text-neutral-500">Enabled</label>
<button <button
onClick={() => updateSetting(['cassette', 'enabled'], cassette.enabled ? 0 : 1)} onClick={() => updateSetting(['devices', 'cassette', 'enabled'], cassette.enabled ? 0 : 1)}
className={`relative w-12 h-6 rounded-full transition-colors ${cassette.enabled ? 'bg-blue-600' : 'bg-neutral-300'}`} className={`relative w-12 h-6 rounded-full transition-colors ${cassette.enabled ? 'bg-blue-600' : 'bg-neutral-300'}`}
> >
<div className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${cassette.enabled ? 'translate-x-6' : 'translate-x-0.5'}`} /> <div className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${cassette.enabled ? 'translate-x-6' : 'translate-x-0.5'}`} />
@ -469,7 +469,7 @@ export default function DevicesPage({ config, setConfig, openDeviceId, onClearOp
<input <input
type="text" type="text"
value={cassette.url || ''} value={cassette.url || ''}
onChange={(e) => updateSetting(['cassette', 'url'], e.target.value)} onChange={(e) => updateSetting(['devices', 'cassette', 'url'], e.target.value)}
className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg" className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg"
/> />
<button <button
@ -485,7 +485,7 @@ export default function DevicesPage({ config, setConfig, openDeviceId, onClearOp
{showCassetteUrlBrowser && ( {showCassetteUrlBrowser && (
<MediaBrowser <MediaBrowser
currentPath={cassette.url || '/'} currentPath={cassette.url || '/'}
onSelect={(p) => updateSetting(['cassette', 'url'], p)} onSelect={(p) => updateSetting(['devices', 'cassette', 'url'], p)}
onClose={() => setShowCassetteUrlBrowser(false)} onClose={() => setShowCassetteUrlBrowser(false)}
/> />
)} )}
@ -496,17 +496,17 @@ export default function DevicesPage({ config, setConfig, openDeviceId, onClearOp
<div className="p-4 flex items-center justify-between"> <div className="p-4 flex items-center justify-between">
<label className="text-sm text-neutral-500">User Port Enabled</label> <label className="text-sm text-neutral-500">User Port Enabled</label>
<button <button
onClick={() => updateSetting(['hardware', 'userport', 'enabled'], hardware.userport?.enabled ? 0 : 1)} onClick={() => updateSetting(['devices', 'userport', 'enabled'], userport.enabled ? 0 : 1)}
className={`relative w-12 h-6 rounded-full transition-colors ${hardware.userport?.enabled ? 'bg-blue-600' : 'bg-neutral-300'}`} className={`relative w-12 h-6 rounded-full transition-colors ${userport.enabled ? 'bg-blue-600' : 'bg-neutral-300'}`}
> >
<div className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${hardware.userport?.enabled ? 'translate-x-6' : 'translate-x-0.5'}`} /> <div className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${userport.enabled ? 'translate-x-6' : 'translate-x-0.5'}`} />
</button> </button>
</div> </div>
<div className="p-4"> <div className="p-4">
<label className="text-sm text-neutral-500 block mb-2">User Port Mode</label> <label className="text-sm text-neutral-500 block mb-2">User Port Mode</label>
<select <select
value={hardware.userport?.mode?.split('|')[0] || 'serial'} value={userport.mode?.split('|')[0] || 'serial'}
onChange={(e) => updateSetting(['hardware', 'userport', 'mode'], e.target.value)} onChange={(e) => updateSetting(['devices', 'userport', 'mode'], e.target.value)}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg" className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
> >
<option value="serial">Serial</option> <option value="serial">Serial</option>

View File

@ -38,7 +38,7 @@ export default function GeneralPage({ config, setConfig }: GeneralPageProps) {
setConfig(newConfig); setConfig(newConfig);
}; };
const general = config.general || {}; const general = config.preferences // alias kept so JSX references below don't need renaming || {};
return ( return (
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
@ -51,7 +51,7 @@ export default function GeneralPage({ config, setConfig }: GeneralPageProps) {
<input <input
type="text" type="text"
value={general.devicename || ''} value={general.devicename || ''}
onChange={(e) => updateSetting(['general', 'devicename'], e.target.value)} onChange={(e) => updateSetting(['preferences', 'devicename'], e.target.value)}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg" className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
/> />
</div> </div>
@ -60,7 +60,7 @@ export default function GeneralPage({ config, setConfig }: GeneralPageProps) {
<label className="text-sm text-neutral-500 block mb-2">Appearance</label> <label className="text-sm text-neutral-500 block mb-2">Appearance</label>
<select <select
value={general.appearance?.split('|')[0] || 'auto'} value={general.appearance?.split('|')[0] || 'auto'}
onChange={(e) => updateSetting(['general', 'appearance'], e.target.value)} onChange={(e) => updateSetting(['preferences', 'appearance'], e.target.value)}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg" className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
> >
<option value="light">Light</option> <option value="light">Light</option>
@ -73,7 +73,7 @@ export default function GeneralPage({ config, setConfig }: GeneralPageProps) {
<label className="text-sm text-neutral-500 block mb-2">Language</label> <label className="text-sm text-neutral-500 block mb-2">Language</label>
<select <select
value={general.language?.split('|')[0] || 'en'} value={general.language?.split('|')[0] || 'en'}
onChange={(e) => updateSetting(['general', 'language'], e.target.value)} onChange={(e) => updateSetting(['preferences', 'language'], e.target.value)}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg" className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
> >
<option value="en">English</option> <option value="en">English</option>
@ -87,7 +87,7 @@ export default function GeneralPage({ config, setConfig }: GeneralPageProps) {
{!tzError && timezones ? ( {!tzError && timezones ? (
<select <select
value={general.timezone || ''} value={general.timezone || ''}
onChange={(e) => updateSetting(['general', 'timezone'], e.target.value)} onChange={(e) => updateSetting(['preferences', 'timezone'], e.target.value)}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg" className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
> >
{general.timezone && !timezones.includes(general.timezone) && ( {general.timezone && !timezones.includes(general.timezone) && (
@ -101,7 +101,7 @@ export default function GeneralPage({ config, setConfig }: GeneralPageProps) {
<input <input
type="text" type="text"
value={general.timezone || ''} value={general.timezone || ''}
onChange={(e) => updateSetting(['general', 'timezone'], e.target.value)} onChange={(e) => updateSetting(['preferences', 'timezone'], e.target.value)}
placeholder={timezones === null && !tzError ? 'Loading…' : 'America/Los_Angeles'} placeholder={timezones === null && !tzError ? 'Loading…' : 'America/Los_Angeles'}
disabled={timezones === null && !tzError} disabled={timezones === null && !tzError}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg disabled:opacity-50" className="w-full px-3 py-2 border border-neutral-300 rounded-lg disabled:opacity-50"
@ -114,7 +114,7 @@ export default function GeneralPage({ config, setConfig }: GeneralPageProps) {
<input <input
type="text" type="text"
value={general.country || ''} value={general.country || ''}
onChange={(e) => updateSetting(['general', 'country'], e.target.value)} onChange={(e) => updateSetting(['preferences', 'country'], e.target.value)}
placeholder="US" placeholder="US"
className="w-full px-3 py-2 border border-neutral-300 rounded-lg" className="w-full px-3 py-2 border border-neutral-300 rounded-lg"
/> />
@ -123,7 +123,7 @@ export default function GeneralPage({ config, setConfig }: GeneralPageProps) {
<div className="p-4 flex items-center justify-between"> <div className="p-4 flex items-center justify-between">
<label className="text-sm text-neutral-500">Rotation Sounds</label> <label className="text-sm text-neutral-500">Rotation Sounds</label>
<button <button
onClick={() => updateSetting(['general', 'rotationsounds'], general.rotationsounds ? 0 : 1)} onClick={() => updateSetting(['preferences', 'rotationsounds'], general.rotationsounds ? 0 : 1)}
className={`relative w-12 h-6 rounded-full transition-colors ${ className={`relative w-12 h-6 rounded-full transition-colors ${
general.rotationsounds ? 'bg-blue-600' : 'bg-neutral-300' general.rotationsounds ? 'bg-blue-600' : 'bg-neutral-300'
}`} }`}
@ -139,7 +139,7 @@ export default function GeneralPage({ config, setConfig }: GeneralPageProps) {
<div className="p-4 flex items-center justify-between"> <div className="p-4 flex items-center justify-between">
<label className="text-sm text-neutral-500">Config Enabled</label> <label className="text-sm text-neutral-500">Config Enabled</label>
<button <button
onClick={() => updateSetting(['general', 'configenabled'], general.configenabled ? 0 : 1)} onClick={() => updateSetting(['preferences', 'configenabled'], general.configenabled ? 0 : 1)}
className={`relative w-12 h-6 rounded-full transition-colors ${ className={`relative w-12 h-6 rounded-full transition-colors ${
general.configenabled ? 'bg-blue-600' : 'bg-neutral-300' general.configenabled ? 'bg-blue-600' : 'bg-neutral-300'
}`} }`}
@ -155,7 +155,7 @@ export default function GeneralPage({ config, setConfig }: GeneralPageProps) {
<div className="p-4 flex items-center justify-between"> <div className="p-4 flex items-center justify-between">
<label className="text-sm text-neutral-500">Status Wait Enabled</label> <label className="text-sm text-neutral-500">Status Wait Enabled</label>
<button <button
onClick={() => updateSetting(['general', 'status_wait_enabled'], general.status_wait_enabled ? 0 : 1)} onClick={() => updateSetting(['preferences', 'status_wait_enabled'], general.status_wait_enabled ? 0 : 1)}
className={`relative w-12 h-6 rounded-full transition-colors ${ className={`relative w-12 h-6 rounded-full transition-colors ${
general.status_wait_enabled ? 'bg-blue-600' : 'bg-neutral-300' general.status_wait_enabled ? 'bg-blue-600' : 'bg-neutral-300'
}`} }`}
@ -171,7 +171,7 @@ export default function GeneralPage({ config, setConfig }: GeneralPageProps) {
<div className="p-4 flex items-center justify-between"> <div className="p-4 flex items-center justify-between">
<label className="text-sm text-neutral-500">Encrypt Passphrase</label> <label className="text-sm text-neutral-500">Encrypt Passphrase</label>
<button <button
onClick={() => updateSetting(['general', 'encrypt_passphrase'], general.encrypt_passphrase ? 0 : 1)} onClick={() => updateSetting(['preferences', 'encrypt_passphrase'], general.encrypt_passphrase ? 0 : 1)}
className={`relative w-12 h-6 rounded-full transition-colors ${ className={`relative w-12 h-6 rounded-full transition-colors ${
general.encrypt_passphrase ? 'bg-blue-600' : 'bg-neutral-300' general.encrypt_passphrase ? 'bg-blue-600' : 'bg-neutral-300'
}`} }`}
@ -187,7 +187,7 @@ export default function GeneralPage({ config, setConfig }: GeneralPageProps) {
<div className="p-4 flex items-center justify-between"> <div className="p-4 flex items-center justify-between">
<label className="text-sm text-neutral-500">Reset with Host</label> <label className="text-sm text-neutral-500">Reset with Host</label>
<button <button
onClick={() => updateSetting(['general', 'reset_with_host'], general.reset_with_host ? 0 : 1)} onClick={() => updateSetting(['preferences', 'reset_with_host'], general.reset_with_host ? 0 : 1)}
className={`relative w-12 h-6 rounded-full transition-colors ${ className={`relative w-12 h-6 rounded-full transition-colors ${
general.reset_with_host ? 'bg-blue-600' : 'bg-neutral-300' general.reset_with_host ? 'bg-blue-600' : 'bg-neutral-300'
}`} }`}

View File

@ -22,14 +22,14 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
setConfig(newConfig); setConfig(newConfig);
}; };
const iec = config.iec || {}; const settings = config.settings || {};
const boolDirEntries = Object.entries(iec.directory || {}).filter(([, v]) => v === 0 || v === 1); const boolDirEntries = Object.entries(settings.directory || {}).filter(([, v]) => v === 0 || v === 1);
const isCompatMode = boolDirEntries.length > 0 && boolDirEntries.every(([, v]) => v === 0); const isCompatMode = boolDirEntries.length > 0 && boolDirEntries.every(([, v]) => v === 0);
const setAllDirBools = (val: 0 | 1) => { const setAllDirBools = (val: 0 | 1) => {
const newConfig = JSON.parse(JSON.stringify(config)); const newConfig = JSON.parse(JSON.stringify(config));
const dir = newConfig.iec?.directory ?? {}; const dir = newConfig.settings?.directory ?? {};
for (const k of Object.keys(dir)) { for (const k of Object.keys(dir)) {
if (dir[k] === 0 || dir[k] === 1) dir[k] = val; if (dir[k] === 0 || dir[k] === 1) dir[k] = val;
} }
@ -44,14 +44,14 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
<div className="p-4 flex items-center justify-between"> <div className="p-4 flex items-center justify-between">
<label className="text-sm text-neutral-500">IEC Enabled</label> <label className="text-sm text-neutral-500">IEC Enabled</label>
<button <button
onClick={() => updateSetting(['iec', 'enabled'], iec.enabled ? 0 : 1)} onClick={() => updateSetting(['settings', 'enabled'], settings.enabled ? 0 : 1)}
className={`relative w-12 h-6 rounded-full transition-colors ${ className={`relative w-12 h-6 rounded-full transition-colors ${
iec.enabled ? 'bg-blue-600' : 'bg-neutral-300' settings.enabled ? 'bg-blue-600' : 'bg-neutral-300'
}`} }`}
> >
<div <div
className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${ className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${
iec.enabled ? 'translate-x-6' : 'translate-x-0.5' settings.enabled ? 'translate-x-6' : 'translate-x-0.5'
}`} }`}
/> />
</button> </button>
@ -60,14 +60,14 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
<div className="p-4 flex items-center justify-between"> <div className="p-4 flex items-center justify-between">
<label className="text-sm text-neutral-500">VIC-20 Mode</label> <label className="text-sm text-neutral-500">VIC-20 Mode</label>
<button <button
onClick={() => updateSetting(['iec', 'vic20_mode'], iec.vic20_mode ? 0 : 1)} onClick={() => updateSetting(['settings', 'vic20_mode'], settings.vic20_mode ? 0 : 1)}
className={`relative w-12 h-6 rounded-full transition-colors ${ className={`relative w-12 h-6 rounded-full transition-colors ${
iec.vic20_mode ? 'bg-blue-600' : 'bg-neutral-300' settings.vic20_mode ? 'bg-blue-600' : 'bg-neutral-300'
}`} }`}
> >
<div <div
className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${ className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${
iec.vic20_mode ? 'translate-x-6' : 'translate-x-0.5' settings.vic20_mode ? 'translate-x-6' : 'translate-x-0.5'
}`} }`}
/> />
</button> </button>
@ -76,14 +76,14 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
<div className="p-4 flex items-center justify-between"> <div className="p-4 flex items-center justify-between">
<label className="text-sm text-neutral-500">VDrive Mode</label> <label className="text-sm text-neutral-500">VDrive Mode</label>
<button <button
onClick={() => updateSetting(['iec', 'vdrive_mode'], iec.vdrive_mode ? 0 : 1)} onClick={() => updateSetting(['settings', 'vdrive_mode'], settings.vdrive_mode ? 0 : 1)}
className={`relative w-12 h-6 rounded-full transition-colors ${ className={`relative w-12 h-6 rounded-full transition-colors ${
iec.vdrive_mode ? 'bg-blue-600' : 'bg-neutral-300' settings.vdrive_mode ? 'bg-blue-600' : 'bg-neutral-300'
}`} }`}
> >
<div <div
className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${ className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${
iec.vdrive_mode ? 'translate-x-6' : 'translate-x-0.5' settings.vdrive_mode ? 'translate-x-6' : 'translate-x-0.5'
}`} }`}
/> />
</button> </button>
@ -94,8 +94,8 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
type="text" type="text"
value={iec.autoboot || ''} value={settings.autoboot || ''}
onChange={(e) => updateSetting(['iec', 'autoboot'], e.target.value)} onChange={(e) => updateSetting(['settings', 'autoboot'], e.target.value)}
className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg" className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg"
/> />
<button <button
@ -110,9 +110,9 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
{showMediaBrowser && ( {showMediaBrowser && (
<MediaBrowser <MediaBrowser
currentPath={iec.autoboot || '/'} currentPath={settings.autoboot || '/'}
onSelect={(path) => { onSelect={(path) => {
updateSetting(['iec', 'autoboot'], path); updateSetting(['settings', 'autoboot'], path);
setShowMediaBrowser(false); setShowMediaBrowser(false);
}} }}
onClose={() => setShowMediaBrowser(false)} onClose={() => setShowMediaBrowser(false)}
@ -128,20 +128,20 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
<button <button
onClick={() => { onClick={() => {
const newConfig = JSON.parse(JSON.stringify(config)); const newConfig = JSON.parse(JSON.stringify(config));
const dr = newConfig.iec.drive_rom ?? {}; const dr = newConfig.settings.drive_roms ?? {};
const current = dr.enabled ?? dr.auto ?? 0; const current = dr.enabled ?? dr.auto ?? 0;
dr.enabled = current ? 0 : 1; dr.enabled = current ? 0 : 1;
delete dr.auto; delete dr.auto;
newConfig.iec.drive_rom = dr; newConfig.settings.drive_roms = dr;
setConfig(newConfig); setConfig(newConfig);
}} }}
className={`relative w-12 h-6 rounded-full transition-colors ${ className={`relative w-12 h-6 rounded-full transition-colors ${
(iec.drive_rom?.enabled ?? iec.drive_rom?.auto) ? 'bg-blue-600' : 'bg-neutral-300' (settings.drive_roms?.enabled ?? settings.drive_roms?.auto) ? 'bg-blue-600' : 'bg-neutral-300'
}`} }`}
> >
<div <div
className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${ className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${
(iec.drive_rom?.enabled ?? iec.drive_rom?.auto) ? 'translate-x-6' : 'translate-x-0.5' (settings.drive_roms?.enabled ?? settings.drive_roms?.auto) ? 'translate-x-6' : 'translate-x-0.5'
}`} }`}
/> />
</button> </button>
@ -154,8 +154,8 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
type="text" type="text"
value={iec.drive_rom?.[key] || ''} value={settings.drive_roms?.[key] || ''}
onChange={(e) => updateSetting(['iec', 'drive_rom', key], e.target.value)} onChange={(e) => updateSetting(['settings', 'drive_roms', key], e.target.value)}
className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg" className="flex-1 px-3 py-2 border border-neutral-300 rounded-lg"
/> />
<button <button
@ -172,9 +172,9 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
{driveRomBrowsingKey && ( {driveRomBrowsingKey && (
<MediaBrowser <MediaBrowser
currentPath={iec.drive_rom?.[driveRomBrowsingKey] || '/'} currentPath={settings.drive_roms?.[driveRomBrowsingKey] || '/'}
onSelect={(path) => { onSelect={(path) => {
updateSetting(['iec', 'drive_rom', driveRomBrowsingKey], path); updateSetting(['settings', 'drive_roms', driveRomBrowsingKey], path);
setDriveRomBrowsingKey(null); setDriveRomBrowsingKey(null);
}} }}
onClose={() => setDriveRomBrowsingKey(null)} onClose={() => setDriveRomBrowsingKey(null)}
@ -198,14 +198,14 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
</button> </button>
</div> </div>
{Object.entries(iec.directory || {}).map(([key, value]) => ( {Object.entries(settings.directory || {}).map(([key, value]) => (
<div key={key} className={`p-4 flex items-center justify-between transition-opacity ${isCompatMode ? 'opacity-40' : ''}`}> <div key={key} className={`p-4 flex items-center justify-between transition-opacity ${isCompatMode ? 'opacity-40' : ''}`}>
<label className="text-sm text-neutral-500"> <label className="text-sm text-neutral-500">
{key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())} {key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</label> </label>
{value === 0 || value === 1 ? ( {value === 0 || value === 1 ? (
<button <button
onClick={() => { if (!isCompatMode) updateSetting(['iec', 'directory', key], value ? 0 : 1); }} onClick={() => { if (!isCompatMode) updateSetting(['settings', 'directory', key], value ? 0 : 1); }}
className={`relative w-12 h-6 rounded-full transition-colors ${value ? 'bg-blue-600' : 'bg-neutral-300'} ${isCompatMode ? 'cursor-not-allowed' : ''}`} className={`relative w-12 h-6 rounded-full transition-colors ${value ? 'bg-blue-600' : 'bg-neutral-300'} ${isCompatMode ? 'cursor-not-allowed' : ''}`}
> >
<div className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${value ? 'translate-x-6' : 'translate-x-0.5'}`} /> <div className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${value ? 'translate-x-6' : 'translate-x-0.5'}`} />
@ -215,7 +215,7 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
type="number" type="number"
value={value} value={value}
disabled={isCompatMode} disabled={isCompatMode}
onChange={(e) => updateSetting(['iec', 'directory', key], parseInt(e.target.value))} onChange={(e) => updateSetting(['settings', 'directory', key], parseInt(e.target.value))}
className="w-24 px-3 py-1 border border-neutral-300 rounded-lg text-right disabled:cursor-not-allowed" className="w-24 px-3 py-1 border border-neutral-300 rounded-lg text-right disabled:cursor-not-allowed"
/> />
)} )}
@ -229,13 +229,13 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
<div className="p-3 bg-neutral-50"> <div className="p-3 bg-neutral-50">
<h3 className="text-xs text-neutral-600">Serial</h3> <h3 className="text-xs text-neutral-600">Serial</h3>
</div> </div>
{Object.entries(iec.fastloaders?.hardware?.serial || {}).map(([key, value]) => ( {Object.entries(settings.fastloaders?.hardware?.serial || {}).map(([key, value]) => (
<div key={key} className="p-4 flex items-center justify-between"> <div key={key} className="p-4 flex items-center justify-between">
<label className="text-sm text-neutral-500"> <label className="text-sm text-neutral-500">
{key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())} {key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</label> </label>
<button <button
onClick={() => updateSetting(['iec', 'fastloaders', 'hardware', 'serial', key], value ? 0 : 1)} onClick={() => updateSetting(['settings', 'fastloaders', 'hardware', 'serial', key], value ? 0 : 1)}
className={`relative w-12 h-6 rounded-full transition-colors ${ className={`relative w-12 h-6 rounded-full transition-colors ${
value ? 'bg-blue-600' : 'bg-neutral-300' value ? 'bg-blue-600' : 'bg-neutral-300'
}`} }`}
@ -252,13 +252,13 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
<div className="p-3 bg-neutral-50"> <div className="p-3 bg-neutral-50">
<h3 className="text-xs text-neutral-600">Parallel</h3> <h3 className="text-xs text-neutral-600">Parallel</h3>
</div> </div>
{Object.entries(iec.fastloaders?.hardware?.parallel || {}).map(([key, value]) => ( {Object.entries(settings.fastloaders?.hardware?.parallel || {}).map(([key, value]) => (
<div key={key} className="p-4 flex items-center justify-between"> <div key={key} className="p-4 flex items-center justify-between">
<label className="text-sm text-neutral-500"> <label className="text-sm text-neutral-500">
{key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())} {key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</label> </label>
<button <button
onClick={() => updateSetting(['iec', 'fastloaders', 'hardware', 'parallel', key], value ? 0 : 1)} onClick={() => updateSetting(['settings', 'fastloaders', 'hardware', 'parallel', key], value ? 0 : 1)}
className={`relative w-12 h-6 rounded-full transition-colors ${ className={`relative w-12 h-6 rounded-full transition-colors ${
value ? 'bg-blue-600' : 'bg-neutral-300' value ? 'bg-blue-600' : 'bg-neutral-300'
}`} }`}
@ -276,13 +276,13 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
<h2 className="text-sm text-neutral-500 pt-4 flex items-center gap-2"><Code2 className="w-4 h-4" /> Software Fastloaders</h2> <h2 className="text-sm text-neutral-500 pt-4 flex items-center gap-2"><Code2 className="w-4 h-4" /> Software Fastloaders</h2>
<div className="bg-white border border-neutral-200 rounded-lg divide-y divide-neutral-200"> <div className="bg-white border border-neutral-200 rounded-lg divide-y divide-neutral-200">
{Object.entries(iec.fastloaders?.software || {}).map(([key, value]) => ( {Object.entries(settings.fastloaders?.software || {}).map(([key, value]) => (
<div key={key} className="p-4 flex items-center justify-between"> <div key={key} className="p-4 flex items-center justify-between">
<label className="text-sm text-neutral-500"> <label className="text-sm text-neutral-500">
{key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())} {key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</label> </label>
<button <button
onClick={() => updateSetting(['iec', 'fastloaders', 'software', key], value ? 0 : 1)} onClick={() => updateSetting(['settings', 'fastloaders', 'software', key], value ? 0 : 1)}
className={`relative w-12 h-6 rounded-full transition-colors ${ className={`relative w-12 h-6 rounded-full transition-colors ${
value ? 'bg-blue-600' : 'bg-neutral-300' value ? 'bg-blue-600' : 'bg-neutral-300'
}`} }`}
@ -300,13 +300,13 @@ export default function IECPage({ config, setConfig }: IECPageProps) {
<h2 className="text-sm text-neutral-500 pt-4 flex items-center gap-2"><Link className="w-4 h-4" /> Chainloaders</h2> <h2 className="text-sm text-neutral-500 pt-4 flex items-center gap-2"><Link className="w-4 h-4" /> Chainloaders</h2>
<div className="bg-white border border-neutral-200 rounded-lg divide-y divide-neutral-200"> <div className="bg-white border border-neutral-200 rounded-lg divide-y divide-neutral-200">
{Object.entries(iec.chainloaders || {}).map(([key, value]) => ( {Object.entries(settings.chainloaders || {}).map(([key, value]) => (
<div key={key} className="p-4 flex items-center justify-between"> <div key={key} className="p-4 flex items-center justify-between">
<label className="text-sm text-neutral-500"> <label className="text-sm text-neutral-500">
{key.toUpperCase()} {key.toUpperCase()}
</label> </label>
<button <button
onClick={() => updateSetting(['iec', 'chainloaders', key], value ? 0 : 1)} onClick={() => updateSetting(['settings', 'chainloaders', key], value ? 0 : 1)}
className={`relative w-12 h-6 rounded-full transition-colors ${ className={`relative w-12 h-6 rounded-full transition-colors ${
value ? 'bg-blue-600' : 'bg-neutral-300' value ? 'bg-blue-600' : 'bg-neutral-300'
}`} }`}

View File

@ -625,10 +625,10 @@ export default function MediaManager({ initialPath, rootPath, title, config, set
const mountOnDevice = async (deviceType: 'drive' | 'meatloaf', key: string) => { const mountOnDevice = async (deviceType: 'drive' | 'meatloaf', key: string) => {
if (!mountEntry || !setConfig || !config) return; if (!mountEntry || !setConfig || !config) return;
const newConfig = JSON.parse(JSON.stringify(config)); const newConfig = JSON.parse(JSON.stringify(config));
if (!newConfig.iec) newConfig.iec = {}; if (!newConfig.devices) newConfig.devices = {};
if (!newConfig.iec.devices) newConfig.iec.devices = {}; if (!newConfig.devices.iec) newConfig.devices.iec = {};
if (!newConfig.iec.devices[key]) newConfig.iec.devices[key] = { type: deviceType }; if (!newConfig.devices.iec[key]) newConfig.devices.iec[key] = { type: deviceType };
const dev = newConfig.iec.devices[key]; const dev = newConfig.devices.iec[key];
if (mountEntry.name.toLowerCase().endsWith('.lst')) { if (mountEntry.name.toLowerCase().endsWith('.lst')) {
try { try {
@ -1165,7 +1165,7 @@ export default function MediaManager({ initialPath, rootPath, title, config, set
<DialogDescription className="truncate">{mountEntry?.name}</DialogDescription> <DialogDescription className="truncate">{mountEntry?.name}</DialogDescription>
</DialogHeader> </DialogHeader>
{(() => { {(() => {
const allDevices = Object.entries(config?.iec?.devices ?? {}); const allDevices = Object.entries(config?.devices?.iec ?? {});
const drives = allDevices const drives = allDevices
.filter(([, v]: [string, any]) => (v as any)?.type === 'drive') .filter(([, v]: [string, any]) => (v as any)?.type === 'drive')
.map(([k, v]: [string, any]) => ({ type: 'drive' as const, key: k, base_url: v?.base_url as string | undefined, url: v?.url as string | undefined, enabled: !!v?.enabled })); .map(([k, v]: [string, any]) => ({ type: 'drive' as const, key: k, base_url: v?.base_url as string | undefined, url: v?.url as string | undefined, enabled: !!v?.enabled }));

View File

@ -121,8 +121,8 @@ export default function SearchOverlay({ config, setConfig, onClose }: SearchOver
const handleMount = (deviceNum: string, result: SearchResult) => { const handleMount = (deviceNum: string, result: SearchResult) => {
const newConfig = JSON.parse(JSON.stringify(config)); const newConfig = JSON.parse(JSON.stringify(config));
if (newConfig.iec?.devices?.[deviceNum]) { if (newConfig.devices?.iec?.[deviceNum]) {
newConfig.iec.devices[deviceNum].url = result.path; newConfig.devices.iec[deviceNum].url = result.path;
setConfig(newConfig); setConfig(newConfig);
toast.success(`Mounted ${result.name} on Device #${deviceNum}`); toast.success(`Mounted ${result.name} on Device #${deviceNum}`);
setShowDeviceMenu(null); setShowDeviceMenu(null);
@ -131,8 +131,8 @@ export default function SearchOverlay({ config, setConfig, onClose }: SearchOver
const getAvailableDevices = () => { const getAvailableDevices = () => {
const devices: { number: string; name: string; url?: string }[] = []; const devices: { number: string; name: string; url?: string }[] = [];
if (config.iec?.devices) { if (config.devices?.iec) {
for (const [num, device] of Object.entries(config.iec.devices)) { for (const [num, device] of Object.entries(config.devices.iec)) {
const d = device as any; const d = device as any;
if (d.type === 'drive' && d.enabled) { if (d.type === 'drive' && d.enabled) {
devices.push({ number: num, name: `Drive ${num}`, url: d.url }); devices.push({ number: num, name: `Drive ${num}`, url: d.url });

View File

@ -29,8 +29,8 @@ export default function StatusPage({ config, setConfig, onOpenFileManager }: Sta
const [showDeviceOverlay, setShowDeviceOverlay] = useState(false); const [showDeviceOverlay, setShowDeviceOverlay] = useState(false);
// Find the first enabled device as the active device // Find the first enabled device as the active device
const findActiveDevice = () => { const findActiveDevice = () => {
if (config.iec?.devices) { if (config.devices?.iec) {
for (const [num, device] of Object.entries(config.iec.devices)) { for (const [num, device] of Object.entries(config.devices.iec)) {
const d = device as any; const d = device as any;
if (d.type === 'drive' && d.enabled) { if (d.type === 'drive' && d.enabled) {
return { number: num, ...d }; return { number: num, ...d };
@ -57,8 +57,8 @@ export default function StatusPage({ config, setConfig, onOpenFileManager }: Sta
const switchActiveMedia = (file: string) => { const switchActiveMedia = (file: string) => {
const newConfig = JSON.parse(JSON.stringify(config)); const newConfig = JSON.parse(JSON.stringify(config));
if (newConfig.iec?.devices?.[activeDevice!.number]) { if (newConfig.devices?.iec?.[activeDevice!.number]) {
newConfig.iec.devices[activeDevice!.number].url = file; newConfig.devices.iec[activeDevice!.number].url = file;
setConfig(newConfig); setConfig(newConfig);
} }
}; };

View File

@ -62,7 +62,7 @@ export async function readSettings(): Promise<SettingsConfig | null> {
const devices = JSON.parse(devicesText); const devices = JSON.parse(devicesText);
if (devices && typeof devices === 'object') { if (devices && typeof devices === 'object') {
// One-level deep merge: devices.json keys are merged into matching // One-level deep merge: devices.json keys are merged into matching
// top-level objects in config (e.g. devices.iec.devices → config.iec.devices). // top-level objects in config (e.g. devices.devices.iec → config.devices.iec).
for (const [k, v] of Object.entries(devices)) { for (const [k, v] of Object.entries(devices)) {
if (config[k] && typeof config[k] === 'object' && !Array.isArray(config[k]) && if (config[k] && typeof config[k] === 'object' && !Array.isArray(config[k]) &&
v && typeof v === 'object' && !Array.isArray(v)) { v && typeof v === 'object' && !Array.isArray(v)) {
@ -87,10 +87,9 @@ export async function readSettings(): Promise<SettingsConfig | null> {
export async function writeSettings(config: SettingsConfig): Promise<void> { export async function writeSettings(config: SettingsConfig): Promise<void> {
try { await createFolder(SETTINGS_DIR, true); } catch { /* exists */ } try { await createFolder(SETTINGS_DIR, true); } catch { /* exists */ }
const { iec, ...mainConfig } = config; const { devices, ...mainConfig } = config;
const { devices: iecDevices, ...iecBusConfig } = (iec ?? {}) as any; const configJson = JSON.stringify(mainConfig, null, 2) + '\n';
const configJson = JSON.stringify({ ...mainConfig, iec: iecBusConfig }, null, 2) + '\n'; const devicesJson = JSON.stringify({ devices }, null, 2) + '\n';
const devicesJson = JSON.stringify({ iec: { devices: iecDevices } }, null, 2) + '\n';
await Promise.all([ await Promise.all([
putFileContents(SETTINGS_PATH, configJson), putFileContents(SETTINGS_PATH, configJson),
@ -220,17 +219,16 @@ export function useSettings(): UseSettingsResult {
// Modern browsers: `fetch` with `keepalive: true` continues even // Modern browsers: `fetch` with `keepalive: true` continues even
// after the page is being unloaded. We don't await it. // after the page is being unloaded. We don't await it.
try { try {
const { iec, ...mainConfig } = configRef.current; const { devices, ...mainConfig } = configRef.current;
const { devices: iecDevices, ...iecBusConfig } = (iec ?? {}) as any;
void fetch(absoluteUrl(SETTINGS_PATH), { void fetch(absoluteUrl(SETTINGS_PATH), {
method: 'PUT', method: 'PUT',
body: JSON.stringify({ ...mainConfig, iec: iecBusConfig }, null, 2) + '\n', body: JSON.stringify(mainConfig, null, 2) + '\n',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
keepalive: true, keepalive: true,
}); });
void fetch(absoluteUrl(DEVICES_PATH), { void fetch(absoluteUrl(DEVICES_PATH), {
method: 'PUT', method: 'PUT',
body: JSON.stringify({ iec: { devices: iecDevices } }, null, 2) + '\n', body: JSON.stringify({ devices }, null, 2) + '\n',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
keepalive: true, keepalive: true,
}); });

View File

@ -1,49 +1,20 @@
{ {
"general": { "preferences": {
"devicename": "Meatloaf", "appearance": "auto",
"appearance": "light",
"language": "en", "language": "en",
"timezone": "America/New_York", "timezone": "America/Los_Angeles",
"country": "US", "country": "US",
"hsioindex": -1, "devicename": "Meatloaf",
"rotationsounds": 1, "rotationsounds": 0,
"configenabled": 1,
"altconfigfile": "",
"boot_mode": 0,
"fnconfig_on_spifs": 1,
"status_wait_enabled": 1,
"encrypt_passphrase": 0,
"reset_with_host": 0 "reset_with_host": 0
}, },
"host": { "host": {
"model": "c64", "model": "c64",
"video": "ntsc", "video": "ntsc",
"language": "en", "language": "en",
"kernal": "Stock", "kernal": "stock",
"basic": "BASIC 2.0" "basic": "2"
}, },
"hardware": {
"ps2": 1,
"userport": {
"enabled": 0,
"mode": ""
}
},
"wifi": [
{
"enabled": 1,
"ssid": "meatloaf",
"passphrase": "meatloaf"
},
{
"ssid": "CODA-23B0",
"passphrase": "meatloaf"
},
{
"ssid": "EEP_OPP_ORK_AH_AH",
"passphrase": "meatloaf"
}
],
"network": { "network": {
"hostname": "meatloaf.local", "hostname": "meatloaf.local",
"sntpserver": "pool.ntp.org", "sntpserver": "pool.ntp.org",
@ -62,27 +33,29 @@
"tcp_console": 1 "tcp_console": 1
} }
}, },
"bluetooth": { "wifi": [
"enabled": 0, {
"devicename": "meatloaf", "ssid": "EEP_OPP_ORK_AH_AH",
"baud": 19200 "passphrase": "meatloaf",
}, "enabled": 1
"modem": { },
"modem_enabled": 1, {
"sniffer_enabled": 0 "enabled": 0,
}, "ssid": "meatloaf",
"cassette": { "passphrase": "meatloaf"
"enabled": 0, },
"play_record": "0 Play", {
"pulldown": "1 Pulldown Resistor", "ssid": "CODA-23B0",
"url": "/" "passphrase": "meatloaf",
}, "enabled": 0
"iec": { }
],
"settings": {
"enabled": 1, "enabled": 1,
"vic20_mode": 0, "vic20_mode": 0,
"vdrive": 0, "vdrive_mode": 0,
"autoboot": "", "autoboot": "",
"rom": { "drive_roms": {
"enabled": 0, "enabled": 0,
"default": "", "default": "",
"d64": "", "d64": "",
@ -136,60 +109,63 @@
"sid": 1, "sid": 1,
"koa": 1, "koa": 1,
"bbs": 1 "bbs": 1
}, }
"devices": { },
"devices": {
"iec": {
"4": { "4": {
"enabled": 1, "enabled": 1,
"type": "printer", "type": "printer",
"name": "MPS803" "name": "MPS803",
"url": "/.print"
}, },
"8": { "8": {
"enabled": 1, "enabled": 1,
"type": "drive", "type": "drive",
"url": "/sd", "mode": 1,
"mode": 1 "url": "/"
}, },
"9": { "9": {
"enabled": 0, "enabled": 0,
"type": "drive", "type": "drive",
"url": "/", "mode": 1,
"mode": 1 "url": "/"
}, },
"10": { "10": {
"enabled": 0, "enabled": 0,
"type": "drive", "type": "drive",
"url": "/", "mode": 1,
"mode": 1 "url": "/"
}, },
"11": { "11": {
"enabled": 0, "enabled": 0,
"type": "drive", "type": "drive",
"url": "/", "mode": 1,
"mode": 1 "url": "/"
}, },
"12": { "12": {
"enabled": 0, "enabled": 0,
"type": "drive", "type": "drive",
"url": "/", "mode": 1,
"mode": 1 "url": "/"
}, },
"13": { "13": {
"enabled": 0, "enabled": 0,
"type": "drive", "type": "drive",
"url": "/", "mode": 1,
"mode": 1 "url": "/"
}, },
"14": { "14": {
"enabled": 0, "enabled": 0,
"type": "drive", "type": "drive",
"url": "/", "mode": 1,
"mode": 1 "url": "/"
}, },
"15": { "15": {
"enabled": 0, "enabled": 0,
"type": "drive", "type": "drive",
"url": "/", "mode": 1,
"mode": 1 "url": "/"
}, },
"16": { "16": {
"enabled": 1, "enabled": 1,
@ -229,14 +205,20 @@
"30": { "30": {
"enabled": 1, "enabled": 1,
"type": "meatloaf", "type": "meatloaf",
"url": "/", "mode": 1,
"mode": 1 "url": "/"
} }
},
"ps2": 0,
"userport": {
"enabled": 0,
"mode": "parallel"
},
"cassette": {
"enabled": 0,
"play_record": "0 Play",
"pulldown": "1 Pulldown Resistor",
"url": "/sd/choppin_mall.t64"
} }
},
"boip": {
"enabled": 0,
"host": "",
"port": ""
} }
} }