From 4fe0adccc6c517d3f81eca77da0eaf7092abaea1 Mon Sep 17 00:00:00 2001 From: Jaime Idolpx Date: Tue, 9 Jun 2026 02:27:17 -0400 Subject: [PATCH] feat(settings): enhance settings management by merging config and devices data from WebDAV --- src/app/settings.ts | 58 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/src/app/settings.ts b/src/app/settings.ts index c7375f9..c2182e4 100644 --- a/src/app/settings.ts +++ b/src/app/settings.ts @@ -18,8 +18,9 @@ import { putFileContents, } from './webdav'; -/** The canonical path on the WebDAV server. */ +/** The canonical paths on the WebDAV server. */ export const SETTINGS_PATH = '/.sys/config.json'; +export const DEVICES_PATH = '/.sys/devices.json'; const SETTINGS_DIR = '/.sys'; /** @@ -37,15 +38,32 @@ const SAVED_INDICATOR_MS = 1500; export type SettingsConfig = Record; -/** Read the settings file from the WebDAV server. Returns null on failure. */ +/** Read both config files from the WebDAV server and merge them. Returns null on failure. */ export async function readSettings(): Promise { try { - const blob = await getFileContents(SETTINGS_PATH); - const text = await blob.text(); - if (!text) return null; - const parsed = JSON.parse(text); - if (parsed && typeof parsed === 'object') return parsed as SettingsConfig; - return null; + const [configText, devicesText] = await Promise.all([ + getFileContents(SETTINGS_PATH).then(b => b.text()), + getFileContents(DEVICES_PATH).then(b => b.text()).catch(() => null), + ]); + if (!configText) return null; + const config = JSON.parse(configText); + if (!config || typeof config !== 'object') return null; + if (devicesText) { + const devices = JSON.parse(devicesText); + if (devices && typeof devices === 'object') { + // One-level deep merge: devices.json keys are merged into matching + // top-level objects in config (e.g. devices.iec.devices → config.iec.devices). + for (const [k, v] of Object.entries(devices)) { + if (config[k] && typeof config[k] === 'object' && !Array.isArray(config[k]) && + v && typeof v === 'object' && !Array.isArray(v)) { + config[k] = { ...config[k], ...(v as object) }; + } else { + config[k] = v; + } + } + } + } + return config as SettingsConfig; } catch { return null; } @@ -62,8 +80,12 @@ export async function writeSettings(config: SettingsConfig): Promise { } catch { /* directory may already exist; ignore */ } - const json = JSON.stringify(config, null, 2) + '\n'; - await putFileContents(SETTINGS_PATH, json); + const { iec, ...mainConfig } = config; + const { devices: iecDevices, ...iecBusConfig } = (iec ?? {}) as any; + await Promise.all([ + putFileContents(SETTINGS_PATH, JSON.stringify({ ...mainConfig, iec: iecBusConfig }, null, 2) + '\n'), + putFileContents(DEVICES_PATH, JSON.stringify({ iec: { devices: iecDevices } }, null, 2) + '\n'), + ]); } export type SaveStatus = @@ -176,9 +198,17 @@ export function useSettings(): UseSettingsResult { // Modern browsers: `fetch` with `keepalive: true` continues even // after the page is being unloaded. We don't await it. try { - void fetch(absoluteSettingsUrl(), { + const { iec, ...mainConfig } = configRef.current; + const { devices: iecDevices, ...iecBusConfig } = (iec ?? {}) as any; + void fetch(absoluteUrl(SETTINGS_PATH), { method: 'PUT', - body: JSON.stringify(configRef.current, null, 2) + '\n', + body: JSON.stringify({ ...mainConfig, iec: iecBusConfig }, null, 2) + '\n', + headers: { 'Content-Type': 'application/json' }, + keepalive: true, + }); + void fetch(absoluteUrl(DEVICES_PATH), { + method: 'PUT', + body: JSON.stringify({ iec: { devices: iecDevices } }, null, 2) + '\n', headers: { 'Content-Type': 'application/json' }, keepalive: true, }); @@ -226,8 +256,8 @@ export function useSettings(): UseSettingsResult { }; } -function absoluteSettingsUrl(): string { +function absoluteUrl(path: string): string { return `${window.location.protocol}//${window.location.hostname}${ window.location.port ? ':' + window.location.port : '' - }${SETTINGS_PATH}`; + }${path}`; }