create-pmos 0.134.0 → 0.136.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 (211) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  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/payload/state/log.md +1 -1
  7. package/workspace/bin/notes-replay.js +235 -0
  8. package/workspace/c3-briefing-test.js +130 -0
  9. package/workspace/c3-foundation-test.js +159 -0
  10. package/workspace/c3-ib-items-test.js +105 -0
  11. package/workspace/c3-ib-more-test.js +120 -0
  12. package/workspace/c3-ib-triage-backlog-test.js +119 -0
  13. package/workspace/c3-ini-harness.js +66 -0
  14. package/workspace/c3-initiatives-test.js +140 -0
  15. package/workspace/c3-one-home-test.js +14 -1
  16. package/workspace/c3-reports-test.js +57 -18
  17. package/workspace/c3-settings-test.js +139 -0
  18. package/workspace/c3-work-test.js +42 -10
  19. package/workspace/fixtures/one-home-baseline.json +1 -9
  20. package/workspace/guide/briefing-cards.md +15 -3
  21. package/workspace/guide/briefing.md +12 -8
  22. package/workspace/guide/calendar-popup.md +2 -2
  23. package/workspace/guide/console-calendar.md +39 -10
  24. package/workspace/guide/index.md +1 -0
  25. package/workspace/guide/initiative-boards.md +65 -0
  26. package/workspace/guide/jira.md +3 -1
  27. package/workspace/guide/layout.md +2 -0
  28. package/workspace/guide/my-work.md +5 -3
  29. package/workspace/guide/notes.md +16 -1
  30. package/workspace/guide/reports-dashboards.md +29 -5
  31. package/workspace/guide/settings.md +68 -0
  32. package/workspace/guide/welcome.md +10 -10
  33. package/workspace/lib/calendar-cli.js +3 -2
  34. package/workspace/lib/calendar-layers.js +21 -10
  35. package/workspace/lib/calendar-tidy.js +230 -0
  36. package/workspace/lib/engine/boards.js +542 -0
  37. package/workspace/lib/engine/breakdown-ops.js +180 -0
  38. package/workspace/lib/engine/calendar-import.js +84 -0
  39. package/workspace/lib/engine/calendar-model.js +185 -0
  40. package/workspace/lib/engine/card-state.js +92 -0
  41. package/workspace/lib/engine/derived/derive.js +92 -0
  42. package/workspace/lib/engine/derived/plan.js +3 -0
  43. package/workspace/lib/engine/derived/trust.js +3 -0
  44. package/workspace/lib/engine/doc-health.js +142 -0
  45. package/workspace/lib/engine/doc-sheet.js +75 -0
  46. package/workspace/lib/engine/intake.js +78 -0
  47. package/workspace/lib/engine/outbox.js +86 -0
  48. package/workspace/lib/engine/plan-forecast.js +342 -0
  49. package/workspace/lib/engine/promises.js +29 -0
  50. package/workspace/lib/engine/records/index.js +114 -0
  51. package/workspace/lib/engine/releases.js +96 -0
  52. package/workspace/lib/engine/replies.js +99 -0
  53. package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
  54. package/workspace/lib/engine/report-families.js +175 -0
  55. package/workspace/lib/engine/roadmap-ops.js +150 -0
  56. package/workspace/lib/engine/sessions-list.js +27 -0
  57. package/workspace/lib/engine/signal-rules.js +95 -0
  58. package/workspace/lib/engine/signals/cards.js +57 -0
  59. package/workspace/lib/engine/signals/detectors.js +370 -0
  60. package/workspace/lib/engine/signals/facts-cli.js +8 -0
  61. package/workspace/lib/engine/signals/facts.js +197 -0
  62. package/workspace/lib/engine/signals/index.js +33 -0
  63. package/workspace/lib/engine/signals/kinds.js +110 -0
  64. package/workspace/lib/engine/signals/layout-paths.js +44 -0
  65. package/workspace/lib/engine/signals/options.js +24 -0
  66. package/workspace/lib/engine/signals/score.js +72 -0
  67. package/workspace/lib/engine/signals/select.js +47 -0
  68. package/workspace/lib/engine/step-planner.js +65 -0
  69. package/workspace/lib/engine/trust/claim-rules.json +93 -0
  70. package/workspace/lib/engine/trust/import-calendar.js +21 -0
  71. package/workspace/lib/engine/trust/import-guard.js +36 -0
  72. package/workspace/lib/engine/trust/ledger.js +53 -0
  73. package/workspace/lib/engine/trust/outbox.js +45 -0
  74. package/workspace/lib/engine/trust/plan.js +5 -0
  75. package/workspace/lib/engine/trust/trust.js +21 -0
  76. package/workspace/lib/engine/truth-table.js +57 -0
  77. package/workspace/lib/engine/workspace-lib.js +24 -0
  78. package/workspace/lib/factory-join.js +1 -1
  79. package/workspace/lib/facts-async.js +4 -4
  80. package/workspace/lib/headless.js +95 -0
  81. package/workspace/lib/import-overlay.js +2 -1
  82. package/workspace/lib/jira-status.js +30 -3
  83. package/workspace/lib/notes-capture.js +9 -1
  84. package/workspace/lib/notes-final.js +38 -13
  85. package/workspace/lib/notes-replay.js +125 -0
  86. package/workspace/lib/notes-screen.js +180 -0
  87. package/workspace/lib/notes-whisper.js +10 -3
  88. package/workspace/lib/outbox-store.js +10 -6
  89. package/workspace/lib/pmos-verbs.js +3 -1
  90. package/workspace/lib/proactive-cli.js +7 -0
  91. package/workspace/lib/proactive.js +15 -2
  92. package/workspace/lib/records-dir.js +29 -0
  93. package/workspace/lib/repo-script.js +36 -0
  94. package/workspace/lib/report-dashboards.js +5 -2
  95. package/workspace/lib/report-frame.js +122 -0
  96. package/workspace/lib/report-schedules.js +113 -0
  97. package/workspace/lib/route-modules.js +9 -2
  98. package/workspace/lib/routes-boards.js +734 -0
  99. package/workspace/lib/routes-brief-page.js +13 -4
  100. package/workspace/lib/routes-briefing.js +22 -11
  101. package/workspace/lib/routes-c3-init.js +9 -8
  102. package/workspace/lib/routes-c3-life.js +2 -1
  103. package/workspace/lib/routes-c3-reports.js +99 -15
  104. package/workspace/lib/routes-c3-strategy.js +7 -5
  105. package/workspace/lib/routes-c3-timeline.js +3 -2
  106. package/workspace/lib/routes-calendar-import.js +3 -2
  107. package/workspace/lib/routes-calendar-views.js +9 -4
  108. package/workspace/lib/routes-doc-health.js +2 -1
  109. package/workspace/lib/routes-doc-sheet.js +2 -1
  110. package/workspace/lib/routes-forecast.js +2 -1
  111. package/workspace/lib/routes-hooks.js +3 -2
  112. package/workspace/lib/routes-initiatives.js +2 -1
  113. package/workspace/lib/routes-intake.js +2 -1
  114. package/workspace/lib/routes-notes.js +55 -9
  115. package/workspace/lib/routes-outbox.js +23 -3
  116. package/workspace/lib/routes-pm-items.js +19 -6
  117. package/workspace/lib/routes-records.js +10 -7
  118. package/workspace/lib/routes-releases.js +2 -1
  119. package/workspace/lib/routes-report-facts.js +4 -3
  120. package/workspace/lib/routes-report-families.js +2 -1
  121. package/workspace/lib/routes-roadmap-ops.js +17 -14
  122. package/workspace/lib/routes-runs.js +6 -5
  123. package/workspace/lib/routes-settings.js +51 -0
  124. package/workspace/lib/routes-signals.js +3 -2
  125. package/workspace/lib/routes-stage-tools.js +220 -0
  126. package/workspace/lib/run-answers.js +2 -1
  127. package/workspace/lib/settings-store.js +226 -0
  128. package/workspace/lib/stage-tools.js +208 -0
  129. package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
  130. package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
  131. package/workspace/lib/work-evidence.js +73 -0
  132. package/workspace/package-lock.json +2 -2
  133. package/workspace/package.json +1 -1
  134. package/workspace/public/pm/briefing-deck.js +63 -17
  135. package/workspace/public/pm/briefing-model.js +36 -7
  136. package/workspace/public/pm/c3-ambient.js +100 -0
  137. package/workspace/public/pm/c3-brief-needs.js +26 -6
  138. package/workspace/public/pm/c3-brief-pulse.js +12 -4
  139. package/workspace/public/pm/c3-brief-today.js +56 -21
  140. package/workspace/public/pm/c3-brief.js +2 -1
  141. package/workspace/public/pm/c3-calendar.js +161 -42
  142. package/workspace/public/pm/c3-calpop.js +5 -5
  143. package/workspace/public/pm/c3-demo-boards.js +649 -0
  144. package/workspace/public/pm/c3-demo-brief-page.js +1 -1
  145. package/workspace/public/pm/c3-demo-cal4.js +3 -2
  146. package/workspace/public/pm/c3-demo-cal5.js +52 -0
  147. package/workspace/public/pm/c3-demo-data.js +17 -27
  148. package/workspace/public/pm/c3-demo-rep3.js +46 -5
  149. package/workspace/public/pm/c3-demo.js +2 -1
  150. package/workspace/public/pm/c3-dock.js +1 -1
  151. package/workspace/public/pm/c3-ib-docs.js +80 -0
  152. package/workspace/public/pm/c3-ib-initiatives.js +134 -0
  153. package/workspace/public/pm/c3-ib-items.js +201 -0
  154. package/workspace/public/pm/c3-ib-releases.js +51 -0
  155. package/workspace/public/pm/c3-ib-sprints.js +55 -0
  156. package/workspace/public/pm/c3-ib-triage.js +178 -0
  157. package/workspace/public/pm/c3-ii-tabs.js +240 -0
  158. package/workspace/public/pm/c3-ini-records.js +197 -0
  159. package/workspace/public/pm/c3-ini-timeline.js +278 -0
  160. package/workspace/public/pm/c3-ini-ui.js +229 -0
  161. package/workspace/public/pm/c3-ini-work.js +139 -0
  162. package/workspace/public/pm/c3-initiatives.css +218 -0
  163. package/workspace/public/pm/c3-initiatives.js +741 -0
  164. package/workspace/public/pm/c3-plan.js +2 -2
  165. package/workspace/public/pm/c3-reports.css +47 -47
  166. package/workspace/public/pm/c3-reports.js +223 -185
  167. package/workspace/public/pm/c3-settings.css +74 -0
  168. package/workspace/public/pm/c3-settings.js +703 -0
  169. package/workspace/public/pm/c3-shell.js +102 -36
  170. package/workspace/public/pm/c3-terminal-icons.js +2 -2
  171. package/workspace/public/pm/c3-terminal.js +2 -2
  172. package/workspace/public/pm/calendar-views.js +1 -1
  173. package/workspace/public/pm/components.css +319 -213
  174. package/workspace/public/pm/console3.css +99 -23
  175. package/workspace/public/pm/dest-notes.js +26 -4
  176. package/workspace/public/pm/notes-screen-capture.js +165 -0
  177. package/workspace/public/pm/notes-screen-view.js +98 -0
  178. package/workspace/public/pm/panel-trees.js +5 -2
  179. package/workspace/public/pm/pm-work-view.js +233 -146
  180. package/workspace/public/pm/shell-bell.js +3 -2
  181. package/workspace/public/pm/shell-recording.js +29 -8
  182. package/workspace/public/pm/tokens.css +2 -2
  183. package/workspace/public/pm/view-notes.js +46 -9
  184. package/workspace/public/pm/welcome.js +197 -89
  185. package/workspace/public/welcome.html +6 -4
  186. package/workspace/server.js +105 -6
  187. package/workspace/skills/day-check.skill +1 -1
  188. package/workspace/skills/jira-sync.skill +6 -4
  189. package/workspace/skills/morning-brief.skill +1 -1
  190. package/workspace/test-files.json +12 -3
  191. package/workspace/test-lists/c3-ib-items.json +9 -0
  192. package/workspace/test-lists/c3-ib-more.json +9 -0
  193. package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
  194. package/workspace/test-lists/c3-initiative-boards.json +6 -0
  195. package/workspace/test-lists/calendar-tidy-up.json +6 -0
  196. package/workspace/test-lists/console3-briefing.json +9 -0
  197. package/workspace/test-lists/console3-foundation.json +9 -0
  198. package/workspace/test-lists/console3-reports.json +3 -3
  199. package/workspace/test-lists/meeting-notes.json +10 -4
  200. package/workspace/test-lists/workspace-headless-jobs.json +5 -0
  201. package/workspace/test-lists/workspace-package-script-closure.json +6 -0
  202. package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
  203. package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
  204. package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
  205. package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
  206. package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
  207. package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
  208. package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
  209. package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
  210. package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
  211. package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
@@ -15,7 +15,8 @@
15
15
  * and a failed POST is toasted while the page keeps the change
16
16
  * reload a stored layout is there after a reload
17
17
  * share the frame (Headline, Sections, Needs your call, Sources) for Leadership and for Team; Save as draft POSTs ONE draft;
18
- * the schedule control is disabled and says why
18
+ * the schedule: three options, the next draft, the warning that nothing is sent; Save schedule POSTs { key, plan, audience }
19
+ * and the head and Drafts > Scheduled show it; a draft says whether a schedule or you made it
19
20
  * library one row per report with data-home, never on the dashboard; a widget carries data-ref-row and opens its case in place
20
21
  * drafts the Drafts tab shows the report families
21
22
  * phone 400px: one column, no horizontal page scroll
@@ -136,6 +137,14 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
136
137
  const tabs = await page.$$eval('[data-rp3] [role="tab"]', (a) => a.map((b) => [b.getAttribute('data-rp3-tab'), b.textContent.trim(), b.getAttribute('aria-selected')]));
137
138
  ok('three tabs: Dashboards, Library, Drafts, with Dashboards selected', tabs.length === 3 && tabs[0][0] === 'dash' && /^Dashboards/.test(tabs[0][1]) && tabs[0][2] === 'true' && /^Library/.test(tabs[1][1]) && /^Drafts/.test(tabs[2][1]), JSON.stringify(tabs));
138
139
 
140
+ ok('the tab row is the mockup\'s calbar: tabs on the left, the scope pills on the same row (no intro paragraph)', await until(() => page.evaluate(() => {
141
+ const bar = document.querySelector('[data-rp3] > .calbar'); if (!bar || !bar.querySelector("[data-rp3-scope]")) return false;
142
+ const t = bar.querySelector('.rp3-tabs').getBoundingClientRect(); const s = bar.querySelector('[data-rp3-scope]').getBoundingClientRect();
143
+ return Math.abs(t.top - s.top) < 40 && !document.querySelector('.rp3-intro') && bar.querySelectorAll('.pill').length === 3;
144
+ })));
145
+ ok('the Library tab carries its count as a badge', /7/.test(await page.textContent('[data-rp3-tab="lib"] .bdg')));
146
+ if (mode === 'shell') ok('the headline deck sits above the tabs (shell)', !!(await until(() => page.$('[data-rp3-deck] .deck > *'))) && await page.evaluate(() => document.querySelector('[data-rp3-deck]').getBoundingClientRect().top < document.querySelector('[data-rp3] > .calbar').getBoundingClientRect().top));
147
+
139
148
  /* ── portfolio ── */
140
149
  ok('Portfolio draws its seven default reports, each a builder card', !!(await loaded(page, 7)), JSON.stringify(await order(page)));
141
150
  ok('the portfolio order is the default order', JSON.stringify(await order(page)) === JSON.stringify(DEF_P), JSON.stringify(await order(page)));
@@ -149,7 +158,7 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
149
158
 
150
159
  /* ── scopes, the chooser, and one read per scope ── */
151
160
  await spy(page);
152
- await page.click(D + ' [data-rp3-scope="board"]');
161
+ await page.click('[data-rp3-scope="board"]');
153
162
  ok('Board shows a chooser of the boards', !!(await until(async () => (await page.$$eval(D + ' [data-rp3-pick="board"] option', (a) => a.map((o) => o.value))).join(',') === 'shop,enterprise')));
154
163
  ok('Board draws its five default reports', !!(await loaded(page, 5)), JSON.stringify(await order(page)));
155
164
  const bRows = await page.$$eval(D + ' [data-rp3-w="delivery"] [data-cs-row]', (a) => a.map((g) => g.getAttribute('data-cs-row')));
@@ -160,7 +169,7 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
160
169
  await loaded(page, 5);
161
170
  const eRows = await page.$$eval(D + ' [data-rp3-w="delivery"] [data-cs-row]', (a) => a.map((g) => g.getAttribute('data-cs-row')));
162
171
  ok('Board Enterprise: delivery shows only SSO', JSON.stringify(eRows) === '["sso"]', JSON.stringify(eRows));
163
- await page.click(D + ' [data-rp3-scope="initiative"]');
172
+ await page.click('[data-rp3-scope="initiative"]');
164
173
  ok('Initiative shows a chooser of the five initiatives', !!(await until(async () => (await page.$$(D + ' [data-rp3-pick="initiative"] option')).length === 5)));
165
174
  await page.selectOption(D + ' [data-rp3-pick="initiative"]', 'sso');
166
175
  await until(async () => /Initiative: SSO/.test(await page.textContent(D + ' [data-rp3-title]')));
@@ -171,15 +180,15 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
171
180
  await until(async () => /onboarding/.test(await page.textContent(D + ' [data-rp3-title]')));
172
181
  await loaded(page, 4);
173
182
  ok('a scope with nothing for a report draws that report\'s own empty card', !!(await page.$(D + ' [data-rp3-w="signals"] [data-cs-empty="true"]')));
174
- await page.click(D + ' [data-rp3-scope="portfolio"]'); await loaded(page, 7);
175
- await page.click(D + ' [data-rp3-scope="board"]'); await loaded(page, 5);
183
+ await page.click('[data-rp3-scope="portfolio"]'); await loaded(page, 7);
184
+ await page.click('[data-rp3-scope="board"]'); await loaded(page, 5);
176
185
  const reads = await page.evaluate(() => window.__rp3spy.reads);
177
186
  ok('the data route is read once per scope and id, and not again within the minute', JSON.stringify(reads) === JSON.stringify(['board|shop', 'board|enterprise', 'initiative|checkout', 'initiative|sso', 'initiative|onb']), JSON.stringify(reads));
178
187
  await page.click(D + ' [data-rp3-edit]');
179
188
  const freeB = await page.$$eval(D + ' [data-rp3-free]', (a) => a.map((n) => n.getAttribute('data-rp3-free')));
180
189
  ok('the Board add list holds only reports that fit a board (no north star, no maturity)', !freeB.includes('north') && !freeB.includes('maturity'), JSON.stringify(freeB));
181
190
  await page.click(D + ' [data-rp3-edit]');
182
- await page.click(D + ' [data-rp3-scope="portfolio"]'); await loaded(page, 7);
191
+ await page.click('[data-rp3-scope="portfolio"]'); await loaded(page, 7);
183
192
 
184
193
  /* ── edit ── */
185
194
  await spy(page);
@@ -229,31 +238,34 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
229
238
  ok('Reset POSTs reports: null', P.length === 1 && P[0].body.reports === null && P[0].body.key === 'portfolio', JSON.stringify(P));
230
239
  await page.click(D + ' [data-rp3-edit]');
231
240
  // a board layout is kept per key
232
- await page.click(D + ' [data-rp3-scope="board"]'); await loaded(page, 5);
241
+ await page.click('[data-rp3-scope="board"]'); await loaded(page, 5);
233
242
  await page.click(D + ' [data-rp3-edit]'); await page.click(D + ' [data-rp3-rm="synergy"]'); await until(async () => (await order(page)).length === 4);
234
243
  await page.click(D + ' [data-rp3-edit]');
235
244
  P = await posts(page, '/api/report-dashboards/layout');
236
245
  ok('a board layout is POSTed under board:<id>', P.length === 2 && P[1].body.key === 'board:shop', JSON.stringify(P.map((q) => q.body.key)));
237
- await page.click(D + ' [data-rp3-scope="portfolio"]'); await loaded(page, 7);
246
+ await page.click('[data-rp3-scope="portfolio"]'); await loaded(page, 7);
238
247
  ok('the portfolio keeps its own layout when a board changes', JSON.stringify(await order(page)) === JSON.stringify(DEF_P));
239
248
 
240
249
  /* ── share ── */
250
+ ok('the head says the portfolio is scheduled, draft only (the demo seeds a monthly schedule)', /Scheduled: Monthly, 1st at 08:00, draft only/.test(await page.textContent(D + ' [data-rp3-schedline]')));
241
251
  await spy(page);
242
252
  await page.click(D + ' [data-rp3-share]');
243
253
  const sheet = await until(() => page.$('[data-rp3-sheet] [data-rp3-frame="Headline"]'));
244
254
  ok('Share opens a sheet with the report frame', !!sheet);
245
- const frames = await page.$$eval('[data-rp3-sheet] [data-rp3-frame] h4', (a) => a.map((h) => h.textContent));
255
+ const frames = await page.$$eval('[data-rp3-sheet] [data-rp3-frame] h2', (a) => a.map((h) => h.textContent.replace(/\d+$/, '')));
246
256
  ok('the frame has Headline, Sections, Needs your call, Sources', JSON.stringify(frames) === JSON.stringify(['Headline', 'Sections', 'Needs your call', 'Sources']), JSON.stringify(frames));
247
257
  ok('Leadership: one sentence per report, no numbers', await page.$eval('[data-rp3-sheet] [data-rp3-aud]', (n) => n.getAttribute('data-rp3-aud')) === 'Leadership'
248
- && (await page.$$('[data-rp3-sheet] [data-rp3-frame="Sections"] h5')).length === 7 && !(await page.$('[data-rp3-sheet] [data-rp3-nums]')));
258
+ && (await page.$$('[data-rp3-sheet] [data-rp3-frame="Sections"] ul.chgl > li')).length === 7 && !(await page.$('[data-rp3-sheet] [data-rp3-nums]')));
249
259
  const need = await page.textContent('[data-rp3-sheet] [data-rp3-frame="Needs your call"]');
250
260
  ok('Needs your call lists what the reports flag', /Conversion is off course/.test(need) && /Checkout v2 is delayed/.test(need), need.slice(0, 200));
251
261
  await page.click('[data-rp3-sheet] [data-rp3-audbtn="Team"]');
252
262
  ok('Team adds the numbers behind each sentence', !!(await until(async () => (await page.$$('[data-rp3-sheet] [data-rp3-nums]')).length >= 5)));
253
263
  ok('the Team numbers come from the records (Conversion now 3.1%)', /Conversion: now 3\.1%, should be 3\.75%, target 4%/.test(await page.textContent('[data-rp3-sheet] [data-rp3-nums="outcomes"]')));
254
- const sched = await page.$('[data-rp3-sheet] [data-rp3-sched]');
255
- ok('the schedule control is disabled', !!sched && await sched.evaluate((s) => s.disabled));
256
- ok('the schedule says sending on a schedule is not available, and why', /not available yet, because nothing is sent outward/.test(await page.textContent('[data-rp3-sheet] [data-rp3-schedule]')));
264
+ const sched = await page.$$eval('[data-rp3-sheet] [data-rp3-sched] button', (a) => a.map((b) => [b.disabled, b.getAttribute('aria-pressed'), b.textContent.trim()]));
265
+ ok('the schedule control has its three options, enabled, the saved one (Monthly) pressed', sched.length === 3 && sched.every((x) => !x[0]) && sched[2][1] === 'true' && sched[2][2] === 'Monthly, 1st at 08:00', JSON.stringify(sched));
266
+ ok('the next draft is the 1st at 08:00', /^Next draft: \w{3} 1 \w{3}, 08:00$/.test((await page.textContent('[data-rp3-sheet] [data-rp3-next]')).trim()));
267
+ ok('the warning says a schedule makes a ready draft and nothing is sent to the team', /A scheduled send makes a ready draft and notifies you\. Nothing is sent to the team until you connect a channel\./.test(await page.textContent('[data-rp3-sheet] #rp3-sched-why')));
268
+ ok('Save schedule is a primary button', !!(await page.$('[data-rp3-sheet] [data-rp3-sched-save]:not([disabled])')));
257
269
  await page.click('[data-rp3-sheet] [data-rp3-draft]');
258
270
  ok('Save as draft says one draft is in the outbox and nothing was sent', !!(await until(async () => /one draft in the outbox\. Nothing was sent/.test(await page.textContent('[data-rp3-sheet] [data-rp3-share-msg]')))));
259
271
  ok('Save as draft is off once the draft is saved', await page.$eval('[data-rp3-sheet] [data-rp3-draft]', (b) => b.disabled));
@@ -262,11 +274,19 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
262
274
  ok('ONE draft POST, to a known channel, for the chosen audience, its claim lines labelled', P.length === 1 && B.channel === 'customer-note' && B.recipient === 'Team' && /^Portfolio for Team/.test(B.body)
263
275
  && B.body.split('\n').filter((l) => /^- /.test(l)).every((l) => /\[reported\]$/.test(l)), JSON.stringify(B).slice(0, 300));
264
276
  ok('nothing else was POSTed from the sheet (no send, no schedule)', (await page.evaluate(() => window.__rp3spy.posts.length)) === 1);
277
+ await page.click('[data-rp3-sheet] [data-sched="weekly"]');
278
+ ok('choosing Weekly shows a Monday as the next draft', !!(await until(async () => /^Next draft: Mon \d+ \w{3}, 08:00$/.test(((await page.textContent('[data-rp3-sheet] [data-rp3-next]').catch(() => '')) || '').trim()))));
279
+ ok('choosing is not saving: nothing is POSTed yet', (await page.evaluate(() => window.__rp3spy.posts.length)) === 1);
280
+ await page.click('[data-rp3-sheet] [data-rp3-sched-save]');
281
+ ok('Save schedule says you get a draft and a notification', !!(await until(async () => /Scheduled: Weekly, Mondays 08:00\. You get a draft and a notification\./.test(await page.textContent('[data-rp3-sheet] [data-rp3-share-msg]')))));
282
+ P = await posts(page, '/api/report-dashboards/schedule');
283
+ ok('Save schedule POSTs ONE schedule: the key, the plan and the sheet\'s audience', P.length === 1 && P[0].body.key === 'portfolio' && P[0].body.plan === 'weekly' && P[0].body.audience === 'Team', JSON.stringify(P));
284
+ ok('the head now says Weekly, draft only', !!(await until(async () => /Scheduled: Weekly, Mondays 08:00, draft only/.test(await page.textContent(D + ' [data-rp3-schedline]')))));
265
285
  await page.keyboard.press('Escape');
266
286
  ok('Escape closes the sheet', !!(await until(async () => !(await page.$('[data-rp3-sheet]')))));
267
287
 
268
288
  /* ── library and one home ── */
269
- ok('the dashboard has no data-home row; every widget carries data-ref-row', !(await page.$(D + ' [data-home]')) && (await page.$$(D + ' article[data-ref-row]')).length === 7);
289
+ ok('the dashboard has no data-home row; every widget carries data-ref-row', !(await page.$(D + ' [data-home]')) && (await page.$$(D + ' section.rp3-w[data-ref-row]')).length === 7);
270
290
  await page.click(D + ' [data-rp3-about="outcomes"]');
271
291
  ok('a widget opens its case in place (question, source, where used)', !!(await until(async () => { const x = await page.$(D + ' [data-rp3-w="outcomes"] .xp.open'); return x && /Used on: Portfolio/.test(await x.textContent()); })));
272
292
  await page.click(D + ' [data-rp3-golib="outcomes"]');
@@ -276,15 +296,34 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
276
296
  ok('the library has one row per report, each its home', libRows.length === 7 && new Set(libRows).size === 7, JSON.stringify(libRows));
277
297
  ok('library rows are not references', !(await page.$(L + ' [data-ref-row]')));
278
298
  const lrow = await page.textContent(L + ' [data-home="maturity"]');
279
- ok('a library row shows the question, the source and where it is used', /Where does each pillar stand/.test(lrow) && /Pillar maturity records/.test(lrow) && /Used on: Portfolio/.test(lrow), lrow.slice(0, 200));
299
+ ok('a library row shows the question, the source and where it is used (short: On: Portfolio)', /Where does each pillar stand/.test(lrow) && /Pillar maturity records/.test(lrow) && /On: Portfolio/.test(lrow), lrow.slice(0, 200));
280
300
  ok('the row opened from the widget is open, with its card', !!(await until(() => page.$(L + ' [data-home="outcomes"][aria-expanded="true"] [data-cs="outcomes"]'))));
281
301
  await page.click(L + ' [data-home="bets"]');
282
302
  ok('a library row opens the report\'s card in place', !!(await until(() => page.$(L + ' [data-home="bets"] .xp.open [data-cs="bets"]'))));
283
- ok('a board layout shows in where it is used', /Board dashboards by default/.test(await page.textContent(L + ' [data-home="synergy"]')));
303
+ ok('a board layout shows in where it is used', /On: .*Board/.test(await page.textContent(L + ' [data-home="synergy"]')));
284
304
 
285
305
  /* ── drafts ── */
306
+ const DR = '[data-rp3-pane="drafts"]';
307
+ ok('the Drafts tab carries a count once drafts are read (the seeded two plus the one just made)', !!(await until(async () => (await page.textContent('[data-rp3-draftcount]').catch(() => '')) === '3')));
286
308
  await page.click('[data-rp3-tab="drafts"]');
287
- ok('the Drafts tab shows the report families', !!(await until(() => page.$('[data-rp3-pane="drafts"] [data-report-families] [data-family]'), 15000)));
309
+ ok('the Drafts tab says nothing here is sent, and that a schedule only makes a draft', !!(await until(async () => /Nothing here is sent\. No sender channel is connected yet, so a schedule only makes a ready draft and notifies you\./.test(await page.textContent(DR + ' .fold.w')))));
310
+ ok('the dashboard drafts are listed, with Preview and Discard, newest first', !!(await until(async () => (await page.$$(DR + ' [data-draft-row]')).length === 3)) && (await page.textContent(DR + ' [data-draft-row]')).includes('Portfolio') && (await page.$$(DR + ' [data-draft="preview"]')).length === 3);
311
+ const dtext = await page.textContent(DR + ' [data-draft-row]');
312
+ ok('a draft row shows its audience and that it was not sent', /For Team/.test(dtext) && /Not sent/.test(dtext), dtext);
313
+ const sch = await page.textContent(DR + ' [data-rp3-scheduled]');
314
+ ok('Scheduled lists the portfolio schedule, its audience and its next draft', /Portfolio/.test(sch) && /Weekly, Mondays 08:00/.test(sch) && /For Team/.test(sch) && /Next draft Mon \d+ \w{3}, 08:00/.test(sch), sch);
315
+ ok('the drafts say who made them: a schedule or you', (await page.$$(DR + ' [data-rp3-madeby="schedule"]')).length === 1 && (await page.$$(DR + ' [data-rp3-madeby="you"]')).length === 2
316
+ && /Made by schedule/.test(await page.textContent(DR + ' [data-draft-row="out-demo-rep3-s1"]')));
317
+ await page.click(DR + ' [data-draft="preview"]');
318
+ ok('Preview opens the draft text in a sheet', !!(await until(async () => /Headline:/.test((await page.textContent('[data-rp3-draft-text]').catch(() => '')) || ''))));
319
+ await page.keyboard.press('Escape');
320
+ await spy(page);
321
+ await page.click(DR + ' [data-draft="discard"]');
322
+ ok('Discard takes the draft off the list and POSTs the discard', !!(await until(async () => (await page.$$(DR + ' [data-draft-row]')).length === 2)) && (await posts(page, '/api/outbox/discard')).length === 1);
323
+ await page.click('[data-c3-toast-action]');
324
+ ok('Undo brings the draft back', !!(await until(async () => (await page.$$(DR + ' [data-draft-row]')).length === 3)));
325
+ ok('Drafts carries what the Briefing used to: Handled, What I could stop, Data you can lean on (the demo has them)', !!(await until(() => page.$(DR + ' [data-rp3-more] [data-c3pg="handled"]'))) && !!(await page.$(DR + ' [data-rp3-more] [data-c3pg="trust"]')));
326
+ ok('Reports to send holds the report families', !!(await until(() => page.$(DR + ' .card.sect [data-report-families] [data-family]'), 15000)) && /Reports to send/.test(await page.textContent(DR + ' [data-rp3-more] ~ .card.sect > .sh2')));
288
327
  ok('the dashboards are hidden while Drafts is open', await page.$eval(D, (n) => n.hidden));
289
328
  await page.click('[data-rp3-tab="dash"]');
290
329
 
@@ -296,7 +335,7 @@ const DEF_P = ['north', 'outcomes', 'signals', 'delivery', 'bets', 'synergy', 'm
296
335
  ok('400px: no horizontal page scroll on the dashboard', await noScroll(pp), String(await pp.evaluate(() => document.documentElement.scrollWidth)));
297
336
  const lefts = await pp.$$eval(D + ' [data-rp3-w]', (a) => [...new Set(a.map((w) => Math.round(w.getBoundingClientRect().left)))]);
298
337
  ok('400px: the widgets stack in one column', lefts.length === 1, JSON.stringify(lefts));
299
- await pp.click(D + ' [data-rp3-scope="initiative"]'); await loaded(pp, 4);
338
+ await pp.click('[data-rp3-scope="initiative"]'); await loaded(pp, 4);
300
339
  await pp.click(D + ' [data-rp3-edit]');
301
340
  ok('400px: Initiative scope in edit mode has no horizontal scroll', await noScroll(pp));
302
341
  await pp.click(D + ' [data-rp3-edit]');
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /* c3-settings-test.js — the console3 Settings page (initiative console3-settings).
4
+ *
5
+ * Loads /c3.html against a real server with its own state directory and checks: the gear, the More menu item and
6
+ * ?tab=settings open Settings; every section draws; search finds a setting across sections; a change to a store setting
7
+ * is saved through POST /api/settings and read back; the theme switch sets the page theme; the start page and the
8
+ * time-of-day switches take effect on the next load; and the page throws no error.
9
+ */
10
+ const path = require('path');
11
+ const { spawn } = require('child_process');
12
+ const { findBrowser } = require('./lib/find-browser');
13
+ const T = require('./test-run').configure({ name: 'c3-settings' });
14
+
15
+ const BASE = T.BASE;
16
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
17
+ const results = [];
18
+ const ok = (name, cond, extra = '') => { results.push({ name, cond: !!cond }); console.log(`${cond ? '✓' : '✗'} ${name}${!cond && extra !== '' ? ' — ' + extra : ''}`); };
19
+ let srv = null; let browser = null;
20
+ const finish = async (code) => {
21
+ if (browser) await browser.close().catch(() => {});
22
+ if (srv) { try { srv.kill('SIGKILL'); } catch { /* gone */ } }
23
+ T.killTmux();
24
+ const bad = results.filter((x) => !x.cond).length;
25
+ console.log(`\n${results.length - bad}/${results.length} c3-settings assertions passed`);
26
+ process.exit(code !== undefined ? code : (bad ? 1 : 0));
27
+ };
28
+ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
29
+
30
+ const SECTIONS = ['about', 'workspaces', 'boards', 'connectors', 'recording', 'time', 'sharing', 'access', 'sessions', 'privacy', 'appearance', 'updates'];
31
+
32
+ async function waitFor(page, fn, arg, ms = 8000) {
33
+ try { await page.waitForFunction(fn, arg, { timeout: ms }); return true; } catch { return false; }
34
+ }
35
+
36
+ (async () => {
37
+ const exe = findBrowser();
38
+ if (!exe) { console.log('✗ no Chrome/Chromium on this machine — the c3-settings suite cannot run'); return finish(1); }
39
+ let chromium;
40
+ try { ({ chromium } = require('playwright-core')); } catch { console.log('✗ playwright-core is not installed — the c3-settings suite cannot run'); return finish(1); }
41
+
42
+ srv = spawn(process.execPath, [path.join(__dirname, 'server.js')], { cwd: __dirname, stdio: 'ignore', env: T.env({ PMOS_WS_PORT: String(T.PORT) }) });
43
+ await T.waitForServer(BASE);
44
+ browser = await chromium.launch({ executablePath: exe });
45
+ const ctx = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
46
+ const page = await ctx.newPage();
47
+ const errors = [];
48
+ page.on('pageerror', (e) => errors.push(String(e && e.message || e)));
49
+ page.on('dialog', (d) => d.dismiss().catch(() => {}));
50
+
51
+ /* ── A: the ways in ── */
52
+ await page.goto(BASE + '/c3.html');
53
+ ok('A1 the header has a Settings gear', await waitFor(page, () => !!document.querySelector('[data-c3pg="settings"]')));
54
+ await page.click('[data-c3pg="settings"]');
55
+ ok('A2 the gear opens Settings', await waitFor(page, () => { const p = document.querySelector('[data-c3-pane="settings"]'); return p && !p.hidden && !!p.querySelector('[data-c3set]'); }));
56
+ ok('A3 the title says Settings', (await page.textContent('[data-c3pg="title"]')).trim() === 'Settings');
57
+ ok('A4 the address names the page', new URL(page.url()).searchParams.get('tab') === 'settings');
58
+ ok('A5 no tab is selected while Settings shows', await page.evaluate(() => ![...document.querySelectorAll('[role="tab"]')].some((b) => b.getAttribute('aria-selected') === 'true')));
59
+ await page.click('[data-c3-tab="work"]');
60
+ ok('A6 a tab press leaves Settings', await waitFor(page, () => document.querySelector('[data-c3-pane="settings"]').hidden && !document.querySelector('[data-c3-pane="work"]').hidden));
61
+ await page.click('[data-c3pg="more"]');
62
+ ok('A7 the More menu has Settings', await waitFor(page, () => !!document.querySelector('.c3pg-menu [data-a="settings"]')));
63
+ await page.click('.c3pg-menu [data-a="settings"]');
64
+ ok('A8 the More menu item opens Settings', await waitFor(page, () => !document.querySelector('[data-c3-pane="settings"]').hidden));
65
+
66
+ /* ── B: every section draws ── */
67
+ await waitFor(page, () => !!document.querySelector('[data-c3set-section] .st-row'));
68
+ ok('B0 Settings opens on About you, drawn as console3 section cards', await waitFor(page, () => !!document.querySelector('[data-c3set-section="about"] .card.sect .list .nrow.st-row #c3set-you-name')));
69
+ ok('B1 the nav lists twelve sections', (await page.$$eval('[data-c3set-nav] [data-c3set-sec]', (b) => b.length)) === SECTIONS.length);
70
+ for (const id of SECTIONS) {
71
+ await page.click(`[data-c3set-nav] [data-c3set-sec="${id}"]`);
72
+ const drew = await waitFor(page, (s) => { const x = document.querySelector('[data-c3set-section="' + s + '"]'); return x && x.querySelectorAll('.st-row, .sect, .fold').length > 0; }, id, 4000);
73
+ ok(`B2 section ${id} draws`, drew);
74
+ }
75
+
76
+ /* ── C: search ── */
77
+ await page.fill('[data-c3set-search]', 'language');
78
+ ok('C1 search finds the recording language', await waitFor(page, () => !!document.querySelector('[data-c3set-body] [data-c3set-row="notes.language"]')));
79
+ await page.fill('[data-c3set-search]', 'zzqx');
80
+ ok('C2 search with no match says so', await waitFor(page, () => !!document.querySelector('[data-c3set-none]')));
81
+ await page.fill('[data-c3set-search]', '');
82
+
83
+ /* ── D: a store setting is saved and read back ── */
84
+ await page.click('[data-c3set-nav] [data-c3set-sec="time"]');
85
+ await waitFor(page, () => !!document.querySelector('#c3set-time\\.workStart'));
86
+ await page.selectOption('#c3set-time\\.workStart', '8');
87
+ const saved = await waitFor(page, async () => { const r = await fetch('/api/settings'); const j = await r.json(); const s = j.settings.find((x) => x.key === 'time.workStart'); return s && s.value === 8 && s.changed; });
88
+ ok('D1 a change is saved through the settings API', saved);
89
+ ok('D1b the save says so in a toast with Undo', await waitFor(page, () => { const t = document.querySelector('[data-c3-toast]'); return !!t && /Saved: Working hours start/.test(t.textContent) && !!t.querySelector('[data-c3-toast-action]'); }));
90
+ ok('D2 the section shows the change', await waitFor(page, () => !!document.querySelector('[data-c3set-row="time.workStart"] .st-dot')));
91
+ ok('D3 the nav counts the change', await waitFor(page, () => { const b = document.querySelector('[data-c3set-sec="time"] .bdg'); return b && b.textContent === '1'; }));
92
+ ok('D3b the section offers Reset section', await waitFor(page, () => !!document.querySelector('[data-c3set-reset-sec="time"]')));
93
+ const rs = await page.$('[data-c3set-row="time.workStart"] [data-c3set-reset]');
94
+ if (rs) await rs.click();
95
+ ok('D4 Reset puts the default back', await waitFor(page, async () => { const j = await (await fetch('/api/settings')).json(); const s = j.settings.find((x) => x.key === 'time.workStart'); return s && s.value === 9 && !s.changed; }));
96
+ const refused = await page.evaluate(async () => { const { pmPost } = await import('/pm/http.js'); return pmPost('/api/settings', { key: 'time.workStart', value: 99 }); });
97
+ ok('D5 a value out of range is refused', refused && refused.ok === false && !/page only/.test(refused.error || ''), JSON.stringify(refused));
98
+
99
+ /* ── D6: About you saves to the proactive settings the Welcome page uses ── */
100
+ await page.click('[data-c3set-nav] [data-c3set-sec="about"]');
101
+ await waitFor(page, () => !!document.querySelector('#c3set-you-role'));
102
+ await page.fill('#c3set-you-role', 'Senior PM, Payments');
103
+ await page.press('#c3set-you-role', 'Tab');
104
+ ok('D6 About you saves the role', await waitFor(page, async () => { const j = await (await fetch('/api/proactive/settings')).json(); return j.settings && j.settings.about && j.settings.about.role === 'Senior PM, Payments'; }));
105
+
106
+ /* ── D7: a feature PMOS does not have yet shows as a row with its control off (decision 9) ── */
107
+ await page.click('[data-c3set-nav] [data-c3set-sec="access"]');
108
+ ok('D7 phone pairing shows, turned off, with the reason', await waitFor(page, () => { const r = document.querySelector('[data-c3set-row="access.pair"]'); return !!r && !!r.querySelector('button:disabled') && !!r.querySelector('.fold'); }));
109
+
110
+ /* ── E: theme ── */
111
+ await page.click('[data-c3set-nav] [data-c3set-sec="appearance"]');
112
+ await waitFor(page, () => !!document.querySelector('[data-c3set-row="look.theme"] [data-v="dark"]'));
113
+ await page.click('[data-c3set-row="look.theme"] [data-v="dark"]');
114
+ ok('E1 the theme switch sets the page theme', await waitFor(page, () => document.documentElement.getAttribute('data-pm-page-theme') === 'dark'));
115
+ await page.click('[data-c3set-row="look.theme"] [data-v="light"]');
116
+
117
+ /* ── F: start page and time of day take effect on the next load ── */
118
+ const post = (key, value) => page.evaluate(async ([k, v]) => { const { pmPost } = await import('/pm/http.js'); return pmPost('/api/settings', { key: k, value: v }); }, [key, value]);
119
+ const f0 = [await post('look.start', 'cal'), await post('time.tint', false), await post('time.strip', false)];
120
+ ok('F0 the start page and switches save', f0.every((r) => r && r.ok), JSON.stringify(f0));
121
+ await page.goto(BASE + '/c3.html');
122
+ ok('F1 the saved start page opens', await waitFor(page, () => { const p = document.querySelector('[data-c3-pane="cal"]'); return p && !p.hidden; }));
123
+ ok('F2 the tint switch reaches the page', await waitFor(page, () => document.getElementById('c3-root').getAttribute('data-c3-tint') === 'off'));
124
+ ok('F3 the daylight strip is hidden', await waitFor(page, () => { const s = document.querySelector('.c3pg-daylight'); return !s || getComputedStyle(s).display === 'none'; })); // the stylesheet loads after the attribute is set
125
+ await page.goto(BASE + '/c3.html?tab=settings');
126
+ ok('F4 ?tab=settings opens Settings directly', await waitFor(page, () => { const p = document.querySelector('[data-c3-pane="settings"]'); return p && !p.hidden && !!p.querySelector('[data-c3set]'); }));
127
+ await post('look.start', 'brief'); await post('time.tint', true); await post('time.strip', true);
128
+
129
+ /* ── G: the palette ── */
130
+ await page.goto(BASE + '/c3.html');
131
+ await waitFor(page, () => !!document.querySelector('[data-c3pg="search"]'));
132
+ await page.click('[data-c3pg="search"]');
133
+ await sleep(200);
134
+ await page.keyboard.type('Settings');
135
+ ok('G1 the palette lists Settings', await waitFor(page, () => [...document.querySelectorAll('[role="option"], .pal li, .pal button, [data-pal-item]')].some((x) => /Settings/.test(x.textContent)), null, 3000));
136
+
137
+ ok('H1 the page throws no error', errors.length === 0, errors.slice(0, 3).join(' | '));
138
+ finish();
139
+ })();
@@ -5,7 +5,7 @@
5
5
  * so nothing reaches the server and the fixture (public/pm/c3-demo-data.js pmItems) is placed by the same rule the server uses.
6
6
  *
7
7
  * views six views in order (Inbox, Today, Upcoming, Waiting, Someday, Review), each with its count; no Triage, Week, Later
8
- * lens and no Flow today card
8
+ * lens; the right column is Focus on every view, with no Flow today (console3-deck-flow-fixes); no Jobs card, Group by job, ring or plan deck (console3-work-parity)
9
9
  * one home none of the decisions and approvals whose home is Briefing · Needs you is a Work row or Work text
10
10
  * Focus Today opens with up to three suggestions, each with the reason; Accept pins, Not today hides, Edit title saves, Unpin
11
11
  * offline an item done outside PMOS shows "outside PMOS" and the person; Looks done? has Confirm (with Undo); Not yet moves it
@@ -46,7 +46,9 @@ const LABELS = ['Inbox', 'Today', 'Upcoming', 'Waiting', 'Someday', 'Review'];
46
46
  const DECISIONS = ['Approve pricing-page copy before handoff', 'Confirm the growth assumption in the Q4 forecast', 'Say yes to Sam: ship the logging change',
47
47
  'Self-serve onboarding: continue or park?', 'Pick interview slots with 3 customers', 'Assign owners to 2 unowned workshop actions',
48
48
  'Confirm the 1 Oct numbers before the 10:30 update', 'Accept the 15 Oct interview invite', 'Review the 13:00 interview guide',
49
- 'Cut Checkout v2 to 3 core flows to hold 24 Oct?', 'Reply to Marta: SSO 14 Oct, SCIM in Nov', 'Weekly status for leadership', 'Prep note for 10:30 Checkout sync'];
49
+ 'Cut Checkout v2 to 3 core flows to hold 24 Oct?'];
50
+ /* The agent's drafts moved from Briefing to the Review view of Work (console3-work-parity R10): they are not Work items, but they show in Review. */
51
+ const DRAFTS = ['Reply to Marta: SSO 14 Oct, SCIM in Nov', 'Weekly status for leadership', 'Prep note for 10:30 Checkout sync'];
50
52
  const W = '[data-pm-work]';
51
53
 
52
54
  (async () => {
@@ -90,7 +92,10 @@ const W = '[data-pm-work]';
90
92
  ok('each view shows its count (3, 5, 6, 2, 2, 3)', JSON.stringify(VIEWS.map((v) => c0[v])) === '[3,5,6,2,2,3]', JSON.stringify(c0));
91
93
  ok('the page opens on Today', await st((w) => (document.querySelector(w + ' [data-pm-lens-head]') || {}).getAttribute && document.querySelector(w + ' [data-pm-lens-head]').getAttribute('data-pm-lens-head') === 'today', W));
92
94
  const text0 = await st((w) => document.querySelector(w).textContent, W);
93
- ok('no Triage, Week or Later view and no Flow today card', !/Triage|Flow today/.test(text0) && !(await st((w) => !!document.querySelector(w + ' [data-pm-flow], ' + w + ' [data-lens="triage"], ' + w + ' [data-lens="week"], ' + w + ' [data-lens="later"]'), W)), (text0.match(/.{0,40}(Triage|Flow today).{0,40}/) || [''])[0]);
95
+ ok('no Triage, Week or Later view', !(await st((w) => !!document.querySelector(w + ' [data-lens="triage"], ' + w + ' [data-lens="week"], ' + w + ' [data-lens="later"]'), W)));
96
+ ok('no Flow today card (the PM removed it)', !(await st((w) => !!document.querySelector(w + ' [data-pm-flow]'), W)) && !/Flow today|Your part is the commit/.test(text0));
97
+ ok('no Jobs card, Group by job, Today ring or plan deck', !(await st((w) => !!document.querySelector(w + ' [data-pm-jobs], ' + w + ' [data-pm-group], ' + w + ' [data-c3pg="plan"], ' + w + ' .lh svg[role="img"]'), W)) && !/Jobs to be done|Group by job/.test(text0));
98
+ ok('Focus leads the right column', await st((w) => { const r = document.querySelector(w + ' .bcol.r'); return !!r && !!r.firstElementChild && r.firstElementChild.matches('[data-pm-focus]'); }, W));
94
99
 
95
100
  /* one home (R9): no decision is a Work row in any view, nor Work text */
96
101
  const seen = new Set(); let allText = '';
@@ -102,16 +107,29 @@ const W = '[data-pm-work]';
102
107
  /* Today and Focus (R4) */
103
108
  await view('today');
104
109
  const today0 = await titles();
105
- ok('Today lists overdue first, then flagged, then due today', today0[0] === 'Call Priya about the Q4 budget variance' && today0[1] === 'Send the revised Checkout v2 timeline to Dara' && today0[today0.length - 1] === 'Update the Checkout v2 risk log', JSON.stringify(today0));
110
+ ok('Today lists Overdue, then Due today, then Flagged', today0[0] === 'Call Priya about the Q4 budget variance' && today0[1] === 'Send the revised Checkout v2 timeline to Dara' && today0[4] === 'Prep the one-pager for the Search relevance review', JSON.stringify(today0));
111
+ const sects = await st((w) => [...document.querySelectorAll(w + ' [data-pm-sect]')].map((n) => [n.getAttribute('data-pm-sect'), n.querySelectorAll('[data-pm-item]').length, !!n.querySelector('[data-pm-legend]')]), W);
112
+ ok('…each in its own card with a count, the legend on the first only', JSON.stringify(sects) === '[["overdue",2,true],["due",2,false],["flagged",1,false]]', JSON.stringify(sects));
113
+ const chipsTxt = await st((w) => ({ over: (document.querySelector(w + ' [data-pm-due="overdue"]') || {}).textContent, today: (document.querySelector(w + ' [data-pm-due="today"]') || {}).textContent, iso: /\d{4}-\d{2}-\d{2}/.test(document.querySelector(w + ' [data-pm-list="today"]').textContent), done: document.querySelectorAll(w + ' [data-pm-list="today"] [data-pm-done]').length }), W);
114
+ ok('rows read "N days overdue", "Due today", no ISO dates, and each has a Done button', /overdue/.test(chipsTxt.over) && chipsTxt.today === 'Due today' && !chipsTxt.iso && chipsTxt.done === 5, JSON.stringify(chipsTxt));
106
115
  const foc = await st((w) => [...document.querySelectorAll(w + ' [data-pm-focus] [data-pm-focus-card]')].map((c) => ({ key: c.getAttribute('data-pm-focus-card'), state: c.getAttribute('data-pm-focus-state'), why: (c.querySelector('.wkfc-why') || {}).textContent || '' })), W);
107
116
  ok('Today opens with Focus: three suggested jobs, each with the reason', foc.length === 3 && foc.every((f) => f.state === 'suggested' && /^Why: \S/.test(f.why)), JSON.stringify(foc));
108
- const focBefore = await st((w) => { const f = document.querySelector(w + ' [data-pm-focus]'); const l = document.querySelector(w + ' [data-pm-list="today"]'); return !!f && !!l && !!(f.compareDocumentPosition(l) & Node.DOCUMENT_POSITION_FOLLOWING); }, W);
109
- ok('…at the top of Today, above the list', focBefore);
117
+ ok('Focus is a list of rows in the right column', await st((w) => !!document.querySelector(w + ' .bcol.r [data-pm-focus] li.nrow[data-pm-focus-card]') && !!document.querySelector(w + ' [data-pm-focus-job]'), W));
110
118
  const [k1, k2, k3] = foc.map((f) => f.key);
111
119
  await click(W + ' [data-pm-focus-accept="' + k1 + '"]');
112
120
  ok('Accept pins the job to Today', !!await until(() => st(([w, k]) => { const c = document.querySelector(w + ' [data-pm-focus-card="' + k + '"]'); return c && c.getAttribute('data-pm-focus-state') === 'accepted'; }, [W, k1]), 5000));
113
121
  await click(W + ' [data-pm-focus-dismiss="' + k2 + '"]');
114
122
  ok('Not today takes a suggestion away', !!await until(() => st(([w, k]) => !document.querySelector(w + ' [data-pm-focus-card="' + k + '"]'), [W, k2]), 5000));
123
+ ok('…and the footer says how many are marked not today, with Bring back', await st((w) => /^1 marked not today/.test((document.querySelector(w + ' [data-pm-focus-hidden]') || {}).textContent || ''), W));
124
+ await click(W + ' [data-pm-focus-restore]');
125
+ ok('Bring back returns it as a suggestion', !!await until(() => st(([w, k]) => { const c = document.querySelector(w + ' [data-pm-focus-card="' + k + '"]'); return c && c.getAttribute('data-pm-focus-state') === 'suggested'; }, [W, k2]), 5000));
126
+ await click(W + ' [data-pm-focus-job]');
127
+ ok('a Focus row with a job opens the job\'s sheet (its words, Keep, Delete)', !!await until(() => st(() => { const sh = document.querySelector('[data-shell="sheet"] [data-pm-case]'); return !!sh && !!sh.querySelector('[data-pm-job-edit$=":want"]'); }), 4000));
128
+ await page.keyboard.press('Escape');
129
+ await click(W + ' [data-pm-job-add-toggle]');
130
+ ok('New job opens a sheet with the one-line draft', !!await until(() => st(() => !!document.querySelector('[data-shell="sheet"] [data-pm-add="/api/pm-jobs/add"]')), 4000));
131
+ await page.keyboard.press('Escape');
132
+ ok('both sheets close with Esc', !!await until(() => st(() => !document.querySelector('[data-shell="sheet"]')), 3000));
115
133
  await click(W + ' [data-pm-focus-edit="' + k3 + '"]');
116
134
  const inp = await until(() => st(([w, k]) => !!document.querySelector(w + ' [data-pm-focus-input="' + k + '"]'), [W, k3]), 4000);
117
135
  if (inp) await page.fill(W + ' [data-pm-focus-input="' + k3 + '"]', 'Prep the search one-pager before lunch');
@@ -134,20 +152,20 @@ const W = '[data-pm-work]';
134
152
  await toastUndo();
135
153
  ok('…and Undo brings it back', !!await until(async () => (await titles()).includes('Talk to Dara about QA capacity for the beta') && (await counts()).today === 5, 5000));
136
154
  const call = await open('Call Priya about the Q4 budget variance');
137
- ok('an item moved before says how often', !!call && /Moved once/.test(await st((s) => document.querySelector(s).textContent, W + ' ' + call)));
155
+ ok('an item moved before says how often', !!call && /Moved 1×/.test(await st((s) => document.querySelector(s).textContent, W + ' ' + call)));
138
156
  if (call) await click(W + ' ' + call + ' [data-pm-notyet="tomorrow"]');
139
157
  ok('Not yet, tomorrow: it leaves Today and Upcoming gains it', !!await until(async () => { const c = await counts(); return !(await titles()).includes('Call Priya about the Q4 budget variance') && c.today === 4 && c.upcoming === 7; }, 5000), JSON.stringify(await counts()));
140
158
 
141
159
  /* Upcoming (R5) */
142
160
  await view('upcoming');
143
- const days = await st((w) => [...document.querySelectorAll(w + ' [data-pm-day]')].map((d) => ({ key: d.getAttribute('data-pm-day'), head: d.querySelector('h3').textContent, rows: [...d.querySelectorAll('[data-pm-title]')].map((t) => t.textContent) })), W);
144
- ok('Upcoming groups the next 7 days by day, then Later', days.length === 8 && days[0].key === '2026-10-09' && /^Tomorrow/.test(days[0].head) && days[7].key === 'later' && /^Later/.test(days[7].head), JSON.stringify(days.map((d) => d.key + ':' + d.head)));
161
+ const days = await st((w) => [...document.querySelectorAll(w + ' [data-pm-day]')].map((d) => ({ key: d.getAttribute('data-pm-day'), head: (d.querySelector('h3, h2') || {}).textContent, rows: [...d.querySelectorAll('[data-pm-title]')].map((t) => t.textContent), clear: /Clear/.test((d.querySelector('.wkday-none') || {}).textContent || '') })), W);
162
+ ok('Upcoming is a Next 7 days card with one heading per day, then a separate Later card', days.length === 8 && days[0].key === '2026-10-09' && /^Tomorrow/.test(days[0].head) && days[7].key === 'later' && /^Later/.test(days[7].head) && days[1].clear, JSON.stringify(days.map((d) => d.key + ':' + d.head)));
145
163
  ok('…each item under the day it arrives', days.length === 8 && days[0].rows.includes('Set up a meeting with Sam about the logging rollout') && days[0].rows.includes('Call Priya about the Q4 budget variance')
146
164
  && days.find((d) => d.key === '2026-10-12').rows.includes('Write release notes for SSO 1.0') && days[7].rows.includes('Write the Q1 discovery plan'), JSON.stringify(days.map((d) => d.rows)));
147
165
  const nb = await st((w) => { const s = [...document.querySelectorAll(w + ' [data-pm-item]')].find((r) => /Write release notes for SSO 1\.0/.test(r.textContent)); const c = s && s.querySelector('[data-pm-notbefore]'); return c ? c.textContent : null; }, W);
148
166
  ok('a deferred item says "Not before" its date', nb === 'Not before Mon 12 Oct', String(nb));
149
167
  const adv = await st((w) => [...document.querySelectorAll(w + ' [data-pm-resolves]')].map((n) => n.textContent), W);
150
- ok('an item that advances a team task names it ("Advances: <title>", or the id when the title is unknown)', adv.includes('Advances: SSO general release checklist') && adv.includes('Advances: returns-interview-5'), JSON.stringify(adv));
168
+ ok('an item that advances a team task names it ("Advances team task <title>", or the id when the title is unknown)', adv.includes('Advances team task SSO general release checklist') && adv.includes('Advances team task returns-interview-5'), JSON.stringify(adv));
151
169
  const sam = await open('Set up a meeting with Sam about the logging rollout');
152
170
  if (sam) { await page.fill(W + ' ' + sam + ' [data-pm-outcome]', 'Booked for Tuesday 10:00'); await click(W + ' ' + sam + ' [data-pm-offline-done]'); }
153
171
  ok('Done with a one-line outcome closes an offline item', !!sam && !!await until(async () => !(await titles()).includes('Set up a meeting with Sam about the logging rollout') && (await counts()).upcoming === 6, 5000));
@@ -160,6 +178,9 @@ const W = '[data-pm-work]';
160
178
 
161
179
  /* Review (R7) */
162
180
  await view('review');
181
+ const drafts = await st((w) => [...document.querySelectorAll(w + ' [data-pm-review-drafts] [data-draft-row] .tt')].map((n) => n.textContent), W);
182
+ ok('Review also carries the agent\'s drafts that Briefing used to show', DRAFTS.every((t) => drafts.includes(t)) && drafts.length >= 6, JSON.stringify(drafts));
183
+ ok('…and its badge still counts the weekly review only', (await counts()).review === 3);
163
184
  const prog = () => st((w) => { const r = document.querySelector(w + ' [data-pm-review]'); return r ? { id: r.getAttribute('data-pm-review'), p: r.querySelector('[data-pm-review-progress]').textContent, t: r.querySelector('[data-pm-title]').textContent } : null; }, W);
164
185
  const r1 = await until(prog, 5000);
165
186
  ok('Review shows one item at a time, the most stale first, with progress', !!r1 && r1.p === '1 of 3' && r1.t === 'Clean up the Checkout Jira labels', JSON.stringify(r1));
@@ -174,6 +195,17 @@ const W = '[data-pm-work]';
174
195
  if (r3) await click(W + ' [data-pm-review-drop="' + r3.id + '"]');
175
196
  ok('Drop ends the pass with "Review done"', !!await until(() => st((w) => { const d = document.querySelector(w + ' [data-pm-review-done]'); return !!d && /Review done/.test(d.textContent); }, W), 5000));
176
197
 
198
+ /* Focus and the header on every view; Inbox and Waiting carry their own furniture */
199
+ const every = [];
200
+ for (const v of VIEWS) { await view(v); every.push(await st((w) => !document.querySelector(w + ' [data-pm-flow]') && !!document.querySelector(w + ' .bcol.r [data-pm-focus]'), W)); }
201
+ ok('Focus shows on every view, and Flow today on none', every.every(Boolean), JSON.stringify(every));
202
+ await view('inbox');
203
+ ok('Inbox: an Inbox card, a Decide button that opens the row, and the note about Initiatives', await st((w) => !!document.querySelector(w + ' [data-pm-list="inbox"] h2') && /^Inbox/.test(document.querySelector(w + ' [data-pm-list="inbox"] h2').textContent) && !!document.querySelector(w + ' [data-pm-decide]') && /triaged in/.test((document.querySelector(w + ' [data-pm-inbox-note]') || {}).textContent || ''), W));
204
+ await click(W + ' [data-pm-decide]');
205
+ ok('…Decide opens the row\'s case file', !!await until(() => st((w) => !!document.querySelector(w + ' .xp.open [data-pm-case]'), W), 4000));
206
+ await view('waiting');
207
+ ok('Waiting: each row says who and since when, and has a Nudge button that sends nothing', await st((w) => { const r = document.querySelector(w + ' [data-pm-list="waiting"] [data-pm-item]'); return !!r && /^Waiting for \S/.test((r.querySelector('[data-pm-waiting-chip]') || {}).textContent || '') && !!r.querySelector('[data-pm-nudge]'); }, W));
208
+ ok('every view header names how many decisions wait in Briefing and offers Open Briefing', await st((w) => /decisions? waits? in Briefing, Needs you\./.test(document.querySelector(w + ' [data-pm-lens-head]').textContent) && !!document.querySelector(w + ' [data-pm-open-briefing]'), W));
177
209
  /* capture (R2): the chip names the view the server will place the line in */
178
210
  const chip = async (line) => { await page.fill(W + ' [data-pm-add]', line); return until(() => st((w) => { const t = document.querySelector(w + ' [data-pm-chips] [data-pm-lands]'); return t ? t.textContent : null; }, W), 3000); };
179
211
  const lands = [await chip('Read the security brief'), await chip('Send Dana the new date by tomorrow'), await chip('Ship the fix due today'), await chip('Get the contract back waiting on Lee')];
@@ -1,12 +1,4 @@
1
1
  {
2
2
  "_comment": "Items the console3 page makes actionable on more than one tab today (c3-one-home-test.js). Known-failing until the Phase 2 restructure of the console3 feedback plan; the suite fails only on a duplicate that is not listed here. Remove an entry when its duplicate is gone.",
3
- "duplicates": [
4
- {
5
- "title": "sso is live with 3 customers",
6
- "tabs": [
7
- "brief",
8
- "init"
9
- ]
10
- }
11
- ]
3
+ "duplicates": []
12
4
  }
@@ -9,9 +9,21 @@ timestamp: 2026-10-11
9
9
 
10
10
  ## Cards
11
11
 
12
- Every card in a deck has the same height. Text that does not fit is cut short. Press a card to turn it over. The card turns in place in about a third of a second. No other card moves, and nothing below the deck moves. The back has the same size as the front and scrolls inside when it is long. With reduced motion on, the card changes at once.
12
+ Every card in a deck has the same height at rest. Text that does not fit is cut short. The front shows the numbers the card carries as tiles, with a short kind label. Press a card to turn it over. The card turns in about a third of a second. The back grows to fit its content, so the options and the confirm control are never cut off. The other cards in the row keep their size, and the page below the deck moves down. Press the top line of the back to turn it again. When you decide, the card shows a check and a message offers **Undo**. With reduced motion on, the card changes at once.
13
13
 
14
- The icon of a card comes from fixed rules over the kind of the card. A kind the rules do not know gets a neutral icon.
14
+ The page background is white. A soft, slow glow of colour drifts behind the page. With reduced motion on, it is one still picture.
15
+
16
+ The tabs show counts: Briefing shows how many things need you, Work shows its inbox, and Initiatives shows new asks to triage. The Sessions button and the dock button show a bolt. In the demo, the Record button and its arrow are visible when nothing is recording.
17
+
18
+ ### Kinds of card
19
+
20
+ The label on a card says what kind of thing happened, for example **Deadline moved**, **Promise out of date**, **Blocked**, **Conflict**, **Risk**, **Waiting on someone** or **Report due**. A win keeps the label **Big win** or **Small win**. The colour of the card shows how serious it is: red is critical, amber is a warning, green is good news, and the accent colour is neutral.
21
+
22
+ Each kind has a short list of what its card must say. For example, a **Deadline moved** card says what moved, by how much, by when you must decide and the one move. When the card does not know a part, the back of the card says so under **Not known yet**. PMOS does not guess a missing part.
23
+
24
+ A card shows only when it is relevant enough and links to a page that you can open. Critical cards always show. When nothing is relevant enough, there is no card.
25
+
26
+ The icon of a card comes from fixed rules over the kind of the card. A kind the rules do not know gets a neutral icon. No card text leaves your machine to choose an icon.
15
27
 
16
28
  ## Confirm an act
17
29
 
@@ -37,4 +49,4 @@ Needs you also lists your own items that are overdue or due today. Overdue items
37
49
 
38
50
  ## Work
39
51
 
40
- The Work page no longer shows **Flow today**. **Jobs to be done** stays.
52
+ The Work page shows **Focus** in its right column. There is no Flow today card and no Jobs card: you open a job from a Focus row.
@@ -7,22 +7,26 @@ timestamp: 2026-10-11
7
7
 
8
8
  # The Briefing: today's brief
9
9
 
10
- The Briefing is the first page of the console at `/c3.html`. It is one column. It reads from top to bottom: the headline cards, Today, Needs you and Portfolio pulse. It holds no calendar. The full calendar is the Calendar tab.
10
+ The Briefing is the first page of the console at `/c3.html`. It has two columns. The left column holds the greeting, the headline cards and Needs you. The right column holds Today and Portfolio pulse. It holds no calendar. The full calendar is the Calendar tab.
11
+
12
+ Review is in the Work page, in its Review lens. Handled, What I could stop and Data you can lean on are in Reports, under Drafts.
11
13
 
12
14
  The Briefing shows only what PMOS already holds. When PMOS holds nothing for a section, the section says so.
13
15
 
14
16
  ## Headline cards
15
17
 
16
- The cards at the top are the signals that pass the relevance bar. Turn a card over to read its case. The day-part buttons (Plan, Unblock, Close, Reflect) re-rank the same signals.
18
+ The cards at the top are the signals that pass the relevance bar. Turn a card over to read its case.
19
+
20
+ Above the cards, the greeting names the part of the day, for example "Good morning, Wawan". The name is the first word of the git user name on the computer where PMOS runs. When git holds no name, the greeting has none. Five buttons stand for morning, midday, afternoon, evening and night. Press one to preview that part of the day: its line and its cards. Nothing is saved. Press the part that is now, or wait for the clock to move on, and the preview ends.
17
21
 
18
22
  ## Today
19
23
 
20
- Today lists the meetings of the day, in time order. Each row shows:
24
+ Today starts with one line: the meeting that is on now or next, how many meetings are left, and how many of them are prepped, with a small ring. **Open calendar** opens the calendar. Under it, Today lists the meetings of the day, in time order. Each row shows:
21
25
 
22
26
  - The start time and the length.
23
27
  - The people and the initiative of the meeting.
24
- - **Prepared**: what PMOS holds for the meeting. This is the agent's prep note, your notes on the event, the notes you started for it, and the last notes of the same meeting. When PMOS holds none, the row says "Nothing prepared yet".
25
- - **Before the meeting**: your open items that name the meeting.
28
+ - **Prepared**, as chips: what PMOS holds for the meeting. This is the agent's prep note, your notes on the event, the notes you started for it, and the last notes of the same meeting. When PMOS holds none, the row says "Nothing prepared yet".
29
+ - **Before you go**, as chips: your open items that name the meeting. A meeting with nothing held says "nothing prepared". A meeting with everything done says "prep ready".
26
30
 
27
31
  A meeting that is over is dimmed. The meeting under way is marked **Now**. The next one is marked **Next**. When the calendar holds no meeting for today, Today says so.
28
32
 
@@ -30,7 +34,7 @@ A Today row is a reference. It has no button that changes anything.
30
34
 
31
35
  ## Needs you
32
36
 
33
- Needs you lists what only you can do. The rows come in this order:
37
+ Needs you lists what only you can do. Its header says how many decisions, how many items are yours, and how many are overdue. A decision row has a **decision** tag. When it cannot be undone it also says **hard to undo**. An item of yours has a **Work** chip: Work is where the item lives. The rows come in this order:
34
38
 
35
39
  1. Your own items that are overdue. The most late is first. The row says how late.
36
40
  2. Your own items due today.
@@ -47,7 +51,7 @@ PMOS can see evidence that an outside item is done. For example, a meeting with
47
51
 
48
52
  ## Portfolio pulse
49
53
 
50
- Portfolio pulse shows one bar for the status of the initiatives and one row for each initiative. Press a row to open it in place. It shows why the initiative has that status, and the promised date against the forecast. The page does not change.
54
+ Portfolio pulse says how many initiatives need attention, then shows one bar for the status of the initiatives and one row for each initiative. **All initiatives** at the bottom opens the Initiatives page. Press a row to open it in place. It shows why the initiative has that status, and the promised date against the forecast. The page does not change.
51
55
 
52
56
  To go to the initiative, press **Open initiative** inside the opened row.
53
57
 
@@ -57,4 +61,4 @@ An item has its actions in one place only. On the Briefing, Today and Portfolio
57
61
 
58
62
  ## On a phone
59
63
 
60
- At 400 pixels wide the Briefing is one column with no sideways scroll. Each button in an opened row is 44 pixels high.
64
+ At 400 pixels wide the Briefing is one column (greeting, cards, Needs you, Today, Portfolio pulse) with no sideways scroll. Each button in an opened row is 44 pixels high.
@@ -7,7 +7,7 @@ timestamp: 2026-10-11
7
7
 
8
8
  # Calendar popup
9
9
 
10
- The calendar popup shows the full calendar over any page of the console (`/c3.html`). You do not leave the page you are on.
10
+ The calendar popup shows the Calendar tab's grid in a drawer over any page of the console (`/c3.html`). You do not leave the page you are on.
11
11
 
12
12
  ## Open it
13
13
 
@@ -23,7 +23,7 @@ The key **C** does nothing in these cases:
23
23
 
24
24
  ## Use it
25
25
 
26
- The popup has the same five zoom levels as the Calendar tab: Day, Week, Month, Quarter and Year. It has no ribbon. Press **+** or **-** to zoom. The popup opens at the zoom you used last. The console keeps that zoom until you change it.
26
+ The popup has the same grid as the Calendar tab, with the same five zoom levels, the layer chips, the detail, the Free time list and the lanes. It has no headline deck. It opens on the Week the first time. Press **+** or **-** to zoom. The popup opens at the zoom you used last, and with the layers you hid left hidden. The console keeps that zoom until you change it.
27
27
 
28
28
  The popup and the Calendar tab are two separate views. A zoom or a date you choose in one view does not change the other.
29
29