@bongos/core 1.19.1005 → 1.19.1006
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 -29
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +56 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +22 -18
- package/docs/copy-registry.json +67 -21
- package/docs/module-api-changelog.md +2 -0
- package/modules/hall-ui/public/collab-lib.js +31 -0
- package/modules/hall-ui/public/collab.css +44 -0
- package/modules/hall-ui/public/collab.html +39 -2
- package/modules/hall-ui/public/collab.js +68 -4
- package/modules/lifecycle/collab-settle.js +77 -0
- package/modules/lifecycle/help-requests.js +99 -3
- package/modules/lifecycle/migrations/lifecycle_012_collab_self_settling.sql +114 -0
- package/modules/lifecycle/routes/help-requests.js +80 -0
- package/modules/lifecycle/routes/recommendations.js +19 -0
- package/modules/lifecycle/task-recommendations.js +71 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/collab_self_settling.mjs +382 -0
- package/tests/hall_collab_world.mjs +18 -3
- package/tests/ideator_full_idea_shapes_space_proof.mjs +638 -0
package/docs/copy-registry.json
CHANGED
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
"generator": "scripts/gds/copy-inventory.js",
|
|
4
4
|
"goal": 1000074,
|
|
5
5
|
"counts": {
|
|
6
|
-
"strings":
|
|
7
|
-
"unique_ids":
|
|
8
|
-
"certain":
|
|
6
|
+
"strings": 1950,
|
|
7
|
+
"unique_ids": 1687,
|
|
8
|
+
"certain": 1950,
|
|
9
9
|
"heuristic": 0,
|
|
10
|
-
"duplicate_ids":
|
|
10
|
+
"duplicate_ids": 181
|
|
11
11
|
},
|
|
12
12
|
"surfaces": {
|
|
13
13
|
"builders-hall": {
|
|
14
14
|
"at": "modules/hall-ui/public",
|
|
15
15
|
"files": 97,
|
|
16
|
-
"strings":
|
|
16
|
+
"strings": 1459
|
|
17
17
|
},
|
|
18
18
|
"status-dashboard": {
|
|
19
19
|
"at": "modules/status-ui/public",
|
|
@@ -580,6 +580,16 @@
|
|
|
580
580
|
"modules/hall-ui/public/goals-render.js:1039"
|
|
581
581
|
]
|
|
582
582
|
},
|
|
583
|
+
{
|
|
584
|
+
"id": "688cc6dd38ee",
|
|
585
|
+
"surface": "builders-hall",
|
|
586
|
+
"text": "Asked of you",
|
|
587
|
+
"occurrences": 2,
|
|
588
|
+
"at": [
|
|
589
|
+
"modules/hall-ui/public/collab.html:48",
|
|
590
|
+
"modules/hall-ui/public/collab.html:122"
|
|
591
|
+
]
|
|
592
|
+
},
|
|
583
593
|
{
|
|
584
594
|
"id": "b2656d0b2402",
|
|
585
595
|
"surface": "builders-hall",
|
|
@@ -1176,8 +1186,8 @@
|
|
|
1176
1186
|
"text": "Post reply",
|
|
1177
1187
|
"occurrences": 2,
|
|
1178
1188
|
"at": [
|
|
1179
|
-
"modules/hall-ui/public/collab.js:
|
|
1180
|
-
"modules/hall-ui/public/collab.js:
|
|
1189
|
+
"modules/hall-ui/public/collab.js:168",
|
|
1190
|
+
"modules/hall-ui/public/collab.js:738"
|
|
1181
1191
|
]
|
|
1182
1192
|
},
|
|
1183
1193
|
{
|
|
@@ -2710,7 +2720,7 @@
|
|
|
2710
2720
|
"surface": "builders-hall",
|
|
2711
2721
|
"text": "Answering is optional and declining costs nothing. Nobody is assigned by being asked.",
|
|
2712
2722
|
"file": "modules/hall-ui/public/collab.html",
|
|
2713
|
-
"line":
|
|
2723
|
+
"line": 143,
|
|
2714
2724
|
"origin": "html-text",
|
|
2715
2725
|
"confidence": "certain"
|
|
2716
2726
|
},
|
|
@@ -2813,6 +2823,15 @@
|
|
|
2813
2823
|
"origin": "js-markup",
|
|
2814
2824
|
"confidence": "certain"
|
|
2815
2825
|
},
|
|
2826
|
+
{
|
|
2827
|
+
"id": "9b02ba5e6d23",
|
|
2828
|
+
"surface": "builders-hall",
|
|
2829
|
+
"text": "Asked of others",
|
|
2830
|
+
"file": "modules/hall-ui/public/collab.html",
|
|
2831
|
+
"line": 131,
|
|
2832
|
+
"origin": "html-text",
|
|
2833
|
+
"confidence": "certain"
|
|
2834
|
+
},
|
|
2816
2835
|
{
|
|
2817
2836
|
"id": "688cc6dd38ee",
|
|
2818
2837
|
"surface": "builders-hall",
|
|
@@ -2822,6 +2841,15 @@
|
|
|
2822
2841
|
"origin": "html-text",
|
|
2823
2842
|
"confidence": "certain"
|
|
2824
2843
|
},
|
|
2844
|
+
{
|
|
2845
|
+
"id": "688cc6dd38ee",
|
|
2846
|
+
"surface": "builders-hall",
|
|
2847
|
+
"text": "Asked of you",
|
|
2848
|
+
"file": "modules/hall-ui/public/collab.html",
|
|
2849
|
+
"line": 122,
|
|
2850
|
+
"origin": "html-text",
|
|
2851
|
+
"confidence": "certain"
|
|
2852
|
+
},
|
|
2825
2853
|
{
|
|
2826
2854
|
"id": "b2656d0b2402",
|
|
2827
2855
|
"surface": "builders-hall",
|
|
@@ -3556,7 +3584,7 @@
|
|
|
3556
3584
|
"surface": "builders-hall",
|
|
3557
3585
|
"text": "Close this ask",
|
|
3558
3586
|
"file": "modules/hall-ui/public/collab.js",
|
|
3559
|
-
"line":
|
|
3587
|
+
"line": 270,
|
|
3560
3588
|
"origin": "js-markup",
|
|
3561
3589
|
"confidence": "certain"
|
|
3562
3590
|
},
|
|
@@ -3700,7 +3728,7 @@
|
|
|
3700
3728
|
"surface": "builders-hall",
|
|
3701
3729
|
"text": "Copy all {…} for Session Start",
|
|
3702
3730
|
"file": "modules/hall-ui/public/collab.js",
|
|
3703
|
-
"line":
|
|
3731
|
+
"line": 461,
|
|
3704
3732
|
"origin": "js-text-assign",
|
|
3705
3733
|
"confidence": "certain"
|
|
3706
3734
|
},
|
|
@@ -3754,7 +3782,7 @@
|
|
|
3754
3782
|
"surface": "builders-hall",
|
|
3755
3783
|
"text": "Copy for Session Start",
|
|
3756
3784
|
"file": "modules/hall-ui/public/collab.js",
|
|
3757
|
-
"line":
|
|
3785
|
+
"line": 137,
|
|
3758
3786
|
"origin": "js-markup",
|
|
3759
3787
|
"confidence": "certain"
|
|
3760
3788
|
},
|
|
@@ -4006,7 +4034,7 @@
|
|
|
4006
4034
|
"surface": "builders-hall",
|
|
4007
4035
|
"text": "Could not load requests — reload to try again.",
|
|
4008
4036
|
"file": "modules/hall-ui/public/collab.js",
|
|
4009
|
-
"line":
|
|
4037
|
+
"line": 808,
|
|
4010
4038
|
"origin": "js-text-assign",
|
|
4011
4039
|
"confidence": "certain"
|
|
4012
4040
|
},
|
|
@@ -6985,7 +7013,7 @@
|
|
|
6985
7013
|
"surface": "builders-hall",
|
|
6986
7014
|
"text": "Moot — the metrics stack was removed on the 15th, so there is nothing left to rotate.",
|
|
6987
7015
|
"file": "modules/hall-ui/public/collab.js",
|
|
6988
|
-
"line":
|
|
7016
|
+
"line": 168,
|
|
6989
7017
|
"origin": "js-markup",
|
|
6990
7018
|
"confidence": "certain"
|
|
6991
7019
|
},
|
|
@@ -7493,6 +7521,15 @@
|
|
|
7493
7521
|
"origin": "js-markup",
|
|
7494
7522
|
"confidence": "certain"
|
|
7495
7523
|
},
|
|
7524
|
+
{
|
|
7525
|
+
"id": "4016b9cf8d57",
|
|
7526
|
+
"surface": "builders-hall",
|
|
7527
|
+
"text": "None of your asks have been settled yet.",
|
|
7528
|
+
"file": "modules/hall-ui/public/collab.html",
|
|
7529
|
+
"line": 134,
|
|
7530
|
+
"origin": "html-text",
|
|
7531
|
+
"confidence": "certain"
|
|
7532
|
+
},
|
|
7496
7533
|
{
|
|
7497
7534
|
"id": "428a62539050",
|
|
7498
7535
|
"surface": "builders-hall",
|
|
@@ -7534,7 +7571,7 @@
|
|
|
7534
7571
|
"surface": "builders-hall",
|
|
7535
7572
|
"text": "Not now",
|
|
7536
7573
|
"file": "modules/hall-ui/public/collab.js",
|
|
7537
|
-
"line":
|
|
7574
|
+
"line": 651,
|
|
7538
7575
|
"origin": "js-markup",
|
|
7539
7576
|
"confidence": "certain"
|
|
7540
7577
|
},
|
|
@@ -7628,6 +7665,15 @@
|
|
|
7628
7665
|
"origin": "js-notice",
|
|
7629
7666
|
"confidence": "certain"
|
|
7630
7667
|
},
|
|
7668
|
+
{
|
|
7669
|
+
"id": "1376762e97db",
|
|
7670
|
+
"surface": "builders-hall",
|
|
7671
|
+
"text": "Nothing anyone asked of you has been settled yet.",
|
|
7672
|
+
"file": "modules/hall-ui/public/collab.html",
|
|
7673
|
+
"line": 125,
|
|
7674
|
+
"origin": "html-text",
|
|
7675
|
+
"confidence": "certain"
|
|
7676
|
+
},
|
|
7631
7677
|
{
|
|
7632
7678
|
"id": "cd652d04d616",
|
|
7633
7679
|
"surface": "builders-hall",
|
|
@@ -8236,7 +8282,7 @@
|
|
|
8236
8282
|
"surface": "builders-hall",
|
|
8237
8283
|
"text": "Post reply",
|
|
8238
8284
|
"file": "modules/hall-ui/public/collab.js",
|
|
8239
|
-
"line":
|
|
8285
|
+
"line": 168,
|
|
8240
8286
|
"origin": "js-markup",
|
|
8241
8287
|
"confidence": "certain"
|
|
8242
8288
|
},
|
|
@@ -8245,7 +8291,7 @@
|
|
|
8245
8291
|
"surface": "builders-hall",
|
|
8246
8292
|
"text": "Post reply",
|
|
8247
8293
|
"file": "modules/hall-ui/public/collab.js",
|
|
8248
|
-
"line":
|
|
8294
|
+
"line": 738,
|
|
8249
8295
|
"origin": "js-text-assign",
|
|
8250
8296
|
"confidence": "certain"
|
|
8251
8297
|
},
|
|
@@ -8623,7 +8669,7 @@
|
|
|
8623
8669
|
"surface": "builders-hall",
|
|
8624
8670
|
"text": "Read the full ask",
|
|
8625
8671
|
"file": "modules/hall-ui/public/collab.js",
|
|
8626
|
-
"line":
|
|
8672
|
+
"line": 133,
|
|
8627
8673
|
"origin": "js-markup",
|
|
8628
8674
|
"confidence": "certain"
|
|
8629
8675
|
},
|
|
@@ -8875,7 +8921,7 @@
|
|
|
8875
8921
|
"surface": "builders-hall",
|
|
8876
8922
|
"text": "Replied.",
|
|
8877
8923
|
"file": "modules/hall-ui/public/collab.js",
|
|
8878
|
-
"line":
|
|
8924
|
+
"line": 734,
|
|
8879
8925
|
"origin": "js-notice",
|
|
8880
8926
|
"confidence": "certain"
|
|
8881
8927
|
},
|
|
@@ -9244,7 +9290,7 @@
|
|
|
9244
9290
|
"surface": "builders-hall",
|
|
9245
9291
|
"text": "Say yes",
|
|
9246
9292
|
"file": "modules/hall-ui/public/collab.js",
|
|
9247
|
-
"line":
|
|
9293
|
+
"line": 651,
|
|
9248
9294
|
"origin": "js-markup",
|
|
9249
9295
|
"confidence": "certain"
|
|
9250
9296
|
},
|
|
@@ -9568,7 +9614,7 @@
|
|
|
9568
9614
|
"surface": "builders-hall",
|
|
9569
9615
|
"text": "Show all {…}",
|
|
9570
9616
|
"file": "modules/hall-ui/public/collab.js",
|
|
9571
|
-
"line":
|
|
9617
|
+
"line": 415,
|
|
9572
9618
|
"origin": "js-text-assign",
|
|
9573
9619
|
"confidence": "certain"
|
|
9574
9620
|
},
|
|
@@ -9784,7 +9830,7 @@
|
|
|
9784
9830
|
"surface": "builders-hall",
|
|
9785
9831
|
"text": "Someone helped",
|
|
9786
9832
|
"file": "modules/hall-ui/public/collab.js",
|
|
9787
|
-
"line":
|
|
9833
|
+
"line": 296,
|
|
9788
9834
|
"origin": "js-markup",
|
|
9789
9835
|
"confidence": "certain"
|
|
9790
9836
|
},
|
|
@@ -2484,5 +2484,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2484
2484
|
landed since 1.19.1003 with no explicit bump. run 36030644887. (task 1002620)
|
|
2485
2485
|
1.19.1005 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2486
2486
|
landed since 1.19.1004 with no explicit bump. run 36032559593. (task 1002620)
|
|
2487
|
+
1.19.1006 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2488
|
+
landed since 1.19.1005 with no explicit bump. run 36036374443. (task 1002620)
|
|
2487
2489
|
---------------------------------------------------------------------------
|
|
2488
2490
|
```
|
|
@@ -560,6 +560,36 @@
|
|
|
560
560
|
return !!req.needs_craft && crafts.indexOf(req.needs_craft) !== -1;
|
|
561
561
|
}
|
|
562
562
|
|
|
563
|
+
// ---- the archive (task 1004224) -------------------------------------------
|
|
564
|
+
//
|
|
565
|
+
// HOW AN ASK ENDED, in one sentence. This is the only thing an archived row has
|
|
566
|
+
// that an open one does not, and it is the whole reason the archive is worth
|
|
567
|
+
// drawing: a row that left the board without anybody watching it go has to be
|
|
568
|
+
// able to say why, or the page has silently disappeared its own history.
|
|
569
|
+
//
|
|
570
|
+
// THE AUTOMATIC CLOSE IS NAMED, NOT DISGUISED. `settled_reason` is the server's
|
|
571
|
+
// word for "the ledger did this", and a row carrying one has no person behind
|
|
572
|
+
// it — `settled_by` is null. Rendering that as "Closed" with no author would
|
|
573
|
+
// read as an anonymous human, and rendering it as if the asker had closed it
|
|
574
|
+
// would attribute an act to someone who never performed one. So it says what
|
|
575
|
+
// happened: the task shipped.
|
|
576
|
+
//
|
|
577
|
+
// PURE, and total — every row gets a sentence. A settled row whose reason and
|
|
578
|
+
// settler are both absent is a real state (settled before ADR 0330 added
|
|
579
|
+
// `settled_by`), and "Closed." is the true thing to say about it.
|
|
580
|
+
function outcomeLine(req) {
|
|
581
|
+
if (!req) return '';
|
|
582
|
+
var status = String(req.status || '');
|
|
583
|
+
if (status === 'open') return '';
|
|
584
|
+
if (req.settled_reason === 'task_shipped') {
|
|
585
|
+
var t = req.task_id == null ? '' : ' ' + req.task_id;
|
|
586
|
+
return 'Closed by itself when task' + t + ' shipped.';
|
|
587
|
+
}
|
|
588
|
+
var who = req.settled_by_name || req.settled_by_login || '';
|
|
589
|
+
if (status === 'withdrawn') return who ? 'Withdrawn by ' + who + '.' : 'Withdrawn.';
|
|
590
|
+
return who ? 'Closed by ' + who + '.' : 'Closed.';
|
|
591
|
+
}
|
|
592
|
+
|
|
563
593
|
// A reply is capped at the same length as an ask. Stated here so the page can
|
|
564
594
|
// refuse before the round trip and the counter can read from one number.
|
|
565
595
|
var MAX_REPLY = 4000;
|
|
@@ -584,5 +614,6 @@
|
|
|
584
614
|
queueStartPrompt: queueStartPrompt, contextSentence: contextSentence,
|
|
585
615
|
saidLabel: saidLabel, replyAuthor: replyAuthor, threadOf: threadOf,
|
|
586
616
|
canClose: canClose, validateReplyBody: validateReplyBody, MAX_REPLY: MAX_REPLY,
|
|
617
|
+
outcomeLine: outcomeLine,
|
|
587
618
|
};
|
|
588
619
|
})();
|
|
@@ -300,3 +300,47 @@
|
|
|
300
300
|
that ring, not a replacement for it. */
|
|
301
301
|
.cb-reply-input:focus { border-color: var(--accent-ink); }
|
|
302
302
|
.cb-reply-actions { display: flex; gap: 8px; margin: 7px 0 0; }
|
|
303
|
+
|
|
304
|
+
/* ---- the archive (task 1004224) -------------------------------------------
|
|
305
|
+
History, not a queue. The whole section is a <details> that starts closed:
|
|
306
|
+
the three open blocks are what a reader came to Collab for, and an archive
|
|
307
|
+
that opened on its own would push them up the screen on behalf of work that
|
|
308
|
+
is already finished.
|
|
309
|
+
|
|
310
|
+
It is deliberately QUIETER than the blocks above it — no faces stack in the
|
|
311
|
+
head, no age pills in the rows, no accent anywhere. DESIGN.md spends the
|
|
312
|
+
accent on what is pressable or live, and nothing in here is either. */
|
|
313
|
+
.cb-archive { margin-top: 18px; }
|
|
314
|
+
.cb-archive__summary {
|
|
315
|
+
display: flex;
|
|
316
|
+
align-items: baseline;
|
|
317
|
+
gap: 9px;
|
|
318
|
+
cursor: pointer;
|
|
319
|
+
list-style: none;
|
|
320
|
+
padding: 2px 0;
|
|
321
|
+
}
|
|
322
|
+
.cb-archive__summary::-webkit-details-marker { display: none; }
|
|
323
|
+
/* The disclosure triangle drawn as the hall draws its own, and rotated by the
|
|
324
|
+
element's own open state — no script, so it can never disagree with the
|
|
325
|
+
section it labels. */
|
|
326
|
+
.cb-archive__summary::before {
|
|
327
|
+
content: "›";
|
|
328
|
+
display: inline-block;
|
|
329
|
+
color: var(--ink-faint);
|
|
330
|
+
transition: transform 120ms ease;
|
|
331
|
+
}
|
|
332
|
+
.cb-archive[open] > .cb-archive__summary::before { transform: rotate(90deg); }
|
|
333
|
+
.cb-archive .cb-block__head { margin-bottom: 4px; }
|
|
334
|
+
|
|
335
|
+
/* An archived row carries no thread, no controls and no age, so it sits lighter
|
|
336
|
+
than the row it used to be — the same words, visibly spent.
|
|
337
|
+
|
|
338
|
+
IN INK, NEVER IN OPACITY. The copy desk's own lesson (task 1003471): a state
|
|
339
|
+
drawn as opacity dims the PANEL through the row as well as the row's words,
|
|
340
|
+
which is why that page read muddy rather than quiet. The row's title steps
|
|
341
|
+
down to the soft ink and the sub line is already there. */
|
|
342
|
+
.cb-row--archived .ov-row__title { color: var(--ink-soft); }
|
|
343
|
+
/* On the facts line beside the direction and the context chip, so it reads as
|
|
344
|
+
one more fact about the ask rather than as an action in the column where
|
|
345
|
+
actions live. */
|
|
346
|
+
.cb-outcome { color: var(--ink-faint); }
|
|
@@ -100,6 +100,43 @@
|
|
|
100
100
|
</div>
|
|
101
101
|
</section>
|
|
102
102
|
|
|
103
|
+
<!-- THE ARCHIVE (task 1004224, owner direction). Settled asks used to
|
|
104
|
+
simply vanish, which stopped being tolerable once the ledger began
|
|
105
|
+
settling them on its own: a row can now leave the board with nobody
|
|
106
|
+
watching it go. Two lists, because "what was asked of me" and "what I
|
|
107
|
+
asked of others" are different questions — a merged list would need a
|
|
108
|
+
per-row label saying which, and that is the same fact said worse.
|
|
109
|
+
|
|
110
|
+
CLOSED BY DEFAULT, and the whole section is behind one <details>.
|
|
111
|
+
This is history, not a queue: the page's three open blocks are what a
|
|
112
|
+
reader came for, and an archive that opened on its own would push
|
|
113
|
+
them up the screen on behalf of work that is finished. -->
|
|
114
|
+
<details class="scroll cb-block cb-archive" id="cb-archive">
|
|
115
|
+
<summary class="cb-archive__summary">
|
|
116
|
+
<span class="scroll__h">Archive</span>
|
|
117
|
+
<span class="cb-block__n" id="cb-archive-n"></span>
|
|
118
|
+
</summary>
|
|
119
|
+
<div class="view-zone view-zone--band cb-band">
|
|
120
|
+
<section class="cb-block" aria-labelledby="cb-arch-in-h">
|
|
121
|
+
<div class="cb-block__head">
|
|
122
|
+
<h3 class="scroll__h" id="cb-arch-in-h">Asked of you <span class="cb-block__n" id="cb-arch-in-n"></span></h3>
|
|
123
|
+
</div>
|
|
124
|
+
<ul class="ov-rows" id="cb-arch-in"></ul>
|
|
125
|
+
<div class="empty-state cb-empty" id="cb-arch-in-empty" hidden><p>Nothing anyone asked of you has been settled yet.</p></div>
|
|
126
|
+
<button class="btn-ghost cb-more" id="cb-arch-in-more" type="button" aria-expanded="false" aria-controls="cb-arch-in" hidden></button>
|
|
127
|
+
</section>
|
|
128
|
+
|
|
129
|
+
<section class="cb-block" aria-labelledby="cb-arch-out-h">
|
|
130
|
+
<div class="cb-block__head">
|
|
131
|
+
<h3 class="scroll__h" id="cb-arch-out-h">Asked of others <span class="cb-block__n" id="cb-arch-out-n"></span></h3>
|
|
132
|
+
</div>
|
|
133
|
+
<ul class="ov-rows" id="cb-arch-out"></ul>
|
|
134
|
+
<div class="empty-state cb-empty" id="cb-arch-out-empty" hidden><p>None of your asks have been settled yet.</p></div>
|
|
135
|
+
<button class="btn-ghost cb-more" id="cb-arch-out-more" type="button" aria-expanded="false" aria-controls="cb-arch-out" hidden></button>
|
|
136
|
+
</section>
|
|
137
|
+
</div>
|
|
138
|
+
</details>
|
|
139
|
+
|
|
103
140
|
<!-- The one sentence of rules, said ONCE, at the foot. Answering is
|
|
104
141
|
optional by design (ADR 0187 §3), and the page still says so; it
|
|
105
142
|
just stops saying it first. -->
|
|
@@ -114,7 +151,7 @@
|
|
|
114
151
|
<script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
|
|
115
152
|
<script src="/builders/palette.js" defer></script>
|
|
116
153
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
117
|
-
<script src="/builders/collab-lib.js?v=2026-09-
|
|
118
|
-
<script src="/builders/collab.js?v=2026-09-
|
|
154
|
+
<script src="/builders/collab-lib.js?v=2026-09-24-archive"></script>
|
|
155
|
+
<script src="/builders/collab.js?v=2026-09-24-archive"></script>
|
|
119
156
|
</body>
|
|
120
157
|
</html>
|
|
@@ -114,7 +114,13 @@
|
|
|
114
114
|
// do by hand the one thing it knows they came to do. It sits beside the door in
|
|
115
115
|
// the CLOSED state on purpose — taking the ask away does not require reading it
|
|
116
116
|
// here first.
|
|
117
|
-
|
|
117
|
+
//
|
|
118
|
+
// `spent` (task 1004224) drops the copy control on an ARCHIVED row. Copying an
|
|
119
|
+
// ask for a session start is the affordance for GOING AND DOING IT, and the
|
|
120
|
+
// one thing that is certain about an archived ask is that nobody is waiting on
|
|
121
|
+
// it. The door onto the full words stays: reading what was asked is the whole
|
|
122
|
+
// reason the archive exists.
|
|
123
|
+
function askHtml(req, key, spent) {
|
|
118
124
|
const s = L.summaryOf(req.what_is_stuck);
|
|
119
125
|
let html = `<span class="ov-row__title">${escapeHtml(s.title)}</span>`;
|
|
120
126
|
if (s.lead) html += `<p class="ov-row__sub">${escapeHtml(s.lead)}</p>`;
|
|
@@ -127,9 +133,10 @@
|
|
|
127
133
|
? `<button class="cb-ask__door" type="button" data-ask-toggle aria-expanded="false" aria-controls="${fid}">Read the full ask</button>`
|
|
128
134
|
: '';
|
|
129
135
|
const full = s.more ? `<p class="cb-ask__full" id="${fid}" hidden>${escapeHtml(s.more)}</p>` : '';
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
136
|
+
const copy = spent ? ''
|
|
137
|
+
: `<button class="cb-ask__copy" type="button" data-ask-copy="${escapeHtml(String(req.id))}">Copy for Session Start</button>`;
|
|
138
|
+
if (!door && !copy) return html + full;
|
|
139
|
+
return html + `<div class="cb-ask">${door}${copy}</div>${full}`;
|
|
133
140
|
}
|
|
134
141
|
|
|
135
142
|
// ---- the thread (task 1004216, ADR 0330) -----------------------------------
|
|
@@ -314,6 +321,59 @@
|
|
|
314
321
|
</li>`;
|
|
315
322
|
}
|
|
316
323
|
|
|
324
|
+
// ---- the archive (task 1004224) ---------------------------------------------
|
|
325
|
+
//
|
|
326
|
+
// An archived row is a settled ask, and what it owes a reader is ONE fact the
|
|
327
|
+
// open rows never carry: how it ended. L.outcomeLine writes that sentence (and
|
|
328
|
+
// names the ledger when the ledger did it, rather than leaving an automatic
|
|
329
|
+
// close looking like an anonymous person). Everything else is deliberately
|
|
330
|
+
// LESS than an open row: no thread, no close control, no copy-for-session, no
|
|
331
|
+
// age pill. Those are all affordances for acting on an ask, and an archived ask
|
|
332
|
+
// is not actionable — a reply box under a closed question is an invitation to
|
|
333
|
+
// answer something nobody is waiting for. The face, the words and the context
|
|
334
|
+
// chip stay, because that is what makes the row recognisable as the one you
|
|
335
|
+
// remember.
|
|
336
|
+
//
|
|
337
|
+
// `dir` is which side of the ask the reader is on, and it picks both the face
|
|
338
|
+
// and the sentence: 'in' on the asks made OF them (the asker's face), 'out' on
|
|
339
|
+
// the asks they made (the addressee's). Exactly the pairing forMeRow and
|
|
340
|
+
// mineRow use, which is why an archived row reads like the open row it was.
|
|
341
|
+
function archiveRow(dir) {
|
|
342
|
+
return (req, nowMs, extra, key) => {
|
|
343
|
+
const outcome = L.outcomeLine(req);
|
|
344
|
+
// THE OUTCOME SITS WITH THE FACTS, NOT IN THE RIGHT COLUMN, and that is
|
|
345
|
+
// `mineRow`'s precedent rather than a new idea (artist verdict, task
|
|
346
|
+
// 1004041). Both archive lists are half-width, so the right column here is
|
|
347
|
+
// a few centimetres wide — a sentence put in it breaks mid-clause, and
|
|
348
|
+
// "Closed by itself when task / 1004200 shipped." is two lines saying one
|
|
349
|
+
// thing. How an ask ended IS a fact about it, so it reads in the line that
|
|
350
|
+
// holds the other facts, and the row keeps no right column at all: there
|
|
351
|
+
// is nothing on an archived row to act on.
|
|
352
|
+
const fact = outcome ? `<span class="ov-fact cb-outcome">${escapeHtml(outcome)}</span>` : '';
|
|
353
|
+
return `<li class="ov-row ov-row--lead cb-row cb-row--archived" data-id="${escapeHtml(String(req.id))}"${extra ? ' data-extra hidden' : ''}>
|
|
354
|
+
${faceHtml(L.faceOf(req, dir))}
|
|
355
|
+
<div class="ov-row__main">${askHtml(req, key, true)}
|
|
356
|
+
<div class="ov-row__facts">${directionFact(req, dir)}${contextChip(req)}${fact}</div>
|
|
357
|
+
</div>
|
|
358
|
+
</li>`;
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// The archive's own load, kept OUT of the page's opening fan-out on purpose.
|
|
363
|
+
// It is behind a closed <details>, so making the three blocks a reader actually
|
|
364
|
+
// came for wait on a fourth read of history would be paying for the one thing
|
|
365
|
+
// on the page nobody is looking at. Best-effort for the same reason the mingle
|
|
366
|
+
// panel is: an archive that will not load must never take Collab down with it.
|
|
367
|
+
async function loadArchive(nowMs) {
|
|
368
|
+
const r = await api.request('GET', '/help-requests/archive');
|
|
369
|
+
const inRows = (r.ok && r.data && r.data.asked_of_you) || [];
|
|
370
|
+
const outRows = (r.ok && r.data && r.data.asked_of_others) || [];
|
|
371
|
+
paintBlock('arch-in', inRows, archiveRow('in'), nowMs, false);
|
|
372
|
+
paintBlock('arch-out', outRows, archiveRow('out'), nowMs, false);
|
|
373
|
+
const total = $('#cb-archive-n');
|
|
374
|
+
if (total) total.textContent = inRows.length + outRows.length ? String(inRows.length + outRows.length) : '';
|
|
375
|
+
}
|
|
376
|
+
|
|
317
377
|
// ---- a block: count, faces, the newest three, the door to the rest -----------
|
|
318
378
|
//
|
|
319
379
|
// `key` names the block's ids (cb-<key>, cb-<key>-n, -faces, -empty, -more).
|
|
@@ -542,6 +602,10 @@
|
|
|
542
602
|
: `With no ${axis} set, only asks addressed to you by name land in your block.`;
|
|
543
603
|
}
|
|
544
604
|
|
|
605
|
+
// The archive, after the blocks are painted. Best-effort: history that will
|
|
606
|
+
// not load must not take the board down with it.
|
|
607
|
+
try { await loadArchive(nowMs); } catch (_) { /* the archive stays as it was */ }
|
|
608
|
+
|
|
545
609
|
// The social half. Best-effort and LAST: an introductions read that fails
|
|
546
610
|
// must never take the help requests down with it — those are the page.
|
|
547
611
|
try { await loadMingle(); } catch (_) { /* the panel stays as it was */ }
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// modules/lifecycle/collab-settle.js — settle the collab tables from the ledger
|
|
2
|
+
// at most once per short window, however many list routes ask (task 1004224).
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS. `settleFromLedger` is idempotent and bounded, so calling it on
|
|
5
|
+
// every list read is CORRECT — but one Collab page load fans out across four
|
|
6
|
+
// help-request routes plus, on other surfaces, three recommendation ones, and
|
|
7
|
+
// each of those fired its own UPDATE. Three to six of them found nothing and
|
|
8
|
+
// wrote nothing, which is wasted write volume on a read path and, with many
|
|
9
|
+
// builders opening Collab at once, avoidable row-lock contention on the same
|
|
10
|
+
// still-open rows. The settles were right; their FREQUENCY was not.
|
|
11
|
+
//
|
|
12
|
+
// A TIME WINDOW, NOT A PER-REQUEST MEMO. The obvious fix — settle once per
|
|
13
|
+
// request — does not help, because the page's four reads are four separate HTTP
|
|
14
|
+
// requests and there is no request to share. What they do share is a few hundred
|
|
15
|
+
// milliseconds, so this collapses a burst into one write.
|
|
16
|
+
//
|
|
17
|
+
// THE WINDOW IS SHORT ON PURPOSE. It is a burst collapser, not a cache: the
|
|
18
|
+
// latency it can add is bounded by WINDOW_MS, so a task that ships now is
|
|
19
|
+
// reflected on the next load a second later rather than on some poller's
|
|
20
|
+
// schedule. Making it minutes would turn a correctness property into a staleness
|
|
21
|
+
// budget nobody asked for.
|
|
22
|
+
//
|
|
23
|
+
// FAILURE DOES NOT POISON THE WINDOW. A settle that throws clears its own
|
|
24
|
+
// timestamp, so the next read retries instead of waiting out a window on the
|
|
25
|
+
// strength of an attempt that did nothing. The inverse — marking the window on
|
|
26
|
+
// entry and never clearing — would turn one transient database error into a
|
|
27
|
+
// silent stall.
|
|
28
|
+
//
|
|
29
|
+
// PER PROCESS, and that is fine. Several web processes each settling once per
|
|
30
|
+
// window is still an order of magnitude fewer writes than every route settling
|
|
31
|
+
// on every read, and the UPDATE is idempotent, so a second process racing the
|
|
32
|
+
// first simply finds nothing to do.
|
|
33
|
+
|
|
34
|
+
'use strict';
|
|
35
|
+
|
|
36
|
+
const WINDOW_MS = 1000;
|
|
37
|
+
|
|
38
|
+
const lastRun = new Map();
|
|
39
|
+
const inFlight = new Map();
|
|
40
|
+
|
|
41
|
+
// `key` names the table being settled, so the two collab tables do not share a
|
|
42
|
+
// window — a Collab page load settles help requests, and a surface that reads
|
|
43
|
+
// recommendations must not be told the work is already done because somebody
|
|
44
|
+
// opened Collab a moment ago.
|
|
45
|
+
//
|
|
46
|
+
// A settle already in flight is AWAITED rather than skipped: two concurrent
|
|
47
|
+
// requests should both see the settled rows, and returning early from the second
|
|
48
|
+
// would hand it the pre-settle state while claiming the settle had happened.
|
|
49
|
+
function throttled(key, run) {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
const pending = inFlight.get(key);
|
|
52
|
+
if (pending) return pending;
|
|
53
|
+
const last = lastRun.get(key);
|
|
54
|
+
if (last != null && now - last < WINDOW_MS) return Promise.resolve(null);
|
|
55
|
+
|
|
56
|
+
lastRun.set(key, now);
|
|
57
|
+
const p = Promise.resolve()
|
|
58
|
+
.then(run)
|
|
59
|
+
.catch((err) => {
|
|
60
|
+
// Let the next read try again rather than waiting out a window on the
|
|
61
|
+
// strength of an attempt that wrote nothing.
|
|
62
|
+
lastRun.delete(key);
|
|
63
|
+
throw err;
|
|
64
|
+
})
|
|
65
|
+
.finally(() => { inFlight.delete(key); });
|
|
66
|
+
inFlight.set(key, p);
|
|
67
|
+
return p;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Test seam. The window is real time, so a test that wanted to observe two
|
|
71
|
+
// settles would otherwise have to sleep.
|
|
72
|
+
function _reset() {
|
|
73
|
+
lastRun.clear();
|
|
74
|
+
inFlight.clear();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { WINDOW_MS, throttled, _reset };
|