diff --git a/internal/check-batch-selection-export.mjs b/internal/check-batch-selection-export.mjs index 8ffa3d2d8..237908801 100644 --- a/internal/check-batch-selection-export.mjs +++ b/internal/check-batch-selection-export.mjs @@ -183,6 +183,21 @@ ok(!disagree.length, `Copy's regroup warning disagrees with apply-grouping: ${di `a state-only selection is titled "Gujarāt"; a state + a country names both; a province with no table is not named; Mizoram alone reads "${empty}"`) } +// ── A7. A selection too big for a PHONE'S PRINT starts nothing there ──────────────────────────────────── +// Printing the whole selection (2,209 groups, 113 sheets) crashed the page in a 2.5 GB browser; 1,857 finished. The +// ceiling (half of that, for a phone tab's memory) is recorded beside its measurement in data/selection-export.js. +// Self-test: a phone over the ceiling is blocked; a desktop over it is not; a phone at or under it (India alone, 905) +// is not. The wiring (arm C) checks each of Prayer list, Card deck and Poster asks BEFORE it prints. +{ + const c = X.PHONE_GROUP_CEILING + const src = readFileSync(P('plugin-designs/my-upg-100-list/data/selection-export.js'), 'utf8') + const cases = [[2209, true, true], [1001, true, true], [2209, false, false], [c, true, false], [905, true, false]] + const wrong = cases.filter(([n, phone, want]) => X.overPhoneCeiling(n, phone) !== want).map(([n, phone]) => `${n} on a ${phone ? 'phone' : 'desktop'}`) + ok(c === 1000 && !wrong.length && /2,209 groups +113 sheets +the page CRASHED/.test(src) && /1,857 groups/.test(src), + `phone ceiling ${c}: wrong for ${wrong.join(', ') || '(none)'}; the measurement beside it is ${/2,209 groups/.test(src) ? 'present' : 'MISSING'}`, + `a phone over ${c} groups prints nothing (2,209 → blocked); a desktop is never blocked; India alone (905) still prints on a phone`) +} + // ── B. the CSV ─────────────────────────────────────────────────────────────────────────────────── function parseCsv(s) { // RFC 4180: quoted fields, doubled quotes, CRLF records const rows = []; let row = [], cell = '', q = false @@ -236,6 +251,14 @@ const wired = { const body = vue.slice(at, vue.indexOf('\n })', at)) return !/toggleTerritory|toggleByProps|toggleZoneSelection/.test(body) })(), + // Each print action asks the phone ceiling BEFORE it prints, and says so in the house words. + 'a big selection on a phone prints nothing': (() => { + const body = (name) => { const at = vue.indexOf(`async function ${name}() {`); return at < 0 ? '' : vue.slice(at, vue.indexOf('\n}\n', at)) } + const before = (name, call) => { const b = body(name); const g = b.indexOf('phoneTooBig('), p = b.indexOf(call); return g > 0 && p > g } + return before('makeSelectionPacket', 'packetPrints.printPacket(') && before('makeSelectionCards', 'packetPrints.printCardDeck(') + && before('makeSelectionPoster', 'packetPrints.printPoster(') && /flashStatus\(t\('poster\.phoneLimitAll'\)/.test(vue) + && /const ON_PHONE = isPhonePrint\(\);/.test(vue) + })(), 'the regroup warning on Copy': /const warn = regroupable\(sel\) \? '' : t\('upgMap\.copyNotRegroupable'\)/.test(vue), // The packet / card-deck title goes through selectionTitleNames (arm A6 proves it), with the keyed empty case. 'the title names come from selectionTitleNames': /const names = selectionTitleNames\(zoneNames, selectedTerritories\.value, from,\s*\n\s*\(\) => keyedError\('wall\.hud\.noRecords'/.test(vue) @@ -249,6 +272,7 @@ ok(!missingWires.length, `the HUD is missing: ${missingWires.join(', ')}`, 'the // ── D. the strings, ×11 ────────────────────────────────────────────────────────────────────────── const LANGS = ['en', 'es', 'fr', 'pt', 'de', 'it', 'ro', 'ru', 'ar', 'hi', 'zh'] const KEYS = { + poster: ['phoneLimitAll'], upgMap: ['copySelection', 'exportCsv', 'copyNotRegroupable', 'peopleStillLoading', 'selectionColourGroup', 'selectionColour', 'customColour', 'copiedSelection', 'copiedTerritoriesOnly', 'csvSaved', 'csvFailed'] } const cat = (l, f) => JSON.parse(readFileSync(P('library/i18n/locales', l, `${f}.json`), 'utf8')) const bad = [] diff --git a/plugin-designs/my-upg-100-list/data/selection-export.js b/plugin-designs/my-upg-100-list/data/selection-export.js index 93b4a7f65..f10c7ddbb 100644 --- a/plugin-designs/my-upg-100-list/data/selection-export.js +++ b/plugin-designs/my-upg-100-list/data/selection-export.js @@ -112,6 +112,23 @@ export function selectionTitleNames(zoneNames, territories, from, noRecords) { return [...zoneNames, ...countries, ...states]; } +/** + * ★ A SELECTION TOO BIG FOR A PHONE'S PRINT. Building the document is never the problem: on a 4×-throttled phone + * context the WHOLE selection (all 28 territories, 2,209 groups) is ready in 19.5 s as a Prayer list and 27 s as a + * card deck. Printing it is. The print/preview path, measured as page.pdf of the same document in a browser capped + * at 2.5 GB: + * 905 groups (India) 54 sheets 50 MB PDF 45 s at 4× + * 1,324 / 1,497 / 1,857 groups 75 / 83 / 101 sheets 98 / 127 / 138 MB 54-62 s at 1× + * 2,209 groups 113 sheets the page CRASHED (at 1× and at 4×; ~1.8 GB resident when it died) + * A phone tab gets roughly half that memory, so the ceiling is about half the largest that finished: 1,000. India + * alone (905) still prints on a phone; anything bigger asks for a computer, in the words Country Posters uses for + * whole India (poster.phoneLimitAll). Copy and CSV are cheap and stay allowed. + */ +export const PHONE_GROUP_CEILING = 1000; +export function overPhoneCeiling(count, onPhone, ceiling = PHONE_GROUP_CEILING) { + return Boolean(onPhone) && Number(count) > ceiling; +} + /** A file name a partner can recognise: the selection's label, reduced to safe characters. */ export function selectionCsvFileName(label) { const base = String(label || 'selection').normalize('NFKD').replace(/[\u0300-\u036f]/g, '') @@ -119,4 +136,4 @@ export function selectionCsvFileName(label) { return `upg-100-${base}.csv`; } -export default { SELECTION_CSV_COLUMNS, csvCell, payloadName, regroupKind, regroupable, selectionCsv, selectionClipboard, settleWithin, asReaderError, selectionTitleNames, selectionCsvFileName }; +export default { SELECTION_CSV_COLUMNS, csvCell, payloadName, regroupKind, regroupable, selectionCsv, selectionClipboard, settleWithin, asReaderError, selectionTitleNames, PHONE_GROUP_CEILING, overPhoneCeiling, selectionCsvFileName }; 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 7c3aaa535..8f9419a5b 100644 --- a/plugin-designs/my-upg-100-list/upg-100-list.vue +++ b/plugin-designs/my-upg-100-list/upg-100-list.vue @@ -128,7 +128,8 @@ import { colorForZone } from './data/zone-colors.js' // clicked tapestry feature carries shapeName (ADM1) OR iso3 (ADM0) and this maps // either to { key, name, count } from admin-counts/membership (NEVER edits them). import { resolveTerritory, buildCountryNameMap, formatClipboard, ADM1_COUNTS, ISO3_NAME } from './data/territory-counts.js' -import { selectionCsv, selectionClipboard, selectionCsvFileName, regroupable, settleWithin, asReaderError, selectionTitleNames } from './data/selection-export.js' +import { selectionCsv, selectionClipboard, selectionCsvFileName, regroupable, settleWithin, asReaderError, selectionTitleNames, overPhoneCeiling } from './data/selection-export.js' +import { isPhonePrint } from '@map/composables/printInPlace.js' import visionFunnel from '../../data/people-groups/vision-funnel.json' import { upgTotal, countInSentence } from './data/upg-total.js' // 09b-search-bar §1(a-c): unified search index (PG + zone + territory) + the @@ -1998,12 +1999,22 @@ async function exportSelectionCsv() { } finally { batchBusy.value = false; busyAction.value = ''; } } +// A selection too big for a phone's print (data/selection-export.js has the measurement) starts nothing and says so. +// The same platform test the print path uses (isPhonePrint: a phone or tablet prints the page, with a phone's memory). +const ON_PHONE = isPhonePrint(); +function phoneTooBig(count) { + if (!overPhoneCeiling(count, ON_PHONE)) return false; + flashStatus(t('poster.phoneLimitAll'), 6000); + return true; +} + async function makeSelectionPacket() { if (batchBusy.value) return; batchBusy.value = true; busyAction.value = 'packet'; try { const sel = await selectionAsList(); + if (phoneTooBig(sel.count)) return; // The packet's map page is per-zone artwork; a multi-zone selection has no single poster to // lift it from, so it prints a cover WITHOUT the map rather than one zone's map standing in // for all of them. A packet with no sheet says so on page 1. @@ -2031,7 +2042,9 @@ async function makeSelectionCards() { batchBusy.value = true; busyAction.value = 'cards'; try { - await packetPrints.printCardDeck(await selectionAsList()); + const sel = await selectionAsList(); + if (phoneTooBig(sel.count)) return; + await packetPrints.printCardDeck(sel); } catch (e) { flashStatus(t('upgMap.selectionCardsFailed', { reason: errorText(e, t).slice(0, 90) }), 4000); } finally { @@ -2064,6 +2077,7 @@ async function makeSelectionPoster() { flashStatus(t('messages.posterOneTerritory', { count: lists.length }), 5000); return; } + if (phoneTooBig(lists[0].count)) return; await packetPrints.printPoster(lists[0]); } catch (e) { flashStatus(t('messages.selectionPosterFailed', { reason: errorText(e, t).slice(0, 90) }), 4000);