create-pmos 0.130.0 → 0.132.0

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 (51) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/c3-one-home-test.js +4 -2
  7. package/workspace/c3-work-test.js +200 -0
  8. package/workspace/fixtures/one-home-baseline.json +0 -91
  9. package/workspace/guide/console-calendar.md +65 -0
  10. package/workspace/guide/data-freshness.md +40 -0
  11. package/workspace/guide/index.md +2 -0
  12. package/workspace/guide/my-work.md +76 -1
  13. package/workspace/lib/calendar-cli.js +84 -0
  14. package/workspace/lib/calendar-layers.js +372 -0
  15. package/workspace/lib/facts-async.js +144 -35
  16. package/workspace/lib/freshness.js +167 -0
  17. package/workspace/lib/offloop.js +94 -0
  18. package/workspace/lib/pm-items.js +71 -6
  19. package/workspace/lib/pmos-verbs.js +3 -0
  20. package/workspace/lib/report-needs.js +1 -1
  21. package/workspace/lib/route-modules.js +1 -0
  22. package/workspace/lib/routes-board.js +3 -1
  23. package/workspace/lib/routes-calendar-views.js +112 -3
  24. package/workspace/lib/routes-my-work.js +27 -9
  25. package/workspace/lib/routes-pm-items.js +123 -13
  26. package/workspace/lib/routes-sources.js +64 -0
  27. package/workspace/lib/work-focus.js +106 -0
  28. package/workspace/package-lock.json +2 -2
  29. package/workspace/package.json +1 -1
  30. package/workspace/public/pm/c3-calendar.js +392 -0
  31. package/workspace/public/pm/c3-demo-cal4.js +114 -0
  32. package/workspace/public/pm/c3-demo-data.js +79 -77
  33. package/workspace/public/pm/c3-demo-misc2.js +4 -16
  34. package/workspace/public/pm/c3-demo-work-ix.js +19 -67
  35. package/workspace/public/pm/c3-demo.js +10 -3
  36. package/workspace/public/pm/c3-shell.js +5 -2
  37. package/workspace/public/pm/calendar-views.js +10 -3
  38. package/workspace/public/pm/components.css +34 -0
  39. package/workspace/public/pm/console3.css +155 -0
  40. package/workspace/public/pm/dest-settings.js +10 -1
  41. package/workspace/public/pm/freshness-line.js +123 -0
  42. package/workspace/public/pm/pm-work-view.js +364 -172
  43. package/workspace/public/pm/work-place.js +58 -0
  44. package/workspace/server.js +39 -2
  45. package/workspace/skills/calendar-sync.skill +30 -0
  46. package/workspace/test-files.json +2 -0
  47. package/workspace/test-lists/calendar-layers.json +5 -0
  48. package/workspace/test-lists/calendar-time-page.json +5 -0
  49. package/workspace/test-lists/console3-work-page.json +9 -0
  50. package/workspace/test-lists/workspace-data-freshness.json +6 -0
  51. package/workspace/public/pm/c3-demo-work2.js +0 -13
@@ -5,7 +5,8 @@
5
5
  *
6
6
  * Fields the real routes do not send and the views draw only when present (demo-only, additive): on a briefing card `sub`, `kpis`,
7
7
  * `factsX`, `pov`, `hero.rich`, `move.suggested.conf`; on the briefing `needs`; on a work item `mock_row` (which switches on `kind`, `time`, `mins`, `src`, `init`,
8
- * `agent_move`, `left`); on a calendar event `mock_routine`, `mock_hot`, `mock_decide_by`; on the forecast `mock_status_w`; on the calendar response `free_gaps`. */
8
+ * `agent_move`, `left`) and `resolves_title`; on a calendar event `mock_routine`, `mock_hot`, `mock_decide_by`; on the forecast `mock_status_w`; on the calendar response `free_gaps`. */
9
+ import { buildWork, arrives } from './work-place.js';
9
10
 
10
11
  export const NOW_ISO = '2026-10-08T08:12:00';
11
12
  export const TODAY = '2026-10-08';
@@ -118,48 +119,36 @@ const CAL_DN = card('dn', 'Just in', 'Marta confirmed 3 slots and Priya replied'
118
119
  { kind: 'rows', rows: [['mail', 'Marta: 3 interview slots confirmed', 'ok', '08:05'], ['user', 'Priya: replied on the numbers', 'ok', '08:10']] },
119
120
  { kpis: [['2', 'new', C.acc], ['<2 min', 'each', C.ok], ['88%', 'confidence', C.ok]], factsX: [['bolt', '2 new'], ['clock', 'Both take under 2 minutes']] });
120
121
 
121
- /* Work tab (the mockup's workCards, Triage) */
122
- const WORK_KQ = card('kq', 'Small win', 'Do 3 quick ones in one go', '3 items take under 2 minutes. Anything outward stays a draft for you to send.',
123
- { kind: 'segs', total: '4 min', segs: [['Sam: ship the logging change', 1, C.acc], ['15 Oct interview invite', 1, C.ok], ['interview slots with 3 customers', 2, C.opp]] },
124
- { kpis: [['3', 'items', C.acc], ['4 min', 'total', C.ok], ['0', 'sent for you', C.mfg]], factsX: [['bolt', 'Under 2 minutes each'], ['shield', 'Nothing is sent for you']],
125
- move: { suggested: { id: 'all', label: 'Do all 3 now', conf: 90 }, options: [{ id: 'all', label: 'Do all 3 now' }] } });
126
- const WORK_KD = card('kd', 'Critical', '5 need a real decision', 'Put them in Today with time blocked, so they do not rot in the inbox.',
127
- { kind: 'segs', total: '25 min', segs: [['Assign owners to 2 unowned workshop actions', 5, C.bad], ['Approve pricing-page copy before handoff', 5, C.warn], ['Review the 13:00 interview guide', 5, C.acc], ['Confirm the growth assumption in the Q4 forecast', 10, C.opp]] },
128
- { kpis: [['5', 'decisions', C.bad], ['40 min', 'total', C.warn], ['78%', 'confidence', C.ok]], factsX: [['scale', 'Decide or review'], ['clock', 'About 40 min in total']],
129
- move: { suggested: { id: 'td', label: 'Put them in Today and block time', conf: 78 }, options: [{ id: 'td', label: 'Put them in Today and block time' }] } });
130
- const WORK_KA = card('ka', 'Opportunity', 'The agent drafted 3 replies', 'Review them as a set. Nothing leaves until you send it.',
131
- { kind: 'rows', rows: [['reply', 'Confirm the 1 Oct numbers before th…', 'ok', '10:30'], ['reply', 'Say yes to Sam: ship the logging ch…', 'ok', 'today'], ['reply', 'Accept the 15 Oct interview invite', 'ok', 'today']] },
132
- { kpis: [['84%', 'confidence', C.acc], ['2', 'options', C.opp], ['3', 'evidence', C.ok]], factsX: [['spark', 'Drafted from your history'], ['out', 'Goes to Review first']],
133
- move: { suggested: { id: 'ap', label: 'Approve all as drafts', conf: 84 }, options: [{ id: 'ap', label: 'Approve all as drafts' }, { id: 'ed', label: 'Open each first' }] } });
134
- const WORK_KJ = card('kj', 'Just in', "Priya's request is the newest and has a clock", 'The 1 Oct numbers, due 10:30. Two sources disagree, so the case file has the answer.',
135
- { kind: 'ring', done: 14, total: 152, size: 96, cap: '2h 18m left', sub: 'until the 10:30 update' },
136
- { kpis: [['2h 18m', 'left', C.warn], ['2', 'sources disagree', C.bad], ['90%', 'confidence', C.ok]], factsX: [['user', 'Priya, 07:58'], ['sheet', 'Sheet says 3.1%']],
137
- move: { suggested: { id: 'r', label: 'Reply 3.1% and cite the sheet', conf: 90 }, options: [{ id: 'r', label: 'Reply 3.1% and cite the sheet' }, { id: 't', label: 'Put it in Today' }] } });
122
+ /* Work tab (the deck per view: Inbox, Today, Upcoming, Waiting, Someday, Review). The decisions the mockup's Triage cards pointed at
123
+ * ("5 need a real decision", "The agent drafted 3 replies") live in Briefing · Needs you now, so no Work card names them. */
124
+ const WORK_KI = card('ki', 'Small win', 'Three items have no date yet', 'One has waited two weeks. Give each a day, a Not before date, or park it in Someday.',
125
+ { kind: 'rows', rows: [['inbox', 'Read the competitor pricing teardown', '', 'yesterday'], ['inbox', 'Book five user interviews for returns discovery', '', 'Tue'], ['alert', 'Idea: weekly digest for partner admins', 'warn', '14 days']] },
126
+ { kpis: [['3', 'to decide', C.acc], ['14 d', 'oldest', C.warn]], factsX: [['inbox', 'Capture now, decide later']] });
127
+ const WORK_KRV = card('krv', 'Review', 'Three items wait for your weekly review', 'Nobody touched them for a week or more. The oldest is 20 days.',
128
+ { kind: 'bars', bars: [['Clean up the Checkout Jira labels', 1, C.warn, '20 d'], ['Idea: weekly digest for partner admins', 0.7, C.warn, '14 d'], ['Annual-plan label', 0.45, C.acc, '9 d']] },
129
+ { eyebrow: { label: 'Review', icon: 'eye' }, kpis: [['3', 'to review', C.acc], ['20 d', 'oldest', C.warn]], factsX: [['eye', 'One at a time: keep, change or drop']] });
138
130
 
139
- /* Work tab, the other lenses (the mockup's workCards: Today, Week, Waiting, Later; Triage's fourth card too) */
131
+ /* Work tab, the other views (the mockup's workCards: Today, Week, Waiting, Later) */
140
132
  const mv = (id, label, conf, more) => ({ suggested: { id, label, conf }, options: [{ id, label }, ...(more || [])] });
141
- const WORK_KN = card('kn', 'Small win', 'Drop 3 noise items', 'Newsletter, digest and file updates. The agent has read them and found nothing for you.',
142
- { kind: 'tiles', tiles: [['3', 'FYI items', C.mfg], ['9', 'real work', C.acc]] },
143
- { kpis: [['94%', 'confidence', C.acc], ['2', 'options', C.opp], ['3', 'evidence', C.ok]], factsX: [['filter', 'Kept out of your list']], move: mv('ar', 'Archive all 3', 94) });
144
- const WK_KO = card('ko', 'Critical', 'Do these three first', 'Ranked by what each one unblocks and when it is due.',
133
+ const WK_KO = card('ko', 'Critical', 'Do these three first', 'Focus has three jobs for today, each with the reason.',
145
134
  { kind: 'axis', focus: [11.25, 13], now: 8.2, marks: [[10.5, '10:30', C.bad, 19], [13, '13:00', C.acc, 27], [14, '14:00', C.bad, 19]] },
146
- { kpis: [['6', 'committed', C.acc], ['1.8 h', 'focus time', C.opp], ['78%', 'confidence', C.ok]], factsX: [['target', 'Hold the 24 Oct date'], ['clock', 'Best window 11:15']], move: mv('lk', 'Lock these and block 11:15 to 13:00', 78) });
147
- const WK_KR = card('kr', 'Critical', 'Two items are at risk or overdue', 'The sandbox keys are 2 days late and the scope decision is due at 14:00.',
148
- { kind: 'rows', rows: [['alert', 'Payments sandbox keys', 'bad', 'overdue'], ['alert', 'Confirm the 1 Oct numbers before …', 'warn', 'at risk'], ['alert', 'Cut Checkout v2 to 3 core flows t…', 'warn', 'at risk']] },
149
- { kpis: [['80%', 'confidence', C.ok], ['2', 'options', C.opp], ['2', 'evidence', C.ok]], factsX: [['alert', 'Both block 24 Oct']], move: mv('es', 'Escalate the keys, then decide before 13:30', 80) });
135
+ { kpis: [['5', 'for today', C.acc], ['1.8 h', 'focus time', C.opp], ['78%', 'confidence', C.ok]], factsX: [['target', 'Hold the 24 Oct date'], ['clock', 'Best window 11:15']], move: mv('lk', 'Accept Focus and block 11:15 to 13:00', 78) });
136
+ const WK_KR = card('kr', 'Critical', 'Two promises are late', 'The timeline to Dara was due yesterday and the call with Priya is 2 days late.',
137
+ { kind: 'rows', rows: [['alert', 'Send the revised Checkout v2 timeline…', 'bad', '1 d late'], ['alert', 'Call Priya about the Q4 budget variance', 'bad', '2 d late']] },
138
+ { kpis: [['2', 'late', C.bad], ['1', 'outside PMOS', C.opp], ['80%', 'confidence', C.ok]], factsX: [['alert', 'Both are promises']], move: mv('es', 'Do the timeline first, then call Priya', 80) });
150
139
  const WK_KW = card('kw', 'Just in', '2 things wait on others', 'Dara is 2 days silent on the keys. A nudge is drafted for each.',
151
140
  { kind: 'bars', bars: [['Dara: Payments sandbox keys', 1, C.bad, 'asked Tue'], ['Lee: Annual-plan label', 0.4, C.warn, 'asked Wed']] },
152
141
  { kpis: [['2', 'waiting', C.warn], ['2 d', 'oldest', C.bad], ['82%', 'confidence', C.ok]], factsX: [['user', 'Dara, Lee'], ['out', 'Nudges go to Review']], move: mv('nu', 'Nudge both (drafted)', 82) });
153
142
  const WK_KL = card('kl', 'Opportunity', 'Today is 1.5 h of work against 1.8 h of focus', 'Move the two lowest-priority items to tomorrow and the day fits.',
154
143
  { kind: 'tiles', tiles: [['1.5 h', 'planned', C.acc], ['1.8 h', 'focus time', C.ok], ['0 h', 'over', C.bad]] },
155
144
  { kpis: [['2', 'to move', C.warn], ['0', 'promises hurt', C.ok], ['72%', 'confidence', C.acc]], factsX: [['clock', 'Capacity is the constraint']], move: mv('mv', 'Move the two lowest to tomorrow', 72) });
156
- const WK_KDN = card('kdn', 'Big win', '0 done so far today', 'Each one closed without a context switch.', { kind: 'ring', done: 0, total: 6, size: 96, cap: '0 closed', sub: '6 still open' },
145
+ const WK_KDN = card('kdn', 'Big win', '0 done so far today', 'Each one closed without a context switch.', { kind: 'ring', done: 0, total: 5, size: 96, cap: '0 closed', sub: '5 still open' },
157
146
  { kpis: [['1', 'facts', C.acc]], factsX: [['check', 'Momentum is good']] });
158
147
  const DAYS7 = (dots) => ['Thu 8', 'Fri 9', 'Mon 12', 'Tue 13', 'Wed 14', 'Thu 15', 'Fri 16'].map((d, i) => [d, dots.includes(i)]);
159
- const WK_KWK = card('kwk', 'This week', '2 items land in the next 7 days', 'Nothing is overdue. The next one is a promise to a customer.', { kind: 'days', days: DAYS7([2, 3]) },
160
- { eyebrow: { label: 'This week', icon: 'cal' }, kpis: [['2', 'facts', C.acc]], factsX: [['cal', 'Next: Mon 12 Oct']] });
148
+ const WK_KWK = card('kwk', 'Upcoming', '5 items land in the next 7 days', 'Two happen outside PMOS. One waits for its Not before date on Monday.', { kind: 'days', days: DAYS7([1, 2, 3, 4]) },
149
+ { eyebrow: { label: 'Upcoming', icon: 'cal' }, kpis: [['5', 'facts', C.acc]], factsX: [['cal', 'Next: Fri 9 Oct']] });
161
150
  const WK_KJB = card('kjb', 'Jobs', '4 jobs to be done, in progress', 'Work grouped by the outcome it serves, not by where it came from.',
162
- { kind: 'bars', bars: [['Hold the 24 Oct date', 1 / 7, C.acc, '1 of 7'], ['Keep leadership informed', 0, C.acc, '0 of 1'], ['Learn why returns hurt', 0, C.acc, '0 of 4'], ['Keep customer promises', 0, C.acc, '0 of 1']] },
151
+ { kind: 'bars', bars: [['Hold the 24 Oct date', 1 / 6, C.acc, '1 of 6'], ['Keep leadership informed', 0, C.acc, '0 of 1'], ['Learn why returns hurt', 0, C.acc, '0 of 3'], ['Keep customer promises', 0, C.acc, '0 of 1']] },
163
152
  { eyebrow: { label: 'Jobs', icon: 'target' }, kpis: [['4', 'facts', C.acc]], factsX: [['target', 'JTBD']] });
164
153
  const WK_KPR = card('kpr', 'Opportunity', 'A customer promise falls on Monday', 'Confirm the SSO date to Marta after the freeze. It is yours and dated.', { kind: 'days', days: DAYS7([2]) },
165
154
  { kpis: [['86%', 'confidence', C.acc], ['2', 'options', C.opp], ['2', 'evidence', C.ok]], factsX: [['user', 'Marta'], ['flag', 'Keeps trust']], move: mv('td', 'Bring it into Today for Monday', 86) });
@@ -176,11 +165,13 @@ const LT_KLP = card('klp', 'Opportunity', 'The Q1 discovery plan is ready to pro
176
165
  { kpis: [['68%', 'confidence', C.acc], ['2', 'options', C.opp], ['1', 'evidence', C.ok]], factsX: [['spark', 'Has a window now']], move: mv('pr', 'Promote to this week', 68) });
177
166
  const LT_KLS = card('kls', 'Small win', 'Nothing is stale', 'No someday item is older than 30 days.', { kind: 'ring', done: 1, total: 1, size: 96, cap: 'All fresh', sub: 'reviewed monthly' },
178
167
  { kpis: [['1', 'facts', C.acc]], factsX: [['check', 'Next review 1 Nov']] });
179
- const WK_KEV = card('kev', 'Just in', '6 open at the end of the day', "Migrate, schedule or drop each one. Carry-overs lead tomorrow's briefing.",
180
- { kind: 'rows', rows: [['moon', 'Cut Checkout v2 to 3 core flows to…', '', '2026-10-08'], ['moon', 'Reply to Marta: SSO 14 Oct, SCIM in…', '', '2026-10-08'], ['moon', 'Weekly status for leadership', '', '2026-10-08']] },
181
- { eyebrow: { label: 'Evening review', icon: 'moon' }, kpis: [['6', 'open', C.acc]], factsX: [['moon', 'Evening review']], move: mv('mg', 'Migrate all to tomorrow', 80) });
182
- /* the deck the Work tab asks for, by lens (scope "work:<lens>"); Close on Today adds the evening card first, as the mockup's S.tod === 'e' does */
183
- const WORK_LENS = { triage: [WORK_KQ, WORK_KD, WORK_KA, WORK_KN, WORK_KJ], today: [WK_KO, WK_KR, WK_KW, WK_KL, WK_KDN], week: [WK_KWK, WK_KJB, WK_KPR], waiting: [WT_KAG, WT_KCH, WT_KDP], later: [LT_KLP, LT_KLS] };
168
+ const WK_KEV = card('kev', 'Just in', '5 open at the end of the day', "Say Not yet, schedule or drop each one. Carry-overs lead tomorrow's briefing.",
169
+ { kind: 'rows', rows: [['moon', 'Update the Checkout v2 risk log', '', '2026-10-08'], ['moon', 'Talk to Dara about QA capacity for t…', '', '2026-10-08'], ['moon', 'Prep the one-pager for the Search r…', '', 'flagged']] },
170
+ { eyebrow: { label: 'Evening review', icon: 'moon' }, kpis: [['5', 'open', C.acc]], factsX: [['moon', 'Evening review']], move: mv('mg', 'Not yet: all to tomorrow', 80) });
171
+ /* the deck the Work tab asks for, by view (scope "work:<view>"); Close on Today adds the evening card first, as the mockup's S.tod === 'e' does.
172
+ * triage, week and later are the old lens names: the shell asks for "work:triage" before the view reports its own. */
173
+ const WORK_LENS = { inbox: [WORK_KI, WK_KJB], today: [WK_KO, WK_KR, WK_KW, WK_KL, WK_KDN], upcoming: [WK_KWK, WK_KPR, WK_KJB], waiting: [WT_KAG, WT_KCH, WT_KDP], someday: [LT_KLS, LT_KLP], review: [WORK_KRV, LT_KLS] };
174
+ WORK_LENS.triage = WORK_LENS.inbox; WORK_LENS.week = WORK_LENS.upcoming; WORK_LENS.later = WORK_LENS.someday;
184
175
 
185
176
  /* Initiatives tab (the mockup's iniCards, Strategize) */
186
177
  const INI_DIR = card('ydir', 'Year direction', 'North star is 4% behind and widening', 'It leaves tolerance in Dec \'26 and reaches the pivot line in Mar \'27.',
@@ -206,7 +197,7 @@ const PLAN_BRIEF = [DECISION, METRIC, DATE, INSIGHT, SSO].map(unfilled);
206
197
  const BY_SCOPE = {
207
198
  brief: PLAN_BRIEF,
208
199
  cal: [CAL_DM, CAL_DD, CAL_DF, CAL_DL, CAL_DN],
209
- work: [WORK_KQ, WORK_KD, WORK_KA, { ...METRIC, key: 'kj', tag: 'Just in', headline: 'Priya\'s request is the newest and has a clock', sub: 'The 1 Oct numbers, due 10:30. Two sources disagree, so the case file has the answer.' }, { ...SEARCH, key: 'kdn', tag: 'Big win', headline: '0 done so far today', sub: 'Each one closed without a context switch.' }],
200
+ work: [WK_KO, WK_KR, WORK_KI, WORK_KRV, WK_KDN],
210
201
  init: [INI_DIR, INI_MAT, INI_SIG, { ...SSO, key: 'ysso' }, { ...BOARD, key: 'ybd' }],
211
202
  rep: [REP_LEAD, REP_NEXT, { ...BOARD, key: 'rbd', pov: 'PRODUCT' }, { ...SEARCH, key: 'rsr' }, { ...SSO, key: 'rss' }],
212
203
  };
@@ -367,57 +358,68 @@ export function calendarViews(url) {
367
358
  selected: day ? { id: 'm4', title: 'Sprint planning', time: '14:00 to 15:00', text: 'Checkout v2: the sprint cannot start until scope is decided. The 24 Oct date depends on it.', status: 'Blocked on your decision', tone: 'bad', who: ['D', 'L', 'S'] } : null };
368
359
  }
369
360
 
370
- /* ── work items (the mockup's S.items by place and bucket) and the four jobs ───────────────────────────────────────────────────── */
361
+ /* ── work items: the PM's own work (initiative console3-work-page) and the four jobs ───────────────────────────────────────────────
362
+ * One home (PRD R9): the decisions and approvals the mockup listed in Work ("Approve pricing-page copy", "Say yes to Sam", the Review
363
+ * drafts, the scope decision) live in Briefing · Needs you and are not Work items. What is left is the PM's own to-do, with the console3
364
+ * feedback mockup's flavour: things done outside PMOS (talk to, call, set up a meeting), a "not before" date, a flag, a link to the
365
+ * team task an item advances, and one offline item with proof that it looks done. The answer is placed by work-place.js, the same rule
366
+ * the server applies, so the demo shows `work`, `review`, `focus` and `counts` in the real route's shape. */
371
367
 
372
368
  const JOBS = [
373
- { id: 'j1', when: 'the scope is too big for the date', want: 'a decision fast and a clear plan', so: 'we ship on 24 Oct', mock_name: 'Hold the 24 Oct date', done: 1, total: 7 },
374
- { id: 'j2', when: 'leadership asks for numbers', want: 'one answer that is true', so: 'their picture stays accurate', mock_name: 'Keep leadership informed', done: 0, total: 1 },
375
- { id: 'j3', when: 'customers return items', want: 'to see where it hurts', so: 'we fix the right thing', mock_name: 'Learn why returns hurt', done: 0, total: 4 },
376
- { id: 'j4', when: 'I promise a customer a date', want: 'to keep it or tell them early', so: 'trust holds', mock_name: 'Keep customer promises', done: 0, total: 1 },
369
+ { id: 'j1', when: 'the scope is too big for the date', want: 'a decision fast and a clear plan', so: 'we ship on 24 Oct', mock_name: 'Hold the 24 Oct date' },
370
+ { id: 'j2', when: 'leadership asks for numbers', want: 'one answer that is true', so: 'their picture stays accurate', mock_name: 'Keep leadership informed' },
371
+ { id: 'j3', when: 'customers return items', want: 'to see where it hurts', so: 'we fix the right thing', mock_name: 'Learn why returns hurt' },
372
+ { id: 'j4', when: 'I promise a customer a date', want: 'to keep it or tell them early', so: 'trust holds', mock_name: 'Keep customer promises' },
377
373
  ];
378
374
  let seq = 0;
379
- const it = (title, o) => ({ title, due: null, priority: null, waiting_on: null, job: null, id: 'item-demo' + String(++seq).padStart(3, '0'), source: 'local', created_at: '2026-10-08T05:00:00.000Z', steps: [], mock_row: true, ...(o || {}) });
380
- const TRIAGE = [
381
- it('Confirm the 1 Oct numbers before the 10:30 update', { kind: 'reply', priority: 'P1', time: '10:30', mins: 5, job: 'j2', src: 'Slack', init: 'Checkout v2', agent_move: true, left: { text: '2h 18m left', pct: 0.3, tone: 'warn' } }),
382
- it('Assign owners to 2 unowned workshop actions', { kind: 'decide', priority: 'P2', time: 'today', mins: 5, job: 'j1', src: 'Image', init: 'Checkout v2', agent_move: true }),
383
- it('Approve pricing-page copy before handoff', { kind: 'review', priority: 'P2', time: 'Fri', mins: 5, job: 'j1', src: 'Figma', init: 'Checkout v2', agent_move: true }),
384
- it('Review the 13:00 interview guide', { kind: 'review', priority: 'P2', time: '13:00', mins: 5, job: 'j3', src: 'Agent', init: 'Returns experience', agent_move: true, left: { text: '4h 48m left', pct: 0.55, tone: 'acc' } }),
385
- it('Confirm the growth assumption in the Q4 forecast', { kind: 'review', priority: 'P2', time: 'Fri', mins: 10, job: 'j1', src: 'Sheets', init: 'Checkout v2', agent_move: true }),
386
- it('Say yes to Sam: ship the logging change', { kind: 'reply', priority: 'P3', time: 'today', mins: 1, src: 'Slack', init: 'Search relevance', agent_move: true }),
387
- it('Accept the 15 Oct interview invite', { kind: 'reply', priority: 'P3', time: 'today', mins: 1, job: 'j3', src: 'Gmail', init: 'Returns experience' }),
388
- it('Pick interview slots with 3 customers', { kind: 'reply', priority: 'P3', time: 'this week', mins: 2, job: 'j3', src: 'Gmail', init: 'Returns experience', agent_move: true }),
389
- it('Self-serve onboarding: continue or park?', { kind: 'decide', priority: 'P3', time: 'this week', mins: 15, src: 'Notes', init: 'Self-serve onboarding', agent_move: true }),
390
- ];
391
- const TODAYL = [
392
- it('Cut Checkout v2 to 3 core flows to hold 24 Oct?', { due: TODAY, priority: 'P1', kind: 'decide', time: '14:00', mins: 15, job: 'j1', src: 'Confluence', init: 'Checkout v2', left: { text: '5h 48m left', pct: 0.36, tone: 'acc' } }),
393
- it('Reply to Marta: SSO 14 Oct, SCIM in Nov', { due: TODAY, priority: 'P2', kind: 'reply', mins: 5, job: 'j4', src: 'Gmail', init: 'SSO' }),
394
- it('Weekly status for leadership', { due: TODAY, priority: 'P2', kind: 'review', mins: 10, job: 'j2', src: 'Slack', init: 'Checkout v2' }),
395
- it('Prep note for 10:30 Checkout sync', { due: TODAY, kind: 'review', mins: 5, job: 'j1', src: 'Agent', init: 'Checkout v2' }),
396
- it('Close 6 duplicate tickets, assign 3 more', { due: TODAY, kind: 'do', mins: 5, job: 'j3', src: 'Jira', init: 'Search relevance' }),
397
- it('Clean diagram of the whiteboard flow', { due: TODAY, kind: 'review', mins: 5, job: 'j1', src: 'Image', init: 'Checkout v2' }),
398
- ];
399
- const WEEK = [
400
- it('Confirm the SSO date to Marta', { due: '2026-10-12', priority: 'P2', job: 'j4', kind: 'reply', src: 'Jira', init: 'SSO' }),
401
- it('Share returns insights with sales', { due: '2026-10-13', job: 'j3', kind: 'do', src: 'Notes', init: 'Returns experience' }),
402
- ];
403
- const WAITING = [
404
- it('Payments sandbox keys', { waiting_on: 'Dara', due: '2026-10-06', job: 'j1', priority: 'P2', kind: 'do', src: 'Jira', init: 'Checkout v2' }),
405
- it('Annual-plan label', { waiting_on: 'Lee', due: '2026-10-07', job: 'j1', priority: 'P3', kind: 'do', src: 'Figma', init: 'Checkout v2' }),
406
- ];
407
- const LATER = [it('Write the Q1 discovery plan', { due: '2026-11-16', kind: 'do', src: 'You' })];
375
+ const it = (title, o) => ({ title, due: null, priority: null, waiting_on: null, job: null, id: 'item-demo' + String(++seq).padStart(3, '0'), source: 'local', created_at: '2026-10-08T05:00:00.000Z', touched_at: '2026-10-07T16:00:00.000Z', steps: [], ...(o || {}) });
376
+ const at = (d) => d + 'T09:00:00.000Z';
377
+ /* Today: overdue, due today and flagged */
378
+ const W_TIMELINE = it('Send the revised Checkout v2 timeline to Dara', { due: '2026-10-07', priority: 'P1', job: 'j1', mock_row: true, kind: 'do', mins: 10, src: 'Jira', init: 'Checkout v2', agent_move: true });
379
+ const W_QA = it('Talk to Dara about QA capacity for the beta', { kind: 'offline', person: 'Dara', due: TODAY, flagged: true, job: 'j1',
380
+ evidence: 'Dara wrote in #checkout at 07:40: "QA plan agreed, 2 testers from Monday".' });
381
+ const W_CALL = it('Call Priya about the Q4 budget variance', { kind: 'offline', person: 'Priya', due: '2026-10-06', not_yet_count: 1, job: 'j2', touched_at: at('2026-10-06') });
382
+ const W_RISK = it('Update the Checkout v2 risk log', { due: TODAY, priority: 'P2', job: 'j1' });
383
+ const W_PAGER = it('Prep the one-pager for the Search relevance review', { flagged: true, resolves: 'search-beta-pack', resolves_title: 'Search relevance beta: review pack' });
384
+ /* Upcoming: the next 7 days, then Later */
385
+ const W_SAM = it('Set up a meeting with Sam about the logging rollout', { kind: 'offline', person: 'Sam', due: '2026-10-09' });
386
+ const W_SSO = it('Confirm the SSO date to Marta', { due: '2026-10-12', priority: 'P2', job: 'j4' });
387
+ const W_NOTES = it('Write release notes for SSO 1.0', { defer: '2026-10-12', due: '2026-10-14', resolves: 'sso-ga-checklist', resolves_title: 'SSO general release checklist' });
388
+ const W_SALES = it('Share returns insights with sales', { due: '2026-10-13', job: 'j3' });
389
+ const W_TAGS = it('Ask Marta for the top ten support tags', { kind: 'offline', person: 'Marta', due: '2026-10-14', job: 'j3', resolves: 'returns-interview-5' });
390
+ const W_Q1 = it('Write the Q1 discovery plan', { due: '2026-11-16' });
391
+ /* Inbox: captured, no date yet */
392
+ const W_TEAR = it('Read the competitor pricing teardown', { created_at: '2026-10-07T15:20:00.000Z' });
393
+ const W_BOOK = it('Book five user interviews for returns discovery', { job: 'j3', created_at: '2026-10-06T10:00:00.000Z', touched_at: at('2026-10-06') });
394
+ const W_IDEA = it('Idea: weekly digest for partner admins', { created_at: '2026-09-24T11:00:00.000Z', touched_at: at('2026-09-24') });
395
+ /* Waiting and Someday */
396
+ const W_KEYS = it('Payments sandbox keys', { waiting_on: 'Dara', due: '2026-10-06', job: 'j1', priority: 'P2', touched_at: at('2026-10-06') });
397
+ const W_LABEL = it('Annual-plan label', { waiting_on: 'Lee', due: '2026-10-07', job: 'j1', priority: 'P3', touched_at: at('2026-09-29') });
398
+ const W_JIRA = it('Clean up the Checkout Jira labels', { parked: true, created_at: '2026-09-10T09:00:00.000Z', touched_at: at('2026-09-18') });
399
+ const W_NOMEET = it('Try a no-meeting Wednesday', { parked: true, touched_at: at('2026-10-02') });
400
+ /* done yesterday: counts in its job, never drawn */
401
+ const W_PRD = it('Send revised PRD to Dara', { due: '2026-10-07', job: 'j1', done: true, done_at: '2026-10-07T17:10:00.000Z' });
402
+ const ITEMS = [W_TIMELINE, W_QA, W_CALL, W_RISK, W_PAGER, W_SAM, W_SSO, W_NOTES, W_SALES, W_TAGS, W_Q1, W_TEAR, W_BOOK, W_IDEA, W_KEYS, W_LABEL, W_JIRA, W_NOMEET, W_PRD];
403
+ /* Focus: up to three jobs for today, each with the reason (rules first: an overdue promise, an offline item due, a flag) */
404
+ const DAY_FOCUS = { suggestions: [
405
+ { key: 'job:j1', title: 'Hold the 24 Oct date', why: 'An overdue promise: the revised timeline to Dara was due yesterday.', item_ids: [W_TIMELINE.id, W_QA.id, W_RISK.id], job: 'j1' },
406
+ { key: 'item:' + W_CALL.id, title: 'Call Priya about the Q4 budget variance', why: 'Outside PMOS and 2 days overdue. You moved it once.', item_ids: [W_CALL.id], job: 'j2' },
407
+ { key: 'item:' + W_PAGER.id, title: 'Prep the one-pager for the Search relevance review', why: 'You flagged it for today.', item_ids: [W_PAGER.id] },
408
+ ], accepted: [] };
408
409
  export function pmItems() {
409
- const lists = { triage: TRIAGE, today: TODAYL, week: WEEK, waiting: WAITING, later: LATER };
410
+ const { work, review, counts } = buildWork(ITEMS, TODAY, 7);
411
+ const jobs = JOBS.map((j) => { const its = ITEMS.filter((i) => i.job === j.id); return { ...j, done: its.filter((i) => i.done).length, total: its.length }; });
412
+ /* the old five lists, kept for the readers that still use them (the shell's Work badge, the Calendar's dated tasks) */
413
+ const week = addDays(TODAY, 7);
414
+ const lists = { triage: work.inbox, today: work.today, week: work.upcoming.filter((i) => arrives(i, TODAY) <= week), waiting: work.waiting,
415
+ later: [...work.upcoming.filter((i) => arrives(i, TODAY) > week), ...work.someday] };
410
416
  const due_today = [
411
417
  { id: 'i9', title: 'Payments sandbox keys', sub: 'asked Tue · Checkout v2', right: '2 d late', tone: 'bad' },
412
418
  { id: 'i2', title: 'Confirm the 1 Oct numbers before the 10:30 update', sub: '10:30 · Checkout v2', right: '2h 18m left', tone: 'warn', left: { text: '2h 18m left', pct: 0.3, tone: 'warn' } },
413
419
  { id: 'i1', title: 'Cut Checkout v2 to 3 core flows to hold 24 Oct?', sub: '14:00 · Checkout v2', right: 'At risk', tone: 'warn', left: { text: '5h 48m left', pct: 0.36, tone: 'acc' } },
414
420
  { id: 'i6', title: 'Review the 13:00 interview guide', sub: '13:00 · Returns experience', right: '4h 48m left', tone: 'warn', left: { text: '4h 48m left', pct: 0.55, tone: 'acc' } },
415
421
  ];
416
- return { ok: true, today: TODAY, lists, jobs: JOBS, items: Object.values(lists).flat(), due_today,
417
- flow_note: 'The agent clarified every item before you saw it. Your part is the commit.',
418
- noise: [{ title: 'Weekly product newsletter', src: 'Gmail' }, { title: 'Jira digest: 14 updates', src: 'Jira' }, { title: 'Figma: 3 files updated', src: 'Figma' }],
419
- flow: [['Captured', 12, 'var(--muted-foreground)'], ['Clarified', 9, 'var(--acc)'], ['Committed', 1, 'var(--warning)'], ['Done', 0, 'var(--success)']],
420
- lens_text: { triage: ['9 to triage, 3 take under 2 minutes', 'Nothing stays past 17:30. The agent has read all of it and drafted 3 replies.'] } };
422
+ return { ok: true, today: TODAY, lists, work, review, review_days: 7, focus: DAY_FOCUS, counts, jobs, items: ITEMS, due_today };
421
423
  }
422
424
 
423
425
  /* ── initiatives (the roadmap rows the Initiatives tab reads), forecast, releases ──────────────────────────────────────────────── */
@@ -1,18 +1,11 @@
1
- /* pm/c3-demo-misc2.js — DEMO ONLY (/c3.html?demo=1): the sample content for three panels the content audit found missing (planning/research/console3-content-parity.md), in the REAL
1
+ /* pm/c3-demo-misc2.js — DEMO ONLY (/c3.html?demo=1): the sample content for panels the content audit found missing (planning/research/console3-content-parity.md), in the REAL
2
2
  * response shapes. Real mode never loads this file. installMisc2() is called from installDemo() in c3-demo.js, after the briefing fixtures, and wraps window.fetch once:
3
3
  * GET /api/briefing (the Briefing's own scope) → adds `stop`: [{ icon, text }], the agent's proposals of what to stop (c3-misc-panels.js stopCard shows them in the Reflect lens only).
4
4
  * The real route sends no `stop`, so a real Reflect lens draws no such panel.
5
- * GET /api/pm-items → lists.today is the mockup's Today (console3.html wItems, line 3215): the work not yet done that is due today, and the inbox items
6
- * due today. Those are the same item objects as in lists.triage, which is how the mockup's one item list shows them in both lenses.
7
- * This puts "Say yes to Sam", which has no job, in Today, so the grouped list ends with "No job yet".
8
5
  * window.__c3repix.INM → the initiative names the Project report's forecast card uses (mockup INM, line 3438).
9
- * The Project forecast chart itself needs no fixture here: it reads /api/c3-timeline, which c3-demo-time.js answers. */
10
-
11
- import { pmItems } from './c3-demo-data.js';
12
-
13
- /* The fixture's work set for today also holds five Review drafts the mockup does not list in Today. The Work view reads /api/pm-items before the fetch wrapper below exists,
14
- * so the array (the very one pmItems() returns) is cut in place, once, when this file loads. The inbox items due today join Today in pm-work-view.js. */
15
- { const T = pmItems().lists.today; const keep = T.filter((x) => /^Cut Checkout v2/.test(x.title)); T.splice(0, T.length, ...keep); }
6
+ * The Project forecast chart itself needs no fixture here: it reads /api/c3-timeline, which c3-demo-time.js answers.
7
+ * The Work page's Today no longer comes from here: the server places every item (initiative console3-work-page), and the demo places its fixture with the same rule
8
+ * (work-place.js, called from c3-demo-data.js pmItems). */
16
9
 
17
10
  const STOP = [
18
11
  { icon: 'cal', text: 'Skip Sprint planning, send the decision in writing' },
@@ -34,11 +27,6 @@ export function installMisc2() {
34
27
  if (u.pathname === '/api/briefing' && (!u.searchParams.get('scope') || u.searchParams.get('scope') === 'brief')) {
35
28
  const j = await res.clone().json();
36
29
  if (j && j.ok !== false) return new Response(JSON.stringify({ ...j, stop: STOP }), { status: 200, headers: { 'content-type': 'application/json' } });
37
- } else if (u.pathname === '/api/pm-items') {
38
- const j = await res.clone().json();
39
- if (j && j.lists) {
40
- return new Response(JSON.stringify({ ...j, lens_text: { ...(j.lens_text || {}), today: ['6 committed today, 1 overdue', 'Best focus window is 11:15. 2 items wait on others.'] } }), { status: 200, headers: { 'content-type': 'application/json' } });
41
- }
42
30
  }
43
31
  } catch { /* an unreadable answer goes through as it is */ }
44
32
  return res;
@@ -1,76 +1,28 @@
1
- /* pm/c3-demo-work-ix.js — the case-file content of the Work rows in DEMO mode (/c3.html?demo=1): the console3 mockup's own sample
2
- * evidence, options, confidence, "unsure", "if you wait" and agent runs (console3.html: NEEDC i2 line 2250, the agent moves AM at
3
- * line 4958, genTask 3216). Importing this file attaches an `ix` object to each demo work item of c3-demo-data.js, matched by TITLE
4
- * (the item ids come from a counter in that file). pm-work-view.js reads `item.ix` only when the item has it, so real mode, whose
5
- * items never carry one, draws a case file from the item's own fields and never loads this file.
1
+ /* pm/c3-demo-work-ix.js — the case-file content of the Work rows in DEMO mode (/c3.html?demo=1): sample evidence, options and an agent
2
+ * run, in the console3 mockup's style (console3.html genTask 3216, the agent moves AM at line 4958). Importing this file attaches an `ix`
3
+ * object to demo work items of c3-demo-data.js, matched by TITLE (the item ids come from a counter in that file). pm-work-view.js reads
4
+ * `item.ix` only when the item has it, so real mode, whose items never carry one, draws a case file from the item's own fields.
6
5
  *
7
- * Shape of ix: { move, conf, why: [text, source] (the first evidence row, as the mockup's x.why), chg / unsure / ign (a case of its own), steps: [agent run steps],
6
+ * The decisions and approvals that used to have case files here ("Approve pricing-page copy", "Say yes to Sam", the scope decision)
7
+ * are Briefing · Needs you items now (initiative console3-work-page, PRD R9); their case files live in c3-demo-brief-ix.js.
8
+ *
9
+ * Shape of ix: { move, conf, why: [text, source] (the first evidence row), chg / unsure / ign (a case of its own), steps: [agent run steps],
8
10
  * res: { h, draft?, out?, claims: [[label, text]] }, ev: { h, claims } }
9
11
  * Nothing here is sent anywhere: a draft stays a draft (the case file's slide for a send is disabled: no channel connected). */
10
12
  import { pmItems } from './c3-demo-data.js';
11
13
 
12
14
  const FIX = {
13
- 'Confirm the 1 Oct numbers before the 10:30 update': {
14
- move: 'Reply 3.1% and cite the sheet', conf: 78,
15
- why: ['Priya asked in #leadership at 07:58.', 'Slack'],
16
- steps: ['Reading the Slack thread (quarantined)', 'Reading the analytics sheet', 'Drafting the reply'],
17
- res: { h: 'Reply to Priya drafted: 3.1%, from the sheet', draft: 'Hi Priya, the 1 Oct conversion is 3.1% (analytics sheet, refreshed 06:10). The PRD says 3.4% but it is 4 days old; I will correct it today.', out: 'Priya, Slack #leadership',
18
- claims: [['fact', '3.1% from the analytics sheet, today 06:10'], ['fact', 'The PRD says 3.4%, last edited 4 days ago'], ['est', 'The sheet may refresh again before 10:30']] },
19
- ev: { h: 'Where 3.1% comes from', claims: [['fact', 'Sheet "Checkout KPIs", cell B14: 3.1%, refreshed 06:10'], ['rep', 'Priya asked in #leadership at 07:58 (imported, untrusted)'], ['ass', 'Priya means conversion, not checkout starts. Not confirmed.']] },
20
- },
21
- 'Assign owners to 2 unowned workshop actions': {
22
- move: 'Sandbox follow-up to Dara, label review to Lee', conf: 74,
23
- why: ['Two actions on the whiteboard photo have no owner.', 'Image'],
24
- steps: ['Reading the whiteboard photo', 'Matching actions to owners', 'Drawing the owner map'],
25
- res: { h: 'Owners for the two workshop actions', claims: [['est', 'Action 2 was read at medium confidence (handwriting)'], ['fact', 'Dara owns the sandbox work in Jira (CHK-141)'], ['fact', 'Lee owns the pricing label in Figma']] },
26
- ev: { h: 'What was read from the photo', claims: [['est', 'Action 1: sandbox follow-up, read at high confidence'], ['est', 'Action 2: label review, read at medium confidence'], ['rep', 'Both due next week, from the whiteboard']] },
27
- },
28
- 'Approve pricing-page copy before handoff': {
29
- move: '"Save 20% billed yearly"', conf: 70,
30
- why: ['One open Figma comment on the annual-plan label.', 'Figma'],
31
- steps: ['Reading the Figma comment', 'Reading the PRD pricing section', 'Writing the options'],
32
- res: { h: 'Three slides for the pricing-copy call', claims: [['fact', '"Save 20% billed yearly" shows the discount'], ['est', 'Longer on mobile, one line wraps'], ['ass', 'Customers read "Annual" as a price rise']] },
33
- ev: { h: 'What Figma says', claims: [['fact', 'One open comment, from Lee on Tue'], ['est', 'The file was last read Tue 17:10, so newer comments may exist']] },
34
- },
35
- 'Review the 13:00 interview guide': {
36
- move: 'Ask openly: "What did you do when printing failed?"', conf: 76,
37
- why: ['Question 6 may lead the answer.', 'Agent'],
38
- steps: ['Reading the last two calls', 'Reading the guide'],
39
- res: { h: 'Question 6 is set and the guide is ready', claims: [['fact', 'Question 6 now reads "What did you do when printing failed?"'], ['est', 'The open wording does not lead the answer'], ['fact', 'Written to discovery/returns-guide, pushed to its own branch']] },
40
- ev: { h: 'Why question 6 needs your wording', claims: [['fact', '2 of 3 calls mentioned the support route'], ['rep', '"we just call support" (call 2, imported)'], ['ass', 'A direct question about support may lead the answer']] },
41
- },
42
- 'Confirm the growth assumption in the Q4 forecast': {
43
- move: 'Set 4% and tell Marta', conf: 72,
44
- why: ['Forecast row 14 says 6% growth.', 'Sheets'],
45
- steps: ['Reading forecast row 14', 'Reading the PRD section', 'Comparing with 3 months of actuals'],
46
- res: { h: 'The growth assumption has three sources and two disagree', claims: [['fact', 'Actual growth over 3 months is 3.8%'], ['est', 'Setting 4% leaves a gap of 0.2 points, 2 either way'], ['ass', 'Finance locks the forecast tomorrow as planned']] },
47
- ev: { h: 'Where 6% came from', claims: [['fact', 'Row 14 was last edited 3 days ago by Finance'], ['rep', 'The edit note says "growth target" (imported text, untrusted)'], ['est', 'The 6% matches the target, not the trend']] },
48
- },
49
- 'Say yes to Sam: ship the logging change': {
50
- move: 'Open the logging change as a pull request', conf: 84,
51
- why: ['Sam asked for a yes in Slack.', 'Slack'],
52
- steps: ['Reading the change', 'Running lint locally'],
53
- res: { h: 'Logging change is up for review', claims: [['fact', 'Local lint passes'], ['fact', 'Pushed 5c1d7aa to initiative/search-relevance (observed)'], ['fact', 'Pull request opened; it merges when required CI is green']] },
54
- ev: { h: 'What the change touches', claims: [['fact', 'Adds one log line in src/rank.js'], ['est', 'No behaviour change, low risk']] },
55
- },
56
- 'Pick interview slots with 3 customers': {
57
- move: 'Accept the 3 held slots', conf: 80,
58
- why: ['3 slots are held, none overlaps a standing meeting.', 'Calendar'],
59
- steps: ['Reading the calendar', 'Writing the slot brief'],
60
- res: { h: 'A one-page brief on the three held slots', claims: [['fact', '3 slots held, none overlapping (calendar, now)'], ['rep', 'Marta sent five times (Gmail, imported)']] },
61
- ev: { h: 'Why these three slots', claims: [['fact', '3 slots held, none overlapping (calendar, now)'], ['rep', 'Marta sent five times (Gmail, imported)']] },
62
- },
63
- 'Self-serve onboarding: continue or park?': {
64
- move: 'Park and revisit in Q1', conf: 66,
65
- why: ['9 days without movement', 'Jira'],
66
- steps: ['Reading the stop rule'],
67
- res: { h: 'Parked, with a line in the Q1 plan', claims: [['fact', 'Stop rule asked for 2 signals by 30 Sep, found 0'], ['fact', 'One line added to the Q1 planning list'], ['est', 'Frees one discovery slot']] },
68
- ev: { h: 'Why it was kept this long', claims: [['fact', '9 days without movement (Jira)'], ['rep', 'The brief said "2 signals by 30 Sep" (imported)']] },
69
- },
70
- 'Cut Checkout v2 to 3 core flows to hold 24 Oct?': {
71
- sit: 'When the scope is too big for the date, I want a decision fast and a clear plan, so we ship on 24 Oct.',
72
- chg: [['scale', 'Four flows fit 31 Oct, three fit 24 Oct.', 'Plan run 09:12', 'warn'], ['doc', 'The PRD lists 4 flows (v5)', 'Confluence', 'warn'], ['clock', 'The decision meeting is at 14:00', 'Calendar']],
73
- unsure: 'The 24 Oct promise was made to leadership.', ign: 'The forecast stays at 31 Oct.',
15
+ 'Send the revised Checkout v2 timeline to Dara': {
16
+ move: 'Draft the email with the 24 Oct plan and the two risks', conf: 80,
17
+ why: ['You told Dara on Tuesday the timeline would follow by Wednesday.', 'Gmail'],
18
+ steps: ['Reading the Jira board', 'Reading the risk log', 'Drafting the email'],
19
+ res: { h: 'Email to Dara drafted: the 24 Oct plan and two risks', draft: 'Hi Dara, here is the revised Checkout v2 timeline. Code freeze 20 Oct, release 24 Oct. Two risks: the sandbox keys and QA capacity. I will confirm QA with you today.', out: 'Dara, email',
20
+ claims: [['fact', 'Code freeze 20 Oct, from the Jira board'], ['fact', 'Two open risks in the risk log'], ['est', 'QA capacity is not confirmed yet']] },
21
+ ev: { h: 'Where the dates come from', claims: [['fact', 'Jira board "Checkout v2", fix version 24 Oct'], ['rep', 'Dara asked on Tuesday (Gmail, imported)']] },
22
+ },
23
+ 'Update the Checkout v2 risk log': {
24
+ sit: 'Two risks changed this week: the sandbox keys are late and QA capacity is open. The log still shows both as green.',
25
+ chg: [['alert', 'Payments sandbox keys: 2 days late', 'Jira', 'warn'], ['user', 'QA capacity: talk to Dara today', 'Work'], ['clock', 'Last edited 6 days ago', 'Confluence']],
74
26
  },
75
27
  };
76
28
 
@@ -15,12 +15,12 @@ import './c3-demo-cal-ix.js'; // Calendar and the ribbon: sets window.__c3cali
15
15
  import './c3-demo-cal3.js'; // Calendar Quarter and Year: answers GET /api/c3-strategy (chained under this file's fetch wrapper)
16
16
  import './c3-demo-rep-ix.js'; // Reports fixtures: sets window.__c3repix (report-families-view.js reads it in demo only)
17
17
  import './c3-demo-cal2.js'; // Calendar After notes, Week grid, Progress and timing: sets window.__c3cal2 (calendar-views.js reads it in demo only)
18
- import './c3-demo-work2.js'; // Work Jobs card: sets window.__c3work2 (pm-work-view.js reads it in demo only)
19
18
  import './c3-demo-work-ix.js'; // Work case files: attaches `ix` (evidence, options, agent steps) to the demo work items (pm-work-view.js reads it in demo only)
20
19
  import './c3-demo-term.js'; // Agent surfaces: registers the run fixtures (window.__c3termfx_make); c3-terminal-data.js reads them in demo only
21
20
  import './c3-demo-life.js'; // Lifecycle panels: ideas, hypotheses, lessons, gates, evaluations, corrections (wraps window.fetch for those GETs; innermost, so it loads before realFetch is taken)
22
- import './c3-demo-misc2.js'; // Work Today lens as the mockup's; the Reflect lens's "What I could stop" (installMisc2 wraps the fetch after the briefing fixtures)
21
+ import './c3-demo-misc2.js'; // the Reflect lens's "What I could stop" (installMisc2 wraps the fetch after the briefing fixtures)
23
22
  import { timeline } from './c3-demo-time.js'; // Initiatives Timeline: every check as a frame, in the real /api/c3-timeline shape
23
+ import { withCalendarPage } from './c3-demo-cal4.js'; // Calendar page: the layers, the free blocks and the meeting context on the calendar-views answer
24
24
  import './c3-demo-plan2.js'; // Commitments, Why it slips, Dependencies and Releases of Deliver: sets window.__c3planfx (c3-plan-panels.js, timeline-forecast-view.js, releases-view.js read it in demo only)
25
25
  import { installRep3 } from './c3-demo-rep3.js'; // Reports dashboards: GET /api/report-dashboards and /data, POST layout (installed OVER this file's wrapper, see its header)
26
26
 
@@ -49,7 +49,7 @@ function getRoute(path, url) {
49
49
  const q = new URL(url, location.origin).searchParams;
50
50
  switch (path) {
51
51
  case '/api/briefing': return D.briefing(url);
52
- case '/api/calendar-views': return D.calendarViews(url);
52
+ case '/api/calendar-views': return withCalendarPage(D.calendarViews(url));
53
53
  case '/api/pm-items': return D.pmItems();
54
54
  case '/api/runs': return D.runs();
55
55
  case '/api/releases': return D.releases();
@@ -75,6 +75,13 @@ function postRoute(path, body) {
75
75
  try { b = body ? JSON.parse(body) : {}; } catch { b = {}; }
76
76
  if (path === '/api/pm-items/add') return { ok: true, item: { id: 'item-demo-new' + (++added), title: String(b.line || '').trim() || 'New item', due: null, priority: null, waiting_on: null, job: null, steps: [], created_at: D.NOW_ISO + 'Z' } };
77
77
  if (path === '/api/pm-items/remove') return { ok: true, undo_ms: 8000 };
78
+ /* the Work page's routes (initiative console3-work-page): each answers as the real route does; pm-work-view.js lays the press over the fixtures */
79
+ if (path === '/api/pm-items/notyet') {
80
+ const when = String(b.when || ''); const n = when === 'tomorrow' ? 1 : when === 'next-week' ? 7 : 0;
81
+ return { ok: true, defer: n ? new Date(Date.parse(D.TODAY + 'T00:00:00Z') + n * 86400000).toISOString().slice(0, 10) : null };
82
+ }
83
+ if (['/api/pm-items/flag', '/api/pm-items/defer', '/api/pm-items/someday', '/api/pm-items/waiting', '/api/pm-items/keep', '/api/pm-items/done'].includes(path)) return { ok: true, item: { id: b.id } };
84
+ if (path.startsWith('/api/pm-items/focus/')) return { ok: true, key: b.key };
78
85
  if (path === '/api/pm-items/undo') return { ok: true, item: { id: b.id } };
79
86
  if (path === '/api/pm-items/steps/add' || path === '/api/pm-items/steps/plan') return { ok: true, step: { id: 'step-demo' + (++added), text: b.text || 'Review the plan', who: b.who || 'you', done: false } };
80
87
  if (path === '/api/pm-items/steps/who') return { ok: true, who: b.who };
@@ -44,6 +44,7 @@ import { mountTicker } from './shell-ticker.js';
44
44
  import { mountBell, openBellList } from './shell-bell.js';
45
45
  import { onBriefing, loadBriefing } from './shell-ticker.js';
46
46
  import { registry, filterPalette, rowStep } from './briefing-model.js';
47
+ import { freshnessLine } from './freshness-line.js';
47
48
 
48
49
  /* In demo mode each tab's deck asks for its own cards (the mockup draws a different deck per tab); real mode sends no scope. */
49
50
  const sc = (name) => (DEMO ? name : undefined);
@@ -105,7 +106,9 @@ function header() {
105
106
  mountCalPopupTrigger(S.tools); // the calendar button (before Search) and the key C (c3-calpop.js)
106
107
  S.moreBtn = ibtn('More', 'more', { 'aria-haspopup': 'menu', 'aria-expanded': 'false', 'data-c3pg': 'more', onclick: toggleMore });
107
108
  S.tools.append(el('span', { class: 'c3pg-menuwrap' }, S.moreBtn));
108
- return el('div', { class: 'c3top' }, el('div', null, S.ttl, S.sub), S.tools);
109
+ /* workspace-data-freshness R9: how old the sources are, quiet when they are up to date (freshness-line.js) */
110
+ S.fresh = S.demo ? null : freshnessLine(S.ws);
111
+ return el('div', { class: 'c3top' }, el('div', null, S.ttl, S.sub, S.fresh), S.tools);
109
112
  }
110
113
 
111
114
  /* the More menu (console3.html menuPop, line 1940): why each element is here, incident day, agent playbooks, phone preview; and ours: the theme and a way to
@@ -484,7 +487,7 @@ function draftsTab(pane) {
484
487
  });
485
488
  }
486
489
 
487
- const BUILD = { brief: briefTab, cal: (p) => mountCalendarViews(p, { view: 'day', plan: planNode(sc('cal')), due: dueCard(), scope: sc('cal') }), work: (p) => { const plan = planNode(sc('work') && 'work:triage'); mountPmWork(p, { plan, onLens: (l) => { if (DEMO) setPlanScope(plan, 'work:' + l); } }); }, init: initTab, rep: repTab };
490
+ const BUILD = { brief: briefTab, cal: (p) => mountCalendarViews(p, { view: 'week', page: true, scope: sc('cal') }), work: (p) => { const plan = planNode(sc('work') && 'work:triage'); mountPmWork(p, { plan, onLens: (l) => { if (DEMO) setPlanScope(plan, 'work:' + l); } }); }, init: initTab, rep: repTab };
488
491
 
489
492
  function showTab(id, push) {
490
493
  if (!BUILD[id]) id = 'brief';
@@ -18,6 +18,7 @@ import { renderCase } from './c3-case.js';
18
18
  import { makeXp, attachExpand } from './c3-expand.js';
19
19
  import { mountQuarter, mountYear, dispose as disposeStrategy } from './c3-cal-strategy.js'; // the Quarter and Year panes
20
20
  import { simDock, simStart, simState, isDemo } from './shell-recording.js';
21
+ import { drawCalendarPage } from './c3-calendar.js'; // the Calendar tab as a page about time (ctx.page)
21
22
 
22
23
  /* ── shared state of the /c3.html page (console3 wave C3) ──────────────────────────────────────────────────────────
23
24
  * `live` holds every mounted instance, so a press in the Briefing's ribbon can select a meeting on the Calendar tab's instance.
@@ -28,7 +29,8 @@ let SHARED_SEL = null; // a meeting chosen on the Briefing, read when the Cale
28
29
  const IX = () => (typeof window !== 'undefined' && window.__c3calix) || null;
29
30
  const baseId = (e) => String((e && e.id) || '').split('@')[0];
30
31
  const gotoTab = (id) => { const b = document.querySelector('[data-c3-tab="' + id + '"]'); if (b) b.click(); };
31
- const isC3 = (r) => !!(r && r.ctx && (r.ctx.ribbon || r.ctx.plan));
32
+ const isC3 = (r) => !!(r && r.ctx && (r.ctx.ribbon || r.ctx.plan || r.ctx.page));
33
+ const isCalTab = (x) => !!(x && x.ctx && (x.ctx.plan || x.ctx.page)); // the Calendar tab's own instance (the older mount passes plan, the page passes page)
32
34
 
33
35
  const NS = 'http://www.w3.org/2000/svg';
34
36
  const VIEWS = [['day', 'Day'], ['week', 'Week'], ['month', 'Month'], ['quarter', 'Quarter'], ['year', 'Year']];
@@ -564,7 +566,7 @@ const todayOf = (r) => (r.data && r.data.today) || todayStr();
564
566
  /* a day (and a meeting) on the Calendar tab's own instance; if that tab has not mounted yet its instance reads PENDING when it does */
565
567
  let PENDING = null;
566
568
  function openDay(date, sel) {
567
- const cal = [...live].find((x) => x.ctx && x.ctx.plan);
569
+ const cal = [...live].find(isCalTab);
568
570
  if (cal) { cal.sel = sel || null; cal.phase = null; go(cal.host, cal, { view: 'day', date, anim: 'zi' }); }
569
571
  else PENDING = { date, sel: sel || null };
570
572
  gotoTab('cal');
@@ -878,6 +880,7 @@ function weekHead(data) {
878
880
 
879
881
  function draw(host, r) {
880
882
  if (r.ctx && r.ctx.ribbon) { r.root.replaceChildren(ribbonCard(host, r)); if (!r.loading) r.anim = null; return; }
883
+ if (r.ctx && r.ctx.page) { disposeStrategy(r.root); r.root.replaceChildren(drawCalendarPage(host, r, PAGE)); if (!r.loading) r.anim = null; return; } // the Calendar tab (c3-calendar.js): no deck, no due list, no strategy lens
881
884
  const phone = r.root.classList.contains('ph');
882
885
  const head = bar(host, r, phone);
883
886
  let body;
@@ -904,6 +907,10 @@ function draw(host, r) {
904
907
  r.root.replaceChildren(head, body);
905
908
  }
906
909
 
910
+ /* What the Calendar page (c3-calendar.js) reuses from this file: the zoom bar with the Today chip, the reads and the date helpers. */
911
+ const PAGE = { bar, go: (host, r, change) => go(host, r, change), redraw: (host, r) => draw(host, r), shift, dayLabel, timeOf, endH, hhmm, baseId, dowOf, MN, DN,
912
+ gotoTab, goInitiative, openInit };
913
+
907
914
  /* Change the view or the date and read again. anim names the mockup's transition (.zi, .zo, .sn, .sp, console3.css). */
908
915
  function go(host, r, change) {
909
916
  if (change.view) r.view = change.view;
@@ -982,7 +989,7 @@ function wire(host, r) {
982
989
  let simOnWin = false;
983
990
  function installSim() {
984
991
  if (simOnWin || typeof window === 'undefined') return; simOnWin = true;
985
- const calInst = () => [...live].filter((x) => x.ctx && x.ctx.plan && x.root.isConnected);
992
+ const calInst = () => [...live].filter((x) => isCalTab(x) && x.root.isConnected);
986
993
  window.addEventListener('c3-sim-rec', () => calInst().forEach((x) => { if (selEvent(x)) refreshSel(x.host, x); }));
987
994
  window.addEventListener('c3-sim-open', () => { gotoTab('cal'); calInst().forEach((x) => { if (x.view !== 'day') x.view = 'day'; x.phase = 'during'; if (x.data && x.data.unit === 'day' && x.data.range.start === x.data.range.end) refreshSel(x.host, x); else go(x.host, x, { view: 'day', date: todayOf(x) }); }); });
988
995
  }
@@ -3499,6 +3499,39 @@
3499
3499
  .fr-set__envrows { display: flex; flex-direction: column; gap: 6px; }
3500
3500
  .fr-set__flags { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; min-width: 0; width: 100%; }
3501
3501
 
3502
+ /* workspace-data-freshness: the console header's freshness line and Settings › Data freshness (pm/freshness-line.js) */
3503
+ .fr-fresh {
3504
+ display: flex;
3505
+ flex-wrap: wrap;
3506
+ align-items: center;
3507
+ gap: 8px;
3508
+ font-size: 12px;
3509
+ color: var(--muted-foreground);
3510
+ }
3511
+ .fr-fresh__state--fresh { color: var(--foreground); }
3512
+ .fr-fresh__state--stale { color: var(--warning); }
3513
+ .fr-fresh__state--failed { color: var(--destructive); }
3514
+ .fr-fresh__state--never,
3515
+ .fr-fresh__state--building,
3516
+ .fr-fresh__state--off { color: var(--muted-foreground); }
3517
+ .fr-fresh-table__grid {
3518
+ width: 100%;
3519
+ border-collapse: collapse;
3520
+ font-size: 12px;
3521
+ }
3522
+ .fr-fresh-table__grid th {
3523
+ padding: 4px 8px;
3524
+ text-align: left;
3525
+ font-weight: 600;
3526
+ }
3527
+ .fr-fresh-table__grid td {
3528
+ padding: 4px 8px;
3529
+ vertical-align: top;
3530
+ }
3531
+ .fr-fresh-table__watch {
3532
+ margin-top: 8px;
3533
+ }
3534
+
3502
3535
  /* -- layouts · .td- Today (Run 3, slice 3B) -- */
3503
3536
  .td-board {
3504
3537
  display: flex;
@@ -7932,4 +7965,5 @@
7932
7965
  containers). The tab strip and the scratchpad are drawn with inline radii (workspace-cockpit-nav); the inline radii
7933
7966
  (8px tab strip, 6px controls) stay. A tab's colour dot is a dot and keeps its circle. */
7934
7967
 
7968
+ /* -- layouts · .cal3- the console3 Calendar page (calendar-time-page): its rules sit in console3.css beside the .vday rules they extend -- */
7935
7969
  /* -- end of the layout sections: add nothing below this line -- */