@bongos/core 1.21.53 → 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.
@@ -51,6 +51,14 @@
51
51
  // for the day in this browser), a row that clears away (a CSS class, then the
52
52
  // redraw), a cleared floor that lights its windows and shows a tick in the
53
53
  // drawing, and the toast after every act.
54
+ //
55
+ // THE RECORDED SIGN-OFF (task 1004579). 'record it' on a promise that needs a
56
+ // recording opens its one line on the desk row or the card: pick or drop the
57
+ // video, "tested it live" sends it (its progress painted on that line in place)
58
+ // and signs off, through GovMatterCard.recordRun and the goal page's own two
59
+ // routes. Signed off, it is answered like every other act: the row clears, the
60
+ // meter takes a pip, the toast speaks, the record gains the line. Refused, the
61
+ // line says why and offers to try again; nothing is counted.
54
62
 
55
63
  (function () {
56
64
  'use strict';
@@ -67,6 +75,7 @@
67
75
  tasks: function (goalId) { return '/api/bongos/tasks?goal_id=' + encodeURIComponent(goalId); },
68
76
  room: '/api/bongos/government/board/items',
69
77
  constitution: '/api/bongos/government/constitution',
78
+ uat: function (id) { return '/api/bongos/done-when/' + encodeURIComponent(id) + '/uat'; },
70
79
  };
71
80
  var NARROW = 900;
72
81
  var I = window.GovInside;
@@ -88,6 +97,10 @@
88
97
  // the floors you cleared, the row whose facts or chips are open
89
98
  // order: the Priorities desk's order as the viewer has moved it, per floor (task 1004560)
90
99
  var desk = { done: {}, mine: {}, cleared: {}, order: {}, open: null, ask: null, busy: false, roomAsked: false, model: null };
100
+ // the recorded sign-off (task 1004579), per matter: its file and phase
101
+ // (GovMatterCard.recordLine), the painter of its line as last drawn, and the
102
+ // checks read from GET /done-when/:id/uat (null reading, false unreadable)
103
+ var rec = { state: {}, paint: {}, checks: {} };
91
104
  var TODAY_KEY = 'gov-desk-today';
92
105
  var today = { day: '', n: 0 };
93
106
  var todayDrawn = null; // the count the meter's pips last drew: they are rebuilt only when it moves
@@ -623,7 +636,11 @@
623
636
  name.setAttribute('data-matter', r.matter);
624
637
  name.appendChild(node('span', 'desk-name__t', r.name));
625
638
  name.appendChild(node('span', 'desk-name__f', r.fact));
626
- name.addEventListener('click', function () { desk.open = desk.open === r.key ? null : r.key; desk.ask = null; draw(); focusRow(r.key, 'name'); });
639
+ name.addEventListener('click', function () {
640
+ desk.open = desk.open === r.key ? null : r.key; desk.ask = null;
641
+ if (desk.open && recorded(r)) readChecks(criterionOf(r.card));
642
+ draw(); focusRow(r.key, 'name');
643
+ });
627
644
  main.appendChild(name);
628
645
  // promises kept of total, or a vote's seats: one pip each
629
646
  if (r.pips && r.pips.length) {
@@ -660,9 +677,20 @@
660
677
  });
661
678
  main.appendChild(acts);
662
679
  row.appendChild(main);
680
+ if (recorded(r)) {
681
+ // a recording dropped anywhere on its row is taken, and its line opens
682
+ row.addEventListener('dragover', function (e) { if (e && e.preventDefault) e.preventDefault(); });
683
+ row.addEventListener('drop', function (e) {
684
+ if (e && e.preventDefault) e.preventDefault();
685
+ var f = e && e.dataTransfer && e.dataTransfer.files ? e.dataTransfer.files[0] : null;
686
+ if (f && !desk.busy) deskPick(r, f);
687
+ });
688
+ }
663
689
  if (desk.open === r.key) {
664
690
  var facts = node('div', 'desk-facts');
665
- r.facts.forEach(function (f) { facts.appendChild(node('span', 'desk-fact', f)); });
691
+ // a recorded sign-off's detail is the goal page's three checks, once read
692
+ var checks = recorded(r) && rec.checks[criterionOf(r.card)];
693
+ (checks ? window.GovMatterCard.uatChecks(checks).facts : r.facts).forEach(function (f) { facts.appendChild(node('span', 'desk-fact', f)); });
666
694
  row.appendChild(facts);
667
695
  }
668
696
  var asked = desk.ask && desk.ask.indexOf(r.key + '|') === 0 ? (r.choices || {})[desk.ask.slice(r.key.length + 1)] : null;
@@ -671,6 +699,14 @@
671
699
  if (asked) {
672
700
  var chips = node('div', 'desk-choices');
673
701
  chips.appendChild(node('span', 'desk-choices__ask', asked.ask));
702
+ if (asked.file) {
703
+ var strip = window.GovMatterCard.recordStrip(document, recState(r.matter), {
704
+ pick: function (f) { deskPick(r, f); },
705
+ send: function () { deskRecord(r); },
706
+ }, 'desk', desk.busy);
707
+ rec.paint[r.matter] = strip;
708
+ chips.appendChild(strip.node);
709
+ }
674
710
  asked.options.forEach(function (c) { chips.appendChild(chipBtn('desk-chip--choice', c.label, function () { deskAct(r, asked.for, c.input); })); });
675
711
  if (asked.field) {
676
712
  // the one typed value a desk takes: a name to invite (never a reason)
@@ -751,19 +787,83 @@
751
787
  pause: 'paused', resume: 'resumed', tier: 'new tier', deactivate: 'deactivated', give: 'gave', take: 'took back',
752
788
  check: 'checked', rot: 'set', module_on: 'turned on', module_off: 'turned off', backup: 'backed up', invite: 'invited',
753
789
  decide: 'decided', ratify: 'ratified', ratify_goal: 'made a goal of',
754
- budget: 'set the limit of', unsign: 'undid', withdraw: 'withdrew',
790
+ budget: 'set the limit of', unsign: 'undid', withdraw: 'withdrew', record: 'signed off',
755
791
  };
792
+ // ---------- the recorded sign-off (task 1004579) ----------
793
+ function recState(key) { return rec.state[key] || (rec.state[key] = { file: null, phase: 'empty', pct: 0 }); }
794
+ function recorded(r) { return !!(r && r.choices && r.choices.record && r.choices.record.file); }
795
+ function criterionOf(cv) { return cv && cv.subject && cv.subject.kind === 'criteria_uat' ? cv.subject.id : null; }
796
+ // the goal page's three checks, read once per promise for its detail
797
+ function readChecks(id) {
798
+ if (id == null || id === '' || rec.checks[id] !== undefined) return Promise.resolve();
799
+ rec.checks[id] = null;
800
+ return getJson(FEEDS.uat(id)).then(function (v) {
801
+ rec.checks[id] = v && v.checks ? v : false;
802
+ draw();
803
+ if (view.open) fillCard();
804
+ });
805
+ }
806
+ function upload(url, file, onPct) { return window.GovMatterCard.uploadXhr(window.XMLHttpRequest, url, file, onPct); }
807
+ // send it, then sign off; a phase change redraws its surface, progress only paints its line
808
+ function runRecord(key, id, redraw) {
809
+ return window.GovMatterCard.recordRun(id, recState(key), {
810
+ upload: upload, send: send,
811
+ change: function () { redraw(); },
812
+ progress: function (st) { var p = rec.paint[key]; if (p) p.paint(st); },
813
+ });
814
+ }
815
+ function focusAct(host, act) {
816
+ var b = findAct(host, act);
817
+ if (b && b.tagName && String(b.tagName).toLowerCase() === 'label' && b.children && b.children[1]) b = b.children[1];
818
+ if (b && b.focus) b.focus();
819
+ }
820
+ function rowEl(key) {
821
+ var list = deskRowsEl();
822
+ var rows = list ? list.children : [];
823
+ for (var i = 0; i < rows.length; i++) if (rows[i].getAttribute('data-row') === key) return rows[i];
824
+ return null;
825
+ }
826
+ function deskPick(r, f) {
827
+ window.GovMatterCard.recordPick(recState(r.matter), f);
828
+ desk.ask = r.key + '|record'; desk.open = null;
829
+ draw();
830
+ var at = rowEl(r.key);
831
+ focusAct(at, findAct(at, 'record-send') ? 'record-send' : 'record-pick');
832
+ }
833
+ function deskRecord(r) {
834
+ return deskAct(r, 'record', {}, function () { return runRecord(r.matter, criterionOf(r.card), draw); }).then(function (done) {
835
+ if (!done) { var at = rowEl(r.key); focusAct(at, findAct(at, 'record-send') ? 'record-send' : 'record-pick'); }
836
+ return done;
837
+ });
838
+ }
839
+ // the card's handlers for the same line, on the card's own matter
840
+ function cardRecord(cv) {
841
+ var subj = cv.row && cv.row.card ? cv.row.card : cv;
842
+ var key = String(cv.key);
843
+ var refocus = function () { focusAct($('streetCardBody'), findAct($('streetCardBody'), 'record-send') ? 'record-send' : 'record-pick'); };
844
+ return {
845
+ state: function () { return recState(key); },
846
+ painted: function (s) { rec.paint[key] = s; },
847
+ pick: function (f) { window.GovMatterCard.recordPick(recState(key), f); fillCard(); refocus(); },
848
+ send: function () {
849
+ return actOn('record', {}, function () { return runRecord(key, criterionOf(subj), function () { if (view.open) fillCard(); }); })
850
+ .then(function (done) { if (!done && view.open) refocus(); return done; });
851
+ },
852
+ };
853
+ }
756
854
  // One act from a row: checked, sent through the card's own request (the
757
855
  // server's permission checks decide), then the row clears, the floor counts
758
856
  // it, the meter takes a pip and the toast says what happened. An act that
759
857
  // records nothing (hold) clears the row for this visit and says it still waits.
760
- function deskAct(r, key, input) {
858
+ // go: the act's own sender in place of its one request (the recorded
859
+ // sign-off's upload then sign-off); its refusal is said on its own line.
860
+ function deskAct(r, key, input, go) {
761
861
  var C = window.GovMatterCard, st = view.inside;
762
862
  if (!C || !st || !r.card || desk.busy) return Promise.resolve(false);
763
863
  var refused = C.check(r.card, key, input);
764
864
  if (refused) { say(refused, true); return Promise.resolve(false); }
765
865
  var k = st.key + ':' + (view.insideModel ? view.insideModel.selFloor : 0);
766
- var req = C.request(r.card, key, input);
866
+ var req = go ? null : C.request(r.card, key, input);
767
867
  var settle = function (body) {
768
868
  var a = C.answer(r.card, key, body);
769
869
  // an act at a building a sitting decides went to it as a matter (task 1004561)
@@ -777,12 +877,13 @@
777
877
  if (a.building && a.kind) card.answer = { building: a.building, matter: r.matter, kind: a.kind, at: Date.now() };
778
878
  say(a.toast || 'Done.', true);
779
879
  };
780
- if (!req) return new Promise(function (res) { clearRow(r.key, function () { settle({}); draw(); res(true); }); });
880
+ if (!req && !go) return new Promise(function (res) { clearRow(r.key, function () { settle({}); draw(); res(true); }); });
781
881
  desk.busy = true;
782
882
  draw();
783
- return send(req).then(function (out) {
883
+ return (go ? go() : send(req)).then(function (out) {
784
884
  desk.busy = false;
785
- if (!out.ok) { say(C.refusal(out.status, out.body), true); draw(); return false; }
885
+ if (!out.ok) { if (!go) say(C.refusal(out.status, out.body), true); draw(); return false; }
886
+ if (go) delete rec.state[r.matter];
786
887
  return new Promise(function (res) {
787
888
  clearRow(r.key, function () {
788
889
  settle(out.body);
@@ -1081,12 +1182,14 @@
1081
1182
  var ctx = { room: data.room, constitution: data.constitution, now: Date.now(), goals: (data.goals || []).filter(function (g) { return g && g.status === 'open'; }), pick: card.pick };
1082
1183
  var cv = D && D.cardFor ? D.cardFor(view.open, data.docket, ctx) : C ? C.model({ docket: data.docket, key: view.open, room: data.room, constitution: data.constitution, now: Date.now() }) : null;
1083
1184
  if (!cv) { closeCard(false); return null; }
1185
+ var recordable = !!(cv.row && cv.row.choices && cv.row.choices.record && cv.row.choices.record.file);
1186
+ if (recordable) { var cid = criterionOf(cv.row.card); C.withChecks(cv, rec.checks[cid] || null, rec.checks[cid] === false); }
1084
1187
  card.cv = cv;
1085
1188
  $('streetCardWhere').textContent = cv.where;
1086
1189
  $('streetCardWhen').textContent = cv.when;
1087
1190
  $('streetCardTitle').textContent = cv.title;
1088
1191
  var pager = C.pager ? C.pager(card.page, view.open) : null;
1089
- C.render(document, $('streetCardBody'), cv, { act: actOn, openForm: openForm, ask: askFor, more: toggleMore, pick: pickQueued, page: turnPage }, { form: card.form, ask: card.ask, more: card.more, error: card.error, busy: card.busy, fresh: card.fresh, pager: pager });
1192
+ C.render(document, $('streetCardBody'), cv, { act: actOn, openForm: openForm, ask: askFor, more: toggleMore, pick: pickQueued, page: turnPage, record: recordable ? cardRecord(cv) : null }, { form: card.form, ask: card.ask, more: card.more, error: card.error, busy: card.busy, fresh: card.fresh, pager: pager });
1090
1193
  var frame = $('streetCard'); if (frame) frame.setAttribute('data-more', card.more ? 'open' : 'closed');
1091
1194
  placeCard();
1092
1195
  return cv;
@@ -1100,6 +1203,8 @@
1100
1203
  }
1101
1204
  function toggleMore() {
1102
1205
  card.more = !card.more;
1206
+ var cv = card.cv;
1207
+ if (card.more && cv && cv.row && cv.row.choices && cv.row.choices.record) readChecks(criterionOf(cv.row.card));
1103
1208
  fillCard();
1104
1209
  var mb = $('streetCardBody'), t = mb && mb.querySelector ? mb.querySelector('[data-act="more"]') : null;
1105
1210
  if (t && t.focus) t.focus();
@@ -1206,20 +1311,22 @@
1206
1311
  // answered without a request.
1207
1312
  // An act from the card goes with the desk row's own card when a row stands
1208
1313
  // behind it (its subject: which route, the council's), else the model's.
1209
- function actOn(key, input) {
1314
+ // go: the act's own sender (the recorded sign-off), whose refusal its line says
1315
+ function actOn(key, input, go) {
1210
1316
  var C = window.GovMatterCard, cv = card.cv;
1211
1317
  if (!C || !cv || card.busy) return Promise.resolve(false);
1212
1318
  var subj = cv.row && cv.row.card ? cv.row.card : cv;
1213
1319
  var refused = C.check(subj, key, input);
1214
1320
  if (refused) { card.error = refused; fillCard(); return Promise.resolve(false); }
1215
1321
  card.error = '';
1216
- var req = C.request(subj, key, input);
1217
- if (!req) return Promise.resolve(answered(subj, key, {}, false));
1322
+ var req = go ? null : C.request(subj, key, input);
1323
+ if (!req && !go) return Promise.resolve(answered(subj, key, {}, false));
1218
1324
  card.busy = true;
1219
1325
  fillCard();
1220
- return send(req).then(function (out) {
1326
+ return (go ? go() : send(req)).then(function (out) {
1221
1327
  card.busy = false;
1222
- if (!out.ok) { card.error = C.refusal(out.status, out.body); if (view.open) fillCard(); return false; }
1328
+ if (!out.ok) { if (!go) card.error = C.refusal(out.status, out.body); if (view.open) fillCard(); return false; }
1329
+ if (go) { delete rec.state[String(cv.key)]; desk.done[String(cv.key)] = true; }
1223
1330
  return answered(subj, key, out.body, true);
1224
1331
  });
1225
1332
  }
@@ -68,3 +68,18 @@ Left on purpose, not counted as differences:
68
68
  - **The city sits inside the hall's shell** (sidebar and top bar). The prototype's home page carries only the wordmark, the switch, one button and one line; standalone hall pages were retired by task 1002868, and the city page adopted the shell after its record was written.
69
69
  - **The home line never says "pieces of work stuck at Review".** The prototype adds it while Review is a vacant lot; the city draws Review as held in this project (the header of `city-draw.js`), so the clause has nothing to stand on.
70
70
  - **Open owner choices met on the walk, untouched:** the "1" on a single-item pin, the light-mode legend hint wording, the recording sign-off going to its page, failed passed-vote actions on the record, a Discord update on withdraw, and the Work categories link.
71
+
72
+ ## The recorded sign-off, from the card (task 1004579)
73
+
74
+ Owner decision 2026-10-04: "all these processes should be integrated". The recorded sign-off was the one Review row whose act still left for the goal page (the open choice both walkthroughs above left as found). It is done in place now, on the desk row and on the street card alike, through the same two routes the goal page uses (`POST /done-when/:id/uat/recording` with the raw video, then `POST /done-when/:id/uat` with the stored name). No new server path and no database change: the docket still hands the row `record`; only the page reads it differently.
75
+
76
+ - **One line, the boards' chip style.** The boards draw no upload, so the card was extended in their own words: "record it" opens the row's choice line ("the recording"), the way a reason's chips open. The line says one thing at a time: "mp4 or webm, up to 100 MB" with "pick or drop it"; then the file and its size with "pick another" and "tested it live"; then "42% sent" over a hairline bar; then "sent, signing off…". The file chip is a label wrapping the real file input, so a tap opens the chooser and the keyboard reaches it; a file dropped anywhere on the row (or on the card's line) is taken too.
77
+ - **"tested it live" is the attestation and the send.** The server needs the signer's word that they tried it on the live site when it cannot read the deploy itself; the goal page asked with a checkbox, the card asks with the same one-tap chip the backend-only sign-off already uses. The goal page's optional note is not carried: a typed note is not a chip.
78
+ - **Progress paints the line in place.** The upload goes by XMLHttpRequest (fetch cannot report an upload's progress), and each tick moves only the line's text and the bar, never a redraw of the desk. The bar stops at 99% until the server answers.
79
+ - **A refusal is one sentence on the line.** A wrong file is caught before anything is sent; the server's refusals map to short words ("Over the 100 MB cap. Record a shorter pass.", "You built this, so someone else signs it off.", "It is not on the live site yet. Sign off once it is.", "The upload broke off. Try again."). The line offers "try again" when trying again can work, "pick another" when the file was the problem, and nothing when no try can (you built it, already signed off). An upload that went through is kept, so a retry after a refused sign-off does not send the video twice. A refusal counts nothing and claims nothing in a toast.
80
+ - **Signed off, it is answered like every other act.** The row clears away, the today meter takes a pip, the desk's toast says "Signed off with the recording. The promise counts as kept." (or that the goal is achieved), and the record gains "signed off …". From the street card the card closes and the Review building rises.
81
+ - **"more" is the goal page's detail.** Opening the row's name, or "more" on the card, reads `GET /done-when/:id/uat` once and shows its three checks: the code (every linked task done), live (running, not yet, or the site cannot tell), and the sign-off (not yet, or an earlier one that lapsed). The earlier recording's video player is not drawn on the card.
82
+
83
+ Walked in the hall preview harness at 1440 and 375, light and dark, desk and card: the empty line, a picked file, progress mid-upload, a refused file, a broken upload and its retry, the checks under "more", and the cleared row with pip, toast and record line. The harness docket holds no recorded criterion and refuses writes, so the walk added one recorded matter and answered the upload and the sign-off inside the page; nothing was signed off on the live site.
84
+
85
+ Tests: `tests/hall_street_desk.mjs` (the `1004579` cases) hold the line in every phase, the routes against the goal page's and the server's own, the short refusals, the run's two steps and its retry rules, the upload with its progress, and on the page the desk row and the card end to end: the cleared row, the pip, the toast and the record line, a refused upload and a refused sign-off that keep the row and count nothing, the drop, and the checks.
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.53",
3
+ "version": "1.21.54",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.53",
9
+ "version": "1.21.54",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.53",
3
+ "version": "1.21.54",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -9086,5 +9086,15 @@
9086
9086
  "id": "1004498",
9087
9087
  "text": "Ideating mode now has its own tool. An \"Ideas\" button in the top bar opens a side panel where an ideator writes about the page they are on. \"Turn into ideas\" has an AI split the text into separate ideas, each filed under their"
9088
9088
  }
9089
+ ],
9090
+ "1.21.54": [
9091
+ {
9092
+ "id": "1004579",
9093
+ "text": "A recorded sign-off is now done from the card, in place, inside the Review building. \"record it\" no longer sends you to the goal page: it opens one line where you pick or drop the walkthrough video, tap \"tested it live\", watch"
9094
+ },
9095
+ {
9096
+ "id": "1002973",
9097
+ "text": "A single test now checks every privacy rule at once — who can see a private, hidden or not-yet-active account on every page that shows people — so a change that breaks one rule fails straight away."
9098
+ }
9089
9099
  ]
9090
9100
  }
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.53'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.54'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');