diff --git a/internal/check-selection-kinds.mjs b/internal/check-selection-kinds.mjs index c37295aaa..8d71157c5 100644 --- a/internal/check-selection-kinds.mjs +++ b/internal/check-selection-kinds.mjs @@ -86,10 +86,30 @@ ok(adm1Cases === 0, // from an unresponsive one, so the label has to change — and it must be CLEARED in `finally`, or an // error leaves the button reading "Working…" forever. ok(/busyAction/.test(VUE), 'the busy indicator is gone — a slow packet build will look like a dead button again') +// ⚠ COUNT EVERY SETTER, NOT JUST THE LITERAL ONES. The first version of this matched +// `busyAction.value = 'packet'|'poster'` and so was blind to a ternary assignment — which is exactly +// what the legend path uses. A regex that enumerates the shapes it knows about will always miss the +// next shape; match the ASSIGNMENT and exclude the clear. const finallys = (VUE.match(/finally\s*\{[^}]*busyAction\.value\s*=\s*''/g) || []).length -const setters = (VUE.match(/busyAction\.value\s*=\s*'(packet|poster)'/g) || []).length -ok(finallys >= setters && setters >= 2, - `busyAction is set ${setters} times but cleared in only ${finallys} finally blocks — a failed print would leave the button stuck on "Working…"`) +const setters = (VUE.match(/busyAction\.value\s*=\s*(?!'')/g) || []).length - finallys +ok(finallys >= setters && setters >= 3, + `busyAction is set ${setters} times but cleared in only ${finallys} finally blocks — a failed print would leave the surface stuck on "Working…"`) + +// ★ EVERY SLOW PRINT PATH, NOT JUST THE ONE THAT WAS REPORTED. The busy state was added to the +// selection HUD after a report that its buttons showed no sign of work; the LEGEND's Export path +// (onLegendExport → printPrayerPacket / printPoster) does the same slow work — two dynamic imports, +// a map-SVG fetch, a funnel fetch, a multi-sheet render — and had no busy state at all. It was found +// by sweeping for the CLASS rather than the instance, and this pins it so the surface cannot be +// forgotten again. Asserted per FUNCTION, because "some handler somewhere sets it" is what let the +// legend path stay uncovered while the file as a whole looked fine. +for (const fn of ['onLegendExport', 'makeSelectionPacket', 'makeSelectionPoster']) { + const body = VUE.match(new RegExp(`async function ${fn}\\s*\\([^)]*\\)\\s*\\{[\\s\\S]{0,2600}?\\n\\}`)) + if (!body) { fails.push(`${fn}() not found — this gate can no longer see the print paths it guards`); continue } + ok(/busyAction\.value\s*=/.test(body[0]), + `${fn}() does slow work with NO busy state — a user gets no sign anything is happening, which is the defect already reported on the other print surface`) + ok(/finally\s*\{/.test(body[0]), + `${fn}() sets a busy state without a finally — a throw would leave the surface permanently busy and the control dead`) +} if (fails.length) { console.error('SELECTION-KINDS: FAIL') diff --git a/library/packet/packet-render.js b/library/packet/packet-render.js index cd70613ac..f59687465 100644 --- a/library/packet/packet-render.js +++ b/library/packet/packet-render.js @@ -181,7 +181,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de .lc-grid{display:grid;grid-template-columns:repeat(52,1fr);grid-auto-rows:13px;gap:2px;width:100%} .lc-c{display:block;width:100%;height:100%;border:1px solid #d8d3c4;border-radius:1px;background:#fff} .lc-on{background:#b9b2a0;border-color:#b9b2a0} - /* The shaded share on page 3 is the REACHED share, so it takes the accent colour and the + /* The shaded share on the %-evangelical grid is the REACHED share, so it takes the accent colour and the unshaded majority stays quiet — the visual weight belongs to what is still waiting. */ .lc-reached{background:#2f6d4f;border-color:#2f6d4f} .lc-legend{display:flex;gap:34px;margin:20px 0 0;font:400 17px Georgia,serif;color:#413d35} diff --git a/plugin-designs/my-upg-100-list/upg-100-list.vue b/plugin-designs/my-upg-100-list/upg-100-list.vue index 6ef84710d..154077bfe 100644 --- a/plugin-designs/my-upg-100-list/upg-100-list.vue +++ b/plugin-designs/my-upg-100-list/upg-100-list.vue @@ -1305,15 +1305,34 @@ provide('onLegendExport', onLegendExport) // PosterWall.vue, so this surface could not reach it. It now lives in usePosterPrints // (printPoster) and BOTH surfaces call the one copy — which is what the card asked for: // "reuse the existing three print actions rather than growing a fourth implementation." +// ★ THE LEGEND'S EXPORT PATH GETS THE BUSY STATE TOO. The selection HUD's buttons were given one +// after a report that pressing them showed no sign of work; THIS surface — the legend's Export ▾ → +// Poster / Packet — had none either and was never looked at. It does the same slow work: two dynamic +// import()s, a map-SVG fetch, a funnel fetch, then a multi-sheet render. Found by sweeping for the +// CLASS of the reported bug rather than the instance, which is the whole point of doing that. +// `busyAction` is declared below, and that is safe: provide() at :1276 only passes the reference, +// and this body runs on a click long after setup has finished. async function onLegendExport(kind) { const zoneId = selectedKey.value; if (!zoneId) return; // the button only shows with a selection - if (kind === 'packet') { await printPrayerPacket(zoneId); return; } - // The legend selects by KEY, while the print actions label by item — hand over the zone - // object so the print dialog's title says the list's name rather than its slug. - // Same resolution printPrayerPacket uses, so both print actions name the list identically. - const zone = allLists().find((z) => z.id === zoneId) || { id: zoneId }; - await packetPrints.printPoster(zone); + // Guard re-entry the way the HUD does: a second click while a print is building would run two + // renders over one print frame. + if (batchBusy.value) return; + batchBusy.value = true; + busyAction.value = kind === 'packet' ? 'packet' : 'poster'; + try { + if (kind === 'packet') { await printPrayerPacket(zoneId); return; } + // The legend selects by KEY, while the print actions label by item — hand over the zone + // object so the print dialog's title says the list's name rather than its slug. + // Same resolution printPrayerPacket uses, so both print actions name the list identically. + const zone = allLists().find((z) => z.id === zoneId) || { id: zoneId }; + await packetPrints.printPoster(zone); + } finally { + // Cleared on EVERY exit path including the early `return` inside the try — a throw would + // otherwise leave the surface permanently busy and the menu dead. + batchBusy.value = false; + busyAction.value = ''; + } } // Build and print a zone's prayer packet without leaving the page.