@bongos/core 1.21.52 → 1.21.54
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 +88 -73
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +7 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +135 -3
- package/docs/api-reference.md +4 -2
- package/docs/copy-inventory.md +23 -11
- package/docs/copy-registry.json +142 -13
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +202 -202
- package/modules/hall-ui/public/atlas.html +3 -3
- package/modules/hall-ui/public/blockers.html +3 -3
- package/modules/hall-ui/public/board-room.html +3 -3
- package/modules/hall-ui/public/city.html +3 -3
- package/modules/hall-ui/public/collab.html +3 -3
- package/modules/hall-ui/public/commands.html +3 -3
- package/modules/hall-ui/public/copy-desk.html +3 -3
- package/modules/hall-ui/public/craft-mode.js +7 -1
- package/modules/hall-ui/public/deploy.html +3 -3
- package/modules/hall-ui/public/diagrams.html +3 -3
- package/modules/hall-ui/public/drachmae.html +3 -3
- package/modules/hall-ui/public/fleet.html +3 -3
- package/modules/hall-ui/public/gate.html +3 -3
- package/modules/hall-ui/public/goal-map.html +3 -3
- package/modules/hall-ui/public/goals.html +3 -3
- package/modules/hall-ui/public/government.html +3 -3
- package/modules/hall-ui/public/idea.html +3 -3
- package/modules/hall-ui/public/ideas.html +3 -3
- package/modules/hall-ui/public/index.html +3 -3
- package/modules/hall-ui/public/lightbulb.js +213 -0
- package/modules/hall-ui/public/modules.html +3 -3
- package/modules/hall-ui/public/primer.html +3 -3
- package/modules/hall-ui/public/profile.html +3 -3
- package/modules/hall-ui/public/project-settings.html +3 -3
- package/modules/hall-ui/public/ranks.html +3 -3
- package/modules/hall-ui/public/roadmap.html +3 -3
- package/modules/hall-ui/public/roster.html +3 -3
- package/modules/hall-ui/public/sessions.html +3 -3
- package/modules/hall-ui/public/settings.html +3 -3
- package/modules/hall-ui/public/street-card.css +18 -0
- package/modules/hall-ui/public/street-card.js +259 -4
- package/modules/hall-ui/public/street-desk.js +11 -7
- package/modules/hall-ui/public/street.html +7 -7
- package/modules/hall-ui/public/street.js +121 -14
- package/modules/hall-ui/public/studio.html +3 -3
- package/modules/hall-ui/public/style.css +1 -0
- package/modules/hall-ui/public/task.html +3 -3
- package/modules/hall-ui/public/thinking.html +3 -3
- package/modules/hall-ui/public/tweak-editor.html +3 -3
- package/modules/hall-ui/public/watch.html +3 -3
- package/modules/hall-ui/public/work.html +3 -3
- package/modules/hall-ui/records/craft-mode.md +8 -0
- package/modules/hall-ui/records/governor-street.md +15 -0
- package/modules/page-notes/db.js +34 -0
- package/modules/page-notes/notes.js +54 -8
- package/modules/page-notes/routes/page-notes.js +49 -1
- package/modules/page-notes/split.js +39 -8
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/src/module-api.js +1 -1
- package/tests/hall_street_desk.mjs +370 -5
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/lightbulb.mjs +385 -0
- package/tests/page_notes_panel.mjs +3 -1
- package/tests/paintbrush.mjs +2 -1
- package/tests/privacy_matrix.mjs +689 -0
|
@@ -111,9 +111,9 @@
|
|
|
111
111
|
|
|
112
112
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
113
113
|
chrome exists before the page script runs. -->
|
|
114
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
115
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
116
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
114
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-lightbulb"></script>
|
|
115
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-lightbulb"></script>
|
|
116
|
+
<script src="/builders/shell.js?v=2026-10-04-lightbulb"></script>
|
|
117
117
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
118
118
|
<script src="/builders/palette.js" defer></script>
|
|
119
119
|
<!-- shared frontend toolkit (window.OTB) — loaded BEFORE sessions.js so it can pull from it. -->
|
|
@@ -435,9 +435,9 @@
|
|
|
435
435
|
|
|
436
436
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
437
437
|
chrome exists before the page scripts run. -->
|
|
438
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
439
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
440
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
438
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-lightbulb"></script>
|
|
439
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-lightbulb"></script>
|
|
440
|
+
<script src="/builders/shell.js?v=2026-10-04-lightbulb"></script>
|
|
441
441
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
442
442
|
<script src="/builders/palette.js" defer></script>
|
|
443
443
|
<!-- shared frontend toolkit (window.OTB) — loaded before consuming IIFEs. -->
|
|
@@ -82,6 +82,24 @@ body[data-page="street"] {
|
|
|
82
82
|
.mcard-chip[disabled] { opacity: .55; cursor: progress; }
|
|
83
83
|
.mcard-chips__field { flex: 1 1 140px; min-width: 0; height: 32px; box-sizing: border-box; padding: 0 10px; font-family: var(--font-body); font-size: 14px; color: var(--ink); background: var(--bg); border: 1px solid var(--street-rule-strong); border-radius: 8px; }
|
|
84
84
|
|
|
85
|
+
/* the recorded sign-off's one line (task 1004579), on the card and the desk row
|
|
86
|
+
alike: the file or the why, a hairline bar that fills while it is sent, and
|
|
87
|
+
the chips. The file input sits inside its chip, out of sight but focusable. */
|
|
88
|
+
.rec { position: relative; flex: 1 1 auto; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding-bottom: 8px; }
|
|
89
|
+
.rec__line { flex: 1 1 160px; min-width: 0; font-size: 13.5px; color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-variant-numeric: tabular-nums; }
|
|
90
|
+
.rec--busy .rec__line { color: var(--ink); }
|
|
91
|
+
.rec--error .rec__line { color: var(--street-accent-text); white-space: normal; }
|
|
92
|
+
.rec__bar { position: absolute; left: 0; right: 0; bottom: 0; height: 3px; border-radius: 2px; background: var(--street-rule); overflow: hidden; }
|
|
93
|
+
.rec__bar[hidden] { display: none; }
|
|
94
|
+
.rec__fill { display: block; width: 0; height: 100%; border-radius: 2px; background: var(--accent); transition: width 200ms linear; }
|
|
95
|
+
.rec__pick { position: relative; display: inline-flex; align-items: center; box-sizing: border-box; }
|
|
96
|
+
.rec__file { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; font-size: 0; }
|
|
97
|
+
.rec__file[disabled] { cursor: progress; }
|
|
98
|
+
.rec__pick:focus-within { outline: 2px solid var(--accent); outline-offset: 3px; }
|
|
99
|
+
.rec--over { outline: 1px dashed var(--accent); outline-offset: 4px; border-radius: 8px; }
|
|
100
|
+
.mcard-chip.rec__go { background: var(--accent); color: var(--accent-on); border-color: transparent; }
|
|
101
|
+
.desk-chip.rec__go { background: var(--ink); color: var(--bg); border-color: var(--ink); }
|
|
102
|
+
|
|
85
103
|
/* more: the detail, in place */
|
|
86
104
|
.mcard-more__toggle { margin-top: -2px; }
|
|
87
105
|
/* the pager: "1 of N" with previous and next, when a pin holds more than one matter (task 1004569) */
|
|
@@ -49,8 +49,16 @@
|
|
|
49
49
|
// the model as `row`, and render() draws row.actions and row.choices, sending
|
|
50
50
|
// each act through request() with the row's own card. "more" opens the detail
|
|
51
51
|
// in place (moreOf): the full text, who and when, and the record. Nothing on
|
|
52
|
-
// the card links to an older page to decide
|
|
53
|
-
//
|
|
52
|
+
// the card links to an older page to decide.
|
|
53
|
+
//
|
|
54
|
+
// THE RECORDED SIGN-OFF (task 1004579). A promise that needs a recording is
|
|
55
|
+
// signed off here too: 'record it' opens one line where the recording is picked
|
|
56
|
+
// or dropped, sent with its progress on that line, then signed off, through the
|
|
57
|
+
// SAME two routes the goal page uses (POST /done-when/:id/uat/recording, then
|
|
58
|
+
// POST /done-when/:id/uat). recordLine() says what the line shows in each
|
|
59
|
+
// phase, recordRun() runs the two steps, recordStrip() draws the line for the
|
|
60
|
+
// desk row and the card alike. A refused or failed upload says why on that
|
|
61
|
+
// line, in one sentence, and offers to try again.
|
|
54
62
|
|
|
55
63
|
(function (root) {
|
|
56
64
|
'use strict';
|
|
@@ -779,6 +787,10 @@
|
|
|
779
787
|
if (key === 'release') return { toast: cv && cv.source === 'gate_approvals' ? 'Released from the gate. It merges and goes live.' : 'Released. It goes live.', building: b, kind: 'rise', close: true };
|
|
780
788
|
if (key === 'fine') return { toast: cv && cv.source === 'goal_join_requests' ? 'Noted. ' + who + ' joins the team.' : 'Noted. ' + who + ' is invited in.', building: b, kind: 'rise', close: false };
|
|
781
789
|
if (key === 'refuse') return { toast: 'Noted. ' + who + ' is not let in.', building: b, kind: 'settle', close: false };
|
|
790
|
+
if (key === 'record') {
|
|
791
|
+
var goal = Array.isArray(r.achieved_goals) && r.achieved_goals.length;
|
|
792
|
+
return { toast: goal ? 'Signed off with the recording. That was the last promise: the goal is achieved.' : 'Signed off with the recording. The promise counts as kept.', building: b, kind: 'rise', close: true };
|
|
793
|
+
}
|
|
782
794
|
if (key === 'take' && cv && cv.subject && cv.subject.kind === 'core_pin') return { toast: ACT_TOASTS.take_core, building: b, kind: 'rise', close: false };
|
|
783
795
|
if (key === 'invite') return { toast: ACT_TOASTS.invite.replace('{who}', r.github_login || 'They'), building: b, kind: 'light', close: false };
|
|
784
796
|
if (ACT_TOASTS[key]) return { toast: ACT_TOASTS[key].replace('{who}', who), building: b, kind: ACT_KINDS[key] || 'light', close: false };
|
|
@@ -822,7 +834,7 @@
|
|
|
822
834
|
bad_github_login: 'That does not look like a GitHub name.',
|
|
823
835
|
backup_already_running: 'A backup is already running.',
|
|
824
836
|
intent_conflict: 'Another core run is going. Try again when it settles.',
|
|
825
|
-
criterion_needs_recording: 'This one needs a recording
|
|
837
|
+
criterion_needs_recording: 'This one needs a recording: record it here.',
|
|
826
838
|
not_proposer: 'Only the person who put it up may withdraw it.',
|
|
827
839
|
votes_cast: 'Someone has voted on it, so it is the sitting\'s to decide now.',
|
|
828
840
|
item_changed: 'Someone voted just now, so it stays with the sitting.',
|
|
@@ -835,6 +847,185 @@
|
|
|
835
847
|
return e.message && e.message !== code ? String(e.message) : 'It could not be done just now.';
|
|
836
848
|
}
|
|
837
849
|
|
|
850
|
+
// ---------- the recorded sign-off (task 1004579) ----------
|
|
851
|
+
// The goal page's own limits (criterion-uat-ui.js; the server screens again).
|
|
852
|
+
var RECORD_TYPES = ['video/mp4', 'video/webm'];
|
|
853
|
+
var RECORD_MAX = 100 * 1024 * 1024;
|
|
854
|
+
// Why an upload or a sign-off was refused, in one short sentence a person can
|
|
855
|
+
// act on. The server's own sentence stands in for a code not named here.
|
|
856
|
+
var RECORD_WHY = {
|
|
857
|
+
bad_type: 'Only an mp4 or webm video can go.',
|
|
858
|
+
empty: 'That file is empty.',
|
|
859
|
+
too_big: 'Over the 100 MB cap. Record a shorter pass.',
|
|
860
|
+
sniff_mismatch: 'That file is not the video it says it is.',
|
|
861
|
+
recording_not_found: 'The upload was lost on the way. Send it again.',
|
|
862
|
+
signer_shipped_this_work: 'You built this, so someone else signs it off.',
|
|
863
|
+
work_not_live: 'It is not on the live site yet. Sign off once it is.',
|
|
864
|
+
criterion_already_satisfied: 'Someone has already signed this off.',
|
|
865
|
+
criterion_not_awaiting_uat: 'Some of its work is still open.',
|
|
866
|
+
criterion_is_backend_only: 'This one is signed off without a recording.',
|
|
867
|
+
live_attestation_required: 'Say that you tried it on the live site.',
|
|
868
|
+
preview_readonly: 'This preview records nothing.',
|
|
869
|
+
};
|
|
870
|
+
// A refusal no new file can fix is not offered again ("try again" would only
|
|
871
|
+
// be refused the same way); a file the server refused asks for another file.
|
|
872
|
+
var RECORD_FINAL = { signer_shipped_this_work: 1, criterion_already_satisfied: 1, criterion_not_awaiting_uat: 1, criterion_is_backend_only: 1, preview_readonly: 1 };
|
|
873
|
+
var RECORD_FILE = { bad_type: 1, empty: 1, too_big: 1, sniff_mismatch: 1 };
|
|
874
|
+
function recordCheck(file) {
|
|
875
|
+
if (!file) return 'Pick the recording first.';
|
|
876
|
+
if (RECORD_TYPES.indexOf(str(file.type)) < 0) return RECORD_WHY.bad_type;
|
|
877
|
+
if (!(Number(file.size) > 0)) return RECORD_WHY.empty;
|
|
878
|
+
if (Number(file.size) > RECORD_MAX) return RECORD_WHY.too_big;
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
function firstSentence(s) {
|
|
882
|
+
var t = norm(s);
|
|
883
|
+
var hit = /^.*?[.!?](\s|$)/.exec(t);
|
|
884
|
+
return clip(hit ? hit[0] : t, 72);
|
|
885
|
+
}
|
|
886
|
+
function recordWhy(status, body) {
|
|
887
|
+
var e = body && body.error ? body.error : {};
|
|
888
|
+
var code = str(e.code || (body && body.code));
|
|
889
|
+
if (RECORD_WHY[code]) return RECORD_WHY[code];
|
|
890
|
+
if (status === 413) return RECORD_WHY.too_big;
|
|
891
|
+
if (!status) return 'The upload broke off. Try again.';
|
|
892
|
+
if (e.message && e.message !== code) return firstSentence(e.message);
|
|
893
|
+
if (status === 403) return 'Only a reviewer who did not build it may sign off.';
|
|
894
|
+
return 'It did not go through. Try again.';
|
|
895
|
+
}
|
|
896
|
+
// The two routes, the goal page's own: the raw video, then the sign-off with
|
|
897
|
+
// the stored name. live_attested is what the "tested it live" chip says.
|
|
898
|
+
function recordRoutes(id) {
|
|
899
|
+
var e = encodeURIComponent(str(id));
|
|
900
|
+
return {
|
|
901
|
+
upload: API + '/done-when/' + e + '/uat/recording',
|
|
902
|
+
signoff: function (recording) { return post('/done-when/' + e + '/uat', { kind: 'uat', recording: str(recording), live_attested: true }); },
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
function sizeWords(n) {
|
|
906
|
+
var b = Number(n) || 0;
|
|
907
|
+
if (b >= 1024 * 1024) return (Math.round(b / 1024 / 102.4) / 10) + ' MB';
|
|
908
|
+
return Math.max(1, Math.round(b / 1024)) + ' KB';
|
|
909
|
+
}
|
|
910
|
+
function fileWords(f) { return f ? clip(str(f.name) || 'the recording', 28) : ''; }
|
|
911
|
+
// The line in each phase: { text, pct (while sending), busy, error, chips }.
|
|
912
|
+
// st { file, phase: 'empty'|'picked'|'uploading'|'signing'|'failed', pct, why, retry }
|
|
913
|
+
function recordLine(state) {
|
|
914
|
+
var st = state || {};
|
|
915
|
+
var name = fileWords(st.file);
|
|
916
|
+
var pick = { key: 'pick', label: st.file ? 'pick another' : 'pick or drop it' };
|
|
917
|
+
if (st.phase === 'uploading') return { text: name + ' · ' + Math.max(0, Math.min(100, Math.floor(Number(st.pct) || 0))) + '% sent', pct: Math.floor(Number(st.pct) || 0), busy: true, error: false, chips: [] };
|
|
918
|
+
if (st.phase === 'signing') return { text: name + ' · sent, signing off…', pct: 100, busy: true, error: false, chips: [] };
|
|
919
|
+
if (st.phase === 'failed') return { text: str(st.why) || 'It did not go through.', pct: null, busy: false, error: true, chips: (st.retry && st.file ? [{ key: 'send', label: 'try again' }] : []).concat(st.final ? [] : [pick]) };
|
|
920
|
+
if (st.file) return { text: name + ' · ' + sizeWords(st.file.size), pct: null, busy: false, error: false, chips: [pick, { key: 'send', label: 'tested it live' }] };
|
|
921
|
+
return { text: 'mp4 or webm, up to 100 MB', pct: null, busy: false, error: false, chips: [pick] };
|
|
922
|
+
}
|
|
923
|
+
// A file picked or dropped: checked at once, so a wrong one says why before
|
|
924
|
+
// anything is sent.
|
|
925
|
+
function recordPick(state, file) {
|
|
926
|
+
var st = state || {};
|
|
927
|
+
st.file = file || null;
|
|
928
|
+
st.recording = null; st.pct = 0;
|
|
929
|
+
var bad = recordCheck(file);
|
|
930
|
+
if (bad) { st.phase = 'failed'; st.why = bad; st.retry = false; st.final = false; } else { st.phase = 'picked'; st.why = ''; }
|
|
931
|
+
return st;
|
|
932
|
+
}
|
|
933
|
+
// Send it, then sign off. deps { upload(url, file, onPct) → {ok,status,body},
|
|
934
|
+
// send(req) → {ok,status,body}, change(st), progress(st) }. An upload that
|
|
935
|
+
// went through is kept, so trying again after a refused sign-off does not send
|
|
936
|
+
// the video twice. Resolves to the sign-off's answer, or the refusal.
|
|
937
|
+
function recordRun(id, state, deps) {
|
|
938
|
+
var st = state || {};
|
|
939
|
+
var d = deps || {};
|
|
940
|
+
var tell = function () { if (d.change) d.change(st); };
|
|
941
|
+
var fail = function (out) {
|
|
942
|
+
var code = str(out && out.body && out.body.error && out.body.error.code);
|
|
943
|
+
// a file the server refused (or one too big for the body parser) needs another file
|
|
944
|
+
var badFile = !!RECORD_FILE[code] || (out && out.status) === 413;
|
|
945
|
+
st.phase = 'failed';
|
|
946
|
+
st.why = recordWhy(out && out.status, out && out.body);
|
|
947
|
+
st.final = !!RECORD_FINAL[code];
|
|
948
|
+
st.retry = !st.final && !badFile;
|
|
949
|
+
if (code === 'recording_not_found' || badFile) st.recording = null;
|
|
950
|
+
tell();
|
|
951
|
+
return { ok: false, status: out && out.status || 0, body: out && out.body || {} };
|
|
952
|
+
};
|
|
953
|
+
var bad = recordCheck(st.file);
|
|
954
|
+
if (bad) { st.phase = 'failed'; st.why = bad; st.retry = false; st.final = false; tell(); return Promise.resolve({ ok: false, status: 0, body: {} }); }
|
|
955
|
+
var routes = recordRoutes(id);
|
|
956
|
+
var sent;
|
|
957
|
+
if (st.recording) sent = Promise.resolve({ ok: true, status: 201, body: { recording: st.recording } });
|
|
958
|
+
else {
|
|
959
|
+
st.phase = 'uploading'; st.pct = 0; st.why = '';
|
|
960
|
+
tell();
|
|
961
|
+
sent = Promise.resolve(d.upload(routes.upload, st.file, function (p) { st.pct = p; if (d.progress) d.progress(st); }));
|
|
962
|
+
}
|
|
963
|
+
return sent.then(function (up) {
|
|
964
|
+
if (!up || !up.ok) return fail(up);
|
|
965
|
+
var name = up.body && up.body.recording;
|
|
966
|
+
if (!name) return fail({ status: up.status, body: {} });
|
|
967
|
+
st.recording = str(name);
|
|
968
|
+
st.phase = 'signing'; st.pct = 100;
|
|
969
|
+
tell();
|
|
970
|
+
return Promise.resolve(d.send(routes.signoff(st.recording))).then(function (out) {
|
|
971
|
+
if (!out || !out.ok) return fail(out);
|
|
972
|
+
st.phase = 'done';
|
|
973
|
+
return out;
|
|
974
|
+
});
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
// The raw upload with its progress (fetch cannot report an upload's progress).
|
|
978
|
+
function uploadXhr(X, url, file, onPct) {
|
|
979
|
+
return new Promise(function (res) {
|
|
980
|
+
var x;
|
|
981
|
+
try { x = new X(); } catch (e) { res({ ok: false, status: 0, body: {} }); return; }
|
|
982
|
+
x.open('POST', url, true);
|
|
983
|
+
x.withCredentials = true;
|
|
984
|
+
x.setRequestHeader('Content-Type', str(file && file.type));
|
|
985
|
+
x.setRequestHeader('Accept', 'application/json');
|
|
986
|
+
if (x.upload) x.upload.onprogress = function (e) { if (e && e.lengthComputable && e.total) onPct(Math.min(99, Math.floor(e.loaded * 100 / e.total))); };
|
|
987
|
+
x.onload = function () {
|
|
988
|
+
var b = {};
|
|
989
|
+
try { b = JSON.parse(x.responseText || '{}') || {}; } catch (e) { b = {}; }
|
|
990
|
+
res({ ok: x.status >= 200 && x.status < 300, status: x.status, body: b });
|
|
991
|
+
};
|
|
992
|
+
x.onerror = function () { res({ ok: false, status: 0, body: {} }); };
|
|
993
|
+
x.onabort = x.onerror;
|
|
994
|
+
x.send(file);
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
// What the goal page showed above its form, from GET /done-when/:id/uat:
|
|
998
|
+
// the three checks, short. { facts: up to three, for the desk row; lines: for
|
|
999
|
+
// the card's "more" }.
|
|
1000
|
+
function uatChecks(view) {
|
|
1001
|
+
var v = view || {};
|
|
1002
|
+
var c = v.checks || {};
|
|
1003
|
+
var facts = [], lines = [];
|
|
1004
|
+
facts.push(c.code ? 'code done' : 'code still open');
|
|
1005
|
+
lines.push(c.code ? 'code · every linked task is done' : 'code · some linked work is still open');
|
|
1006
|
+
var live = c.live;
|
|
1007
|
+
if (live) {
|
|
1008
|
+
if (!live.available) { facts.push('live: your word'); lines.push('live · this site cannot tell, so the sign-off says you tried it live'); }
|
|
1009
|
+
else if (live.live) { facts.push('running live'); lines.push('live · running on the live site'); }
|
|
1010
|
+
else { facts.push('not live yet'); lines.push('live · shipped but not on the live site yet'); }
|
|
1011
|
+
}
|
|
1012
|
+
var s = c.uat || null;
|
|
1013
|
+
var who = function (x) { return str(x.signed_off_by_login || 'someone') + (dayOf(x.created_at) ? ' on ' + dayOf(x.created_at) : ''); };
|
|
1014
|
+
if (s) { facts.push('signed off'); lines.push('sign-off · by ' + who(s)); }
|
|
1015
|
+
else if (c.stale_uat) { facts.push('old sign-off lapsed'); lines.push('sign-off · an earlier one by ' + who(c.stale_uat) + ' no longer counts: more work shipped after it'); }
|
|
1016
|
+
else { facts.push('not signed off yet'); lines.push('sign-off · not yet'); }
|
|
1017
|
+
return { facts: facts.slice(0, 3), lines: lines };
|
|
1018
|
+
}
|
|
1019
|
+
// The checks under the card's "more", once read (or "reading…" until then).
|
|
1020
|
+
function withChecks(cv, view, failed) {
|
|
1021
|
+
if (!cv) return cv;
|
|
1022
|
+
var mo = cv.more || { text: [], who: '', when: '', history: [] };
|
|
1023
|
+
var words = view ? uatChecks(view).lines : [failed ? 'the checks could not be read just now' : 'reading the checks…'];
|
|
1024
|
+
mo.text = mo.text.filter(function (x) { return !x.checks; }).concat(words.map(function (w, i) { return { label: i ? '' : 'the checks', words: w, checks: true }; }));
|
|
1025
|
+
cv.more = mo;
|
|
1026
|
+
return cv;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
838
1029
|
// ---------- the DOM ----------
|
|
839
1030
|
function el(doc, tag, cls, text) {
|
|
840
1031
|
var n = doc.createElement(tag);
|
|
@@ -995,7 +1186,11 @@
|
|
|
995
1186
|
var box = el(doc, 'div', 'mcard-chips');
|
|
996
1187
|
box.setAttribute('data-chips', open);
|
|
997
1188
|
box.appendChild(el(doc, 'span', 'mcard-chips__ask', ch.ask));
|
|
998
|
-
if (ch.
|
|
1189
|
+
if (ch.file && on.record) {
|
|
1190
|
+
var strip = recordStrip(doc, on.record.state(), on.record, 'card', u.busy);
|
|
1191
|
+
if (on.record.painted) on.record.painted(strip);
|
|
1192
|
+
box.appendChild(strip.node);
|
|
1193
|
+
} else if (ch.field) {
|
|
999
1194
|
var input = el(doc, 'input', 'mcard-chips__field');
|
|
1000
1195
|
input.setAttribute('id', 'mcardField');
|
|
1001
1196
|
input.setAttribute('type', 'text');
|
|
@@ -1011,6 +1206,63 @@
|
|
|
1011
1206
|
host.appendChild(box);
|
|
1012
1207
|
}
|
|
1013
1208
|
|
|
1209
|
+
// The recording's one line (task 1004579), for the desk row ('desk') and the
|
|
1210
|
+
// card ('card'): what the line says, a thin bar while it is sent, and its
|
|
1211
|
+
// chips. 'pick' is a label wrapping the file input, so a tap opens the file
|
|
1212
|
+
// chooser and the keyboard reaches it; a drop on the line takes the file too.
|
|
1213
|
+
// on { pick(file), send() }. Returns { node, paint(st) }: paint() moves the
|
|
1214
|
+
// line and the bar in place, so progress never redraws the desk.
|
|
1215
|
+
function recordStrip(doc, state, on, kind, busy) {
|
|
1216
|
+
var desk = kind === 'desk';
|
|
1217
|
+
var chipCls = desk ? 'desk-chip desk-chip--choice' : 'mcard-chip';
|
|
1218
|
+
var ln = recordLine(state);
|
|
1219
|
+
var box = el(doc, 'span', 'rec' + (ln.busy ? ' rec--busy' : '') + (ln.error ? ' rec--error' : ''));
|
|
1220
|
+
box.setAttribute('data-record', ln.error ? 'failed' : (state && state.phase) || 'empty');
|
|
1221
|
+
var line = el(doc, 'span', 'rec__line', ln.text);
|
|
1222
|
+
line.setAttribute('role', ln.error ? 'alert' : 'status');
|
|
1223
|
+
box.appendChild(line);
|
|
1224
|
+
var bar = el(doc, 'span', 'rec__bar');
|
|
1225
|
+
var fill = el(doc, 'span', 'rec__fill');
|
|
1226
|
+
bar.setAttribute('aria-hidden', 'true');
|
|
1227
|
+
bar.appendChild(fill);
|
|
1228
|
+
if (ln.pct == null) bar.hidden = true;
|
|
1229
|
+
else fill.style.setProperty('width', ln.pct + '%');
|
|
1230
|
+
box.appendChild(bar);
|
|
1231
|
+
ln.chips.forEach(function (c) {
|
|
1232
|
+
if (c.key === 'pick') {
|
|
1233
|
+
var lab = el(doc, 'label', chipCls + ' rec__pick');
|
|
1234
|
+
lab.setAttribute('data-act', 'record-pick');
|
|
1235
|
+
lab.appendChild(el(doc, 'span', '', c.label));
|
|
1236
|
+
var input = el(doc, 'input', 'rec__file');
|
|
1237
|
+
input.setAttribute('type', 'file');
|
|
1238
|
+
input.setAttribute('accept', RECORD_TYPES.join(','));
|
|
1239
|
+
input.setAttribute('aria-label', c.label + ': the recording, mp4 or webm');
|
|
1240
|
+
if (busy) input.setAttribute('disabled', '');
|
|
1241
|
+
input.addEventListener('change', function () { var f = input.files && input.files[0]; if (f) on.pick(f); });
|
|
1242
|
+
lab.appendChild(input);
|
|
1243
|
+
box.appendChild(lab);
|
|
1244
|
+
} else {
|
|
1245
|
+
box.appendChild(button(doc, chipCls + (c.key === 'send' && !ln.error ? ' rec__go' : ''), c.label, 'record-' + c.key, function () { on.send(); }, busy));
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
var stop = function (e) { if (e && e.preventDefault) e.preventDefault(); if (e && e.stopPropagation) e.stopPropagation(); };
|
|
1249
|
+
box.addEventListener('dragover', function (e) { stop(e); box.className += /\brec--over\b/.test(box.className) ? '' : ' rec--over'; });
|
|
1250
|
+
box.addEventListener('dragleave', function () { box.className = box.className.replace(/\s*rec--over\b/g, ''); });
|
|
1251
|
+
box.addEventListener('drop', function (e) {
|
|
1252
|
+
stop(e);
|
|
1253
|
+
var f = e && e.dataTransfer && e.dataTransfer.files ? e.dataTransfer.files[0] : null;
|
|
1254
|
+
if (f && !busy) on.pick(f);
|
|
1255
|
+
});
|
|
1256
|
+
return {
|
|
1257
|
+
node: box,
|
|
1258
|
+
paint: function (st) {
|
|
1259
|
+
var now = recordLine(st);
|
|
1260
|
+
line.textContent = now.text;
|
|
1261
|
+
if (now.pct != null) { bar.hidden = false; fill.style.setProperty('width', now.pct + '%'); }
|
|
1262
|
+
},
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1014
1266
|
// "more": the card grows in place with the detail (task 1004566). The toggle
|
|
1015
1267
|
// says whether it is open (aria-expanded); the queue's other pieces of work
|
|
1016
1268
|
// are listed there, each opening in this same card.
|
|
@@ -1078,6 +1330,9 @@
|
|
|
1078
1330
|
API: API, SECTIONS: SECTIONS, needsRoom: needsRoom, itemIdOf: itemIdOf, subjectKind: subjectKind,
|
|
1079
1331
|
model: model, actsCard: actsCard, moreOf: moreOf, request: request, orderRequest: orderRequest, check: check, answer: answer, refusal: refusal, render: render, pager: pager,
|
|
1080
1332
|
memberWords: memberWords, windowWords: windowWords, LINE: LINE, compact: compact,
|
|
1333
|
+
// the recorded sign-off from the card (task 1004579)
|
|
1334
|
+
RECORD_TYPES: RECORD_TYPES, RECORD_MAX: RECORD_MAX, recordCheck: recordCheck, recordWhy: recordWhy, recordRoutes: recordRoutes,
|
|
1335
|
+
recordLine: recordLine, recordPick: recordPick, recordRun: recordRun, recordStrip: recordStrip, uploadXhr: uploadXhr, uatChecks: uatChecks, withChecks: withChecks,
|
|
1081
1336
|
// the wording change, for a drafted amendment's struck and inserted text (task 1004530)
|
|
1082
1337
|
change: changeOf,
|
|
1083
1338
|
};
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
recheck: 'check again',
|
|
174
174
|
});
|
|
175
175
|
// What a row's chips ask, per act (a reason or an option is one tap, never text).
|
|
176
|
-
const ASKS = Object.freeze({ recheck: 'why', budget: 'monthly limit', unsign: 'why', decide: 'pick one', ratify_goal: 'which version', check: 'check', take_core: 'move to', rot: 'stale after', module_off: 'sure', invite: 'their GitHub name', object: 'why', force: 'why', switch_off: 'why', pause: 'why', deactivate: 'why', signoff: 'confirm', allow: 'which goal', tier: 'which tier', give: 'to whom', take: 'from whom' });
|
|
176
|
+
const ASKS = Object.freeze({ recheck: 'why', budget: 'monthly limit', unsign: 'why', decide: 'pick one', ratify_goal: 'which version', check: 'check', take_core: 'move to', rot: 'stale after', module_off: 'sure', invite: 'their GitHub name', object: 'why', force: 'why', switch_off: 'why', pause: 'why', deactivate: 'why', signoff: 'confirm', record: 'the recording', allow: 'which goal', tier: 'which tier', give: 'to whom', take: 'from whom' });
|
|
177
177
|
// How a floor counts rows nobody is waiting on (desk rows), by its desk.
|
|
178
178
|
const DESK_NOUNS = Object.freeze({
|
|
179
179
|
'people.builders': 'builders', 'people.ranks': 'ranks', 'disputes.confirmed': 'to fix',
|
|
@@ -287,7 +287,9 @@
|
|
|
287
287
|
// 1004559). An act with options asks first, with its options as one-tap chips;
|
|
288
288
|
// `pick: 'open_goals'` offers the open goals the street already reads; an act
|
|
289
289
|
// whose chips come out empty is left off rather than drawn dead. 'record'
|
|
290
|
-
//
|
|
290
|
+
// (a sign-off that needs a recording) asks for the recording in place: its
|
|
291
|
+
// choice is the file line (`file: true`), drawn by GovMatterCard.recordStrip
|
|
292
|
+
// (task 1004579), never a link to the goal's page.
|
|
291
293
|
function actedRow(acts, card, ctx) {
|
|
292
294
|
const titles = new Map((ctx.goals || []).map((g) => [str(g.id), str(g.title || 'goal ' + g.id)]));
|
|
293
295
|
// an act at a building a sitting decides is filed as its matter (task 1004561)
|
|
@@ -297,7 +299,7 @@
|
|
|
297
299
|
// 'open' leaves for an older page: a row that can act here keeps its detail
|
|
298
300
|
// in place (tap the name) and drops the link; only a row with no act of its
|
|
299
301
|
// own may link out (owner standard, task 1004562)
|
|
300
|
-
const inPlace = acts.some((a) => a && a.key !== 'open'
|
|
302
|
+
const inPlace = acts.some((a) => a && a.key !== 'open');
|
|
301
303
|
acts.forEach((a) => {
|
|
302
304
|
if (a.key === 'open' && inPlace) return;
|
|
303
305
|
// the core's 'take' is its own word ('take it'), not the rank's 'take back'
|
|
@@ -311,11 +313,13 @@
|
|
|
311
313
|
if (!options.length) return;
|
|
312
314
|
}
|
|
313
315
|
options = options.slice(0, LIMITS.chips);
|
|
314
|
-
const goes = a.key === '
|
|
316
|
+
const goes = a.key === 'open';
|
|
315
317
|
if (goes && !card.href) return;
|
|
316
318
|
const field = a.field || null;
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
+
const file = a.key === 'record';
|
|
320
|
+
const ask = options.length > 0 || !!field || file;
|
|
321
|
+
if (file) choices[a.key] = { for: a.key, ask: ASKS[word], file: true, options: [] };
|
|
322
|
+
else if (ask) choices[a.key] = field ? { for: a.key, ask: ASKS[word] || 'which', field: field, options: [] } : { for: a.key, ask: ASKS[word] || 'which', options: options };
|
|
319
323
|
actions.push({ key: a.key, label: ACT_WORDS[word], tone: actions.length ? 'quiet' : 'primary', go: goes ? card.href : null, ask: ask });
|
|
320
324
|
});
|
|
321
325
|
const first = actions.find((a) => a.ask);
|
|
@@ -568,7 +572,7 @@
|
|
|
568
572
|
if (r.actions.some((a) => a.ask) && !r.chips) bad('a reason without chips', r.name);
|
|
569
573
|
Object.keys(r.choices || {}).forEach((k) => {
|
|
570
574
|
const c = r.choices[k];
|
|
571
|
-
if ((!c.field && !c.options.length) || c.options.length > LIMITS.chips || c.options.some((x) => !x.label || x.label.length > LIMITS.chip)) bad('chips', c.options.map((x) => x.label));
|
|
575
|
+
if ((!c.field && !c.file && !c.options.length) || c.options.length > LIMITS.chips || c.options.some((x) => !x.label || x.label.length > LIMITS.chip)) bad('chips', c.options.map((x) => x.label));
|
|
572
576
|
});
|
|
573
577
|
if (r.actions.some((a) => a.ask && !(r.choices || {})[a.key])) bad('an ask without its own chips', r.name);
|
|
574
578
|
if (r.pips && r.pips.length > LIMITS.pips) bad('more than twelve pips', r.pips.length);
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
(tests/hall_tokens.mjs: no hall page hardcodes a web-font stylesheet). -->
|
|
13
13
|
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
14
14
|
<link rel="stylesheet" href="/builders/street.css?v=2026-10-04-pins1">
|
|
15
|
-
<link rel="stylesheet" href="/builders/street-card.css?v=2026-10-04-
|
|
15
|
+
<link rel="stylesheet" href="/builders/street-card.css?v=2026-10-04-record1">
|
|
16
16
|
</head>
|
|
17
17
|
<!-- THE STREET (task 1004525, goal 1000125, R03). The street view of
|
|
18
18
|
docs/design/mocks/governor-city/Street.dc.html (renderVals' street branch,
|
|
@@ -204,16 +204,16 @@
|
|
|
204
204
|
</div>
|
|
205
205
|
</div>
|
|
206
206
|
|
|
207
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
208
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
209
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
207
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-lightbulb"></script>
|
|
208
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-lightbulb"></script>
|
|
209
|
+
<script src="/builders/shell.js?v=2026-10-04-lightbulb"></script>
|
|
210
210
|
<script src="/builders/palette.js" defer></script>
|
|
211
211
|
<script src="/builders/city-draw.js?v=2026-10-04-pins1"></script>
|
|
212
212
|
<script src="/builders/street-draw.js?v=2026-10-04-pins1"></script>
|
|
213
|
-
<script src="/builders/street-card.js?v=2026-10-04-
|
|
213
|
+
<script src="/builders/street-card.js?v=2026-10-04-record1"></script>
|
|
214
214
|
<script src="/builders/street-inside.js?v=2026-10-03-desk1"></script>
|
|
215
|
-
<script src="/builders/street-desk.js?v=2026-10-
|
|
215
|
+
<script src="/builders/street-desk.js?v=2026-10-04-record1"></script>
|
|
216
216
|
<script src="/builders/street-amend.js?v=2026-10-02-amend1"></script>
|
|
217
|
-
<script src="/builders/street.js?v=2026-10-04-
|
|
217
|
+
<script src="/builders/street.js?v=2026-10-04-record1"></script>
|
|
218
218
|
</body>
|
|
219
219
|
</html>
|