Rebuild printable roast plans and remove machine-based adjustments
Test and deploy / test-and-deploy (push) Successful in 2m28s
Test and deploy / test-and-deploy (push) Successful in 2m28s
This commit is contained in:
1 parent
e9f2393c96
commit
824a964f9d
30 files changed
+1721
-908
No files matched your search
+15
-81
@@ -11,9 +11,10 @@ import {
|
||||
findProcess,
|
||||
findRoastLevel,
|
||||
} from "/shared/reference-data.js?v=__ASSET_VERSION__";
|
||||
import { buildPlanCurve, pointsToPathD, tToX, tempToY } from "/shared/curve.js?v=__ASSET_VERSION__";
|
||||
import { buildPlanCurve, pointsToPathD, tToX, tempToY, curveWarnings } from "/shared/curve.js?v=__ASSET_VERSION__";
|
||||
import { initPrefillPanel } from "./prefill-ui.js?v=__ASSET_VERSION__";
|
||||
import { initAlogPanel } from "./alog-ui.js?v=__ASSET_VERSION__";
|
||||
import { renderReportCurve } from "/shared/report.js?v=__ASSET_VERSION__";
|
||||
import { initPrint } from "./print.js?v=__ASSET_VERSION__";
|
||||
import { initLotPicker } from "./lot-picker.js?v=__ASSET_VERSION__";
|
||||
import { api, protectedFetch, csrfToken } from "./api.js?v=__ASSET_VERSION__";
|
||||
@@ -37,73 +38,10 @@ let deferredInstallPrompt = null;
|
||||
let lotPicker = null;
|
||||
const BAND_DOMAIN = [60, 220]; // shared °C domain for every band-track in the Machine Plan section
|
||||
|
||||
// Learned per-user machine profile (pace factor + temperature bands) from shared/learn.js — see
|
||||
// its header comment. Cached to localStorage per user so it's available offline, same pattern as
|
||||
// the plan draft cache below.
|
||||
const MACHINE_PROFILE_KEY_PREFIX = "roastPlannerMachineProfile.v1";
|
||||
let machineProfile = null;
|
||||
// Learned roaster behavior from uploaded .alogs (server-aggregated) — used as the plan
|
||||
// curve's fallback milestone temps so the suggested curve matches this user's machine.
|
||||
let roasterProfile = null;
|
||||
|
||||
async function loadRoasterProfile() {
|
||||
try {
|
||||
const response = await fetch("/api/roaster-profile");
|
||||
if (response.ok) roasterProfile = (await response.json()).profile;
|
||||
// A manual pace tweak on the default machine (Machines page) beats the plan-log
|
||||
// learned pace in the ledger.
|
||||
if (Number.isFinite(roasterProfile?.paceFactorOverride))
|
||||
machineProfile = {
|
||||
...(machineProfile ?? {}),
|
||||
pace: {
|
||||
value: roasterProfile.paceFactorOverride,
|
||||
source: "override",
|
||||
roasterName: roasterProfile.roaster?.name ?? null,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
/* offline — curve falls back to reference machine temps */
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMachineProfile(userId) {
|
||||
const cacheKey = `${MACHINE_PROFILE_KEY_PREFIX}:${userId}`;
|
||||
try {
|
||||
const response = await fetch("/api/machine-profile");
|
||||
if (response.ok) {
|
||||
machineProfile = (await response.json()).profile;
|
||||
localStorage.setItem(cacheKey, JSON.stringify(machineProfile));
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
/* offline — fall through to the cached profile, if any */
|
||||
}
|
||||
try {
|
||||
const cached = localStorage.getItem(cacheKey);
|
||||
if (cached) machineProfile = JSON.parse(cached);
|
||||
} catch {
|
||||
/* no cached profile available; ledger falls back to pace 1.0 (reference) */
|
||||
}
|
||||
}
|
||||
|
||||
const machineProfile = null;
|
||||
function renderMachineProfileNote() {
|
||||
const el = document.getElementById("machine-profile-note");
|
||||
if (!el) return;
|
||||
const pace = machineProfile?.pace;
|
||||
if (pace?.source === "override") {
|
||||
el.textContent = `Pace ×${pace.value.toFixed(2)} — your manual tweak${pace.roasterName ? ` for ${pace.roasterName}` : ""} (Machines page).`;
|
||||
return;
|
||||
}
|
||||
if (!pace || pace.source !== "learned") {
|
||||
el.textContent =
|
||||
"Using reference timing (one operator's Hottop KN-8828B-2K+) — recalibrates to your own machine as you log roasts with an actual first-crack time.";
|
||||
return;
|
||||
}
|
||||
const paceText =
|
||||
Math.abs(pace.value - 1) < 0.01
|
||||
? "running close to reference pace"
|
||||
: `running ${pace.value > 1 ? `${Math.round((pace.value - 1) * 100)}% slower` : `${Math.round((1 - pace.value) * 100)}% faster`} than reference`;
|
||||
el.textContent = `Learned from ${pace.n} of your own roasts — ${paceText}.`;
|
||||
document.getElementById("machine-profile-note").textContent = "Plans use your inputs. Roaster selection and roast history do not change timing or temperature suggestions.";
|
||||
}
|
||||
|
||||
// Temperature fields are always stored in state.plan as canonical °C — this is purely a
|
||||
@@ -111,7 +49,7 @@ function renderMachineProfileNote() {
|
||||
// the actuator schedule respectively; every other numeric field (%, g, days) is untouched.
|
||||
const TEMP_UNIT_KEY = "roastPlannerTempUnit.v1";
|
||||
const TEMP_FIELD_RE = /\.(tempC|actualBt|expectedBt)$/;
|
||||
let tempUnit = localStorage.getItem(TEMP_UNIT_KEY) === "F" ? "F" : "C";
|
||||
let tempUnit = localStorage.getItem(TEMP_UNIT_KEY) === "C" ? "C" : "F";
|
||||
const cToF = (c) => (c * 9) / 5 + 32;
|
||||
const fToC = (f) => ((f - 32) * 5) / 9;
|
||||
function isTempField(name) {
|
||||
@@ -357,6 +295,7 @@ function renderLedger() {
|
||||
fmtOut("check-dtr", pct(ledger.checks.dtr.pct));
|
||||
fmtOut("check-ceiling", d(ledger.checks.ceiling.valueS));
|
||||
|
||||
ledger.warnings.push(...curveWarnings(buildPlanCurve(state.plan, ledger)));
|
||||
const warnings = document.getElementById("ledger-warnings");
|
||||
warnings.replaceChildren(
|
||||
...ledger.warnings.map((warning) => {
|
||||
@@ -495,7 +434,7 @@ function paintCurveInto(planGroup, refGroup, planPoints, ref) {
|
||||
}
|
||||
|
||||
function renderCurve(ledger) {
|
||||
const planPoints = buildPlanCurve(state.plan, ledger, roasterProfile);
|
||||
const planPoints = buildPlanCurve(state.plan, ledger);
|
||||
const ref = state.plan.reference;
|
||||
|
||||
paintCurveInto(
|
||||
@@ -504,12 +443,10 @@ function renderCurve(ledger) {
|
||||
planPoints,
|
||||
ref,
|
||||
);
|
||||
paintCurveInto(
|
||||
document.getElementById("plan-curve-live"),
|
||||
document.getElementById("ref-curve-live"),
|
||||
planPoints,
|
||||
ref,
|
||||
);
|
||||
const live = document.getElementById("curve-svg-live");
|
||||
live.setAttribute("viewBox", "0 0 900 440");
|
||||
live.innerHTML = renderReportCurve(planPoints, ledger, ref).replace(/^<svg[^>]*>/, "").replace(/<\/svg>$/, "");
|
||||
|
||||
}
|
||||
|
||||
// The blank shape has some non-empty defaults of its own (e.g. planActual.charge.actualTime
|
||||
@@ -750,7 +687,7 @@ function updateTempUnitUI() {
|
||||
const conv = (c) => Math.round(tempUnit === "F" ? cToF(c) : c);
|
||||
for (const el of document.querySelectorAll("[data-tempc]"))
|
||||
el.textContent = String(conv(Number(el.dataset.tempc)));
|
||||
for (const el of document.querySelectorAll("[data-band-lo]")) {
|
||||
for (const el of document.querySelectorAll("td[data-band-lo], text[data-band-lo]")) {
|
||||
const lo = conv(Number(el.dataset.bandLo));
|
||||
const hi = conv(Number(el.dataset.bandHi));
|
||||
el.textContent = `${el.dataset.bandPrefix ?? ""}${lo}–${hi} ${label}${el.dataset.bandSuffix ?? ""}`;
|
||||
@@ -1177,14 +1114,12 @@ async function init() {
|
||||
applyAvatar(document.getElementById("nav-user-avatar"), user);
|
||||
if (user.role === "admin")
|
||||
document.getElementById("nav-admin")?.classList.remove("hidden");
|
||||
document.getElementById("alog-library-refresh").hidden = user.role !== "admin";
|
||||
const localDraft = loadFromStorage(user.id);
|
||||
state.plan = localDraft ?? blankPlan();
|
||||
const draftRemoteId = remotePlanId;
|
||||
const draftSyncedAtAtLoad = draftSyncedAt;
|
||||
// Roaster profile loads after the machine profile on purpose: its pace override (if
|
||||
// any) must be applied on top of the freshly loaded machine profile, not raced with it.
|
||||
await Promise.all([loadPlans(), loadMachineProfile(user.id)]);
|
||||
await loadRoasterProfile();
|
||||
await loadPlans();
|
||||
const requestedId = new URLSearchParams(location.search).get("plan");
|
||||
const selected = plans.find((plan) => plan.id === requestedId);
|
||||
// Prefer the local draft only when it targets this exact plan AND was last confirmed
|
||||
@@ -1213,7 +1148,6 @@ async function init() {
|
||||
const offlineUserId = localStorage.getItem(`${STORAGE_PREFIX}:last-user`);
|
||||
if (offlineUserId && csrfToken()) {
|
||||
state.plan = loadFromStorage(offlineUserId) ?? blankPlan();
|
||||
await loadMachineProfile(offlineUserId);
|
||||
}
|
||||
} catch {
|
||||
/* no local draft is available */
|
||||
@@ -1251,7 +1185,7 @@ async function init() {
|
||||
getRemotePlanId: () => remotePlanId,
|
||||
flushCurrentPlan,
|
||||
});
|
||||
initPrint({ beforePrint: renderFormFromPlan });
|
||||
initPrint({ getPlan: () => state.plan, getName: () => currentPlanName });
|
||||
lotPicker = initLotPicker({
|
||||
state,
|
||||
recompute,
|
||||
|
||||
Reference in new issue
Block a user