Compare commits

..

No commits in common. "d9f95c6864c21f9bb6c252f6040da3114f058740" and "488d2304be632bb325598e270e7feb5c2a10e3a9" have entirely different histories.

2 changed files with 40 additions and 78 deletions

View File

@ -413,9 +413,7 @@ export default function DeviceDetailOverlay({
)} )}
</div> </div>
{(deviceData.cache !== undefined || {deviceData.cache !== undefined && (
(deviceData.base_url ?? '').includes('://') ||
(deviceData.url ?? '').includes('://')) && (
<div> <div>
<label className="text-sm text-neutral-500 block mb-2">Cache</label> <label className="text-sm text-neutral-500 block mb-2">Cache</label>
<div className="flex gap-2"> <div className="flex gap-2">

View File

@ -164,62 +164,34 @@ export async function readSettings(): Promise<ReadSettingsResult | null> {
} }
} }
export interface SavedJson {
configJson: string;
devicesJson: string;
/** True if at least one file was actually written. */
wrote: boolean;
}
/** Serialize config into the two canonical JSON strings without writing anything. */
export function serializeConfig(config: SettingsConfig): SavedJson {
const { devices, ...mainConfig } = config;
return {
configJson: JSON.stringify(mainConfig, null, 2) + '\n',
devicesJson: JSON.stringify({ devices }, null, 2) + '\n',
wrote: false,
};
}
/** /**
* Persist the settings object to the WebDAV server, creating the * Persist the settings object to the WebDAV server, creating the
* containing `/.sys/` directory if needed. Pretty-prints with 2-space * containing `/.sys/` directory if needed. Pretty-prints with 2-space
* indentation so the file is diff-friendly. * indentation so the file is diff-friendly.
*
* Pass `lastSaved` to skip writing files whose content hasn't changed.
* Returns the canonical JSON strings that were written (or would have been).
*/ */
export async function writeSettings( export async function writeSettings(config: SettingsConfig): Promise<void> {
config: SettingsConfig,
lastSaved?: SavedJson | null,
): Promise<SavedJson> {
const next = serializeConfig(config);
const writeConfig = !lastSaved || next.configJson !== lastSaved.configJson;
const writeDevices = !lastSaved || next.devicesJson !== lastSaved.devicesJson;
if (!writeConfig && !writeDevices) return { ...next, wrote: false };
try { await createFolder(SETTINGS_DIR, true); } catch { /* exists */ } try { await createFolder(SETTINGS_DIR, true); } catch { /* exists */ }
const writes: Promise<void>[] = []; const { devices, ...mainConfig } = config;
if (writeConfig) writes.push(putFileContents(SETTINGS_PATH, next.configJson)); const configJson = JSON.stringify(mainConfig, null, 2) + '\n';
if (writeDevices) writes.push(putFileContents(DEVICES_PATH, next.devicesJson)); const devicesJson = JSON.stringify({ devices }, null, 2) + '\n';
await Promise.all(writes);
await Promise.all([
putFileContents(SETTINGS_PATH, configJson),
putFileContents(DEVICES_PATH, devicesJson),
]);
// Mirror to /sd/.sys/ if the SD card is mounted. // Mirror to /sd/.sys/ if the SD card is mounted.
try { try {
const sdStat = await stat('/sd'); const sdStat = await stat('/sd');
if (sdStat) { if (sdStat) {
try { await createFolder(SD_SYS_DIR, true); } catch { /* exists */ } try { await createFolder(SD_SYS_DIR, true); } catch { /* exists */ }
const sdWrites: Promise<void>[] = []; await Promise.all([
if (writeConfig) sdWrites.push(putFileContents(SD_SYS_DIR + '/config.json', next.configJson)); putFileContents(SD_SYS_DIR + '/config.json', configJson),
if (writeDevices) sdWrites.push(putFileContents(SD_SYS_DIR + '/devices.json', next.devicesJson)); putFileContents(SD_SYS_DIR + '/devices.json', devicesJson),
await Promise.all(sdWrites); ]);
} }
} catch { /* /sd unreachable — skip mirror */ } } catch { /* /sd unreachable — skip mirror */ }
return { ...next, wrote: true };
} }
export type SaveStatus = export type SaveStatus =
@ -268,13 +240,12 @@ export function useSettings(): UseSettingsResult {
// Refs so the debounced writer always sees the latest values without // Refs so the debounced writer always sees the latest values without
// needing to re-subscribe on every render. // needing to re-subscribe on every render.
const configRef = useRef<SettingsConfig>(config); const configRef = useRef<SettingsConfig>(config);
const loadedRef = useRef<boolean>(false); const loadedRef = useRef<boolean>(false);
const timerRef = useRef<number | null>(null); const timerRef = useRef<number | null>(null);
const dirtyRef = useRef<boolean>(false); const dirtyRef = useRef<boolean>(false);
const pendingRef = useRef<number>(0); const pendingRef = useRef<number>(0);
const savedTimerRef = useRef<number | null>(null); const savedTimerRef = useRef<number | null>(null);
const lastSavedRef = useRef<SavedJson | null>(null);
const load = useCallback(async () => { const load = useCallback(async () => {
setSaveStatus('loading'); setSaveStatus('loading');
@ -286,7 +257,7 @@ export function useSettings(): UseSettingsResult {
// Persist the migrated config immediately so the device is updated. // Persist the migrated config immediately so the device is updated.
setSaveStatus('saving'); setSaveStatus('saving');
try { try {
lastSavedRef.current = await writeSettings(result.config); await writeSettings(result.config);
setSaveStatus('saved'); setSaveStatus('saved');
savedTimerRef.current = window.setTimeout(() => { savedTimerRef.current = window.setTimeout(() => {
setSaveStatus((s) => (s === 'saved' ? 'idle' : s)); setSaveStatus((s) => (s === 'saved' ? 'idle' : s));
@ -302,8 +273,6 @@ export function useSettings(): UseSettingsResult {
setLoaded(true); setLoaded(true);
return; return;
} }
// Record what the server holds so flushNow can skip unchanged files.
lastSavedRef.current = serializeConfig(result.config);
} }
loadedRef.current = true; loadedRef.current = true;
setLoaded(true); setLoaded(true);
@ -330,16 +299,12 @@ export function useSettings(): UseSettingsResult {
setPendingCount(0); setPendingCount(0);
setSaveStatus('saving'); setSaveStatus('saving');
try { try {
const result = await writeSettings(configRef.current, lastSavedRef.current); await writeSettings(configRef.current);
lastSavedRef.current = result; setSaveStatus('saved');
if (result.wrote) { // Drop the "saved" badge after a short delay so it doesn't linger.
setSaveStatus('saved'); savedTimerRef.current = window.setTimeout(() => {
savedTimerRef.current = window.setTimeout(() => { setSaveStatus((s) => (s === 'saved' ? 'idle' : s));
setSaveStatus((s) => (s === 'saved' ? 'idle' : s)); }, SAVED_INDICATOR_MS);
}, SAVED_INDICATOR_MS);
} else {
setSaveStatus('idle');
}
} catch (e) { } catch (e) {
// Roll back: the change is still pending. // Roll back: the change is still pending.
dirtyRef.current = true; dirtyRef.current = true;
@ -359,20 +324,19 @@ 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 next = serializeConfig(configRef.current); const { devices, ...mainConfig } = configRef.current;
const last = lastSavedRef.current; void fetch(absoluteUrl(SETTINGS_PATH), {
if (!last || next.configJson !== last.configJson) { method: 'PUT',
void fetch(absoluteUrl(SETTINGS_PATH), { body: JSON.stringify(mainConfig, null, 2) + '\n',
method: 'PUT', body: next.configJson, headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json' }, keepalive: true, keepalive: true,
}); });
} void fetch(absoluteUrl(DEVICES_PATH), {
if (!last || next.devicesJson !== last.devicesJson) { method: 'PUT',
void fetch(absoluteUrl(DEVICES_PATH), { body: JSON.stringify({ devices }, null, 2) + '\n',
method: 'PUT', body: next.devicesJson, headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json' }, keepalive: true, keepalive: true,
}); });
}
} catch { } catch {
/* ignore */ /* ignore */
} }