Files
roast_command_center/public/js/alog-ui.js
T
snowspeederandClaude Fable 5 385474ecef
Test and deploy / test-and-deploy (push) Successful in 1m8s
Removable reference curves, audit fixes across pages, shaded Academy art
The planner's Artisan .alog drawer now shows what's attached with a
"Remove reference curve" control (re-rendered per drawer open), so adding
a reference curve is no longer a one-way door; /api/alog shares the 8 MB
body cap so real-sized logs parse instead of failing with "bad_request".

Deep-evaluation fixes: editing a brew of an archived bean no longer
silently detaches the bean; the roasts pending-review poll no longer
wipes in-progress after-roast edits; roasters gain an Edit (rename/model)
action; the gear page refuses to autosave over a failed load; duplicating
a plan carries its custom name; cupping sessions can attach a plan after
creation (ownership-checked PUT + selector); admin user deletion also
refreshes plans/audit; cupping cup-count subtitle stays live; roasts
error-row colspan corrected. Regression tests cover the new cupping PUT
and the /api/alog body cap.

Academy scenes drop the flat paper-cutout look: shared defs provide
radial-gradient shading on every bean/half-bean/particle, flame gradients
with radiant halos, soft ground shadows, and a warm-lit stage background;
fill-shift animations now ride a partial-opacity tint overlay so shading
survives the color change.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-11 16:59:28 -04:00

180 lines
5.7 KiB
JavaScript

const csrf = () =>
document.cookie
.split("; ")
.find((v) => v.startsWith("rp_csrf="))
?.split("=")[1] || "";
const setText = (el, text, error = false) => {
const node = document.createElement("p");
node.textContent = text;
if (error) node.style.color = "#a8371a";
el.replaceChildren(node);
};
export function initAlogPanel({
state,
recompute,
getRemotePlanId,
flushCurrentPlan,
}) {
const fileInput = document.getElementById("alog-file"),
libraryBtn = document.getElementById("alog-library-refresh"),
libraryList = document.getElementById("alog-library-list"),
resultEl = document.getElementById("alog-result"),
actualInput = document.getElementById("actual-alog-file"),
actualResultEl = document.getElementById("actual-alog-result");
// A plan keeps its attached reference curve until explicitly removed — show what's
// attached (including on page load) with a Remove control, so adding a curve is
// never a one-way door.
function renderReference(summary) {
const ref = state.plan.reference;
if (!ref?.roast) {
resultEl.replaceChildren();
return;
}
const node = document.createElement("p");
node.textContent =
summary ??
`Attached: ${ref.roast.title || "reference curve"}${ref.roast.roastDate || "no date"}.`;
const removeBtn = document.createElement("button");
removeBtn.type = "button";
removeBtn.className = "ghost-btn small";
removeBtn.style.marginLeft = "8px";
removeBtn.textContent = "Remove reference curve";
removeBtn.addEventListener("click", () => {
state.plan.reference = null;
recompute();
setText(resultEl, "Reference curve removed.");
});
node.append(removeBtn);
resultEl.replaceChildren(node);
}
renderReference();
// Re-render on every drawer open: the user may have switched plans since init, and the
// summary must always describe the plan currently loaded.
document
.getElementById("nav-alog")
?.addEventListener("click", () => renderReference());
fileInput.addEventListener("change", async (e) => {
const file = e.target.files?.[0];
if (!file) return;
setText(resultEl, "Parsing…");
try {
const res = await fetch("/api/alog", {
method: "POST",
headers: { "content-type": "application/json", "x-csrf-token": csrf() },
body: JSON.stringify({
filename: file.name,
content: await file.text(),
}),
});
applyResult(await res.json());
} catch (err) {
setText(resultEl, err.message, true);
}
});
libraryBtn.addEventListener("click", async () => {
libraryList.classList.remove("hidden");
libraryList.replaceChildren(
Object.assign(document.createElement("li"), { textContent: "Loading…" }),
);
try {
const body = await (await fetch("/api/alog/library")).json();
if (!body.ok || !body.files.length) {
libraryList.replaceChildren(
Object.assign(document.createElement("li"), {
textContent:
"No .alog files found. Set ALOG_DIR or drop files in ~/Roastetta.",
}),
);
return;
}
libraryList.replaceChildren(
...body.files.map((f) => {
const li = document.createElement("li");
li.textContent = `${f.filename} (${Math.round(f.sizeBytes / 1024)} KB)`;
li.onclick = async () => {
setText(resultEl, "Loading…");
applyResult(
await (
await fetch(
`/api/alog/library/${encodeURIComponent(f.filename)}`,
)
).json(),
);
};
return li;
}),
);
} catch (err) {
const li = document.createElement("li");
li.textContent = err.message;
li.style.color = "#a8371a";
libraryList.replaceChildren(li);
}
});
// Finished-roast uploads: each file becomes an actual_roasts row attached to the open plan
// (which must exist server-side first — hence the flush), and the LLM reviews it
// asynchronously; the roast history page is where results land.
actualInput?.addEventListener("change", async (e) => {
const files = [...(e.target.files ?? [])];
e.target.value = "";
if (!files.length) return;
setText(actualResultEl, "Syncing plan…");
await flushCurrentPlan();
const planId = getRemotePlanId();
if (!planId) {
setText(
actualResultEl,
"Could not sync this plan to the server — connect and try again, or upload from the Roasts page without a plan.",
true,
);
return;
}
let uploaded = 0;
for (const file of files) {
setText(actualResultEl, `Uploading ${file.name}…`);
try {
const res = await fetch("/api/roasts", {
method: "POST",
headers: { "content-type": "application/json", "x-csrf-token": csrf() },
body: JSON.stringify({
roastPlanId: planId,
filename: file.name,
content: await file.text(),
}),
});
const body = await res.json();
if (!body.ok) throw new Error(body.error ?? body.code);
uploaded++;
} catch (err) {
setText(actualResultEl, `${file.name}: ${err.message}`, true);
return;
}
}
const node = document.createElement("p");
node.append(
`Uploaded ${uploaded} roast${uploaded === 1 ? "" : "s"} to this plan. `,
);
const link = document.createElement("a");
link.href = "/roasts";
link.textContent = "See the review →";
node.append(link);
actualResultEl.replaceChildren(node);
});
function applyResult(body) {
if (!body.ok) {
setText(resultEl, body.error ?? body.code, true);
return;
}
state.plan.reference = body;
recompute();
renderReference(
`${body.roast.title}${body.roast.roastDate || "no date"}; first crack ${fmt(body.derived?.firstCrackS)}; development ${fmt(body.derived?.developmentS)}; drop ${fmt(body.derived?.dropS)}; DTR ${body.derived?.dtrPct ?? "—"}%.`,
);
}
}
function fmt(seconds) {
if (seconds == null) return "—";
const s = Math.round(seconds);
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
}