mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
refactor: extract application construction and lifecycle
This commit is contained in:
1 parent
092f28e301
commit
4d37760314
36 files changed
+1363
-382
No files matched your search
+34
-10
@@ -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 };
|
||||
}
|
||||
Reference in new issue
Block a user