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
+58
-1
@@ -1,5 +1,5 @@
|
||||
import { api, protectedFetch } from "./api.js?v=__ASSET_VERSION__";
|
||||
import { initSideNav, loadNavUser } from "./nav.js?v=__ASSET_VERSION__";
|
||||
import { applyAvatar, initSideNav, loadNavUser } from "./nav.js?v=__ASSET_VERSION__";
|
||||
import { showToast } from "./toast.js?v=__ASSET_VERSION__";
|
||||
|
||||
function fmtDate(value) {
|
||||
@@ -331,6 +331,62 @@ function wirePwaButtons() {
|
||||
});
|
||||
}
|
||||
|
||||
// Profile picture: resized client-side to a small square JPEG data URL before upload, so the
|
||||
// server only ever stores a few tens of KB per user.
|
||||
function wireAvatar(user) {
|
||||
const refresh = (hasAvatar) => {
|
||||
const shownUser = { ...user, hasAvatar };
|
||||
applyAvatar(document.getElementById("profile-avatar"), shownUser, true);
|
||||
applyAvatar(document.getElementById("nav-user-avatar"), shownUser, true);
|
||||
document.getElementById("avatar-remove").classList.toggle("hidden", !hasAvatar);
|
||||
};
|
||||
refresh(user.hasAvatar);
|
||||
document.getElementById("avatar-file").addEventListener("change", async (event) => {
|
||||
const file = event.target.files?.[0];
|
||||
event.target.value = "";
|
||||
if (!file) return;
|
||||
try {
|
||||
const bitmap = await createImageBitmap(file);
|
||||
const size = 128;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const context = canvas.getContext("2d");
|
||||
// Cover-crop the centre square so faces aren't squished.
|
||||
const side = Math.min(bitmap.width, bitmap.height);
|
||||
context.drawImage(
|
||||
bitmap,
|
||||
(bitmap.width - side) / 2,
|
||||
(bitmap.height - side) / 2,
|
||||
side,
|
||||
side,
|
||||
0,
|
||||
0,
|
||||
size,
|
||||
size,
|
||||
);
|
||||
const dataUrl = canvas.toDataURL("image/jpeg", 0.85);
|
||||
await api("/api/account/avatar", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ dataUrl }),
|
||||
});
|
||||
showToast("Profile picture updated.");
|
||||
refresh(true);
|
||||
} catch (error) {
|
||||
showToast(error.message || "Could not read that image.", "fail");
|
||||
}
|
||||
});
|
||||
document.getElementById("avatar-remove").addEventListener("click", async () => {
|
||||
try {
|
||||
await api("/api/account/avatar", { method: "DELETE" });
|
||||
showToast("Profile picture removed.");
|
||||
refresh(false);
|
||||
} catch (error) {
|
||||
showToast(error.message, "fail");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadTokens() {
|
||||
const body = document.getElementById("tokens-body");
|
||||
try {
|
||||
@@ -413,6 +469,7 @@ async function init() {
|
||||
wireForms(user);
|
||||
wirePwaButtons();
|
||||
wireTokenForm();
|
||||
wireAvatar(user);
|
||||
await Promise.all([loadSessions(), loadPlans(), loadTokens()]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user