Test and deploy / test-and-deploy (push) Successful in 1m39s
- POST /api/roasts stores the original .alog verbatim (multiple per plan), parses it server-side, and kicks off an async zero-tool Pi agent review (grade, highlights, concerns, next-batch suggestions, plan-vs-actual) - /roasts page: table of historical actual roasts with review summaries; row detail renders the BT curve as SVG with the plan curve overlaid, shows the full review, and offers original-.alog backup download, re-evaluate, and delete - Planner's Reference curve drawer gains a multi-file finished-roast uploader that attaches to the open (synced) plan - Failed reviews record the error on the row and are retryable via POST /api/roasts/:id/evaluate (e.g. once a model is configured) Co-Authored-By: Claude Fable 5 <[email protected]>
149 lines
4.5 KiB
JavaScript
149 lines
4.5 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");
|
|
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 Pi agent 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();
|
|
setText(
|
|
resultEl,
|
|
`${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")}`;
|
|
}
|