@@ -54,6 +54,7 @@ import {
5454 openFlashforgeFiles, closeFlashforgeFiles,
5555} from './printers/flashforge/index.js';
5656import { renderFfgCamBanner, renderFfgCamWallBanner, ffgRefreshCamBanner, ffgCamBaseUrl } from './printers/flashforge/widget_camera.js';
57+ import { c5fwIsCreator5, c5fwEnsureLatest, c5fwStatus, c5fwLabel } from './printers/flashforge/creator5-fw.js';
5758import { ffgMuxStart, ffgMuxStop, ffgMuxStopAll, ffgMuxRestart, ffgMuxRegister, ffgMuxUnregister } from './printers/flashforge/cam_mux.js';
5859import { renderSnapCamBanner } from './printers/snapmaker/widget_camera.js';
5960import { openSnapAddFlow } from './printers/snapmaker/add-flow.js';
@@ -16992,7 +16993,10 @@ import { elgFanStep } from './printers/elegoo/widget_control.js';
1699216993 function _rebuildDetailKeepVideo(opts) {
1699316994 const keepV = $("detailPanel")?.querySelector(".panel-video-player video") || null;
1699416995 const vState = keepV ? { node: keepV, time: keepV.currentTime, paused: keepV.paused } : null;
16995- openDetail(state.selected, opts);
16996+ /* Keep the scroll too: every caller refreshes the spool ALREADY on screen
16997+ (a container change, a structural diff) — without it the card jumped
16998+ back to the top on each save made from further down. */
16999+ openDetail(state.selected, { preserveScroll: true, ...opts });
1699617000 if (!vState) return;
1699717001 const newV = $("detailPanel")?.querySelector(".panel-video-player video");
1699817002 if (newV && newV.getAttribute("src") === vState.node.getAttribute("src")) {
@@ -21950,6 +21954,8 @@ import { elgFanStep } from './printers/elegoo/widget_control.js';
2195021954 return String(a.printerName || "").localeCompare(String(b.printerName || ""));
2195121955 });
2195221956 state.printers = all;
21957+ // Anycubic cloud printers added before the TLS-identity fields existed.
21958+ _backfillAnycubicCloudCerts().catch(e => console.warn("[anycubic] cloud cert backfill:", e?.message));
2195321959 // The machines have just arrived — their units can now be recorded.
2195421960 syncAllPrinterUnits();
2195521961 scheduleStudioStateRecord(); // re-arm deferred telemetry (printer count changed)
@@ -26540,6 +26546,11 @@ import { elgFanStep } from './printers/elegoo/widget_control.js';
2654026546 // When a brand reaches "connected", auto-clear forced-offline so the
2654126547 // badge and panel return to their live state correctly.
2654226548 if (status === "connected" && key) _ppForcedOfflineKeys.delete(key);
26549+ // A FlashForge online has read its firmware version — a Creator 5 may be
26550+ // due the Cloud + LAN firmware. Slight delay: `/detail` lands after connect.
26551+ if (status === "connected" && String(key || "").startsWith("flashforge:")) {
26552+ setTimeout(() => { _maybeNotifyFfgFirmware().catch(() => {}); }, 4000);
26553+ }
2654326554 if (typeof refreshOpenPrinterDetail === "function") refreshOpenPrinterDetail();
2654426555 // In cam mode, a connection/disconnection must refresh the wall so new
2654526556 // feeds appear (or stale cards disappear) without the user leaving the view.
@@ -26762,10 +26773,50 @@ import { elgFanStep } from './printers/elegoo/widget_control.js';
2676226773 }
2676326774 };
2676426775
26776+ /* The shared Anycubic cloud-MQTT TLS identity, as Firestore fields. Copied
26777+ onto every CLOUD printer doc (never a LAN one) so a device that dials
26778+ mqtt-universe.anycubic.com itself — a TigerSpool — finds everything on the
26779+ doc it already reads (key, machineType, cloudEmail, cloudToken). Fetched
26780+ once per session from the main process; values are never logged. */
26781+ let _acuCertFieldsP = null;
26782+ function _acuCloudCertFields() {
26783+ if (!_acuCertFieldsP) {
26784+ _acuCertFieldsP = Promise.resolve(window.anycubic?.cloud?.certs?.())
26785+ .then(c => (c && c.caDerB64 && c.clientCertPem && c.clientKeyPem) ? {
26786+ acuCloudCaDerB64: String(c.caDerB64),
26787+ acuCloudClientCertPem: String(c.clientCertPem),
26788+ acuCloudClientKeyPem: String(c.clientKeyPem),
26789+ } : null)
26790+ .catch(() => null);
26791+ _acuCertFieldsP.then(f => { if (!f) _acuCertFieldsP = null; }); // retry next time
26792+ }
26793+ return _acuCertFieldsP;
26794+ }
26795+ // Catch-up for cloud printers added before the fields existed. Runs on EVERY
26796+ // printers snapshot — the brands arrive one subcollection at a time, so the
26797+ // first snapshot may not hold the Anycubic docs yet — and tries each doc once
26798+ // per session (a failed write is not hammered on every later snapshot).
26799+ const _acuCertBackfillTried = new Set();
26800+ async function _backfillAnycubicCloudCerts() {
26801+ const uid = state.activeAccountId;
26802+ if (!uid) return;
26803+ const missing = (state.printers || []).filter(p => p.brand === "anycubic" && p.mode === "cloud"
26804+ && !_acuCertBackfillTried.has(p.id)
26805+ && !(p.acuCloudCaDerB64 && p.acuCloudClientCertPem && p.acuCloudClientKeyPem));
26806+ if (!missing.length) return;
26807+ const fields = await _acuCloudCertFields();
26808+ if (!fields) return; // main process not ready — next snapshot retries
26809+ missing.forEach(p => _acuCertBackfillTried.add(p.id));
26810+ const devices = fbDb(uid).collection("users").doc(uid).collection("printers").doc("anycubic").collection("devices");
26811+ await Promise.all(missing.map(p => devices.doc(p.id).set(fields, { merge: true })
26812+ .catch(e => console.warn("[anycubic] cloud cert backfill failed:", e?.code)))); // code only — never the values
26813+ }
26814+
2676526815 _printerCtx.addAnycubicCloudPrinter = async (rec) => {
2676626816 const uid = state.activeAccountId;
2676726817 if (!uid) return { ok: false, error: "no-account" };
2676826818 try {
26819+ const certFields = (await _acuCloudCertFields()) || {};
2676926820 const ref = fbDb(uid).collection("users").doc(uid)
2677026821 .collection("printers").doc("anycubic")
2677126822 .collection("devices").doc("cloud_" + rec.cloudPrinterId);
@@ -26784,6 +26835,7 @@ import { elgFanStep } from './printers/elegoo/widget_control.js';
2678426835 printerModelId: String(rec.printerModelId || "0"),
2678526836 isActive: existing ? !!existing.isActive : false,
2678626837 sortIndex: existing && Number.isFinite(existing.sortIndex) ? existing.sortIndex : state.printers.length,
26838+ ...certFields, // acuCloudCaDerB64 / acuCloudClientCertPem / acuCloudClientKeyPem
2678726839 updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
2678826840 }, { merge: true });
2678926841 return { ok: true, id: ref.id };
@@ -26801,12 +26853,13 @@ import { elgFanStep } from './printers/elegoo/widget_control.js';
2680126853 if (!uid || !token) return { ok: false };
2680226854 try {
2680326855 const db = fbDb(uid);
26856+ const certFields = (await _acuCloudCertFields()) || {};
2680426857 const cloudDocs = (state.printers || []).filter(p => p.brand === "anycubic" && p.mode === "cloud");
2680526858 await Promise.all(cloudDocs.map(p => {
2680626859 p.cloudToken = token; if (email) p.cloudEmail = email; // keep state in sync now
2680726860 return db.collection("users").doc(uid).collection("printers").doc("anycubic")
2680826861 .collection("devices").doc(p.id)
26809- .set({ cloudToken: token, ...(email ? { cloudEmail: email } : {}) }, { merge: true });
26862+ .set({ cloudToken: token, ...(email ? { cloudEmail: email } : {}), ...certFields }, { merge: true });
2681026863 }));
2681126864 return { ok: true, n: cloudDocs.length };
2681226865 } catch (e) {
@@ -34588,6 +34641,54 @@ import { elgFanStep } from './printers/elegoo/widget_control.js';
3458834641 if (n?.tag) localStorage.setItem(PAXX_DISMISSED_LS_KEY, n.tag);
3458934642 _clearLocalNotif("paxx");
3459034643 }
34644+ // ── FlashForge Creator 5 "Cloud + LAN" firmware notice ────────────────────
34645+ // Same contract as the paxx notice above (card click / "Remind me later" =
34646+ // this session only, ✕ = this release never again), for the firmware
34647+ // FlashForge built with TigerTag (printers/flashforge/creator5-fw.js). The
34648+ // installed version comes from the LIVE connection (`/detail` →
34649+ // firmwareVersion), so it is checked when a FlashForge comes online rather
34650+ // than on the Firestore snapshot, where nothing has been read yet.
34651+ const FFGFW_DISMISSED_LS_KEY = "tigertag.ffgCreator5Fw.dismissedTag";
34652+ let _ffgFwSessionSnoozeTag = null;
34653+ async function _maybeNotifyFfgFirmware() {
34654+ const creators = (state.printers || []).filter(p => p.brand === "flashforge" && c5fwIsCreator5(p));
34655+ if (!creators.length) return;
34656+ const rec = await c5fwEnsureLatest();
34657+ if (!rec?.tag) return;
34658+ const outdated = creators.find(p =>
34659+ c5fwStatus(ffgGetConn(ffgKey(p))?.data?.info?.firmware, rec.tag) === "update");
34660+ if (!outdated) return;
34661+ if (localStorage.getItem(FFGFW_DISMISSED_LS_KEY) === rec.tag) return;
34662+ if (_ffgFwSessionSnoozeTag === rec.tag) return;
34663+ const pName = (outdated.printerName || "").trim() || PRINTER_BRAND_META.flashforge?.label || "FlashForge";
34664+ _setLocalNotif({
34665+ id: "ffgfw", icon: "download", action: "ffgfw", brand: "flashforge",
34666+ title: t("notifFfgFwTitle", { name: pName }),
34667+ text: t("notifFfgFwText", { version: c5fwLabel(rec.tag) }),
34668+ tag: rec.tag, printerId: outdated.id, printerName: pName,
34669+ });
34670+ }
34671+ function _ffgFwSnooze() {
34672+ const n = (state.localNotifications || []).find(x => x.id === "ffgfw");
34673+ if (n?.tag) _ffgFwSessionSnoozeTag = n.tag;
34674+ _clearLocalNotif("ffgfw");
34675+ return n;
34676+ }
34677+ // Card click → the machine's page and its Info window, where the firmware
34678+ // block hands over the right file and the USB walkthrough.
34679+ function _openFfgFwPrinter() {
34680+ const n = _ffgFwSnooze();
34681+ const printer = (state.printers || []).find(p => p.id === n?.printerId && p.brand === "flashforge");
34682+ if (!printer) return;
34683+ openPrinterDetail("flashforge", printer.id);
34684+ openFlashforgeInfo(printer);
34685+ }
34686+ function _dismissFfgFwNotif() {
34687+ const n = (state.localNotifications || []).find(x => x.id === "ffgfw");
34688+ if (n?.tag) localStorage.setItem(FFGFW_DISMISSED_LS_KEY, n.tag);
34689+ _clearLocalNotif("ffgfw");
34690+ }
34691+
3459134692
3459234693 // Remove a local notification by id (e.g. the avatar nudge once a photo is set).
3459334694 function _clearLocalNotif(id) {
@@ -34760,7 +34861,8 @@ import { elgFanStep } from './printers/elegoo/widget_control.js';
3476034861 const localHtml = locals.map(n => {
3476134862 // Community nudges + avatar: the whole row IS the call-to-action (no button).
3476234863 const isCommunity = !!COMMUNITY[n.action];
34763- const clickable = isCommunity || n.action === "avatar" || n.action === "paxx" || n.action === "lowstock";
34864+ const isFw = n.action === "paxx" || n.action === "ffgfw"; // firmware notices: later + ✕
34865+ const clickable = isCommunity || n.action === "avatar" || isFw || n.action === "lowstock";
3476434866 const brandIc = isCommunity ? ` notif-ic--${n.action}` // branded square icon
3476534867 : (n.action === "lowstock" ? " notif-ic--lowstock" : ""); // amber alert icon
3476634868 // A notice originating from a printer speaks WITH its brand logo (the
@@ -34780,10 +34882,10 @@ import { elgFanStep } from './printers/elegoo/widget_control.js';
3478034882 <div class="fp-friend-main">
3478134883 ${n.title ? `<div class="notif-title">${esc(n.title)}</div>` : ""}
3478234884 <div class="notif-text">${esc(n.text)}${n.version ? ` (v${esc(n.version)})` : ""}</div>
34783- ${n.action === "paxx" ? `<button type="button" class="primary sm notif-later" title="${esc(t("notifRemindLater"))}"><span class="icon icon-bell icon-12"></span>${esc(t("notifRemindLater"))}</button>` : ""}
34885+ ${isFw ? `<button type="button" class="primary sm notif-later" title="${esc(t("notifRemindLater"))}"><span class="icon icon-bell icon-12"></span>${esc(t("notifRemindLater"))}</button>` : ""}
3478434886 </div>
3478534887 ${n.action === "restart" ? `<button class="primary sm notif-restart">${esc(t("btnRestartUpdate"))}</button>` : ""}
34786- ${n.action === "paxx" ? `<button type="button" class="fp-friend-btn notif-dismiss notif-dismiss-paxx" title="${esc(t("cancelLabel"))}"><span class="icon icon-close icon-13"></span></button>` : ""}
34888+ ${isFw ? `<button type="button" class="fp-friend-btn notif-dismiss notif-dismiss-paxx" title="${esc(t("cancelLabel"))}"><span class="icon icon-close icon-13"></span></button>` : ""}
3478734889 </div>`;
3478834890 }).join("");
3478934891 const notifHtml = notifs.map(n => {
@@ -34834,17 +34936,19 @@ import { elgFanStep } from './printers/elegoo/widget_control.js';
3483434936 // "Remind me later" on the paxx notice — snooze without permanently
3483534937 // dismissing. stopPropagation so it doesn't also trigger the row's open.
3483634938 list.querySelectorAll(".notif-later").forEach(btn =>
34837- btn.addEventListener("click", e => { e.stopPropagation(); _snoozePaxxNotif(); }));
34939+ btn.addEventListener("click", e => { e.stopPropagation();
34940+ if (btn.closest("[data-local-id]")?.dataset.localId === "ffgfw") _ffgFwSnooze(); else _snoozePaxxNotif(); }));
3483834941 // ✕ close on the paxx notice — the ONLY permanent block (this release never
3483934942 // notifies again). stopPropagation so it doesn't also trigger the row's open.
3484034943 list.querySelectorAll(".notif-dismiss-paxx").forEach(btn =>
34841- btn.addEventListener("click", e => { e.stopPropagation(); _dismissPaxxNotif(); }));
34944+ btn.addEventListener("click", e => { e.stopPropagation(); if (btn.closest("[data-local-id]")?.dataset.localId === "ffgfw") _dismissFfgFwNotif(); else _dismissPaxxNotif(); }));
3484234945 list.querySelectorAll(".notif-item--clickable").forEach(row =>
3484334946 row.addEventListener("click", () => {
3484434947 closeNotifs();
3484534948 const a = row.dataset.localAction;
3484634949 if (a === "avatar") _changeMyAvatar();
3484734950 else if (a === "paxx") _openPaxxPrinter();
34951+ else if (a === "ffgfw") _openFfgFwPrinter();
3484834952 else if (a === "lowstock") _openLowStockProduct(row.dataset.localId);
3484934953 else if (COMMUNITY[a]) _openCommunityLink(a);
3485034954 }));
0 commit comments