@bongos/core 1.19.1063 → 1.19.1065
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +136 -56
- package/.claude/skills/grade-recover/SKILL.md +1 -0
- package/.claude/skills/tweak/SKILL.md +70 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +12 -0
- package/clients/bongos-client/index.cjs +12 -0
- package/clients/bongos-client/index.d.ts +21 -1
- package/clients/bongos-client/index.mjs +12 -0
- package/docs/api/openapi.json +492 -4
- package/docs/api-reference.md +9 -3
- package/docs/copy-inventory.md +60 -41
- package/docs/copy-registry.json +228 -47
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +11 -2
- package/docs/page-readings.json +45 -22
- package/modules/copy-desk/page-decisions.js +71 -0
- package/modules/copy-desk/page-status.js +96 -0
- package/modules/copy-desk/pages.js +30 -0
- package/modules/copy-desk/routes/copy-desk.js +153 -0
- package/modules/copy-desk/tests/copy_no_cms.mjs +40 -4
- package/modules/hall-ui/public/approval-queue-lib.js +156 -0
- package/modules/hall-ui/public/approval-queue.css +286 -0
- package/modules/hall-ui/public/approval-queue.js +338 -0
- package/modules/hall-ui/public/studio.css +33 -0
- package/modules/hall-ui/public/studio.html +18 -1
- package/modules/hall-ui/public/studio.js +30 -0
- package/modules/hall-ui/public/studio.states.json +198 -0
- package/modules/hall-ui/records/approval-queue.md +32 -0
- package/modules/lifecycle/db-grade.js +96 -58
- package/modules/lifecycle/github-pr-close.js +41 -0
- package/modules/lifecycle/github-push.js +5 -2
- package/modules/lifecycle/lifecycle.js +12 -0
- package/modules/lifecycle/migrations/lifecycle_013_task_visual_slots.sql +52 -0
- package/modules/lifecycle/page-tweak-approve.js +242 -0
- package/modules/lifecycle/page-tweak-hold.js +74 -0
- package/modules/lifecycle/page-tweak-reads.js +33 -1
- package/modules/lifecycle/routes/gate-approvals.js +17 -2
- package/modules/lifecycle/routes/tasks.js +4 -2
- package/modules/lifecycle/routes/visuals.js +82 -0
- package/modules/lifecycle/task-visual-db.js +40 -1
- package/modules/lifecycle/task-visuals.js +24 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/copy-apply.js +277 -1
- package/scripts/gds/page-reader.js +2 -0
- package/scripts/gds/ship-finish.js +27 -2
- package/scripts/gds/ship-flow.js +17 -1
- package/scripts/gds/ship-land.js +30 -7
- package/scripts/gds/ship-merge.js +43 -15
- package/scripts/gds/strand-watch.js +41 -1
- package/scripts/gds/task.js +23 -5
- package/scripts/gds/tweak-renders.js +200 -0
- package/scripts/hall-preview/server.js +18 -0
- package/src/module-api.js +1 -1
- package/tests/copy_desk_page_approve.mjs +307 -0
- package/tests/fixtures/page-ledger.mjs +41 -1
- package/tests/gate_approvals.mjs +93 -0
- package/tests/hall_approval_queue.mjs +327 -0
- package/tests/hall_audit.mjs +4 -0
- package/tests/page_tweak_hold.mjs +265 -0
- package/tests/publish_status_branch.mjs +23 -0
- package/tests/task_visual_slots.mjs +161 -0
- package/tests/tweak_batch_apply.mjs +265 -0
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
// Exit codes: 0 always on --line (fail-open). Otherwise 0 clean, 1 strands found, 2 undetermined.
|
|
39
39
|
|
|
40
40
|
const { apiCall, arg, hasFlag } = require('./cli-lib');
|
|
41
|
+
const tweakHold = require('../../modules/lifecycle/page-tweak-hold.js');
|
|
41
42
|
|
|
42
43
|
// ---- thresholds -------------------------------------------------------------
|
|
43
44
|
// Reasoned, not magic. Written down because a future reader will want to retune them.
|
|
@@ -132,6 +133,31 @@ function rankStrands(strands) {
|
|
|
132
133
|
});
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
// ---- the held pass (task 1004322, ADR 0341 D7) --------------------------------
|
|
137
|
+
//
|
|
138
|
+
// A page tweak whose grade PASSED waits at `completed` for its artist on purpose:
|
|
139
|
+
// the artist approves the applied page before it lands. It is not a strand and not
|
|
140
|
+
// a failed grade, and calling it one would send someone to /grade-recover a task
|
|
141
|
+
// nobody should touch. The task list carries no grade, so each completed page
|
|
142
|
+
// tweak's grade is read on its own (there are a handful at most; the read is
|
|
143
|
+
// bounded anyway). A read that FAILS leaves the task to the ordinary
|
|
144
|
+
// classification: an unknown must stay loud, never be explained away as a hold.
|
|
145
|
+
const HOLD_READ_LIMIT = 25;
|
|
146
|
+
|
|
147
|
+
async function heldPageTweaks(tasks, { call, withTimeout }) {
|
|
148
|
+
const candidates = tasks.filter((t) => t && t.status === 'completed' && tweakHold.isPageTweak(t) && safeId(t.id)).slice(0, HOLD_READ_LIMIT);
|
|
149
|
+
const held = new Set();
|
|
150
|
+
await Promise.all(candidates.map(async (t) => {
|
|
151
|
+
try {
|
|
152
|
+
const res = await withTimeout(call('GET', `/api/bongos/tasks/${safeId(t.id)}?include=grade`), 'page-tweak grade');
|
|
153
|
+
const body = res && (res.data || res);
|
|
154
|
+
const grade = body && body.grade;
|
|
155
|
+
if (tweakHold.isHeldForArtist(t, grade ? grade.passed : null)) held.add(String(t.id));
|
|
156
|
+
} catch { /* unknown: classified as usual */ }
|
|
157
|
+
}));
|
|
158
|
+
return held;
|
|
159
|
+
}
|
|
160
|
+
|
|
135
161
|
// ---- fetch ------------------------------------------------------------------
|
|
136
162
|
|
|
137
163
|
async function check({ nowMs = Date.now(), timeoutMs = DEFAULT_TIMEOUT_MS, confirmedStaleMs, completedStaleMs, call = apiCall } = {}) {
|
|
@@ -157,14 +183,23 @@ async function check({ nowMs = Date.now(), timeoutMs = DEFAULT_TIMEOUT_MS, confi
|
|
|
157
183
|
|
|
158
184
|
const strands = [];
|
|
159
185
|
const truncated = [];
|
|
186
|
+
const waiting = [];
|
|
187
|
+
const completed = (pages.find((p) => p.status === 'completed') || { tasks: [] }).tasks;
|
|
188
|
+
const held = await heldPageTweaks(completed, { call, withTimeout });
|
|
160
189
|
for (const { status, tasks } of pages) {
|
|
161
190
|
if (tasks.length >= PAGE_LIMIT) truncated.push(status);
|
|
162
191
|
for (const t of tasks) {
|
|
192
|
+
if (status === 'completed' && held.has(String(t.id))) {
|
|
193
|
+
const updated = Date.parse(t.updated_at || '');
|
|
194
|
+
const ageMs = Number.isFinite(updated) ? nowMs - updated : NaN;
|
|
195
|
+
waiting.push({ id: safeId(t.id), status, ageMs, age: humanAge(ageMs), shape: tweakHold.HOLD_STATE });
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
163
198
|
const s = classifyStrand(t, { nowMs, confirmedStaleMs, completedStaleMs });
|
|
164
199
|
if (s) strands.push(s);
|
|
165
200
|
}
|
|
166
201
|
}
|
|
167
|
-
return { undetermined: false, ok: strands.length === 0 && !truncated.length, strands: rankStrands(strands), truncated };
|
|
202
|
+
return { undetermined: false, ok: strands.length === 0 && !truncated.length, strands: rankStrands(strands), truncated, waiting };
|
|
168
203
|
}
|
|
169
204
|
|
|
170
205
|
// ---- rendering --------------------------------------------------------------
|
|
@@ -218,6 +253,11 @@ function report(r) {
|
|
|
218
253
|
console.log(` ! coverage capped: the ${r.truncated.join(' and ')} queue(s) returned a full ${PAGE_LIMIT}-row page,`);
|
|
219
254
|
console.log(' so strands older than the cutoff are NOT listed below.');
|
|
220
255
|
}
|
|
256
|
+
// task 1004322: named, and kept apart from the strands — nobody should recover these.
|
|
257
|
+
const waiting = Array.isArray(r.waiting) ? r.waiting : [];
|
|
258
|
+
for (const w of waiting) {
|
|
259
|
+
console.log(` task ${w.id} waiting for the artist (${w.age}) — a page tweak whose grade passed; not a strand, leave it`);
|
|
260
|
+
}
|
|
221
261
|
if (r.ok) {
|
|
222
262
|
console.log(' ✓ none — nothing parked at completed or confirmed past its threshold.');
|
|
223
263
|
console.log('');
|
package/scripts/gds/task.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
// bongos task show <id> [--json]
|
|
14
14
|
// bongos task visual <id> <image> [--alt "caption"] attach/replace the ship-time visual
|
|
15
15
|
// bongos task visual <id> --remove take it back off
|
|
16
|
+
// bongos task visual <id> <image> --slot <name> attach/replace a NAMED visual (task 1004322)
|
|
17
|
+
// bongos task visual <id> --slot <name> --remove take a named one back off
|
|
16
18
|
//
|
|
17
19
|
// Task CREATION is Archon-gated SERVER-SIDE (creating work shapes scope, ADR 0016). This
|
|
18
20
|
// CLI adds no authority — a lower rank gets a clear 403 message pointing at idea capture.
|
|
@@ -84,6 +86,7 @@ function usage() {
|
|
|
84
86
|
console.error(' bongos task show <id> [--json]');
|
|
85
87
|
console.error(' bongos task visual <id> <image> [--alt "caption"] attach or replace the ship-time visual');
|
|
86
88
|
console.error(' bongos task visual <id> --remove remove it again');
|
|
89
|
+
console.error(' bongos task visual <id> <image> --slot <name> attach or replace a named visual (e.g. after-phone-dark)');
|
|
87
90
|
console.error(`\n kinds: ${VALID_KINDS.join(' | ')}`);
|
|
88
91
|
console.error(` disciplines: ${VALID_DISCIPLINES.join(' | ')}`);
|
|
89
92
|
}
|
|
@@ -174,6 +177,21 @@ async function cmdVisual(args) {
|
|
|
174
177
|
}
|
|
175
178
|
const api = await cliClient();
|
|
176
179
|
|
|
180
|
+
// task 1004322: --slot names the visual (lifecycle_013). Validated here with the
|
|
181
|
+
// server's own rule, so a bad name costs a retyped command, not an upload.
|
|
182
|
+
const slot = arg('--slot', args) || null;
|
|
183
|
+
if (slot !== null && !taskVisuals.isSafeSlot(slot)) {
|
|
184
|
+
console.error(`--slot "${slot}" is not a slot name: lowercase words joined by dashes, such as ${taskVisuals.TWEAK_RENDER_SLOTS[0]}.`);
|
|
185
|
+
process.exit(2);
|
|
186
|
+
}
|
|
187
|
+
if (slot && hasFlag('--remove', args)) {
|
|
188
|
+
const r = await api.tasks.deleteTasksIdVisualsSlot({ id, slot });
|
|
189
|
+
if (r.status === 404) { console.error(`task #${id} not found.`); process.exit(1); }
|
|
190
|
+
if (!r.ok) { console.error(`visual remove failed (${r.status}):`, r.data); process.exit(1); }
|
|
191
|
+
console.log(`✓ Task #${id} no longer carries the "${slot}" visual.`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
177
195
|
if (hasFlag('--remove', args)) {
|
|
178
196
|
const r = await api.tasks.deleteTasksIdVisual({ id });
|
|
179
197
|
if (r.status === 404) { console.error(`task #${id} not found.`); process.exit(1); }
|
|
@@ -199,14 +217,14 @@ async function cmdVisual(args) {
|
|
|
199
217
|
}
|
|
200
218
|
|
|
201
219
|
const alt = arg('--alt', args) || '';
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
220
|
+
const body = { image_b64: visual.buf.toString('base64'), content_type: visual.contentType, ...(alt ? { alt } : {}) };
|
|
221
|
+
const r = slot
|
|
222
|
+
? await api.tasks.postTasksIdVisualsSlot({ id, slot, body })
|
|
223
|
+
: await api.tasks.postTasksIdVisual({ id, body });
|
|
206
224
|
if (r.status === 404) { console.error(`task #${id} not found.`); process.exit(1); }
|
|
207
225
|
if (r.status === 403) { console.error(`Only task #${id}'s claim holder (or an Archon) may attach its visual.`); process.exit(1); }
|
|
208
226
|
if (!r.ok) { console.error(`visual upload failed (${r.status}):`, r.data); process.exit(1); }
|
|
209
|
-
console.log(`✓ Task #${id} now carries a visual (${Math.round(visual.buf.length / 1024)} KB).`);
|
|
227
|
+
console.log(`✓ Task #${id} now carries ${slot ? `the "${slot}" visual` : 'a visual'} (${Math.round(visual.buf.length / 1024)} KB).`);
|
|
210
228
|
console.log(' It renders wherever this task\'s value summary does.');
|
|
211
229
|
}
|
|
212
230
|
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
//
|
|
4
|
+
// scripts/gds/tweak-renders.js — the eight renders of a page tweak, shot and
|
|
5
|
+
// attached (task 1004322 / BV2.TW11; ADR 0341 D7 and its builder pick 9).
|
|
6
|
+
//
|
|
7
|
+
// WHY. The artist approves the APPLIED PAGE, not a diff and not a page built from
|
|
8
|
+
// the batch: the Approval queue (TW12) shows images of the applied branch, which
|
|
9
|
+
// is what keeps ADR 0233's "nothing renders a task description as product copy"
|
|
10
|
+
// true. So /tweak renders the page before it applies the batch and again after,
|
|
11
|
+
// on a desktop (1440) and a phone (390), in light and in dark, and attaches the
|
|
12
|
+
// eight pictures to the round's task under their slot names
|
|
13
|
+
// (task-visuals.TWEAK_RENDER_SLOTS: before-desktop-light … after-phone-dark).
|
|
14
|
+
//
|
|
15
|
+
// HOW IT RENDERS. Exactly the way the page reader reads (scripts/gds/page-reader.js):
|
|
16
|
+
// the hall through the hall-preview harness with the pinned fixture account, the
|
|
17
|
+
// landing and status surfaces through the ui-design kit's stub, one browser from
|
|
18
|
+
// the kit's launchBrowser (system Edge or Chrome; nothing is downloaded), the
|
|
19
|
+
// clock pinned, the page's first state from its .states.json. A page that pins
|
|
20
|
+
// one colour mode is shot in that mode for both of its "light" and "dark" slots,
|
|
21
|
+
// and the run says so, rather than inventing a mode the page does not have.
|
|
22
|
+
// Full-page JPEG, so a long page still fits under the store's 4 MB cap.
|
|
23
|
+
//
|
|
24
|
+
// Run: node scripts/gds/tweak-renders.js shoot --page <id> --phase before|after --out <dir>
|
|
25
|
+
// node scripts/gds/tweak-renders.js attach <task-id> --dir <dir> (every slot file in <dir>)
|
|
26
|
+
// node scripts/gds/tweak-renders.js plan --page <id> (what would be shot; no browser)
|
|
27
|
+
//
|
|
28
|
+
// Exit: 0 done · 1 a render or an upload failed · 2 usage · 3 no browser.
|
|
29
|
+
|
|
30
|
+
const fs = require('node:fs');
|
|
31
|
+
const path = require('node:path');
|
|
32
|
+
|
|
33
|
+
const taskVisuals = require('../../modules/lifecycle/task-visuals');
|
|
34
|
+
const { arg, cliClient, cliExit } = require('./cli-lib');
|
|
35
|
+
|
|
36
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
37
|
+
const EXT = 'jpg';
|
|
38
|
+
const JPEG_QUALITY = 82;
|
|
39
|
+
|
|
40
|
+
// ---- pure ------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
function slotName(phase, device, mode) {
|
|
43
|
+
return `${phase}-${device}-${mode}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The shots for one phase of one page: one per device x mode, each carrying the
|
|
47
|
+
// slot it fills and the mode the page will actually render in. `plan` is the
|
|
48
|
+
// page's first render plan from page-reader.planRenders.
|
|
49
|
+
function shotsFor(phase, plan) {
|
|
50
|
+
if (!taskVisuals.TWEAK_RENDER_PHASES.includes(phase)) throw new Error(`phase must be one of ${taskVisuals.TWEAK_RENDER_PHASES.join(', ')}`);
|
|
51
|
+
const pinned = plan && plan.pinnedMode ? plan.pinnedMode : null;
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const [device, width] of Object.entries(taskVisuals.TWEAK_RENDER_DEVICES)) {
|
|
54
|
+
for (const mode of taskVisuals.TWEAK_RENDER_MODES) {
|
|
55
|
+
out.push({ slot: slotName(phase, device, mode), device, width, mode, renderMode: pinned || mode, file: `${slotName(phase, device, mode)}.${EXT}` });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The page's first render plan, and whether its states file pins a mode. Reads
|
|
62
|
+
// the inventory and the states file; no browser.
|
|
63
|
+
function planForPage(pageId, { inventory, planRenders, root = REPO_ROOT }) {
|
|
64
|
+
const page = (inventory.pages || []).find((p) => p.id === pageId);
|
|
65
|
+
if (!page) return { ok: false, code: 'unknown_page', detail: { page: pageId } };
|
|
66
|
+
const plans = planRenders(page, { root });
|
|
67
|
+
const plan = plans[0];
|
|
68
|
+
let modes = null;
|
|
69
|
+
if (page.states_files && page.states_files.length) {
|
|
70
|
+
try { modes = JSON.parse(fs.readFileSync(path.join(root, page.states_files[0]), 'utf8')).modes || null; } catch { modes = null; }
|
|
71
|
+
}
|
|
72
|
+
const pinnedMode = Array.isArray(modes) && modes.length === 1 ? modes[0] : null;
|
|
73
|
+
return { ok: true, page, plan: { ...plan, pinnedMode } };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Which slot files a directory holds, in slot order, and which are missing.
|
|
77
|
+
function slotFilesIn(dir, { exists = (p) => fs.existsSync(p) } = {}) {
|
|
78
|
+
const found = [];
|
|
79
|
+
const missing = [];
|
|
80
|
+
for (const slot of taskVisuals.TWEAK_RENDER_SLOTS) {
|
|
81
|
+
const hit = ['jpg', 'png', 'webp'].map((e) => path.join(dir, `${slot}.${e}`)).find(exists);
|
|
82
|
+
if (hit) found.push({ slot, file: hit });
|
|
83
|
+
else missing.push(slot);
|
|
84
|
+
}
|
|
85
|
+
return { found, missing };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---- the browser half --------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
async function shoot({ pageId, phase, out }) {
|
|
91
|
+
const reader = require('./page-reader.js');
|
|
92
|
+
const kit = require('../../modules/ui-design/kit/lib');
|
|
93
|
+
const inventory = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, reader.INVENTORY_REL), 'utf8'));
|
|
94
|
+
const planned = planForPage(pageId, { inventory, planRenders: reader.planRenders });
|
|
95
|
+
if (!planned.ok) { console.error(`tweak-renders: ${planned.code} (${pageId})`); return 2; }
|
|
96
|
+
const { page, plan } = planned;
|
|
97
|
+
const shots = shotsFor(phase, plan);
|
|
98
|
+
if (plan.pinnedMode) console.log(`tweak-renders: ${pageId} pins the ${plan.pinnedMode} mode, so both of its mode slots are shot in ${plan.pinnedMode}.`);
|
|
99
|
+
fs.mkdirSync(out, { recursive: true });
|
|
100
|
+
|
|
101
|
+
let browser;
|
|
102
|
+
try { browser = await kit.launchBrowser(); } catch (e) {
|
|
103
|
+
console.error(`tweak-renders: ${e.message}`);
|
|
104
|
+
return e instanceof kit.NoBrowserError ? 3 : 1;
|
|
105
|
+
}
|
|
106
|
+
const stoppers = [];
|
|
107
|
+
let failed = 0;
|
|
108
|
+
try {
|
|
109
|
+
let base;
|
|
110
|
+
if (page.surface === 'builders') {
|
|
111
|
+
const h = await reader.startHarness(kit);
|
|
112
|
+
stoppers.push(h.stop);
|
|
113
|
+
base = h.base;
|
|
114
|
+
} else {
|
|
115
|
+
const s = await kit.startStub({ surface: reader.SURFACE_MODULE[page.surface] || page.surface, auth: plan.auth, prefix: plan.stub.prefix, child: plan.stub.child, fixtures: plan.stub.fixtures });
|
|
116
|
+
stoppers.push(s.stop);
|
|
117
|
+
base = s.base;
|
|
118
|
+
}
|
|
119
|
+
for (const shot of shots) {
|
|
120
|
+
const [W, H] = kit.SIZES[shot.width];
|
|
121
|
+
const ctx = await browser.newContext({ viewport: { width: W, height: H }, deviceScaleFactor: 1, colorScheme: shot.renderMode });
|
|
122
|
+
try {
|
|
123
|
+
if (ctx.clock && typeof ctx.clock.setFixedTime === 'function') await ctx.clock.setFixedTime(new Date(reader.PINNED_CLOCK));
|
|
124
|
+
const p = await ctx.newPage();
|
|
125
|
+
p.setDefaultTimeout(6000);
|
|
126
|
+
p.on('dialog', (d) => { d.dismiss().catch(() => {}); });
|
|
127
|
+
await p.goto(base + kit.withMode(plan.url, shot.renderMode, plan.modeQuery), { waitUntil: 'load', timeout: 45000 });
|
|
128
|
+
await p.evaluate(() => document.fonts.ready).catch(() => {});
|
|
129
|
+
await p.waitForTimeout(kit.SETTLE_MS);
|
|
130
|
+
try { await kit.runActions(p, plan.actions); } catch (e) { console.log(` ${shot.slot}: an action failed (${String(e.message).split('\n')[0].slice(0, 100)}); shot as it stood`); }
|
|
131
|
+
await p.screenshot({ path: path.join(out, shot.file), fullPage: true, type: 'jpeg', quality: JPEG_QUALITY });
|
|
132
|
+
const kb = Math.round(fs.statSync(path.join(out, shot.file)).size / 1024);
|
|
133
|
+
console.log(` ${shot.slot}: ${W}px ${shot.renderMode} -> ${shot.file} (${kb} KB)`);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
failed += 1;
|
|
136
|
+
console.error(` ${shot.slot}: NOT rendered (${String(e.message).split('\n')[0].slice(0, 160)})`);
|
|
137
|
+
} finally {
|
|
138
|
+
await ctx.close().catch(() => {});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} finally {
|
|
142
|
+
await browser.close().catch(() => {});
|
|
143
|
+
for (const stop of stoppers) stop();
|
|
144
|
+
}
|
|
145
|
+
console.log(`tweak-renders: ${shots.length - failed} of ${shots.length} ${phase} render(s) of ${pageId} in ${out}`);
|
|
146
|
+
return failed ? 1 : 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---- the upload half ---------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
async function attach({ taskId, dir }, deps = {}) {
|
|
152
|
+
const { found, missing } = slotFilesIn(dir, deps);
|
|
153
|
+
if (!found.length) { console.error(`tweak-renders: no slot files in ${dir} (expected ${taskVisuals.TWEAK_RENDER_SLOTS[0]}.${EXT} and the rest).`); return 1; }
|
|
154
|
+
const client = deps.client || await cliClient();
|
|
155
|
+
let failed = 0;
|
|
156
|
+
for (const { slot, file } of found) {
|
|
157
|
+
const img = taskVisuals.readImageFile(file);
|
|
158
|
+
if (!img.ok) { failed += 1; console.error(` ${slot}: ${img.message}`); continue; }
|
|
159
|
+
const r = await client.tasks.postTasksIdVisualsSlot({ id: taskId, slot, body: { image_b64: img.buf.toString('base64'), content_type: img.contentType, alt: `${slot.replace(/-/g, ' ')} render of the page` } });
|
|
160
|
+
if (r.ok) console.log(` ${slot}: attached (${Math.round(img.buf.length / 1024)} KB)`);
|
|
161
|
+
else { failed += 1; console.error(` ${slot}: upload failed (${r.status})`); }
|
|
162
|
+
}
|
|
163
|
+
if (missing.length) console.error(`tweak-renders: MISSING ${missing.length} slot(s): ${missing.join(', ')}. The Approval queue shows what is attached; shoot the missing phase and attach again.`);
|
|
164
|
+
console.log(`tweak-renders: ${found.length - failed} of ${taskVisuals.TWEAK_RENDER_SLOTS.length} render(s) attached to task ${taskId}.`);
|
|
165
|
+
return failed || missing.length ? 1 : 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function main(args) {
|
|
169
|
+
const cmd = args[0];
|
|
170
|
+
if (cmd === 'shoot' || cmd === 'plan') {
|
|
171
|
+
const pageId = arg('--page', args);
|
|
172
|
+
if (!pageId) { console.error('usage: node scripts/gds/tweak-renders.js shoot --page <id> --phase before|after --out <dir>'); return 2; }
|
|
173
|
+
if (cmd === 'plan') {
|
|
174
|
+
const reader = require('./page-reader.js');
|
|
175
|
+
const inventory = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, reader.INVENTORY_REL), 'utf8'));
|
|
176
|
+
const planned = planForPage(pageId, { inventory, planRenders: reader.planRenders });
|
|
177
|
+
if (!planned.ok) { console.error(`tweak-renders: ${planned.code} (${pageId})`); return 2; }
|
|
178
|
+
for (const phase of taskVisuals.TWEAK_RENDER_PHASES) for (const s of shotsFor(phase, planned.plan)) console.log(`${s.slot}\t${s.width}px\t${s.renderMode}\t${planned.plan.url}`);
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
const phase = arg('--phase', args);
|
|
182
|
+
const out = arg('--out', args);
|
|
183
|
+
if (!taskVisuals.TWEAK_RENDER_PHASES.includes(phase) || !out) { console.error('usage: node scripts/gds/tweak-renders.js shoot --page <id> --phase before|after --out <dir>'); return 2; }
|
|
184
|
+
return shoot({ pageId, phase, out: path.resolve(out) });
|
|
185
|
+
}
|
|
186
|
+
if (cmd === 'attach') {
|
|
187
|
+
const taskId = Number(args[1]);
|
|
188
|
+
const dir = arg('--dir', args);
|
|
189
|
+
if (!Number.isInteger(taskId) || taskId <= 0 || !dir) { console.error('usage: node scripts/gds/tweak-renders.js attach <task-id> --dir <dir>'); return 2; }
|
|
190
|
+
return attach({ taskId, dir: path.resolve(dir) });
|
|
191
|
+
}
|
|
192
|
+
console.error('usage: node scripts/gds/tweak-renders.js shoot|attach|plan … (see the file header)');
|
|
193
|
+
return 2;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (require.main === module) {
|
|
197
|
+
main(process.argv.slice(2)).then((code) => cliExit(code), (e) => { console.error(`tweak-renders: ${e.stack || e.message}`); cliExit(1); });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = { shotsFor, planForPage, attach };
|
|
@@ -136,6 +136,9 @@ const FIXTURE_FALLBACKS = [
|
|
|
136
136
|
// page id, since the editor shows one page. A page id carries a colon, which a
|
|
137
137
|
// Windows file name cannot, so there is no per-id file to find first.
|
|
138
138
|
[/^copy-desk\/pages\/[a-z0-9-]+:[a-z0-9-]+\/draft$/, 'copy-desk__pages__draft'],
|
|
139
|
+
// the Approval queue's renders (task 1004323): a round's named visuals, one
|
|
140
|
+
// fixture for any task id, as the editor read above serves any page.
|
|
141
|
+
[/^tasks\/\d+\/visuals$/, 'tasks__visuals'],
|
|
139
142
|
];
|
|
140
143
|
|
|
141
144
|
// CANNED WRITES (task 1004321). The tweak editor's whole flow is writes (the
|
|
@@ -148,6 +151,10 @@ const CANNED_WRITES = [
|
|
|
148
151
|
['PUT', /^copy-desk\/pages\/[a-z0-9-]+:[a-z0-9-]+\/draft$/, 'copy-desk__pages__draft.put'],
|
|
149
152
|
['POST', /^copy-desk\/pages\/[a-z0-9-]+:[a-z0-9-]+\/submit$/, 'copy-desk__pages__submit.post'],
|
|
150
153
|
['POST', /^copy-desk\/pages\/[a-z0-9-]+:[a-z0-9-]+\/claim$/, 'copy-desk__pages__claim.post'],
|
|
154
|
+
// the Approval queue's two decisions (task 1004323), so the approved and
|
|
155
|
+
// sent-back lines the artboard draws render too.
|
|
156
|
+
['POST', /^copy-desk\/pages\/[a-z0-9-]+:[a-z0-9-]+\/approve$/, 'copy-desk__pages__approve.post'],
|
|
157
|
+
['POST', /^copy-desk\/pages\/[a-z0-9-]+:[a-z0-9-]+\/send-back$/, 'copy-desk__pages__send-back.post'],
|
|
151
158
|
];
|
|
152
159
|
|
|
153
160
|
// A DATE THAT ROTS CANNOT DEMO AN AGE SIGNAL (task 1004041). The help-request
|
|
@@ -252,6 +259,17 @@ app.use((req, res, next) => {
|
|
|
252
259
|
// frame, so the one thing being reviewed is the one thing you cannot see.
|
|
253
260
|
// A generated stand-in; see scripts/hall-preview/task-visual.js.
|
|
254
261
|
if (/^task-visuals\/[A-Za-z0-9._-]+$/.test(apiPath) && req.method === 'GET') {
|
|
262
|
+
// HALL_PREVIEW_VISUALS=<dir> serves a REAL image of that name from <dir>
|
|
263
|
+
// instead (task 1004323): the Approval queue's review sheet shows the renders
|
|
264
|
+
// /tweak really attached, copied under the fixture's names into a scratch
|
|
265
|
+
// dir. Nothing is committed; unset, every visual is the stand-in as before.
|
|
266
|
+
const realDir = process.env.HALL_PREVIEW_VISUALS;
|
|
267
|
+
const name = apiPath.slice('task-visuals/'.length);
|
|
268
|
+
const real = realDir && /^task-\d+-[0-9a-f]{16}\.(png|jpg)$/.test(name) ? path.join(realDir, name) : null;
|
|
269
|
+
if (real && fs.existsSync(real)) {
|
|
270
|
+
res.set({ 'content-type': name.endsWith('.png') ? 'image/png' : 'image/jpeg', 'cache-control': 'no-store' });
|
|
271
|
+
return res.end(fs.readFileSync(real));
|
|
272
|
+
}
|
|
255
273
|
const png = require('./task-visual').placeholderPng();
|
|
256
274
|
res.set({ 'content-type': 'image/png', 'cache-control': 'no-store' });
|
|
257
275
|
return res.end(png);
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.1065'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|