@bongos/core 1.20.34 → 1.20.36
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 +49 -39
- package/.claude/skills/ask-for-help/SKILL.md +5 -3
- package/.claude/skills/collab-review/SKILL.md +8 -5
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +3 -2
- package/docs/copy-inventory.md +68 -56
- package/docs/copy-registry.json +255 -117
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +17 -17
- package/modules/hall-ui/public/approval-queue.js +1 -1
- package/modules/hall-ui/public/collab-lib.js +10 -1
- package/modules/hall-ui/public/collab.js +47 -10
- package/modules/hall-ui/public/deploy.js +231 -11
- package/modules/hall-ui/public/project-settings.css +28 -0
- package/modules/hall-ui/public/studio.js +7 -7
- package/modules/lifecycle/help-requests.js +35 -2
- package/modules/lifecycle/migrations/lifecycle_016_help_request_reopen.sql +23 -0
- package/modules/lifecycle/routes/help-requests.js +15 -1
- package/modules/provisioning/core-upgrade.js +4 -1
- package/modules/provisioning/render-standup.js +4 -1
- package/modules/public-landing/public/projects.html +16 -7
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +19 -0
- package/src/module-api.js +1 -1
- package/tests/collab_page.mjs +27 -1
- package/tests/core_upgrade_door.mjs +8 -1
- package/tests/deploy_page_projects.mjs +165 -1
- package/tests/hall_approval_queue.mjs +5 -1
- package/tests/hall_client_request_shape.mjs +103 -0
- package/tests/hall_studio_home.mjs +7 -1
- package/tests/hall_studio_world.mjs +5 -1
- package/tests/help_requests.mjs +54 -1
- package/tests/projects_hub_app_step.mjs +42 -0
- package/tests/provisioning_render_route.mjs +7 -0
|
@@ -2725,5 +2725,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2725
2725
|
landed since 1.20.32 with no explicit bump. run 36783765458. (task 1002620)
|
|
2726
2726
|
1.20.34 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2727
2727
|
landed since 1.20.33 with no explicit bump. run 36784565937. (task 1002620)
|
|
2728
|
+
1.20.35 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2729
|
+
landed since 1.20.34 with no explicit bump. run 36787635391. (task 1002620)
|
|
2730
|
+
1.20.36 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2731
|
+
landed since 1.20.35 with no explicit bump. run 36788696388. (task 1002620)
|
|
2728
2732
|
---------------------------------------------------------------------------
|
|
2729
2733
|
```
|
package/docs/page-readings.json
CHANGED
|
@@ -136,8 +136,8 @@
|
|
|
136
136
|
"surface": "builders",
|
|
137
137
|
"title": "Collab",
|
|
138
138
|
"states_read": ["board","out"],
|
|
139
|
-
"files_hash": "
|
|
140
|
-
"reading_hash": "
|
|
139
|
+
"files_hash": "793b7af1446ab69e",
|
|
140
|
+
"reading_hash": "cedb8171693a308e",
|
|
141
141
|
"counts": {"lines":68,"placed":23,"shared":4,"unplaced":41},
|
|
142
142
|
"omitted": {"data":12},
|
|
143
143
|
"notes": [],
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
{"key":"L0001","section":"skip link","text":"Skip to content","placement":"shared","file":"modules/hall-ui/public/shell.js","line":767,"string_id":"87cee78ca134"},
|
|
146
146
|
{"key":"L0002","section":"top bar","text":"Collab","placement":"shared"},
|
|
147
147
|
{"key":"L0003","section":"top bar","text":"Jump to…","placement":"shared","file":"modules/hall-ui/public/shell.js","line":487,"string_id":"eba610d25523"},
|
|
148
|
-
{"key":"L0004","section":"top bar","text":"
|
|
148
|
+
{"key":"L0004","section":"top bar","text":"⌘K","placement":"shared"},
|
|
149
149
|
{"key":"L0005","section":"Collab","text":"Collab","placement":"unplaced"},
|
|
150
150
|
{"key":"L0006","section":"Collab","text":"{…} and {…}","placement":"placed","file":"modules/hall-ui/public/profile-nudge.js","line":77,"string_id":"56b00d773232"},
|
|
151
151
|
{"key":"L0007","section":"Collab","text":"Waiting on you","placement":"unplaced"},
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
{"key":"L0018","section":"Asked {…}","text":"task #{…}","placement":"placed","file":"modules/hall-ui/public/copy-desk.js","line":105,"string_id":"a6e803a31a4d"},
|
|
163
163
|
{"key":"L0019","section":"Asked {…}","text":"Reply","placement":"unplaced","occurrences":2},
|
|
164
164
|
{"key":"L0020","section":"Asked {…}","text":"just asked","placement":"unplaced","occurrences":2},
|
|
165
|
-
{"key":"L0021","section":"Asked {…}","text":"
|
|
165
|
+
{"key":"L0021","section":"Asked {…}","text":"Mark resolved","placement":"placed","file":"modules/hall-ui/public/collab.js","line":278,"string_id":"4924438fb299","occurrences":3},
|
|
166
166
|
{"key":"L0022","section":"Asked {…}","text":"The worktree junction keeps dragging the shared node_modules with it when I remove a stale worktree.","placement":"unplaced"},
|
|
167
167
|
{"key":"L0023","section":"Asked {…}","text":"{…} of {…}","placement":"placed","file":"modules/hall-ui/public/sky-panel.js","line":120,"string_id":"73c1372b43d9","occurrences":2},
|
|
168
168
|
{"key":"L0024","section":"Asked {…}","text":"Could you look at the oversight contact sheet before I ship?","placement":"unplaced"},
|
|
@@ -171,9 +171,9 @@
|
|
|
171
171
|
{"key":"L0027","section":"Asked {…}","text":"6 days ago","placement":"unplaced"},
|
|
172
172
|
{"key":"L0028","section":"Asked {…}","text":"5 days ago","placement":"unplaced"},
|
|
173
173
|
{"key":"L0029","section":"Asked {…}","text":"Add a reply","placement":"unplaced"},
|
|
174
|
-
{"key":"L0030","section":"Asked {…}","text":"waiting
|
|
175
|
-
{"key":"L0031","section":"Asked {…}","text":"Show all {…}","placement":"placed","file":"modules/hall-ui/public/collab.js","line":
|
|
176
|
-
{"key":"L0032","section":"Asked {…}","text":"Copy all {…} for Session Start","placement":"placed","file":"modules/hall-ui/public/collab.js","line":
|
|
174
|
+
{"key":"L0030","section":"Asked {…}","text":"waiting 2 days","placement":"unplaced"},
|
|
175
|
+
{"key":"L0031","section":"Asked {…}","text":"Show all {…}","placement":"placed","file":"modules/hall-ui/public/collab.js","line":432,"string_id":"6dde12eb0c78"},
|
|
176
|
+
{"key":"L0032","section":"Asked {…}","text":"Copy all {…} for Session Start","placement":"placed","file":"modules/hall-ui/public/collab.js","line":478,"string_id":"4fb972405d3d"},
|
|
177
177
|
{"key":"L0033","section":"Your asks 2","text":"Your asks","placement":"placed","file":"modules/hall-ui/public/collab.html","line":68,"string_id":"7d26333b5fb0"},
|
|
178
178
|
{"key":"L0034","section":"Your asks 2","text":"{…} and {…}","placement":"placed","file":"modules/hall-ui/public/profile-nudge.js","line":77,"string_id":"56b00d773232","occurrences":2},
|
|
179
179
|
{"key":"L0035","section":"Your asks 2","text":"{…} of {…}","placement":"placed","file":"modules/hall-ui/public/sky-panel.js","line":120,"string_id":"73c1372b43d9"},
|
|
@@ -181,15 +181,15 @@
|
|
|
181
181
|
{"key":"L0037","section":"Your asks 2","text":"You asked","placement":"unplaced"},
|
|
182
182
|
{"key":"L0038","section":"Your asks 2","text":"by name","placement":"unplaced"},
|
|
183
183
|
{"key":"L0039","section":"Your asks 2","text":"task #{…}","placement":"placed","file":"modules/hall-ui/public/copy-desk.js","line":105,"string_id":"a6e803a31a4d"},
|
|
184
|
-
{"key":"L0040","section":"Your asks 2","text":"
|
|
184
|
+
{"key":"L0040","section":"Your asks 2","text":"just asked","placement":"unplaced"},
|
|
185
185
|
{"key":"L0041","section":"Your asks 2","text":"Reply","placement":"unplaced"},
|
|
186
|
-
{"key":"L0042","section":"Your asks 2","text":"
|
|
186
|
+
{"key":"L0042","section":"Your asks 2","text":"Mark resolved","placement":"placed","file":"modules/hall-ui/public/collab.js","line":278,"string_id":"4924438fb299","occurrences":2},
|
|
187
187
|
{"key":"L0043","section":"Your asks 2","text":"Withdraw","placement":"unplaced","occurrences":2},
|
|
188
188
|
{"key":"L0044","section":"Your asks 2","text":"A","placement":"unplaced"},
|
|
189
189
|
{"key":"L0045","section":"Your asks 2","text":"I need an {…}'s read on whether a forest plate under the glass rail is worth shooting, or whether the void stands.","placement":"unplaced"},
|
|
190
190
|
{"key":"L0046","section":"Your asks 2","text":"You asked artists","placement":"unplaced"},
|
|
191
191
|
{"key":"L0047","section":"Your asks 2","text":"goal #1000081","placement":"unplaced"},
|
|
192
|
-
{"key":"L0048","section":"Your asks 2","text":"waiting
|
|
192
|
+
{"key":"L0048","section":"Your asks 2","text":"waiting 5 days","placement":"unplaced"},
|
|
193
193
|
{"key":"L0049","section":"Your asks 2","text":"5 days ago","placement":"unplaced"},
|
|
194
194
|
{"key":"L0050","section":"Your asks 2","text":"Add a reply","placement":"unplaced"},
|
|
195
195
|
{"key":"L0051","section":"All {…} asks 8","text":"All open asks","placement":"placed","file":"modules/hall-ui/public/collab.html","line":79,"string_id":"9c34bccb06ef"},
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
{"key":"L0063","section":"All {…} asks 8","text":"by name","placement":"unplaced"},
|
|
208
208
|
{"key":"L0064","section":"All {…} asks 8","text":"task #{…}","placement":"placed","file":"modules/hall-ui/public/copy-desk.js","line":105,"string_id":"a6e803a31a4d","occurrences":2},
|
|
209
209
|
{"key":"L0065","section":"All {…} asks 8","text":"asked builders","placement":"unplaced"},
|
|
210
|
-
{"key":"L0066","section":"All {…} asks 8","text":"Show all {…}","placement":"placed","file":"modules/hall-ui/public/collab.js","line":
|
|
210
|
+
{"key":"L0066","section":"All {…} asks 8","text":"Show all {…}","placement":"placed","file":"modules/hall-ui/public/collab.js","line":432,"string_id":"6dde12eb0c78"},
|
|
211
211
|
{"key":"L0067","section":"All {…} asks 8","text":"Archive","placement":"unplaced"},
|
|
212
212
|
{"key":"L0068","section":"All {…} asks 8","text":"Answering is optional and declining costs nothing. Nobody is assigned by being asked.","placement":"placed","file":"modules/hall-ui/public/collab.html","line":143,"string_id":"05761b08ec8f"}
|
|
213
213
|
]
|
|
@@ -336,7 +336,7 @@
|
|
|
336
336
|
"surface": "builders",
|
|
337
337
|
"title": "Deploy",
|
|
338
338
|
"states_read": ["work","out"],
|
|
339
|
-
"files_hash": "
|
|
339
|
+
"files_hash": "786235bca48a981a",
|
|
340
340
|
"reading_hash": "eb5acd9bd5fc73e9",
|
|
341
341
|
"counts": {"lines":7,"placed":0,"shared":5,"unplaced":2},
|
|
342
342
|
"omitted": {"data":1},
|
|
@@ -838,7 +838,7 @@
|
|
|
838
838
|
"surface": "builders",
|
|
839
839
|
"title": "Fleet costs",
|
|
840
840
|
"states_read": ["ledger","out"],
|
|
841
|
-
"files_hash": "
|
|
841
|
+
"files_hash": "23ae0ec03adc9b80",
|
|
842
842
|
"reading_hash": "9493385a64bb0e9f",
|
|
843
843
|
"counts": {"lines":22,"placed":6,"shared":5,"unplaced":11},
|
|
844
844
|
"omitted": {"data":11},
|
|
@@ -2222,7 +2222,7 @@
|
|
|
2222
2222
|
"surface": "builders",
|
|
2223
2223
|
"title": "Project settings",
|
|
2224
2224
|
"states_read": ["default"],
|
|
2225
|
-
"files_hash": "
|
|
2225
|
+
"files_hash": "f3007d41f4f59d9f",
|
|
2226
2226
|
"reading_hash": "940a00917a01cd2e",
|
|
2227
2227
|
"counts": {"lines":10,"placed":5,"shared":5,"unplaced":0},
|
|
2228
2228
|
"omitted": {"data":1},
|
|
@@ -2637,7 +2637,7 @@
|
|
|
2637
2637
|
"surface": "builders",
|
|
2638
2638
|
"title": "Studio",
|
|
2639
2639
|
"states_read": ["home","out","flow","flow-pick","status","words-offer","words-edit","reviews","full","full-back","approvals","approvals-before","approvals-phone","approvals-note","approved","sent-back"],
|
|
2640
|
-
"files_hash": "
|
|
2640
|
+
"files_hash": "85650f845556e185",
|
|
2641
2641
|
"reading_hash": "7ad6eaf33f933983",
|
|
2642
2642
|
"counts": {"lines":70,"placed":42,"shared":3,"unplaced":25},
|
|
2643
2643
|
"omitted": {"data":18},
|
|
@@ -2645,7 +2645,7 @@
|
|
|
2645
2645
|
"lines": [
|
|
2646
2646
|
{"key":"L0001","section":"skip link","text":"Skip to content","placement":"shared","file":"modules/hall-ui/public/shell.js","line":767,"string_id":"87cee78ca134"},
|
|
2647
2647
|
{"key":"L0002","section":"top bar","text":"Jump to…","placement":"shared","file":"modules/hall-ui/public/shell.js","line":487,"string_id":"eba610d25523"},
|
|
2648
|
-
{"key":"L0003","section":"top bar","text":"
|
|
2648
|
+
{"key":"L0003","section":"top bar","text":"⌘K","placement":"shared"},
|
|
2649
2649
|
{"key":"L0004","section":"page top","text":"{{worldName}} · studio","placement":"placed","file":"modules/hall-ui/public/studio.html","line":79,"string_id":"3d8611c2e79d"},
|
|
2650
2650
|
{"key":"L0005","section":"page top","text":"pages tweaked","placement":"placed","file":"modules/hall-ui/public/studio.html","line":83,"string_id":"271de72308a4"},
|
|
2651
2651
|
{"key":"L0006","section":"page top","text":"this week","placement":"placed","file":"modules/hall-ui/public/studio.html","line":85,"string_id":"130c3731ad83"},
|
|
@@ -3608,7 +3608,7 @@
|
|
|
3608
3608
|
"surface": "landing",
|
|
3609
3609
|
"title": "{{productName}} — projects",
|
|
3610
3610
|
"states_read": ["out-map","in-map","in-map-projects","in-map-profile","in-map-kbd","in-map-search","in-map-mine-on","in-table","in-table-by-builders","in-mine","in-manage","in-manage-address","in-manage-planet","in-manage-card","in-manage-kind","in-manage-invite","in-manage-invite-handoff","in-manage-app-live","in-manage-app-building","in-manage-app-failed","in-manage-app-retry","in-manage-app-offer","in-manage-app-connect","in-manage-snag","in-manage-gone","in-manage-missing","out-manage","in-new","out-new","in-new-type","in-new-about","in-new-modules","in-new-modules-search","in-new-home","in-new-address","in-new-app","in-new-app-render","in-new-review","in-new-done","in-new-resume-offer","out-map-projects","card","card-noart","card-self","in-mine-empty","card-floor","card-backdrop"],
|
|
3611
|
-
"files_hash": "
|
|
3611
|
+
"files_hash": "f6bf47d00fcd6fee",
|
|
3612
3612
|
"reading_hash": "1b2c6ce40c79925e",
|
|
3613
3613
|
"counts": {"lines":517,"placed":182,"shared":20,"unplaced":315},
|
|
3614
3614
|
"omitted": {"data":107},
|
|
@@ -233,7 +233,7 @@
|
|
|
233
233
|
paint();
|
|
234
234
|
try {
|
|
235
235
|
const url = kind === 'approved' ? writes.approve(item.page_id) : writes.sendBack(item.page_id);
|
|
236
|
-
const res = await api.request('POST', url, body);
|
|
236
|
+
const res = await api.request('POST', url, { body });
|
|
237
237
|
if (!res.ok) {
|
|
238
238
|
state.error = errWords(res.data, 'That could not be recorded. Nothing changed.');
|
|
239
239
|
return;
|
|
@@ -560,6 +560,15 @@
|
|
|
560
560
|
return !!req.needs_craft && crafts.indexOf(req.needs_craft) !== -1;
|
|
561
561
|
}
|
|
562
562
|
|
|
563
|
+
// Whether the reader may REOPEN a settled ask (task 1004458): only the person
|
|
564
|
+
// who asked, ticket style. Mirrors reopenHelpRequest's WHERE; the server stays
|
|
565
|
+
// the authority, this only decides whether to draw the button.
|
|
566
|
+
function canReopen(req, meId) {
|
|
567
|
+
if (!req || meId == null) return false;
|
|
568
|
+
if (String(req.status || '') === 'open') return false;
|
|
569
|
+
return String(req.requested_by) === String(meId);
|
|
570
|
+
}
|
|
571
|
+
|
|
563
572
|
// ---- the archive (task 1004224) -------------------------------------------
|
|
564
573
|
//
|
|
565
574
|
// HOW AN ASK ENDED, in one sentence. This is the only thing an archived row has
|
|
@@ -613,7 +622,7 @@
|
|
|
613
622
|
SKILL_COMMAND: SKILL_COMMAND, sessionStartPrompt: sessionStartPrompt,
|
|
614
623
|
queueStartPrompt: queueStartPrompt, contextSentence: contextSentence,
|
|
615
624
|
saidLabel: saidLabel, replyAuthor: replyAuthor, threadOf: threadOf,
|
|
616
|
-
canClose: canClose, validateReplyBody: validateReplyBody, MAX_REPLY: MAX_REPLY,
|
|
625
|
+
canClose: canClose, canReopen: canReopen, validateReplyBody: validateReplyBody, MAX_REPLY: MAX_REPLY,
|
|
617
626
|
outcomeLine: outcomeLine,
|
|
618
627
|
};
|
|
619
628
|
})();
|
|
@@ -158,10 +158,17 @@
|
|
|
158
158
|
// The composer is CLOSED until asked for. A textarea open on every row would
|
|
159
159
|
// turn a page you read into a page you are being asked to type on, and the
|
|
160
160
|
// blocks are condensed to three rows precisely so the page stays readable.
|
|
161
|
+
//
|
|
162
|
+
// "Post & resolve" (task 1004458) is the answer and the close in one press,
|
|
163
|
+
// drawn only for someone L.canClose says may close the ask. The asker can
|
|
164
|
+
// reopen it from their archive if the answer did not do it.
|
|
161
165
|
function threadHtml(req, key, nowMs) {
|
|
162
166
|
const thread = L.threadOf(req);
|
|
163
167
|
const id = escapeHtml(String(req.id));
|
|
164
168
|
const fid = `cb-reply-${escapeHtml(key)}-${id}`;
|
|
169
|
+
const resolve = L.canClose(req, meId, myCrafts)
|
|
170
|
+
? `<button class="btn-ghost" type="submit" data-reply-send="${id}" data-reply-resolve>Post & resolve</button>`
|
|
171
|
+
: '';
|
|
165
172
|
const list = thread.length
|
|
166
173
|
? `<ul class="cb-thread">${thread.map((r) => replyHtml(r, nowMs)).join('')}</ul>`
|
|
167
174
|
: '';
|
|
@@ -172,6 +179,7 @@
|
|
|
172
179
|
<textarea class="cb-reply-input" name="body" rows="3" maxlength="${L.MAX_REPLY}" placeholder="Write a reply…"></textarea>
|
|
173
180
|
<div class="cb-reply-actions">
|
|
174
181
|
<button class="btn-accent" type="submit" data-reply-send="${id}">Post reply</button>
|
|
182
|
+
${resolve}
|
|
175
183
|
<button class="btn-ghost" type="button" data-reply-cancel="${fid}">Cancel</button>
|
|
176
184
|
</div>
|
|
177
185
|
</form>`;
|
|
@@ -267,7 +275,7 @@
|
|
|
267
275
|
// draw it and the server's own WHERE decides whether it works — so the worst
|
|
268
276
|
// a wrong answer here can do is offer a button that refuses.
|
|
269
277
|
const close = L.canClose(req, meId, myCrafts)
|
|
270
|
-
? `<div class="ov-row__actions"><button class="btn-ghost" data-settle="answered" data-id="${escapeHtml(String(req.id))}">
|
|
278
|
+
? `<div class="ov-row__actions"><button class="btn-ghost" data-settle="answered" data-id="${escapeHtml(String(req.id))}">Mark resolved</button></div>`
|
|
271
279
|
: '';
|
|
272
280
|
return `<li class="ov-row ov-row--lead cb-row${live}" data-id="${escapeHtml(String(req.id))}"${extra ? ' data-extra hidden' : ''}>
|
|
273
281
|
${faceHtml(L.faceOf(req, 'in'))}
|
|
@@ -301,7 +309,7 @@
|
|
|
301
309
|
</div>
|
|
302
310
|
<div class="ov-row__right">
|
|
303
311
|
<div class="ov-row__actions">
|
|
304
|
-
<button class="btn-accent" data-settle="answered" data-id="${id}">
|
|
312
|
+
<button class="btn-accent" data-settle="answered" data-id="${id}">Mark resolved</button>
|
|
305
313
|
<button class="btn-ghost" data-settle="withdrawn" data-id="${id}">Withdraw</button>
|
|
306
314
|
</div>
|
|
307
315
|
</div>
|
|
@@ -338,6 +346,11 @@
|
|
|
338
346
|
// and the sentence: 'in' on the asks made OF them (the asker's face), 'out' on
|
|
339
347
|
// the asks they made (the addressee's). Exactly the pairing forMeRow and
|
|
340
348
|
// mineRow use, which is why an archived row reads like the open row it was.
|
|
349
|
+
//
|
|
350
|
+
// ONE ACTION, ON ONE SIDE (task 1004458): the asker's own archived asks carry
|
|
351
|
+
// "Reopen", ticket style — whoever closed it, the person who needed the answer
|
|
352
|
+
// has the last word. The server's WHERE holds the rule (asker only); the button
|
|
353
|
+
// is only drawn where it can work.
|
|
341
354
|
function archiveRow(dir) {
|
|
342
355
|
return (req, nowMs, extra, key) => {
|
|
343
356
|
const outcome = L.outcomeLine(req);
|
|
@@ -350,11 +363,15 @@
|
|
|
350
363
|
// holds the other facts, and the row keeps no right column at all: there
|
|
351
364
|
// is nothing on an archived row to act on.
|
|
352
365
|
const fact = outcome ? `<span class="ov-fact cb-outcome">${escapeHtml(outcome)}</span>` : '';
|
|
353
|
-
|
|
366
|
+
const id = escapeHtml(String(req.id));
|
|
367
|
+
const reopen = L.canReopen(req, meId)
|
|
368
|
+
? `<div class="ov-row__right"><div class="ov-row__actions"><button class="btn-ghost" data-reopen data-id="${id}">Reopen</button></div></div>`
|
|
369
|
+
: '';
|
|
370
|
+
return `<li class="ov-row ov-row--lead cb-row cb-row--archived" data-id="${id}"${extra ? ' data-extra hidden' : ''}>
|
|
354
371
|
${faceHtml(L.faceOf(req, dir))}
|
|
355
372
|
<div class="ov-row__main">${askHtml(req, key, true)}
|
|
356
373
|
<div class="ov-row__facts">${directionFact(req, dir)}${contextChip(req)}${fact}</div>
|
|
357
|
-
</div
|
|
374
|
+
</div>${reopen}
|
|
358
375
|
</li>`;
|
|
359
376
|
};
|
|
360
377
|
}
|
|
@@ -691,7 +708,7 @@
|
|
|
691
708
|
if (settling) return;
|
|
692
709
|
settling = true;
|
|
693
710
|
try {
|
|
694
|
-
const r = await api.request('POST', `/mingle/pairs/${id}/decide`, { decision });
|
|
711
|
+
const r = await api.request('POST', `/mingle/pairs/${id}/decide`, { body: { decision } });
|
|
695
712
|
if (!r.ok) {
|
|
696
713
|
toast(window.OTB.errMessage ? window.OTB.errMessage(r.data) : 'Could not answer that introduction.');
|
|
697
714
|
return;
|
|
@@ -709,7 +726,11 @@
|
|
|
709
726
|
// Post a reply. One in flight at a time (the `replying` latch), the same guard
|
|
710
727
|
// `settling` gives the settle actions: a double-submit on an APPEND-ONLY table
|
|
711
728
|
// writes the answer twice and there is no edit or delete to take it back with.
|
|
712
|
-
|
|
729
|
+
//
|
|
730
|
+
// `resolve` (task 1004458) closes the ask as 'answered' once the reply has
|
|
731
|
+
// landed — never before, so a refused reply cannot leave an ask closed on words
|
|
732
|
+
// nobody can read.
|
|
733
|
+
async function postReply(form, resolve) {
|
|
713
734
|
if (replying) return;
|
|
714
735
|
const id = form.getAttribute('data-reply-form');
|
|
715
736
|
const input = form.querySelector('.cb-reply-input');
|
|
@@ -725,13 +746,22 @@
|
|
|
725
746
|
const send = form.querySelector('[data-reply-send]');
|
|
726
747
|
if (send) { send.disabled = true; send.textContent = 'Posting…'; }
|
|
727
748
|
try {
|
|
728
|
-
|
|
749
|
+
// The reply's field is ALSO called `body`, so it is nested: the client's
|
|
750
|
+
// `body` option is the whole JSON payload. Passing the bare string sent a
|
|
751
|
+
// top-level JSON string, which the server's strict parser refuses as
|
|
752
|
+
// bad_json (task 1004458).
|
|
753
|
+
const r = await api.request('POST', `/help-requests/${id}/replies`, { body: { body: v.body } });
|
|
729
754
|
if (!r.ok) {
|
|
730
755
|
toast(window.OTB.errMessage ? window.OTB.errMessage(r.data) : 'Could not post that reply.');
|
|
731
756
|
return;
|
|
732
757
|
}
|
|
733
758
|
if (input) input.value = '';
|
|
734
|
-
|
|
759
|
+
if (resolve) {
|
|
760
|
+
const c = await api.helpRequests.patchHelpRequestsId({ id: Number(id), body: { status: 'answered' } });
|
|
761
|
+
toast(c.ok ? 'Replied and resolved.' : 'Replied, but could not resolve it.');
|
|
762
|
+
} else {
|
|
763
|
+
toast('Replied.');
|
|
764
|
+
}
|
|
735
765
|
await load();
|
|
736
766
|
} finally {
|
|
737
767
|
replying = false;
|
|
@@ -758,7 +788,7 @@
|
|
|
758
788
|
toast(window.OTB.errMessage ? window.OTB.errMessage(r.data) : 'Could not update that request.');
|
|
759
789
|
return;
|
|
760
790
|
}
|
|
761
|
-
toast(status === 'answered' ? '
|
|
791
|
+
toast(status === 'answered' ? 'Resolved.' : status === 'open' ? 'Reopened.' : 'Withdrawn.');
|
|
762
792
|
await load();
|
|
763
793
|
} finally {
|
|
764
794
|
settling = false;
|
|
@@ -785,6 +815,12 @@
|
|
|
785
815
|
decideMingle(Number(mingleBtn.getAttribute('data-id')), mingleBtn.getAttribute('data-mingle'));
|
|
786
816
|
return;
|
|
787
817
|
}
|
|
818
|
+
const reopenBtn = closest('[data-reopen]');
|
|
819
|
+
if (reopenBtn) {
|
|
820
|
+
ev.preventDefault();
|
|
821
|
+
settle(Number(reopenBtn.getAttribute('data-id')), 'open');
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
788
824
|
const btn = closest('[data-settle]');
|
|
789
825
|
if (!btn) return;
|
|
790
826
|
ev.preventDefault();
|
|
@@ -799,7 +835,8 @@
|
|
|
799
835
|
const form = ev.target && ev.target.closest ? ev.target.closest('[data-reply-form]') : null;
|
|
800
836
|
if (!form) return;
|
|
801
837
|
ev.preventDefault();
|
|
802
|
-
|
|
838
|
+
const resolve = !!(ev.submitter && ev.submitter.hasAttribute('data-reply-resolve'));
|
|
839
|
+
postReply(form, resolve);
|
|
803
840
|
});
|
|
804
841
|
|
|
805
842
|
load().catch(() => {
|
|
@@ -130,6 +130,178 @@
|
|
|
130
130
|
return v ? `<span class="ov-mono">${escapeHtml(String(v))}</span>` : '<span class="ps-pill ps-pill--absent">no reading</span>';
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// ---- why a move stopped, in words (task 1004450) ---------------------------
|
|
134
|
+
//
|
|
135
|
+
// The runner classifies every failed move (scripts/gds/upgrade-outcome.js) into one of
|
|
136
|
+
// four kinds, each with a fixed reason code. The page used to show only the runner's raw
|
|
137
|
+
// output, addressed to someone holding a shell. These are the same reasons in the
|
|
138
|
+
// owner's words. A code with no entry falls back to a plain "no words for this" rather
|
|
139
|
+
// than a guess — the raw output is still one click away.
|
|
140
|
+
const REASON_WORDS = {
|
|
141
|
+
registry_unreadable: 'the list of published versions could not be read',
|
|
142
|
+
not_published: 'that version is not published yet',
|
|
143
|
+
npm_install: 'installing the new core stopped part-way',
|
|
144
|
+
dirty_tree: 'the project had unsaved file changes on its machine',
|
|
145
|
+
disk_full: 'the machine ran out of disk space',
|
|
146
|
+
schema_pending: 'the project’s database was behind its code',
|
|
147
|
+
outside_channel: 'that version is outside this project’s update rule',
|
|
148
|
+
shape_not_automated: 'this kind of project is not moved from this page',
|
|
149
|
+
downgrade: 'that version is older than the one running',
|
|
150
|
+
artist_gate: 'an artist review has to pass first',
|
|
151
|
+
module_incompatible: 'a module this project uses is not ready for that core',
|
|
152
|
+
remedy_did_not_hold: 'the automatic fix was tried once and the same problem came back',
|
|
153
|
+
remedy_failed: 'the automatic fix itself did not work',
|
|
154
|
+
health: 'the project did not answer after restarting on the new core',
|
|
155
|
+
migrate: 'updating the project’s database failed',
|
|
156
|
+
restart: 'the project would not restart',
|
|
157
|
+
rollback_failed: 'the new core failed, and putting the old one back failed too',
|
|
158
|
+
no_target: 'the move did not say which version to go to',
|
|
159
|
+
bad_slug: 'the project’s name on the machine could not be used',
|
|
160
|
+
installed_unreadable: 'the version installed on the machine could not be read',
|
|
161
|
+
served_mismatch: 'the project was not serving the version its machine had installed',
|
|
162
|
+
already_on: 'the project is already on that version',
|
|
163
|
+
db_unresolvable: 'the machine could not tell which database belongs to this project',
|
|
164
|
+
db_identity: 'the project’s database did not match the one the move expected',
|
|
165
|
+
health_url_missing: 'there was no address to check the project was answering after the move',
|
|
166
|
+
core_source: 'the new core could not be found to install',
|
|
167
|
+
integrity_pin: 'the new core did not match the checksum it was published with',
|
|
168
|
+
};
|
|
169
|
+
const reasonWords = (f) => (f && REASON_WORDS[f.reason]) || 'the upgrade stopped for a reason this page has no words for yet';
|
|
170
|
+
// What the runner's one automatic fix (scripts/gds/wedge-remedy.js) did for a known snag.
|
|
171
|
+
const REMEDY_WORDS = {
|
|
172
|
+
dirty_tree: 'saved those changes',
|
|
173
|
+
disk_full: 'cleared out old backups',
|
|
174
|
+
schema_pending: 'brought the database up to date',
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// A pending move whose next try is in the FUTURE is waiting out a retry, not moving. The
|
|
178
|
+
// runner re-queues a failed move as pending with a not_before (task 1004447), so "pending"
|
|
179
|
+
// alone cannot tell the two apart — and treating a six-hour wait as a move in flight is
|
|
180
|
+
// how the page used to poll for five minutes and then say "still working".
|
|
181
|
+
function waitingOf(i) {
|
|
182
|
+
return !!(i && i.state === 'pending' && i.not_before && new Date(i.not_before).getTime() > Date.now());
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// "14:05, in about 40 min" — when a waiting move tries again, as a clock time the owner
|
|
186
|
+
// can hold the page to, and the distance to it.
|
|
187
|
+
function whenNext(iso) {
|
|
188
|
+
const t = new Date(iso);
|
|
189
|
+
const mins = Math.max(1, Math.round((t.getTime() - Date.now()) / 60000));
|
|
190
|
+
const dist = mins < 90 ? `${mins} min` : `${Math.round(mins / 60)}h`;
|
|
191
|
+
let clock = '';
|
|
192
|
+
try { clock = t.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } catch (e) { clock = ''; }
|
|
193
|
+
return clock ? `${clock}, in about ${dist}` : `in about ${dist}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// A move waiting to try again. The press it offers is the SAME move, sooner: the route
|
|
197
|
+
// brings a waiting move's retry forward rather than refusing it (enqueueCoreUpgrade).
|
|
198
|
+
function waitingHtml(p) {
|
|
199
|
+
const i = p.open_intent;
|
|
200
|
+
const f = i.failure;
|
|
201
|
+
const at = escapeHtml(whenNext(i.not_before));
|
|
202
|
+
const to = escapeHtml(String(i.target_version || ''));
|
|
203
|
+
let body;
|
|
204
|
+
if (f && f.class === 'known_wedge') {
|
|
205
|
+
const did = REMEDY_WORDS[f.reason];
|
|
206
|
+
body = `<strong>A known snag stopped the move to core ${to}:</strong> ${escapeHtml(reasonWords(f))}.
|
|
207
|
+
${did ? `It ${escapeHtml(did)} on its own, and` : 'It'} tries the move once more at ${at}.`;
|
|
208
|
+
} else if (f && f.class === 'transient') {
|
|
209
|
+
body = `<strong>Waiting to try core ${to} again at ${at}.</strong> The last try stopped because
|
|
210
|
+
${escapeHtml(reasonWords(f))} — that is usually temporary, so it tries again on its own.`;
|
|
211
|
+
} else {
|
|
212
|
+
body = `<strong>Waiting to try core ${to} again at ${at}.</strong>`;
|
|
213
|
+
}
|
|
214
|
+
return `
|
|
215
|
+
<div class="dp-settled dp-settled--waiting">
|
|
216
|
+
<p class="ov-row__sub">${body} Nothing needs doing — this page shows the result when it lands.</p>
|
|
217
|
+
<div class="ps-controls">
|
|
218
|
+
<button type="button" class="btn" id="dp-retry-${escapeHtml(String(p.instance_id))}">Try now</button>
|
|
219
|
+
</div>
|
|
220
|
+
</div>`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// A move that stopped for good, worded by what KIND of stop it was. Each kind says what
|
|
224
|
+
// happens next, because that is the question the owner is left with: is anything still
|
|
225
|
+
// being done about this, and is any of it mine to do?
|
|
226
|
+
const BLOCKERS_LINK = '<a href="/blockers">the owner’s blockers</a>';
|
|
227
|
+
function failureHtml(p, m, when, detail) {
|
|
228
|
+
const f = m.failure;
|
|
229
|
+
const cls = f && f.class;
|
|
230
|
+
const id = escapeHtml(String(p.instance_id));
|
|
231
|
+
const to = escapeHtml(String(m.target_version));
|
|
232
|
+
const still = `This project is still running core ${escapeHtml(String(p.served_version))}.`;
|
|
233
|
+
const card = (mod, head, rest, action = '') => `
|
|
234
|
+
<div class="dp-settled dp-settled--${mod}">
|
|
235
|
+
<p class="ov-row__sub"><strong>${head}</strong>${rest}</p>
|
|
236
|
+
${action}
|
|
237
|
+
${detail}
|
|
238
|
+
</div>`;
|
|
239
|
+
// A row from before the runner classified failures: the old wording, unchanged.
|
|
240
|
+
if (!cls) return card('failed', `The move to core ${to} did not land`, ` — attempted${when}. ${still}`);
|
|
241
|
+
const why = escapeHtml(reasonWords(f));
|
|
242
|
+
if (f.reason === 'rollback_failed') {
|
|
243
|
+
return card('failed', `The move to core ${to} failed, and the old core could not be put back`,
|
|
244
|
+
` — attempted${when}. This project may be down. A person needs to look; it has been filed in ${BLOCKERS_LINK}.`);
|
|
245
|
+
}
|
|
246
|
+
if (cls === 'needs_decision') {
|
|
247
|
+
let action = '';
|
|
248
|
+
if (f.reason === 'outside_channel' && p.update_channel !== 'minor') {
|
|
249
|
+
action = `<div class="ps-controls">
|
|
250
|
+
<button type="button" class="btn btn--primary" id="dp-allow-${id}">Allow new releases and move to ${to}</button>
|
|
251
|
+
</div>`;
|
|
252
|
+
}
|
|
253
|
+
const next = {
|
|
254
|
+
outside_channel: action ? ' Allowing new releases changes this project’s update rule, then makes the move.' : ' Change the update rule below, then move again.',
|
|
255
|
+
artist_gate: ' Approve the artist review, or change the artist gate in this project’s settings, then move again.',
|
|
256
|
+
downgrade: ' Nothing to do unless you meant to go back to an older core.',
|
|
257
|
+
module_incompatible: ' Turn that module off in this project’s settings and move again, or wait for a core it works with — the technical detail below names the module.',
|
|
258
|
+
shape_not_automated: ' Whoever runs this project’s machine moves its core there.',
|
|
259
|
+
}[f.reason] || '';
|
|
260
|
+
return card('decision', `Your call: the move to core ${to} is waiting on a decision`,
|
|
261
|
+
` — ${why}. Attempted${when}. ${still}${next}`, action);
|
|
262
|
+
}
|
|
263
|
+
if (cls === 'transient') {
|
|
264
|
+
const retry = String(m.target_version) === String(p.recommended_version)
|
|
265
|
+
? `<div class="ps-controls"><button type="button" class="btn" id="dp-again-${id}">Try again</button></div>` : '';
|
|
266
|
+
return card('failed', `The move to core ${to} kept stopping`,
|
|
267
|
+
` — ${why}. That is usually temporary, so it tried again on its own over several hours, then gave up${when}. ${still}`, retry);
|
|
268
|
+
}
|
|
269
|
+
// NO blockers link here, deliberately: move-escalation.js files one only for an unknown
|
|
270
|
+
// failure or a failed rollback, so pointing at the list would send the owner to look for
|
|
271
|
+
// a row that does not exist. The step is named instead.
|
|
272
|
+
if (cls === 'known_wedge') {
|
|
273
|
+
return card('failed', `The move to core ${to} hit a known snag it could not fix here`,
|
|
274
|
+
` — ${why}. Attempted${when}. ${still} The automatic fix could not be applied on this project’s
|
|
275
|
+
machine, so whoever runs that machine clears the snag; the technical detail below says what it is.
|
|
276
|
+
After that, the move can be pressed again.`);
|
|
277
|
+
}
|
|
278
|
+
return card('failed', `The move to core ${to} did not land, and nothing automatic can fix it`,
|
|
279
|
+
` — ${why}. Attempted${when}. ${still} It has been filed in ${BLOCKERS_LINK} for a person to look at.`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// The project's update rule (task 1004445): how far the runner may move its core. A
|
|
283
|
+
// RULE, never a move — saving one queues nothing and restarts nothing.
|
|
284
|
+
const RULES = [
|
|
285
|
+
['pinned', 'Stay put', 'no new versions are offered; this project stays on the core it runs'],
|
|
286
|
+
['patch', 'Fixes only', 'takes fixes within the release line it is on'],
|
|
287
|
+
['minor', 'Fixes and new releases', 'also takes the next release line when one comes out'],
|
|
288
|
+
];
|
|
289
|
+
function ruleHtml(p) {
|
|
290
|
+
const cur = RULES.find((r) => r[0] === p.update_channel);
|
|
291
|
+
if (!cur) return '';
|
|
292
|
+
const id = escapeHtml(String(p.instance_id));
|
|
293
|
+
const opts = RULES.map(([k, label]) => `<option value="${k}"${k === cur[0] ? ' selected' : ''}>${escapeHtml(label)}</option>`).join('');
|
|
294
|
+
return `
|
|
295
|
+
<div class="dp-rule">
|
|
296
|
+
<p class="ov-row__sub"><strong>Update rule: ${escapeHtml(cur[1])}</strong> — ${escapeHtml(cur[2])}.</p>
|
|
297
|
+
<div class="ps-controls">
|
|
298
|
+
<label class="dp-rule__label" for="dp-rule-sel-${id}">Change to</label>
|
|
299
|
+
<select id="dp-rule-sel-${id}" class="dp-rule__select">${opts}</select>
|
|
300
|
+
<button type="button" class="btn" id="dp-rule-save-${id}">Save rule</button>
|
|
301
|
+
</div>
|
|
302
|
+
</div>`;
|
|
303
|
+
}
|
|
304
|
+
|
|
133
305
|
// ---- the states ONE box can be in ----------------------------------------
|
|
134
306
|
|
|
135
307
|
// Something is already running. The press is not offered at all — offering a button
|
|
@@ -141,6 +313,7 @@
|
|
|
141
313
|
// reload, and neither is being watched — so it says what is true instead.
|
|
142
314
|
function busyHtml(p) {
|
|
143
315
|
const i = p.open_intent;
|
|
316
|
+
if (waitingOf(i)) return waitingHtml(p);
|
|
144
317
|
return `
|
|
145
318
|
<p class="ov-row__sub">A <strong>${escapeHtml(i.action)}</strong> is ${escapeHtml(i.state)} for this box${
|
|
146
319
|
i.target_version ? ` (to core ${escapeHtml(i.target_version)})` : ''}.
|
|
@@ -186,7 +359,7 @@
|
|
|
186
359
|
// verbatim and kept CLOSED: the owner cannot run `sudo -u`, so 30 lines of it (the
|
|
187
360
|
// dirty-tree refusal prints a whole `git status`) is not what a deploy page leads on.
|
|
188
361
|
const detail = m.error
|
|
189
|
-
? `<details class="dp-settled__detail"><summary>
|
|
362
|
+
? `<details class="dp-settled__detail"><summary>Technical detail — what the upgrade said</summary>
|
|
190
363
|
<pre class="dp-preflight__error">${escapeHtml(String(m.error))}</pre></details>`
|
|
191
364
|
: '';
|
|
192
365
|
if (m.state === 'error' && m.superseded) {
|
|
@@ -199,14 +372,7 @@
|
|
|
199
372
|
${detail}
|
|
200
373
|
</div>`;
|
|
201
374
|
}
|
|
202
|
-
if (m.state === 'error')
|
|
203
|
-
return `
|
|
204
|
-
<div class="dp-settled dp-settled--failed">
|
|
205
|
-
<p class="ov-row__sub"><strong>The move to core ${escapeHtml(String(m.target_version))} did not land</strong>
|
|
206
|
-
— attempted${when}. This project is still running core ${escapeHtml(String(p.served_version))}.</p>
|
|
207
|
-
${detail}
|
|
208
|
-
</div>`;
|
|
209
|
-
}
|
|
375
|
+
if (m.state === 'error') return failureHtml(p, m, when, detail);
|
|
210
376
|
// LANDED, BUT GITHUB DID NOT GET IT (task 1004065). The runner pushes a hosted
|
|
211
377
|
// project's new pin to its repo after the move; when it could not, the move is still
|
|
212
378
|
// done — the site runs the new core — and `warning` says why the repo was not updated.
|
|
@@ -439,6 +605,7 @@
|
|
|
439
605
|
: ''}
|
|
440
606
|
${schemaBehindHtml(p)}
|
|
441
607
|
${action}
|
|
608
|
+
${ruleHtml(p)}
|
|
442
609
|
</div></li>`;
|
|
443
610
|
}
|
|
444
611
|
|
|
@@ -463,6 +630,54 @@
|
|
|
463
630
|
if (cancel) cancel.addEventListener('click', () => { steps.set(p.instance_id, 'preview'); repaint(); });
|
|
464
631
|
const confirm = $(`dp-confirm-${id}`);
|
|
465
632
|
if (confirm) confirm.addEventListener('click', () => move(p));
|
|
633
|
+
// Task 1004450: the waiting move, sooner; a stopped temporary one, again; the one-click
|
|
634
|
+
// answer to an update-rule refusal; and the rule itself.
|
|
635
|
+
const retry = $(`dp-retry-${id}`);
|
|
636
|
+
if (retry && p.open_intent) retry.addEventListener('click', () => move(p, p.open_intent.target_version));
|
|
637
|
+
const again = $(`dp-again-${id}`);
|
|
638
|
+
if (again && p.last_move) again.addEventListener('click', () => move(p, p.last_move.target_version));
|
|
639
|
+
const allow = $(`dp-allow-${id}`);
|
|
640
|
+
if (allow && p.last_move) allow.addEventListener('click', () => allowAndMove(p, p.last_move.target_version, allow));
|
|
641
|
+
const save = $(`dp-rule-save-${id}`);
|
|
642
|
+
if (save) save.addEventListener('click', () => saveRule(p, save));
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Change the rule, then report the server's own sentence and re-read the row.
|
|
646
|
+
async function setRule(p, channel) {
|
|
647
|
+
const r = await sendJson('POST', `/provisioning/instances/${p.instance_id}/update-channel`, { channel });
|
|
648
|
+
toast(r.message || 'Update rule saved.');
|
|
649
|
+
return r;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async function saveRule(p, btn) {
|
|
653
|
+
const sel = $(`dp-rule-sel-${p.instance_id}`);
|
|
654
|
+
const channel = sel && sel.value;
|
|
655
|
+
if (!channel) return;
|
|
656
|
+
btn.disabled = true;
|
|
657
|
+
try {
|
|
658
|
+
await setRule(p, channel);
|
|
659
|
+
} catch (e) {
|
|
660
|
+
if (e && e.kind === '401') return;
|
|
661
|
+
toast(e.message || 'Could not change the update rule.');
|
|
662
|
+
btn.disabled = false;
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
await refreshOne(p);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// "Allow new releases and move": the rule first, and only if it took, the move — a move
|
|
669
|
+
// queued under the old rule would be refused the same way again.
|
|
670
|
+
async function allowAndMove(p, to, btn) {
|
|
671
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Changing the rule…'; }
|
|
672
|
+
try {
|
|
673
|
+
await setRule(p, 'minor');
|
|
674
|
+
} catch (e) {
|
|
675
|
+
if (e && e.kind === '401') return;
|
|
676
|
+
toast(e.message || 'Could not change the update rule.');
|
|
677
|
+
if (btn) { btn.disabled = false; btn.textContent = `Allow new releases and move to ${to}`; }
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
await move(p, to);
|
|
466
681
|
}
|
|
467
682
|
|
|
468
683
|
// What the page is currently showing: the ONE box this hall is about, whether that box
|
|
@@ -491,11 +706,12 @@
|
|
|
491
706
|
if (view.box) wire(view.box);
|
|
492
707
|
}
|
|
493
708
|
|
|
494
|
-
|
|
709
|
+
// `to` defaults to the version the owner was SHOWN; the retry buttons pass the move they
|
|
710
|
+
// are retrying, which is also a version on screen.
|
|
711
|
+
async function move(p, to = p.recommended_version) {
|
|
495
712
|
const btn = $(`dp-confirm-${p.instance_id}`);
|
|
496
713
|
if (btn) { btn.disabled = true; btn.textContent = 'Queueing…'; }
|
|
497
714
|
const id = String(p.instance_id);
|
|
498
|
-
const to = p.recommended_version;
|
|
499
715
|
try {
|
|
500
716
|
// `p.recommended_version` — the version this owner was SHOWN for THIS box, not
|
|
501
717
|
// whatever is newest at the moment of the click.
|
|
@@ -532,6 +748,10 @@
|
|
|
532
748
|
// that "done" would drop the row out of its in-flight state early.
|
|
533
749
|
if (!now.open_intent && now.served_version === to) { settled = true; break; }
|
|
534
750
|
if (!now.open_intent && now.last_move && now.last_move.state === 'error') { settled = true; break; }
|
|
751
|
+
// WAITING IS SETTLED TOO (task 1004450). A pending move whose next try is minutes or
|
|
752
|
+
// hours away is not in flight; the row says when it tries again, and watching it for
|
|
753
|
+
// five minutes would end in a "still working" toast about a move that is not running.
|
|
754
|
+
if (waitingOf(now.open_intent)) { settled = true; break; }
|
|
535
755
|
}
|
|
536
756
|
moving.delete(p.instance_id);
|
|
537
757
|
repaint();
|