cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

372 lines
12 KiB
JavaScript

(() => {
const bridge = window.rakazoSetup;
document.documentElement.dataset.platform = bridge?.platform ?? "browser";
const form = document.getElementById("setup");
const serverUrl = document.getElementById("server-url");
const panelNew = document.getElementById("panel-new");
const panelExisting = document.getElementById("panel-existing");
const stackSection = document.getElementById("stack");
const stackPhase = document.getElementById("stack-phase");
const stackOutput = document.getElementById("stack-output");
const stackDetail = document.getElementById("stack-detail");
const stackDetails = document.getElementById("stack-details");
const stackProgress = document.getElementById("stack-progress");
const stackProgressFill = document.getElementById("stack-progress-fill");
const stackDockerHelp = document.getElementById("stack-docker-help");
const status = document.getElementById("status");
const checkButton = document.getElementById("check");
const continueButton = document.getElementById("continue");
const quitButton = document.getElementById("quit");
const STACK_POLL_MS = 1000;
const PHASE_LABELS = {
"checking-docker": "Getting ready…",
preparing: "Getting ready…",
pulling: "Downloading Rakazo…",
starting: "Starting Rakazo…",
"waiting-healthy": "Almost ready…",
ready: "Rakazo is ready.",
};
const TERMINAL_PHASES = new Set([
"idle",
"docker-missing",
"docker-not-running",
"ready",
"failed",
]);
const PHASE_PROGRESS = {
"checking-docker": 0.06,
preparing: 0.12,
pulling: 0.2,
starting: 0.82,
"waiting-healthy": 0.92,
ready: 1,
};
/** Docker reports bytes pulled but never a total, so the bar approaches the next phase without reaching it. */
const PULL_SPAN = 0.6;
const PULL_SCALE_BYTES = 1.2e9;
let defaultLocalUrl = "";
let stackPolling = false;
let lastStack = null;
let lastProgress = 0;
let detailsOpen = false;
/** Set when a failure opened the output; a retry closes it again, a person's own click does not. */
let detailsOpenedByFailure = false;
/** Resolves the poll wait early when the main process pushes a new state. */
let wakePoll = null;
function selectedMode() {
const checked = form.querySelector('input[name="mode"]:checked');
return checked === null ? "new" : checked.value;
}
function setStatus(message, tone) {
status.textContent = message;
if (tone === undefined) status.removeAttribute("data-tone");
else status.setAttribute("data-tone", tone);
}
function setBusy(busy) {
checkButton.disabled = busy;
continueButton.disabled = busy;
}
/** A save in flight cannot be cancelled, so the choice it commits must not change under it. */
function lockMode(locked) {
for (const input of form.querySelectorAll('input[name="mode"]')) input.disabled = locked;
}
function syncPanels() {
const mode = selectedMode();
panelNew.hidden = mode !== "new";
panelExisting.hidden = mode === "new";
checkButton.hidden = mode === "new";
if (mode !== "new") continueButton.textContent = "Continue";
setStatus("");
}
function isDockerPhase(phase) {
return phase === "docker-missing" || phase === "docker-not-running";
}
function downloadedBytes(stack) {
return Object.values(stack.layerBytes ?? {}).reduce((total, bytes) => total + bytes, 0);
}
function formatBytes(bytes) {
if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)} GB`;
if (bytes >= 1e6) return `${Math.round(bytes / 1e6)} MB`;
if (bytes >= 1e3) return `${Math.round(bytes / 1e3)} kB`;
return `${Math.round(bytes)} B`;
}
function progressFor(stack) {
const base = PHASE_PROGRESS[stack.phase] ?? 0;
if (stack.phase !== "pulling") return base;
return base + PULL_SPAN * (1 - Math.exp(-downloadedBytes(stack) / PULL_SCALE_BYTES));
}
function renderProgress(stack) {
const running = stack.phase in PHASE_PROGRESS;
stackProgress.hidden = !running;
if (!running) {
lastProgress = 0;
stackProgressFill.style.width = "0%";
return;
}
// A retry restarts at its phase; otherwise the bar only ever moves forward.
lastProgress =
stack.phase === "checking-docker"
? progressFor(stack)
: Math.max(lastProgress, progressFor(stack));
const percent = (lastProgress * 100).toFixed(1);
stackProgressFill.style.width = `${percent}%`;
stackProgress.setAttribute("aria-valuenow", percent);
}
function renderDetails(stack) {
const bytes = stack.phase === "pulling" ? downloadedBytes(stack) : 0;
stackDetail.textContent = bytes > 0 ? `${formatBytes(bytes)} downloaded` : "";
// A new attempt clears the output, so drop an expansion the person did not ask for.
if (stack.phase === "checking-docker" && detailsOpenedByFailure) {
detailsOpen = false;
detailsOpenedByFailure = false;
}
const hasOutput = stack.output.length > 0;
// A failure asks the person to read the output, so open it for them.
if (stack.phase === "failed" && hasOutput && !detailsOpen) {
detailsOpen = true;
detailsOpenedByFailure = true;
}
stackDetails.hidden = !hasOutput;
stackDetails.setAttribute("aria-expanded", String(detailsOpen && hasOutput));
stackOutput.hidden = !(detailsOpen && hasOutput);
if (!stackOutput.hidden) {
stackOutput.textContent = stack.output.join("\n");
stackOutput.scrollTop = stackOutput.scrollHeight;
}
}
function renderStack(stack) {
lastStack = stack;
const { phase } = stack;
stackSection.hidden = phase === "idle";
if (phase === "idle") {
continueButton.textContent = "Continue";
return;
}
const failed = isDockerPhase(phase) || phase === "failed";
stackPhase.textContent = (failed ? stack.message : PHASE_LABELS[phase]) ?? "";
if (failed) stackPhase.setAttribute("data-tone", "error");
else if (phase === "ready") stackPhase.setAttribute("data-tone", "ok");
else stackPhase.removeAttribute("data-tone");
renderProgress(stack);
renderDetails(stack);
stackDockerHelp.hidden = !isDockerPhase(phase);
if (isDockerPhase(phase)) continueButton.textContent = "Check again";
else if (phase === "failed") continueButton.textContent = "Retry";
else continueButton.textContent = "Continue";
setBusy(!TERMINAL_PHASES.has(phase));
}
async function save(mode, url) {
setBusy(true);
lockMode(true);
setStatus("Connecting…");
try {
const saved = await bridge.save({ mode, serverUrl: url });
if (!saved.ok) setStatus(saved.error ?? "Could not save that address.", "error");
} catch {
setStatus("Could not save that address. Try again.", "error");
} finally {
lockMode(false);
setBusy(false);
}
}
/** Wakes on the next pushed state, and on the timer if a push is ever missed. */
function waitForStackChange() {
return new Promise((resolve) => {
let done = false;
const finish = () => {
if (done) return;
done = true;
if (wakePoll === finish) wakePoll = null;
resolve();
};
wakePoll = finish;
setTimeout(finish, STACK_POLL_MS);
});
}
/**
* Follows a start already in flight until it settles. Leaving "This computer"
* ends the follow so the stack becoming ready never saves over that choice.
*/
async function followStack() {
if (stackPolling) return;
stackPolling = true;
try {
while (true) {
const stack = await bridge.stack.state();
if (stack === null) throw new Error("Setup is not active");
// Checked after the await: a mode change during it hands the form to the change
// handler, and a save it started must stay busy and must not be re-rendered over.
if (selectedMode() !== "new") return;
renderStack(stack);
if (TERMINAL_PHASES.has(stack.phase)) {
if (stack.phase === "ready" && selectedMode() === "new") {
const current = await bridge.state();
if (selectedMode() !== "new") return;
defaultLocalUrl = current.defaultLocalUrl;
await save("new", defaultLocalUrl);
}
return;
}
await waitForStackChange();
}
} catch {
setStatus("Could not follow the local stack. Try again.", "error");
setBusy(false);
} finally {
stackPolling = false;
}
}
async function runStack() {
setStatus("");
setBusy(true);
try {
// A queued start still reads `idle`; leave the panel as it is and let the follow render it.
const started = await bridge.stack.start();
if (started !== null && started.phase !== "idle") renderStack(started);
} catch {
setStatus("Could not start the local stack. Try again.", "error");
setBusy(false);
return;
}
await followStack();
}
async function check() {
const value = serverUrl.value;
if (value.trim() === "") {
setStatus("Enter a server address first.", "error");
return null;
}
setBusy(true);
setStatus("Checking…");
try {
const result = await bridge.test(value);
if (result.ok) {
serverUrl.value = result.url;
setStatus(`Rakazo answered at ${result.url}.`, "ok");
} else {
setStatus(result.error ?? "Could not reach that address.", "error");
}
return result;
} catch {
setStatus("Could not run the connection check. Try again.", "error");
return null;
} finally {
setBusy(false);
}
}
form.addEventListener("change", (event) => {
if (event.target instanceof HTMLInputElement && event.target.name === "mode") {
syncPanels();
// Unlock Continue/Check immediately; followStack exits on its next poll.
if (selectedMode() !== "new") setBusy(false);
}
});
checkButton.addEventListener("click", () => {
void check();
});
stackDetails.addEventListener("click", () => {
detailsOpen = !detailsOpen;
detailsOpenedByFailure = false;
if (lastStack !== null) renderDetails(lastStack);
});
stackDockerHelp.addEventListener("click", (event) => {
const link = event.target instanceof HTMLElement ? event.target.dataset.link : undefined;
if (link) void bridge.openLink(link);
});
quitButton.addEventListener("click", () => {
if (bridge === undefined) {
window.close();
return;
}
void bridge.quit();
});
form.addEventListener("submit", (event) => {
event.preventDefault();
if (selectedMode() === "new") void runStack();
else void save("existing", serverUrl.value);
});
/** Pushed state keeps progress moving even when the OS throttles this window's timers. */
function watchStack() {
bridge.stack.onChange(() => {
wakePoll?.();
// A follow that ended on an error restarts here instead of leaving the panel frozen;
// hold the controls until it has rendered the state it is restarting on.
if (!stackPolling && selectedMode() === "new") {
setBusy(true);
void followStack();
}
});
}
async function init() {
if (bridge === undefined) {
setStatus("Setup bridge unavailable.", "error");
setBusy(true);
return;
}
watchStack();
try {
const state = await bridge.state();
if (state === null) throw new Error("Setup is not active");
defaultLocalUrl = state.defaultLocalUrl;
if (state.saved !== null) {
const modeInput = document.querySelector(`input[name="mode"][value="${state.saved.mode}"]`);
if (modeInput !== null) modeInput.checked = true;
if (state.saved.mode === "existing") serverUrl.value = state.saved.serverUrl;
}
// A relaunch with the stack down starts it before this window opens; show that
// attempt instead of the saved mode, and follow it while it is still running.
const stack = await bridge.stack.state();
const attached = stack !== null && stack.phase !== "idle";
if (attached) document.getElementById("mode-new").checked = true;
syncPanels();
if (state.error) setStatus(state.error, "error");
if (attached) {
renderStack(stack);
if (!TERMINAL_PHASES.has(stack.phase)) void followStack();
} else if (selectedMode() === "existing") {
serverUrl.focus();
} else {
continueButton.focus();
}
} catch {
setStatus("Setup could not start. Quit Rakazo and try again.", "error");
setBusy(true);
}
}
void init();
})();