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
+51
-2
@@ -8,6 +8,7 @@ let roasts = [];
|
||||
let openId = null;
|
||||
let pollTimer = null;
|
||||
let machineProfile = null;
|
||||
let roasterList = [];
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
@@ -42,7 +43,13 @@ function renderTable() {
|
||||
const sub = document.createElement("div");
|
||||
sub.className = "muted";
|
||||
sub.style.fontSize = "11.5px";
|
||||
sub.textContent = `${roast.roast?.roastDate || fmtDate(roast.createdAt)} · ${roast.filename}`;
|
||||
sub.textContent = [
|
||||
roast.roast?.roastDate || fmtDate(roast.createdAt),
|
||||
roast.roasterName,
|
||||
roast.filename,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
roastCell.append(strong, sub);
|
||||
|
||||
const planCell = document.createElement("td");
|
||||
@@ -305,7 +312,8 @@ async function openDetail(id, { keepScroll = false } = {}) {
|
||||
card.classList.remove("hidden");
|
||||
document.getElementById("detail-title").textContent =
|
||||
detail.roast?.title || detail.filename;
|
||||
document.getElementById("detail-meta").textContent = [
|
||||
const meta = document.getElementById("detail-meta");
|
||||
meta.textContent = [
|
||||
detail.roast?.roastDate || fmtDate(detail.createdAt),
|
||||
detail.roast?.roasterType,
|
||||
detail.planTitle ? `Plan: ${detail.planTitle}` : "No plan attached",
|
||||
@@ -313,6 +321,42 @@ async function openDetail(id, { keepScroll = false } = {}) {
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
// Machine assignment: which of the user's roasters this roast trains.
|
||||
if (roasterList.length) {
|
||||
const wrap = document.createElement("span");
|
||||
wrap.append(" · Machine: ");
|
||||
const select = document.createElement("select");
|
||||
select.className = "field-input sm";
|
||||
select.style.display = "inline-block";
|
||||
select.style.width = "auto";
|
||||
select.append(
|
||||
Object.assign(document.createElement("option"), {
|
||||
value: "",
|
||||
textContent: "— none —",
|
||||
}),
|
||||
...roasterList.map((r) =>
|
||||
Object.assign(document.createElement("option"), {
|
||||
value: r.id,
|
||||
textContent: r.name,
|
||||
}),
|
||||
),
|
||||
);
|
||||
select.value = detail.roasterId ?? "";
|
||||
select.addEventListener("change", async () => {
|
||||
try {
|
||||
await api(`/api/roasts/${detail.id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ roasterId: select.value || null }),
|
||||
});
|
||||
showToast("Machine updated — future learning uses this assignment.");
|
||||
await loadRoasts();
|
||||
} catch (error) {
|
||||
showToast(error.message, "fail");
|
||||
}
|
||||
});
|
||||
wrap.append(select);
|
||||
meta.append(wrap);
|
||||
}
|
||||
document.getElementById("detail-download").href =
|
||||
`/api/roasts/${encodeURIComponent(id)}/download`;
|
||||
renderGraph(detail);
|
||||
@@ -406,6 +450,11 @@ async function init() {
|
||||
} catch {
|
||||
machineProfile = null;
|
||||
}
|
||||
try {
|
||||
roasterList = (await api("/api/roasters")).roasters;
|
||||
} catch {
|
||||
roasterList = [];
|
||||
}
|
||||
await loadRoasts();
|
||||
const requested = new URLSearchParams(location.search).get("roast");
|
||||
if (requested && roasts.some((roast) => roast.id === requested))
|
||||
|
||||
Reference in New Issue
Block a user