refactor: extract application construction and lifecycle

This commit is contained in:
Sameh Khamis committed 2026-09-11 17:02:54 -07:00
1 parent 092f28e301
commit 4d37760314
36 files changed
+1363 -382

No files matched your search

+34 -10
View File
@@ -142,23 +142,37 @@ function buildRow(documentRef, key) {
* Wire the chip + dialog. Fire-and-forget from main.js; resolves to null when
* the surface has no business existing (prod build, LAN visitor, no markup).
*/
export async function initKeySetup({ documentRef = globalThis.document, fetchImpl } = {}) {
export async function initKeySetup({ documentRef = globalThis.document, fetchImpl, signal } = {}) {
const chip = documentRef?.getElementById?.('key-setup-chip');
const root = documentRef?.getElementById?.('key-setup');
if (!chip || !root || root.dataset.initialized === 'true') return null;
root.dataset.initialized = 'true';
const lifetime = new AbortController();
let disposed = false;
let disposeControls = () => {};
const destroy = () => {
if (disposed) return;
disposed = true;
lifetime.abort();
signal?.removeEventListener('abort', destroy);
disposeControls();
chip.remove();
root.remove();
};
if (signal?.aborted) { destroy(); return null; }
signal?.addEventListener('abort', destroy, { once: true });
const doFetch = fetchImpl || globalThis.fetch?.bind(globalThis);
let status = null;
try {
const response = await doFetch('/api/setup/status', { cache: 'no-store' });
const response = await doFetch('/api/setup/status', { cache: 'no-store', signal: lifetime.signal });
if (!response.ok) throw new Error(String(response.status));
status = await response.json();
if (disposed) return null;
} catch {
// Prod build or non-loopback visitor: the surface cannot function, so it
// does not exist. (The README covers .env for headless/self-host setups.)
chip.remove();
root.remove();
destroy();
return null;
}
@@ -173,6 +187,7 @@ export async function initKeySetup({ documentRef = globalThis.document, fetchImp
let previouslyFocused = null;
const render = (nextStatus) => {
if (disposed) return;
status = nextStatus;
chipLabel.textContent = keySetupChipLabel(status);
// Fully powered is the owner's clean screen: the chip retires. The dialog
@@ -223,7 +238,7 @@ export async function initKeySetup({ documentRef = globalThis.document, fetchImp
};
const openDialog = () => {
if (open) return;
if (disposed || open) return;
open = true;
previouslyFocused = documentRef.activeElement;
root.hidden = false;
@@ -256,7 +271,7 @@ export async function initKeySetup({ documentRef = globalThis.document, fetchImp
: 'your local .env');
const submitUpdates = async (updates, doneVerb) => {
if (busy) return;
if (disposed || busy) return;
const googleWasUnset = !status?.keys?.find((key) => key.id === 'google-maps')?.set;
busy = true;
applyButton?.setAttribute('aria-disabled', 'true');
@@ -264,10 +279,12 @@ export async function initKeySetup({ documentRef = globalThis.document, fetchImp
try {
const response = await doFetch('/api/setup/keys', {
method: 'POST',
signal: lifetime.signal,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
});
const payload = await response.json().catch(() => ({}));
if (disposed) return;
if (!response.ok || !payload.ok) {
say(payload.error || `Save failed (${response.status}).`);
return;
@@ -286,7 +303,7 @@ export async function initKeySetup({ documentRef = globalThis.document, fetchImp
strip();
// The live share writer may re-serialize the still-OSM stack before
// the restart's reload lands, so strip again at the door.
globalThis.addEventListener?.('pagehide', strip, { once: true });
globalThis.addEventListener?.('pagehide', strip, { once: true, signal: lifetime.signal });
}
say(`${doneVerb} ${storeLabel()}. Restarting — this page reloads itself.`);
} catch (error) {
@@ -298,7 +315,7 @@ export async function initKeySetup({ documentRef = globalThis.document, fetchImp
};
const onApply = async () => {
if (busy) return;
if (disposed || busy) return;
const inputs = [...root.querySelectorAll('input[data-env-var]')];
const updates = collectKeyUpdates(
inputs.map((input) => ({ envVar: input.dataset.envVar, value: input.value })),
@@ -316,7 +333,7 @@ export async function initKeySetup({ documentRef = globalThis.document, fetchImp
// Remove buttons are rendered per row; delegate so re-renders stay wired.
rowsHost?.addEventListener('click', (event) => {
const button = event.target?.closest?.('[data-key-setup-remove]');
if (!button || busy) return;
if (disposed || !button || busy) return;
let envVars = [];
try {
envVars = JSON.parse(button.dataset.keySetupRemove || '[]');
@@ -346,5 +363,12 @@ export async function initKeySetup({ documentRef = globalThis.document, fetchImp
// An unparsable location never blocks init.
}
return { open: openDialog, close, render };
disposeControls = () => {
open = false;
documentRef.removeEventListener('keydown', onKeyDown, true);
chip.removeEventListener('click', openDialog);
closeButton?.removeEventListener('click', close);
applyButton?.removeEventListener('click', onApply);
};
return { open: openDialog, close, render, destroy };
}