Add roaster (machine) management, mobile header-tools redesign, and profile pictures
Test and deploy / test-and-deploy (push) Successful in 1m0s
Test and deploy / test-and-deploy (push) Successful in 1m0s
Machines: - roasters table + actual_roasts.roaster_id (migrations 009): per-user machines with a default; uploads attach to the default roaster and are reassignable from the roast detail view - Learning is now per machine: /api/roaster-profile scopes to a roaster (default when unspecified) and applies the user's override tweaks (milestone temps, TP time, pace factor) on top of learned medians - /roasters page: machine list + analysis report showing learned vs applied values with editable tweaks, typical RoR, and a plain-language list of the adjustments applied to plans; planner honors a manual pace override; evaluations use the roast's own machine profile Mobile header redesign: - The planner's header tools collapse behind a single 'Tools ▾' disclosure (dropdown card) at ≤720px, keeping the fixed three-row mobile header with no wrapped or scrolling button rows; desktop renders inline via display:contents (same DOM, same handlers) Profile pictures: - users.avatar (migration 010) + PUT/GET/DELETE /api/account/avatar; client-side centre-crop resize to 128px JPEG; avatar shows in the nav chip on every page; included in full backups Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e62b9601a2
commit
a341d67467
+49
-5
@@ -17,7 +17,7 @@ import { initAlogPanel } from "./alog-ui.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__";
|
||||
import { initSideNav } from "./nav.js?v=__ASSET_VERSION__";
|
||||
import { applyAvatar, initSideNav } from "./nav.js?v=__ASSET_VERSION__";
|
||||
import { wireWhyPanels } from "./why-panels.js?v=__ASSET_VERSION__";
|
||||
import { initFieldHelp } from "./field-help.js?v=__ASSET_VERSION__";
|
||||
import { initPlanChat } from "./plan-chat-ui.js?v=__ASSET_VERSION__";
|
||||
@@ -47,6 +47,17 @@ 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 */
|
||||
}
|
||||
@@ -76,6 +87,10 @@ 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.";
|
||||
@@ -926,6 +941,33 @@ async function deletePlan(plan) {
|
||||
await loadPlans();
|
||||
}
|
||||
|
||||
// Mobile-only "Tools ▾" disclosure in the header; a no-op on desktop where the group renders
|
||||
// inline (display: contents). Any action inside closes it — every tool opens a drawer anyway.
|
||||
function wireToolsMenu() {
|
||||
const toggle = document.getElementById("btn-tools-menu");
|
||||
const group = document.getElementById("header-tools-group");
|
||||
if (!toggle || !group) return;
|
||||
const close = () => {
|
||||
group.classList.remove("open");
|
||||
toggle.setAttribute("aria-expanded", "false");
|
||||
};
|
||||
toggle.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
const open = group.classList.toggle("open");
|
||||
toggle.setAttribute("aria-expanded", String(open));
|
||||
});
|
||||
group.addEventListener("click", (event) => {
|
||||
if (event.target.closest("button")) close();
|
||||
});
|
||||
document.addEventListener("click", (event) => {
|
||||
if (!group.classList.contains("open")) return;
|
||||
if (!group.contains(event.target) && event.target !== toggle) close();
|
||||
});
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") close();
|
||||
});
|
||||
}
|
||||
|
||||
function wireToolbar() {
|
||||
document.getElementById("btn-toggle-fids").addEventListener("click", (e) => {
|
||||
const on = document.body.classList.toggle("show-fids");
|
||||
@@ -1094,16 +1136,17 @@ async function init() {
|
||||
const { user } = await meResponse.json();
|
||||
document.getElementById("account-email").textContent = user.email;
|
||||
document.getElementById("settings-email").textContent = user.email;
|
||||
document.getElementById("nav-user-avatar").textContent = user.email
|
||||
.charAt(0)
|
||||
.toUpperCase();
|
||||
applyAvatar(document.getElementById("nav-user-avatar"), user);
|
||||
if (user.role === "admin")
|
||||
document.getElementById("nav-admin").classList.remove("hidden");
|
||||
const localDraft = loadFromStorage(user.id);
|
||||
state.plan = localDraft ?? blankPlan();
|
||||
const draftRemoteId = remotePlanId;
|
||||
const draftSyncedAtAtLoad = draftSyncedAt;
|
||||
await Promise.all([loadPlans(), loadMachineProfile(user.id), loadRoasterProfile()]);
|
||||
// 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();
|
||||
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
|
||||
@@ -1153,6 +1196,7 @@ async function init() {
|
||||
initSideNav();
|
||||
wireDrawers();
|
||||
wireToolbar();
|
||||
wireToolsMenu();
|
||||
wireTempUnitToggle();
|
||||
updateTempUnitUI();
|
||||
wirePwa();
|
||||
|
||||
Reference in New Issue
Block a user