@arcforgelabs/flow 2026.10.53 → 2026.11.2

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.
Files changed (45) hide show
  1. package/README.md +1 -0
  2. package/SECURITY.md +7 -4
  3. package/npm-shrinkwrap.json +442 -4
  4. package/package.json +8 -3
  5. package/server/agents/auth-worker.js +7 -0
  6. package/server/agents/auth.js +50 -0
  7. package/server/agents/gate.js +138 -0
  8. package/server/agents/grants.js +51 -0
  9. package/server/agents/reads.js +47 -0
  10. package/server/agents/server.js +74 -0
  11. package/server/agents/settings.js +24 -0
  12. package/server/api/invoicing.js +27 -10
  13. package/server/app.js +9 -0
  14. package/server/core/invoice-rules.js +25 -10
  15. package/server/core/job-board.js +12 -4
  16. package/server/core/ledger.js +110 -22
  17. package/server/core/line-mapping.js +9 -5
  18. package/server/core/read-before-act.js +2 -2
  19. package/server/core/reconcile.js +15 -1
  20. package/server/core/sending.js +33 -9
  21. package/server/db/migrations/0025_powerful_leopardon.sql +1 -0
  22. package/server/db/migrations/0026_bitter_sersi.sql +38 -0
  23. package/server/db/migrations/meta/0025_snapshot.json +4709 -0
  24. package/server/db/migrations/meta/0026_snapshot.json +4926 -0
  25. package/server/db/migrations/meta/_journal.json +14 -0
  26. package/server/db/schema.js +38 -1
  27. package/server/index.js +9 -1
  28. package/server/integrations/port.js +1 -0
  29. package/server/integrations/seazona/history/backfill.js +3 -1
  30. package/server/integrations/seazona/history/index.js +4 -7
  31. package/server/integrations/seazona/index.js +5 -1
  32. package/server/integrations/seazona/lanes.js +44 -27
  33. package/server/integrations/seazona/ledger.js +17 -6
  34. package/server/integrations/seazona/reconcile.js +12 -0
  35. package/server/rate-limit.js +2 -1
  36. package/server/ui/app.js +76 -64
  37. package/server/ui/flow.css +43 -8
  38. package/server/ui/invoice-rules.js +24 -3
  39. package/server/ui/invoicing.js +76 -13
  40. package/server/ui/job-board.js +12 -7
  41. package/server/ui/job-change.js +63 -15
  42. package/server/ui/jobs.js +4 -4
  43. package/server/ui/mobile.js +17 -0
  44. package/server/ui/routes.js +1 -1
  45. package/server/ui/sync-status.js +67 -0
@@ -0,0 +1,67 @@
1
+ import { h } from './kit.js';
2
+ import { applyPhoneRule } from './mobile.js';
3
+
4
+ const DAY = 86_400_000;
5
+ const words = ['Polishing', 'Glazing', 'Milling', 'Sintering'];
6
+ const date = value => value ? new Date(`${value}T00:00:00Z`).toLocaleDateString('en-AU', { day: 'numeric', month: 'short', timeZone: 'UTC' }) : '';
7
+ const duration = ms => { const seconds = Math.max(0, Math.floor(ms / 1000)); return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`; };
8
+
9
+ // Neutral, read-only view of server state. No inferred event history. The caller
10
+ // owns actions and polling; dispose stops the display clock when unmounted.
11
+ export function syncStatus() {
12
+ const word = h('span', { class: 'fx-sync__word' });
13
+ const dots = h('span', { class: 'fx-sync__dots', 'aria-hidden': 'true' }, ...[0, 1, 2].map(() => h('i')));
14
+ const result = h('span');
15
+ const status = h('span', { class: 'fx-sync__status', role: 'status', 'aria-live': 'polite' }, dots, word, result);
16
+ const clock = h('span', { class: 'fx-sync__clock', 'aria-label': 'Elapsed', 'aria-live': 'off' });
17
+ const action = h('span', { class: 'fx-sync__action' });
18
+ const rail = h('progress', { class: 'fx-sync__rail', max: 1, value: 0, 'aria-label': 'Days read' });
19
+ const range = h('span');
20
+ const eta = h('span');
21
+ const track = h('div', { class: 'fx-sync__track' }, rail, h('span', { class: 'fx-sync__range' }, range, eta));
22
+ const element = applyPhoneRule(h('div', { class: 'fx-sync' }, h('div', { class: 'fx-sync__line' }, status, clock, action), track), 'syncStatus');
23
+ let current = {};
24
+ let timer;
25
+ const reduced = matchMedia('(prefers-reduced-motion: reduce)');
26
+ const updateClock = () => {
27
+ const { phase, startedAt, endedAt, nextRunAt, count, from, through, at } = current;
28
+ const active = ['running', 'waiting'].includes(phase);
29
+ const elapsed = (active ? Date.now() : Date.parse(endedAt)) - Date.parse(startedAt);
30
+ clock.textContent = Number.isFinite(elapsed) ? duration(elapsed) : '';
31
+ // Rotating words and ticking clocks are decorative, not live announcements.
32
+ if (phase === 'running' && !count) word.textContent = reduced.matches ? 'Reading' : words[Math.floor(Math.max(0, elapsed) / 2600) % words.length] ?? 'Reading';
33
+ eta.textContent = '';
34
+ if (phase === 'waiting' && nextRunAt) {
35
+ const seconds = Math.max(0, Math.ceil((Date.parse(nextRunAt) - Date.now()) / 1000));
36
+ clock.textContent = `${clock.textContent}${clock.textContent ? ' · ' : ''}${seconds}s until retry`;
37
+ }
38
+ const completed = Date.parse(at) - Date.parse(from);
39
+ const total = Date.parse(through) - Date.parse(from) + DAY;
40
+ if (phase === 'running' && count > 0 && completed > 0 && total > completed && elapsed > 0) {
41
+ eta.textContent = `about ${Math.max(1, Math.ceil(elapsed * (total - completed) / completed / 60000))} min left`;
42
+ }
43
+ };
44
+ const draw = (state, button = null) => {
45
+ current = state;
46
+ const { phase = 'not-started', count, unit = 'jobs', from, through, at, cause } = state;
47
+ element.dataset.phase = phase;
48
+ const running = phase === 'running';
49
+ dots.hidden = !running || count > 0;
50
+ word.setAttribute('aria-hidden', String(running && !count));
51
+ status.setAttribute('aria-label', running && !count ? 'Reading' : '');
52
+ word.textContent = running ? (count > 0 ? `Reading${at ? ` ${date(at)}` : ''}` : 'Reading') : ({ waiting: 'Waiting', stopped: 'Stopped', failed: 'Failed', done: 'Done', loading: 'Checking…' }[phase] ?? 'Not started');
53
+ result.textContent = `${count != null && (count > 0 || !running) ? ` · ${count.toLocaleString('en-AU')} ${unit}` : ''}${cause ? ` · ${cause}` : ''}${phase === 'stopped' && at ? ` · ${date(at)}` : ''}`;
54
+ if (action.firstChild !== button) action.replaceChildren(...(button ? [button] : []));
55
+ const total = Date.parse(through) - Date.parse(from) + DAY;
56
+ track.hidden = !(count > 0 && total > 0);
57
+ rail.value = phase === 'done' ? 1 : Math.min(1, Math.max(0, (Date.parse(at) - Date.parse(from)) / total || 0));
58
+ range.textContent = track.hidden ? '' : `${date(from)} – ${date(through)}`;
59
+ clearInterval(timer);
60
+ updateClock();
61
+ if (['running', 'waiting'].includes(phase)) timer = setInterval(() => {
62
+ if (!element.isConnected) { clearInterval(timer); return; }
63
+ if (!document.hidden) updateClock();
64
+ }, 1000);
65
+ };
66
+ return { element, draw, dispose: () => clearInterval(timer) };
67
+ }