Skip to content

Commit 69028c2

Browse files
v1.4.11 — FlashForge live integration polish + inventory toolbar
FlashForge integration: - Main-process IPC bridge for /detail + /control (Chromium CORS preflight was blocking renderer-side fetch on application/json — Node fetch in main bypasses it cleanly, mirrors the Flutter monolith's http.post path) - MJPEG camera lifecycle: per-session cache-buster on URL, explicit `<img>.src = "about:blank"` on close (forces TCP FIN → mjpg-streamer frees its 1-client slot in milliseconds), graceful fallback view on load error (printer photo + 1-stream-limit overlay + Retry button that bumps the cache-buster and re-attempts) - 5-slot matlStation grid: [Ext.] + [1A 1B 1C 1D] mapping to indepMatlInfo + slotInfos[1..4] respectively. Each slot dispatches the right /control command (ipdMsConfig_cmd vs msConfig_cmd) via a per-slot `slotKind` flag instead of array-length tricks. Three visual states per slot: filled (solid colour), configured-but-empty (4-px coloured ring drawn INSIDE the square via box-shadow inset so layout stays stable), unconfigured (grey hatch — rare) - Auto SN-prefix in ffgAuthBody — chassis label shows the serial without the `SN` prefix that the firmware HTTP API actually expects; driver normalises so user can paste either form - Request log (debug mode): collapsible section mirroring the Snapmaker WS log, every poll pushes `→ POST` + wire body and `← code:N detail · ...` + full response; expanded state persisted on conn.logExpanded so status flaps don't close it - Vendor row hidden when null (FlashForge /detail doesn't carry brand data, no point fabricating one). Edit sheet trimmed to two brands: Generic (default, first) + FlashForge — vendor is purely local UI metadata and isn't part of the wire payload - Status-transition guards in ffgMergeStatus — only flag statusChanged on real transitions (prev !== current), not on every failing poll. Eliminates the 2 s side-card flicker during outages Inventory toolbar: - View selector (Table / Grid / Storage / Printers) moved to its own row below the search bar so the buttons keep their full width when the brand/material/type filters above are open - Clear button (✕) added inside the search input, left of the magnifier, only visible when there's a value to clear i18n: - 3 new keys across 9 locales: ffgCamFailMsg, ffgCamRetry, ffgSlotEmpty
1 parent 3e044a7 commit 69028c2

23 files changed

Lines changed: 1474 additions & 202 deletions

‎README.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
- **Friend inventory view** — A friend appears as a switchable pseudo-account in the avatar dropdown and profiles modal; clicking opens their inventory in the same table/grid UI with a "Read-only" banner and a quick "← My inventory" button
3535
- **3D printer management** — Dedicated "Printers" tab with a drag-and-drop grid of all your printers across the 5 supported brands (Bambu Lab / Creality / Elegoo / FlashForge / Snapmaker); per-brand "Add" flow with model picker, side card with editable connection details, and online / offline indicator driven by an HTTP ping
3636
- **Snapmaker live integration** — Real-time WebRTC camera, live extruder + bed temperatures, filament data per slot (color, brand, material) with click-to-edit, and active-print job card (preview thumbnail + progress + state + layer counter) over the Moonraker WebSocket
37+
- **FlashForge live integration** — HTTP polling on port 8898 (`/detail` every 2 s), real-time MJPEG camera with auto-recovery on connection failure (mjpg-streamer's 1-client limit handled gracefully), 5-slot matlStation grid (`Ext.` + `1A` `1B` `1C` `1D`) showing each bay's assigned colour + material — even when empty — so the user knows what to refill where, click-to-edit per slot via FlashForge's `/control` HTTP API (`msConfig_cmd` / `ipdMsConfig_cmd`), and a debug-mode Request log surfacing every outgoing POST + response in real time
3738
- **Snapmaker LAN discovery** — Side-panel scanner that finds Snapmaker printers on the local network: instant mDNS browse on `_snapmaker._tcp.local.` (TXT record carries IP / model / device name / serial), parallel Moonraker port-scan as fallback (port 7125 with `/printer/info` + `/server/info` + `/machine/system_info`), per-source batch sizing (24 on local LAN, 4 on user-declared extras to survive anti-scan firewalls), brand-confirmed candidates only (`machine_type` containing "Snapmaker"), one-click add that writes the printer doc directly to Firestore and opens its detail panel, plus inline "Add by IP" with live IPv4 validation
3839
- **Twin-pair manual repair** — User-assisted tool to link two RFID tags that the auto-linker missed (timestamps drifted > 2 s at programming). The spool detail panel offers a "Link to a twin spool" picker filtered to candidates with the same brand, material, type, version (`id_tigertag`) and exact RGB; one click writes `twin_tag_uid` cross-referenced on both docs in a single Firestore batch. Empty when no compatible candidate exists, hidden when already paired
3940
- **Spool toolbox** — Action-bundling section in the spool detail panel: scan colour with TD1S, scan TD with TD1S, link to a twin spool (when applicable), remove from rack (when placed) with eject animation, and delete (hold-to-confirm). Contextual visibility — empty rows are skipped so the toolbox never shows a button without a target
@@ -49,6 +50,29 @@
4950

5051
## Changelog
5152

53+
### v1.4.11 — 2026-05-05
54+
55+
**FlashForge live integration** lands as a first-class brand alongside Snapmaker — same UX language, brand-specific protocol underneath. AD5X / 5M and similar models speak HTTP on port 8898 (`/detail` for read, `/control` for write) with a `serialNumber` + `checkCode` auth body; this release brings the full bay-aware, live-streaming, request-loggable side card to that brand.
56+
57+
#### FlashForge live integration
58+
- **HTTP polling** — 2 s tick on `POST /detail` (matches the FlashForge mobile companion app cadence), capped exponential backoff on network errors (2 s → 30 s), idempotent reconnect on side-card open, full tear-down on close. Every poll is bridged through the Electron main process (Node `fetch`, no CORS) — direct renderer-side `fetch` was being blocked by Chromium's CORS preflight on `application/json`, which the FlashForge firmware doesn't handle.
59+
- **Camera (MJPEG)** — full-width edge-to-edge `<img>` stream from `cameraStreamUrl` (typically `:8080/?action=stream`). The printer's mjpg-streamer accepts ONE concurrent client, so the side card now (a) appends a per-session cache-buster to the URL on every open, (b) explicitly tears the `<img>` down to `about:blank` on close (forces TCP FIN → printer frees its slot in milliseconds), and (c) on `error` event swaps in a graceful fallback view: the printer's product photo darkened, an overlay explaining the 1-stream limit, and a **Retry** button that bumps the cache-buster session and re-attempts. Status flapping during a session no longer triggers full re-renders, so the camera doesn't churn and neighbouring open sections (Request log, etc.) stay open.
60+
- **5-slot matlStation grid** — `[Ext.] [1A] [1B] [1C] [1D]` honoring the FlashForge bay numbering convention. The Ext. slot maps to `indepMatlInfo` (the direct-feed bowden, dispatched as `ipdMsConfig_cmd` on save); the 4 matlStation bays map to `slotInfos[1..4]` (dispatched as `msConfig_cmd` with `slot:1-4`). Each slot has THREE visual states: **filled** (solid colour fill — `hasFilament: true`), **configured-but-empty** (4-px coloured ring drawn INSIDE the square via `box-shadow: inset` so layout stays identical across states; user can see at a glance which filament is *intended* for each empty bay), or **unconfigured** (rare — falls back to the existing grey hatch). The vendor row under each card is hidden when null — the `/detail` payload doesn't carry brand data, so we don't fabricate one. The "Change filament" bottom-sheet was trimmed to two brand options (`Generic` first, `FlashForge` second) since vendor is purely local UI metadata and isn't part of the wire payload.
61+
- **Auto SN-prefix** — FlashForge's chassis label shows the serial as e.g. `MQQE9501368`, but the firmware's HTTP API authenticates against the prefixed form `SNMQQE9501368` (queryable via `~M115` on port 8899). The mobile companion app strips the prefix when displaying it, so anyone copying the SN off-screen ends up with the short form and gets `code:1, "SN is different"` on every poll. The driver now auto-prefixes `SN` when missing — a SN that already starts with `SN` is left untouched. Idempotent.
62+
- **Request log (debug mode)** — collapsible section at the bottom of the side card, mirroring the Snapmaker WS log. Every poll pushes one outbound entry (`→ POST http://<ip>:8898/detail` row head + the exact wire body in the expanded view) and one inbound entry (`← code:0 detail · status, rightTemp, …` summary + full pretty-printed response). Click any row to expand the JSON; dedicated Pause / Clear toolbar; entries cap at 100 (FIFO). The user's expanded/collapsed state is now persisted across status flaps via `conn.logExpanded`, so the section doesn't snap shut on every offline → online transition.
63+
- **i18n** — 4 new keys across the 9 locales for the camera fallback (`ffgCamFailMsg`, `ffgCamRetry`) and the slot empty label (`ffgSlotEmpty`).
64+
65+
#### UX — Inventory toolbar redesign
66+
- **View selector moved BELOW the search bar** — was previously squeezed into the same row as the search input + brand/material/type filters, which made the buttons compete for horizontal space when filters were open. Now lives on its own dedicated row directly under the search, keeping its full width regardless of how wide the filters above end up.
67+
- **Search input — clear button (✕)** — appears on the LEFT of the magnifier icon as soon as the input contains a value, hidden when empty. One click wipes the input, refocuses it, re-renders the inventory immediately. Same colour palette as the existing surface tokens, soft hover.
68+
69+
#### Internal
70+
- **CORS bypass IPC** (`main.js`) — new `ipcMain.handle('ffg:http-post', …)` exposed via preload as `electronAPI.ffgHttpPost(url, body)`. Allowlisted to FlashForge `/detail` + `/control` paths (regex on the URL pathname) so a renderer compromise can't pivot it into a generic outbound HTTP proxy. Same `code:-1` / `code:-2` envelope shape as the Flutter monolith for parse / network errors so the renderer's existing error contract just works.
71+
- **Defensive bridge fallback** — every renderer-side call wraps the IPC in a try/catch + null-bridge guard, surfacing failures as a synthetic `code:-2` payload with a clear message ("ffgHttpPost bridge unavailable (full app restart required)" when the user reloads the renderer without restarting main, "IPC failed — No handler registered" when the main process is stale, etc.). The Request log shows the failure verbatim so debugging connection issues is now self-explanatory.
72+
- **Status-transition guards** — `ffgMergeStatus` no longer flags `statusChanged: true` on every error poll. It captures the previous status and only flags transitions (`offline → connecting`, `error → connected`, etc.). Eliminates the 2 s flicker that was rebuilding the entire side card every poll while the printer was unreachable.
73+
74+
---
75+
5276
### v1.4.10 — 2026-05-05
5377

5478
Hot-fix release for the Windows auto-updater + a small UX safety addition on the inventory format upgrade modal.

‎assets/svg/icons/icon_battery.svg‎

Lines changed: 1 addition & 0 deletions
Loading

‎assets/svg/icons/icon_bolt.svg‎

Lines changed: 1 addition & 0 deletions
Loading

‎assets/svg/icons/icon_clock.svg‎

Lines changed: 1 addition & 0 deletions
Loading

‎assets/svg/icons/icon_list.svg‎

Lines changed: 8 additions & 0 deletions
Loading

‎assets/svg/icons/icon_plug.svg‎

Lines changed: 1 addition & 0 deletions
Loading

‎assets/svg/icons/icon_wifi.svg‎

Lines changed: 1 addition & 0 deletions
Loading

‎main.js‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,58 @@ ipcMain.handle('update:check-now', async () => {
762762
}
763763
});
764764

765+
// ── FlashForge HTTP — main-process bridge (CORS bypass) ───────────────────
766+
// Why this can't run in the renderer: Electron renderers ARE Chromium, so
767+
// an HTTP POST from `http://localhost:5784` to `http://192.168.40.107:8898`
768+
// is treated as a cross-origin request. Sending JSON triggers a CORS
769+
// preflight (OPTIONS) that the FlashForge firmware doesn't handle — the
770+
// browser blocks the request before the actual POST is even sent. Node's
771+
// fetch (here in main) is not subject to CORS, so it goes through cleanly.
772+
// Mirrors the Flutter monolith's `http.post()` exactly:
773+
// url → http://<ip>:8898/{detail|control}
774+
// body → { serialNumber, checkCode, … } (already JSON-encoded by caller)
775+
// headers → Content-Type: application/json, Accept: */*
776+
// Returns the parsed JSON body, or { code:-1|-2, message } envelopes that
777+
// match what the Flutter side produces on parse / network errors. The
778+
// renderer treats those exactly like a regular FlashForge error code.
779+
const FFG_TIMEOUT_MS = 4000;
780+
ipcMain.handle('ffg:http-post', async (_evt, url, body) => {
781+
if (!url || typeof url !== 'string') {
782+
return { code: -2, message: 'Network error: missing url' };
783+
}
784+
if (!/^https?:\/\//i.test(url)) {
785+
return { code: -2, message: 'Network error: invalid url scheme' };
786+
}
787+
// Tight allowlist on the path so a renderer compromise can't pivot
788+
// this IPC into a generic outbound HTTP proxy. Both endpoints take
789+
// the SAME auth body so this list will rarely grow.
790+
const ok = /\/(detail|control)$/i.test(new URL(url).pathname);
791+
if (!ok) {
792+
return { code: -2, message: 'Network error: path not allowed' };
793+
}
794+
const ctrl = new AbortController();
795+
const tm = setTimeout(() => ctrl.abort(), FFG_TIMEOUT_MS);
796+
try {
797+
const res = await fetch(url, {
798+
method: 'POST',
799+
signal: ctrl.signal,
800+
headers: { 'Content-Type': 'application/json', 'Accept': '*/*' },
801+
body: typeof body === 'string' ? body : JSON.stringify(body || {}),
802+
});
803+
let parsed;
804+
try {
805+
parsed = await res.json();
806+
} catch (_) {
807+
parsed = { code: -1, message: 'Invalid JSON', httpStatus: res.status };
808+
}
809+
return parsed;
810+
} catch (e) {
811+
return { code: -2, message: `Network error: ${e?.message || e}` };
812+
} finally {
813+
clearTimeout(tm);
814+
}
815+
});
816+
765817
// ── Image cache IPC handler ───────────────────────────────────────────────────
766818
ipcMain.handle('img:get', async (_, url) => {
767819
if (!url || url === '--') return null;

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "tigertag-inventory",
3-
"version": "1.4.10",
3+
"version": "1.4.11",
44
"description": "Tiger Studio Manager — desktop app",
55
"main": "main.js",
66
"engines": {

‎preload.js‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
7070
// multi-VLAN without reflector) — renderer falls back to port-scan.
7171
mdnsBrowseSnapmaker: () => ipcRenderer.invoke('mdns:browse-snapmaker'),
7272

73+
// ── FlashForge HTTP — bridge through main process to bypass CORS.
74+
// The FlashForge firmware (port 8898 /detail + /control) doesn't handle
75+
// CORS preflight, so direct fetch() from the renderer fails. This IPC
76+
// lets the renderer issue the same POST through Node's fetch (no CORS).
77+
// Returns the parsed JSON or { code:-1|-2, message } error envelopes —
78+
// identical shape to what the renderer used to build inline.
79+
ffgHttpPost: (url, body) => ipcRenderer.invoke('ffg:http-post', url, body),
80+
7381
// ── Image cache (main-process side) ─────────────────────────────────────
7482
imgGet: (url) => ipcRenderer.invoke('img:get', url),
7583

0 commit comments

Comments
 (0)