mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
fix(print-modal): disable printers between dispatch-accept and PRINT_START
Reported off-list by a corporate supporter running a multi-operator
farm shift. Backend already rejects double-sends with HTTP 409 (see
background_dispatch._dispatch at lines 283-290), so no double-print
was possible, but PrinterSelector consulted only PrinterStatus.state
({IDLE, FINISH, FAILED}) to decide whether a printer was selectable.
PRINT_START is the only signal that flips the printer out of IDLE, so
during the upload + print-command + firmware-ack window the card
stayed clickable and operators only found out after submitting.
New useDispatchedPrinterIds hook subscribes to the existing
background-dispatch WebSocket event (already consumed for the toast
overlay), collects printer_ids from dispatched_jobs + active_jobs,
and exposes them via useSyncExternalStore so every PrinterSelector
instance shares one snapshot. OR'd into isPrinterBusy; badge label
flips to "Dispatching..." instead of a misleading "Idle".
No backend change — the reservation already exists, the UI just
didn't reflect it. No behaviour change for add-to-queue / edit-queue
modes (disableBusy stays false for those).
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Subscribes to background-dispatch WebSocket events and returns the set of
|
||||
* printer IDs that currently have a queued or active dispatch job.
|
||||
*
|
||||
* Used by PrinterSelector to disable printers between the moment Bambuddy
|
||||
* accepts a dispatch (FTP upload, print command) and the moment the printer
|
||||
* itself reports PRINT_START. The backend already rejects double-sends with
|
||||
* HTTP 409, but the UI gap still let operators pick a printer the server would
|
||||
* refuse — surfaced by a corporate user running multi-operator farm shifts.
|
||||
*
|
||||
* Module-level state + useSyncExternalStore so every PrinterSelector instance
|
||||
* sees the same snapshot, and component mounts mid-batch pick up the latest
|
||||
* state without re-fetching.
|
||||
*/
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
interface DispatchEventJob {
|
||||
printer_id?: unknown;
|
||||
}
|
||||
|
||||
interface DispatchEventDetail {
|
||||
dispatched_jobs?: DispatchEventJob[];
|
||||
active_jobs?: DispatchEventJob[];
|
||||
total?: number;
|
||||
dispatched?: number;
|
||||
processing?: number;
|
||||
}
|
||||
|
||||
const EMPTY: ReadonlySet<number> = new Set();
|
||||
let currentSet: ReadonlySet<number> = EMPTY;
|
||||
const subscribers = new Set<() => void>();
|
||||
let attached = false;
|
||||
|
||||
function recompute(detail: DispatchEventDetail): ReadonlySet<number> {
|
||||
const next = new Set<number>();
|
||||
for (const job of detail.dispatched_jobs ?? []) {
|
||||
if (typeof job.printer_id === 'number') next.add(job.printer_id);
|
||||
}
|
||||
for (const job of detail.active_jobs ?? []) {
|
||||
if (typeof job.printer_id === 'number') next.add(job.printer_id);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function handleEvent(event: Event) {
|
||||
const detail = (event as CustomEvent<DispatchEventDetail>).detail ?? {};
|
||||
const next = recompute(detail);
|
||||
// Keep reference stable when content didn't change — useSyncExternalStore
|
||||
// compares snapshots via Object.is and re-renders on any new reference.
|
||||
if (next.size === currentSet.size && [...next].every((id) => currentSet.has(id))) {
|
||||
return;
|
||||
}
|
||||
currentSet = next;
|
||||
subscribers.forEach((cb) => cb());
|
||||
}
|
||||
|
||||
function ensureAttached() {
|
||||
if (attached || typeof window === 'undefined') return;
|
||||
window.addEventListener('background-dispatch', handleEvent);
|
||||
attached = true;
|
||||
}
|
||||
|
||||
const subscribe = (callback: () => void): (() => void) => {
|
||||
ensureAttached();
|
||||
subscribers.add(callback);
|
||||
return () => {
|
||||
subscribers.delete(callback);
|
||||
};
|
||||
};
|
||||
|
||||
const getSnapshot = (): ReadonlySet<number> => currentSet;
|
||||
|
||||
export function useDispatchedPrinterIds(): ReadonlySet<number> {
|
||||
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
||||
}
|
||||
|
||||
/** Test-only helper — resets the module-level singleton between tests. */
|
||||
export function __resetDispatchedPrinterIdsForTests(): void {
|
||||
currentSet = EMPTY;
|
||||
subscribers.clear();
|
||||
if (attached && typeof window !== 'undefined') {
|
||||
window.removeEventListener('background-dispatch', handleEvent);
|
||||
attached = false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user