create-pmos 0.158.0 → 0.160.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.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-initiatives-test.js +8 -2
- package/workspace/guide/ask-the-agent.md +1 -1
- package/workspace/guide/briefing.md +2 -2
- package/workspace/guide/calendar-popup.md +1 -1
- package/workspace/guide/console-calendar.md +2 -2
- package/workspace/guide/index.md +1 -1
- package/workspace/guide/initiative-boards.md +4 -4
- package/workspace/guide/layout.md +5 -3
- package/workspace/guide/notes.md +1 -1
- package/workspace/guide/settings.md +2 -2
- package/workspace/lib/initiative-rows.js +33 -3
- package/workspace/lib/routes-boards.js +8 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/c3.html +11 -2
- package/workspace/public/pm/c3-demo-boards.js +6 -0
- package/workspace/public/pm/c3-dock.js +7 -7
- package/workspace/public/pm/c3-ib-docs.js +9 -9
- package/workspace/public/pm/c3-ib-initiatives.js +7 -7
- package/workspace/public/pm/c3-ib-items.js +4 -4
- package/workspace/public/pm/c3-ib-releases.js +4 -4
- package/workspace/public/pm/c3-ib-sprints.js +5 -5
- package/workspace/public/pm/c3-ib-triage.js +6 -6
- package/workspace/public/pm/c3-ii-tabs.js +6 -6
- package/workspace/public/pm/c3-ini-records.js +11 -11
- package/workspace/public/pm/c3-ini-timeline.js +3 -3
- package/workspace/public/pm/c3-ini-ui.js +7 -0
- package/workspace/public/pm/c3-ini-work.js +2 -2
- package/workspace/public/pm/c3-initiatives.js +42 -23
- package/workspace/public/pm/c3-plan.js +1 -1
- package/workspace/public/pm/c3-settings.js +2 -2
- package/workspace/public/pm/c3-shell.js +3 -3
- package/workspace/public/pm/c3-tickets.js +1 -1
- package/workspace/public/pm/console3.css +3 -0
- package/workspace/public/pm/pm-work-view.js +1 -1
- package/workspace/public/pm/sessions-panel.js +2 -2
- package/workspace/public/pm/shell-recording.js +1 -1
- package/workspace/public/pm/welcome.js +2 -2
- package/workspace/server.js +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.160.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T06:29:58Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T06:29:58Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T06:29:58Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -90,7 +90,8 @@ const { harness } = require('./c3-ini-harness');
|
|
|
90
90
|
await page.click('[data-ini="new-initiative"]');
|
|
91
91
|
await until(() => st(() => !!document.querySelector('[data-ini="new-ini-form"]')));
|
|
92
92
|
await page.fill('[data-ini="ni-title"]', 'Partner sign-up page');
|
|
93
|
-
await
|
|
93
|
+
await until(() => st(() => { const s = document.querySelector('select[data-ini="ni-anchor"]'); return !!s && !s.disabled; }));
|
|
94
|
+
await page.selectOption('[data-ini="ni-anchor"]', 'KR 2.1 Activation to 45%');
|
|
94
95
|
await page.click('[data-ini="ni-create"]');
|
|
95
96
|
await until(() => st(() => !!document.querySelector('[data-ini-crumb="board"]')));
|
|
96
97
|
await page.click('[data-ini-crumb="board"]');
|
|
@@ -106,7 +107,12 @@ const { harness } = require('./c3-ini-harness');
|
|
|
106
107
|
await page.fill('[data-ini="ni-title"]', 'Passwordless login with email OTP');
|
|
107
108
|
await page.click('[data-ini="ni-create"]');
|
|
108
109
|
ok('NI1 an initiative without an anchor is refused, and the sheet says why', /anchor/.test(await st(() => (document.querySelector('[data-ini="new-ini-form"] [data-ini="error"]') || {}).textContent || '')));
|
|
109
|
-
await
|
|
110
|
+
await until(() => st(() => { const s = document.querySelector('select[data-ini="ni-anchor"]'); return !!s && !s.disabled; }));
|
|
111
|
+
await page.selectOption('[data-ini="ni-anchor"]', 'KR 2.1 Activation to 45%');
|
|
112
|
+
const ap = await st(() => { const s = document.querySelector('select[data-ini="ni-anchor"]'); return { groups: [...s.querySelectorAll('optgroup')].map((g) => g.label), health: [...s.options].filter((o) => /^health:/.test(o.value)).length }; });
|
|
113
|
+
ok('NI3 the anchor is picked from the declared key results and health budgets, not typed', ap.groups.includes('Health budgets') && ap.health === 3 && ap.groups.some((g) => /^Objective/.test(g)), JSON.stringify(ap));
|
|
114
|
+
const nul = await st(() => [...document.querySelector('[data-ini="new-ini-form"]').childNodes].filter((n) => n.nodeType === 3 && /null/.test(n.textContent)).length);
|
|
115
|
+
ok('NI4 the New initiative sheet shows no stray "null" line', nul === 0, String(nul));
|
|
110
116
|
await page.click('[data-ini="new-ini-form"] [data-ini-tpl="research"]');
|
|
111
117
|
await page.click('[data-ini="ni-create"]');
|
|
112
118
|
await until(() => st(() => !!document.querySelector('[data-ini-page]') && !!document.querySelector('[data-ini="stepper"]')));
|
|
@@ -7,7 +7,7 @@ timestamp: 2026-10-12
|
|
|
7
7
|
|
|
8
8
|
# Ask the agent from the page
|
|
9
9
|
|
|
10
|
-
Five presses in the console (
|
|
10
|
+
Five presses in the console (`/`) start an agent from the page. You do not need a terminal, and you do not copy a request anywhere. Each run is listed in Sessions, so you can follow it there.
|
|
11
11
|
|
|
12
12
|
## Ask the agent on a document
|
|
13
13
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "The Briefing: today's brief"
|
|
4
|
-
description: "The Briefing page of the console (/
|
|
4
|
+
description: "The Briefing page of the console (/): the headline cards, Today, Needs you, Portfolio pulse, and what Done, Not yet and Drop do to an item."
|
|
5
5
|
timestamp: 2026-10-12
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# The Briefing: today's brief
|
|
9
9
|
|
|
10
|
-
The Briefing is the first page of the console at
|
|
10
|
+
The Briefing is the first page of the console at `/`. 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
11
|
|
|
12
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.
|
|
13
13
|
|
|
@@ -7,7 +7,7 @@ timestamp: 2026-10-11
|
|
|
7
7
|
|
|
8
8
|
# Calendar popup
|
|
9
9
|
|
|
10
|
-
The calendar popup shows the Calendar tab's grid in a drawer over any page of the console (
|
|
10
|
+
The calendar popup shows the Calendar tab's grid in a drawer over any page of the console (`/`). You do not leave the page you are on.
|
|
11
11
|
|
|
12
12
|
## Open it
|
|
13
13
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Guide: the Calendar tab of the console page"
|
|
4
|
-
description: "The Calendar tab of the console page (/
|
|
4
|
+
description: "The Calendar tab of the console page (/): Calendar and Notes, the five zooms, the highlights, the layer chips, the markers, the context of a meeting, free time, the tidy-up suggestions and the empty states."
|
|
5
5
|
timestamp: 2026-10-12
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Guide: the Calendar tab of the console page
|
|
9
9
|
|
|
10
|
-
The Calendar tab of the console page (
|
|
10
|
+
The Calendar tab of the console page (`/`) shows time. It shows what happens and what falls due, by date. At the top it shows a short deck of headline cards about your calendar, for example a conflict or a meeting with no time to prepare. The deck shows nothing when PMOS has no card about your calendar. The tab shows no task list and no strategy panel. Your due items live in Work. The strategy and progress panels live in Reports and Initiatives.
|
|
11
11
|
|
|
12
12
|
## Calendar and Notes
|
|
13
13
|
|
package/workspace/guide/index.md
CHANGED
|
@@ -22,7 +22,7 @@ On a desktop the workspace is one frame. [Layout](layout.md) explains each part.
|
|
|
22
22
|
| You want | Go to |
|
|
23
23
|
|---|---|
|
|
24
24
|
| What is waiting for you now | Agenda |
|
|
25
|
-
| Your day as a brief: today's meetings, what needs you, the portfolio | The Briefing page of the console (
|
|
25
|
+
| Your day as a brief: today's meetings, what needs you, the portfolio | The Briefing page of the console (`/`) |
|
|
26
26
|
| Every Jira ticket and WBS item assigned to you, ranked, to brief an agent | My work, or press `g w` |
|
|
27
27
|
| The initiatives of a repository, or of every repository | Initiatives. Choose **All repositories** in Repository ▾ for every repository. |
|
|
28
28
|
| The sessions of a repository, by initiative | The agent dock. ⌘K → **Branches and sessions** lists the branches. |
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Initiatives on the console: Portfolio, boards and workflows"
|
|
4
|
-
description: "How to use the Initiatives tab of the console page (/
|
|
4
|
+
description: "How to use the Initiatives tab of the console page (/): the Portfolio of boards, a board and its nine tabs, an initiative with its own workflow and stage records, and the Build and deliver setting."
|
|
5
5
|
timestamp: 2026-10-11
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Initiatives on the console: Portfolio, boards and workflows
|
|
9
9
|
|
|
10
|
-
The Initiatives tab of the console page (
|
|
10
|
+
The Initiatives tab of the console page (`/`) has these levels: **Portfolio** › **Board** › **Initiative** › **Epic** › **Item**. A breadcrumb at the top takes you back one level. The address bar keeps your place in the `init=` parameter, so a reload opens the same page.
|
|
11
11
|
|
|
12
12
|
## Portfolio
|
|
13
13
|
|
|
@@ -26,7 +26,7 @@ The tabs are **Summary**, **Initiatives**, **Triage**, **Backlog**, **Board**, *
|
|
|
26
26
|
|
|
27
27
|
- **Summary** shows the initiatives at a glance and one bar per category. At a glance lists up to six open initiatives. Initiatives with work, and Now initiatives that are not on track, come first. The others follow by end date. Press a bar to open the Initiatives tab with that category.
|
|
28
28
|
- **Initiatives** lists the initiatives of the board, 25 on each page. Use the saved views (Now and next, Mine, At risk, No owner, All), the filters and the sort. Press **Move** on a row to move the initiative to a stage of its own workflow.
|
|
29
|
-
- **Triage** lists the new asks. Add an ask, then open it to see its draft. Press **Accept** to start an initiative. An initiative needs an anchor (a KR or a health budget). Press **Ask for more** to ask a question, or **Decline** and give a reason.
|
|
29
|
+
- **Triage** lists the new asks. Add an ask, then open it to see its draft. Press **Accept** to start an initiative. An initiative needs an anchor (a KR or a health budget). On a board, **New initiative** lists the anchors that `planning/okrs/` declares. Pick one from the list. Press **Ask for more** to ask a question, or **Decline** and give a reason.
|
|
30
30
|
- **Backlog**, **Board** and **Timeline** show the epics and items. Jira items and PMOS tasks are mixed. The **Initiative** and **Source** filters are the same on the three tabs.
|
|
31
31
|
- In Backlog, add a PMOS task to an initiative.
|
|
32
32
|
- In Board, drag a PMOS task to a column, or use its menu. A Jira item has a lock: change it in Jira.
|
|
@@ -62,4 +62,4 @@ A project workflow has the launch checklist and the learnings only. If the works
|
|
|
62
62
|
|
|
63
63
|
## Demo mode
|
|
64
64
|
|
|
65
|
-
Open
|
|
65
|
+
Open `/?demo=1` to see five sample boards and about 310 initiatives. In demo mode nothing is saved, and a reload starts again.
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Guide: layout"
|
|
4
4
|
description: "The frame: one top bar, the place on screen, the detail and the agent dock; the phone tabs and lists; the keys; and the older layout."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-12
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Guide: layout
|
|
9
9
|
|
|
10
|
+
This page describes the classic workspace, at `/classic.html`. The address `/` opens the console (see [the Briefing](briefing.md)). The console's More menu has **Open the classic app**, and a session or branch is started there.
|
|
11
|
+
|
|
10
12
|
On a desktop screen the workspace is one frame. It has one top bar, the place on screen under it, the detail when you open one, and the agent dock at the right. There is no left sidebar.
|
|
11
13
|
|
|
12
14
|
## The top bar
|
|
@@ -102,7 +104,7 @@ When the workspace needs an answer from you, for example the name for a rename o
|
|
|
102
104
|
|
|
103
105
|
The workspace is neutral. A primary button is the text colour on the page colour, in both themes. The one brand colour marks the Agenda dot and the Agenda count, and nothing else. The bar, the dock and the bottom bar follow the theme; only a terminal stays dark. Corners are square. Controls are icons from one set, not text symbols. On a touch screen every field is 16 pixels, so the phone does not zoom when you tap it, and every control is at least 44 pixels high.
|
|
104
106
|
|
|
105
|
-
On the console page (
|
|
107
|
+
On the console page (`/`), text on the blue accent and on a red badge is white in both themes. The page takes this colour from one token, so the colours stay the same when the theme changes.
|
|
106
108
|
|
|
107
109
|
## Keys
|
|
108
110
|
|
|
@@ -111,7 +113,7 @@ On the console page (`/c3.html`), text on the blue accent and on a red badge is
|
|
|
111
113
|
| g t, g i, g b, g r, g d, g p | Show Agenda, Initiatives, Board, Reports, Docs or People. |
|
|
112
114
|
| ⌘K (Ctrl-K) | Open or close the command palette. |
|
|
113
115
|
| ⌘J (Ctrl-J) | Open or fold the dock. |
|
|
114
|
-
| C | On the console page (
|
|
116
|
+
| C | On the console page (`/`): open the [calendar popup](calendar-popup.md). Ignored while you type or while a sheet is open. |
|
|
115
117
|
| ⌘⇧J (Ctrl-Shift-J) | Focus: give the dock the whole window, or show the place again. |
|
|
116
118
|
| j, k | Move down or up the rows of the place. |
|
|
117
119
|
| Enter | Open the row that has the focus. |
|
package/workspace/guide/notes.md
CHANGED
|
@@ -22,7 +22,7 @@ Starting a note, from the Record control or from **Take notes** on a calendar ev
|
|
|
22
22
|
|
|
23
23
|
### The Record control in the console
|
|
24
24
|
|
|
25
|
-
The console (the page at
|
|
25
|
+
The console (the page at `/`) has the same Record control in its top bar, beside the Sessions button. It is always there, also when nothing is recording. **Record** (the microphone) starts an untitled note and listens at once. The arrow lists the meetings of today that have not ended, including one that has started, until 10 minutes after its end. Press one to start a note linked to that calendar event. While a meeting records, the red group shows its name and clock, **Highlight**, **Pause** and **Stop**. While it is paused, the group shows **Resume** and **Stop**. **Stop** asks first and offers **Pause instead**. A red dot shows on the Calendar tab while a meeting is held. Press the name in the red group to open the note on the console's Calendar tab, in **Notes**, on the **During** stage. See [the Calendar tab of the console page](console-calendar.md).
|
|
26
26
|
|
|
27
27
|
When Whisper or Qwen3-ASR transcribes the meeting, the red group also names the engine and the language, for example **Whisper · English**. Check it when lines come out in the wrong language: the language is set in the Notes settings.
|
|
28
28
|
|
|
@@ -7,7 +7,7 @@ timestamp: 2026-10-12
|
|
|
7
7
|
|
|
8
8
|
# Settings in console3: one home for each setting
|
|
9
9
|
|
|
10
|
-
Settings is the page in console3 (
|
|
10
|
+
Settings is the page in console3 (`/`) where you change how PMOS works. Each setting has one home on this page. When another page shows a setting, it shows a link to this page and not a second control.
|
|
11
11
|
|
|
12
12
|
## Open Settings
|
|
13
13
|
|
|
@@ -16,7 +16,7 @@ Use one of these:
|
|
|
16
16
|
1. Press the gear in the top bar, next to More.
|
|
17
17
|
2. Press More, then **Settings**.
|
|
18
18
|
3. Press ⌘K, type `settings`, and press Enter.
|
|
19
|
-
4. Open
|
|
19
|
+
4. Open `/?tab=settings`. Add `§ion=recording` to open one section, for example.
|
|
20
20
|
|
|
21
21
|
Settings is not a tab. While it is open, no tab is selected. Press a tab to go back to that tab.
|
|
22
22
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* answerSig(r) → the records part of a GET's sig, from the answer used (a stale answer is marked)
|
|
12
12
|
* cachedSig(root) → the cache entry as a string; it changes each time a derivation lands
|
|
13
13
|
* recordsSig(root) → sha1 of lib/board-cache.js signature (HEAD, the records, lifecycle and acceptances)
|
|
14
|
-
* anchorsOf(root) → { resolve(raw) → anchor info, period, error }
|
|
14
|
+
* anchorsOf(root) → { resolve(raw) → anchor info, list() → [{ value, kind, group, label }], period, error }
|
|
15
15
|
* workGroupOf(root) → { [lane]: group } from scripts/lane-defs.js through the engine (never a list here)
|
|
16
16
|
*
|
|
17
17
|
* A ROW is one line of `node scripts/workflow-state.js --json` (every record, uncapped), narrowed to what the planning
|
|
@@ -197,7 +197,15 @@ function anchorsOf(root) {
|
|
|
197
197
|
try {
|
|
198
198
|
const file = engine.layout(root).budgets;
|
|
199
199
|
budgets = { ok: true, text: fs.readFileSync(file, 'utf8'), path: path.relative(root, file) };
|
|
200
|
-
} catch {
|
|
200
|
+
} catch {
|
|
201
|
+
// Not at the engine's path. A kit vendors the registry to pmos/planning/okrs/ (build-kit.sh; server.js budgetsFor), so
|
|
202
|
+
// look there too: without it every health:<id> in a kit repository was refused as undeclared.
|
|
203
|
+
budgets = A.readBudgets(root);
|
|
204
|
+
if (!budgets || !budgets.ok) {
|
|
205
|
+
const kit = A.readBudgets(path.join(root, 'pmos'));
|
|
206
|
+
if (kit && kit.ok) budgets = { ...kit, path: path.join('pmos', kit.path) };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
201
209
|
} else error = 'the anchor resolver is not present in this install';
|
|
202
210
|
const period = index && index.ok ? periodLabel(index.period) : null;
|
|
203
211
|
function resolve(raw) {
|
|
@@ -218,7 +226,29 @@ function anchorsOf(root) {
|
|
|
218
226
|
// Written but not resolved (or no resolver): shown as written, in a group of its own, never dropped.
|
|
219
227
|
return { anchor: text, kind: /^health:/i.test(text) ? 'health' : 'kr', key: `raw:${text}`, objective: null, title: text, token: text, status: r ? r.status : 'unknown' };
|
|
220
228
|
}
|
|
221
|
-
|
|
229
|
+
/** Every anchor the OKR files and the health budgets declare, for a picker: [{ value, kind, group, label }]. `value` is
|
|
230
|
+
* what resolve() and the writer accept; an entry that does not resolve is left out, so the list never offers a refusal. */
|
|
231
|
+
function list() {
|
|
232
|
+
const out = [];
|
|
233
|
+
if (index && index.ok) {
|
|
234
|
+
for (const o of index.objectives) {
|
|
235
|
+
const group = o.name ? `${o.token} ${o.name}` : o.token;
|
|
236
|
+
for (const k of o.krs) out.push({ value: `${o.token}.${k.token}`, kind: 'kr', group, label: k.name ? `${k.token} ${k.name}` : k.token });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const seen = new Set();
|
|
240
|
+
for (const m of String((budgets && budgets.text) || '').matchAll(/^\|\s*`?health:([a-z0-9_-]+)`?\s*\|\s*([^|]*?)\s*\|/gim)) {
|
|
241
|
+
const id = m[1].toLowerCase();
|
|
242
|
+
if (seen.has(id)) continue;
|
|
243
|
+
seen.add(id);
|
|
244
|
+
out.push({ value: `health:${id}`, kind: 'health', group: 'Health budgets', label: m[2] ? `health:${id} (${m[2]})` : `health:${id}` });
|
|
245
|
+
}
|
|
246
|
+
for (const d of (budgets && budgets.domains && budgets.domains.ok && budgets.domains.rows) || []) {
|
|
247
|
+
out.push({ value: `domain:${d.id}`, kind: 'domain', group: 'Domains', label: d.goal ? `domain:${d.id} (${d.goal})` : `domain:${d.id}` });
|
|
248
|
+
}
|
|
249
|
+
return out.filter((a) => resolve(a.value).status === 'resolved');
|
|
250
|
+
}
|
|
251
|
+
return { resolve, list, period, error, ok: !!(index && index.ok) };
|
|
222
252
|
}
|
|
223
253
|
|
|
224
254
|
module.exports = { deriveSync, rowsFor, rescan, peek, setBackground, isDeriving, deriveNow, answerSig, cachedSig, recordsSig, invalidate, anchorsOf, workGroupOf, periodLabel, narrow };
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* GET /api/boards/asks?ws&board&state → { ok, asks } state: open (default) | accepted | declined | all
|
|
12
12
|
* GET /api/boards/sprints?ws&board → { ok, source: jira|none, readAt, sprints }
|
|
13
13
|
* GET /api/boards/items?ws&board → { ok, build, jira, items, initiatives } PMOS tasks + Jira items (Build and deliver only)
|
|
14
|
+
* GET /api/boards/anchors?ws&board → { ok, anchors: [{ value, kind, group, label }], error } the declared KRs,
|
|
15
|
+
* health budgets and domains a new initiative can anchor to (D59)
|
|
14
16
|
* POST /api/boards/create { ws, name, domain?, template } → { ok, board }
|
|
15
17
|
* POST /api/boards/workflow { ws, board, stages, moveRemoved?, sprints? } → { ok, board, moved }
|
|
16
18
|
* POST /api/boards/move { ws, board, id, to, why? } → { ok, row, warnings }
|
|
@@ -437,6 +439,12 @@ const READS = {
|
|
|
437
439
|
.sort((a, b) => String(b.at || '').localeCompare(String(a.at || ''))).map(pageAsk);
|
|
438
440
|
ctx.json(res, { ok: true, ws: r.w.id, board: r.board.id, asks });
|
|
439
441
|
},
|
|
442
|
+
'/api/boards/anchors': (ctx, res, url) => {
|
|
443
|
+
const r = boardRead(ctx, res, url, { rows: false });
|
|
444
|
+
if (!r) return;
|
|
445
|
+
const A = rowsLib.anchorsOf(r.w.root);
|
|
446
|
+
ctx.json(res, { ok: true, ws: r.w.id, board: r.board.id, anchors: A.list(), error: A.error || null });
|
|
447
|
+
},
|
|
440
448
|
'/api/boards/items': (ctx, res, url) => {
|
|
441
449
|
const r = boardRead(ctx, res, url);
|
|
442
450
|
if (!r) return;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.160.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.160.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
package/workspace/public/c3.html
CHANGED
|
@@ -5,8 +5,17 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
6
6
|
<meta name="pmos-nonce" content="" />
|
|
7
7
|
<title>PMOS console</title>
|
|
8
|
-
<!--
|
|
9
|
-
|
|
8
|
+
<!-- Add to Home Screen, as the older shell had it (see index.html): the manifest is fetched with credentials because over the
|
|
9
|
+
tailnet every request needs the session cookie. -->
|
|
10
|
+
<link rel="manifest" href="/manifest.webmanifest" crossorigin="use-credentials" />
|
|
11
|
+
<meta name="theme-color" content="#090b0c" />
|
|
12
|
+
<meta name="mobile-web-app-capable" content="yes" />
|
|
13
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
14
|
+
<meta name="apple-mobile-web-app-title" content="PMOS" />
|
|
15
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
|
16
|
+
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
|
|
17
|
+
<!-- The console3 shell (header, tabs, three columns, dock), served as the default page at / (and still at /c3.html). It
|
|
18
|
+
composes the real views from pm/; the older shell (index.html) is served at /classic.html. Tokens and fonts are shared, so light and dark both resolve. pm/c3-shell.js sets the
|
|
10
19
|
theme attribute (default light, like the mockup). The nonce meta is filled by the server the way it is for /, so a press
|
|
11
20
|
from a view (Add, Accept, Allow) carries it. -->
|
|
12
21
|
<link rel="preload" href="/vendor/ibm-plex-mono-400.woff2" as="font" type="font/woff2" crossorigin />
|
|
@@ -341,6 +341,12 @@ export function handle(S, method, path, p, body) {
|
|
|
341
341
|
const state = p.state || 'open';
|
|
342
342
|
return okb({ asks: S.asks.filter((a) => (!b || a.board === b.id) && (state === 'all' || a.state === state)).map((a) => ({ ...a })) });
|
|
343
343
|
}
|
|
344
|
+
if (path === '/api/boards/anchors') {
|
|
345
|
+
const krs = Object.keys(KR).map((k) => ({ value: 'KR ' + k + ' ' + KR[k], kind: 'kr', group: 'Objective ' + k.split('.')[0], label: 'KR ' + k + ' ' + KR[k] }));
|
|
346
|
+
const health = [['tech', 'refactors, debt, tooling'], ['security', 'hardening, rotations, audits'], ['ops', 'pipelines, deploys, monitoring']]
|
|
347
|
+
.map(([id, covers]) => ({ value: 'health:' + id, kind: 'health', group: 'Health budgets', label: 'health:' + id + ' (' + covers + ')' }));
|
|
348
|
+
return okb({ board: b.id, anchors: [...krs, ...health], error: null });
|
|
349
|
+
}
|
|
344
350
|
if (path === '/api/boards/items') {
|
|
345
351
|
const list = S.rows.filter((r) => r.board === b.id);
|
|
346
352
|
return okb({ board: b.id, build: b.build, jira: b.jira, items: itemsOf(S, b),
|
|
@@ -24,8 +24,8 @@ const smOf = (s) => SM[s.shown] || SM[s.state] || ['●', ''];
|
|
|
24
24
|
const shownOf = (s, ended) => (ended && ended.has(s.id) ? 'exited' : s.shown);
|
|
25
25
|
const PI = { claude: 'term', cloud: 'cloud', codex: 'bolt', shell: 'term', local: 'term' }; // spark is reserved for the agent surfaces
|
|
26
26
|
const PN = { claude: 'Claude', cloud: 'Claude cloud', codex: 'Codex', shell: 'Shell', local: 'Local' };
|
|
27
|
-
const NOEND = 'Ending a session needs the page route /api/session/remove, which did not answer. Use the terminal in the
|
|
28
|
-
const MORE = 'Sending a prompt is not available on this page: no safe route sends one. Use the session\'s terminal in the
|
|
27
|
+
const NOEND = 'Ending a session needs the page route /api/session/remove, which did not answer. Use the terminal in the classic app (/classic.html).';
|
|
28
|
+
const MORE = 'Sending a prompt is not available on this page: no safe route sends one. Use the session\'s terminal in the classic app (/classic.html).';
|
|
29
29
|
const MAX_TREES = 10;
|
|
30
30
|
|
|
31
31
|
const ago = (ms) => {
|
|
@@ -50,7 +50,7 @@ export function createDock(opts) {
|
|
|
50
50
|
const side = el('aside', { class: 'c3dock dock', 'aria-label': 'Sessions', 'data-c3pg': 'dock' },
|
|
51
51
|
el('div', { class: 'dk-h' }, c3ic('repo', 'sm'), sel,
|
|
52
52
|
listBtn,
|
|
53
|
-
el('a', { class: 'ib sm', href: '/', title: 'Start a branch and session in the
|
|
53
|
+
el('a', { class: 'ib sm', href: '/classic.html', title: 'Start a branch and session in the classic app. A session starts over the terminal connection; the page has no route for it.', 'aria-label': 'Start a branch and session in the classic app' }, c3ic('plus', 'sm')),
|
|
54
54
|
el('button', { type: 'button', class: 'ib sm', title: 'Fold the dock (Ctrl J)', 'aria-label': 'Fold the dock', onclick: () => o.setOpen && o.setOpen(false) }, c3ic('fold'))),
|
|
55
55
|
body);
|
|
56
56
|
|
|
@@ -73,7 +73,7 @@ export function createDock(opts) {
|
|
|
73
73
|
const ss = inTree(t.dir);
|
|
74
74
|
const body = ss.length ? ss.slice(0, 4).map(row)
|
|
75
75
|
: [t.main ? el('span', { class: 'dim mono', style: 'font-size:var(--fs-1);padding:2px 6px' }, 'no session')
|
|
76
|
-
: el('a', { class: 'chip2 lk', href: '/', title: 'Start a session on this branch in the
|
|
76
|
+
: el('a', { class: 'chip2 lk', href: '/classic.html', title: 'Start a session on this branch in the classic app' }, c3ic('plus', 'sm'), 'session')];
|
|
77
77
|
if (ss.length > 4) body.push(el('span', { class: 'dim mono', style: 'font-size:var(--fs-1);padding:2px 6px' }, '+' + (ss.length - 4) + ' more'));
|
|
78
78
|
return el('div', { class: 'br', 'data-c3-tree': t.dir }, el('button', { type: 'button', class: 'bn', 'data-c3-branch': t.dir, 'aria-current': String(!!S.sel && ss.some((x) => x.id === S.sel)), title: ss.length ? 'Open the session on this branch' : 'This branch has no session', onclick: () => { if (ss[0]) pick((ss.find((x) => x.shown !== 'exited') || ss[0]).id); else { S.sel = null; S.selTree = t.dir; drawTree(); drawDetail(); } },
|
|
79
79
|
style: 'border:0;background:none;text-align:left;width:100%;cursor:pointer' }, c3ic('branch', 'sm'), t.branch || '(detached)'), el('div', { class: 'ss' }, ...body));
|
|
@@ -82,13 +82,13 @@ export function createDock(opts) {
|
|
|
82
82
|
const known = new Set(list.map((t) => t.dir));
|
|
83
83
|
const stray = mine().filter((s) => !known.has(s.tree));
|
|
84
84
|
if (stray.length) nodes.push(el('div', { class: 'br' }, el('div', { class: 'bn' }, c3ic('branch', 'sm'), 'other trees'), el('div', { class: 'ss' }, ...stray.slice(0, 4).map(row))));
|
|
85
|
-
if (all.length > MAX_TREES) nodes.push(el('div', { class: 'dim mono
|
|
85
|
+
if (all.length > MAX_TREES) nodes.push(el('div', { class: 'dim mono dk-more' }, '+' + (all.length - MAX_TREES) + ' more branches in the classic app'));
|
|
86
86
|
tree.replaceChildren(...(nodes.length ? nodes : [el('p', { class: 'dim mono', style: 'font-size:var(--fs-2);padding:4px' }, 'No working tree is registered for this repository.')]));
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
async function drawDetail() {
|
|
90
90
|
const s = mine().find((x) => x.id === S.sel);
|
|
91
|
-
if (!s) { const bt = S.selTree && S.trees && S.trees.find((x) => x.dir === S.selTree); detail.replaceChildren(el('p', { class: 'dim mono
|
|
91
|
+
if (!s) { const bt = S.selTree && S.trees && S.trees.find((x) => x.dir === S.selTree); detail.replaceChildren(el('p', { class: 'dim mono dk-empty', 'data-c3pg': 'dock-empty' }, bt ? (bt.branch || 'This branch') + ' has no session. A session starts in the classic app.' : mine().length ? 'Pick a session above.' : 'No session runs in this repository.')); return; }
|
|
92
92
|
const t = S.trees && S.trees.find((x) => x.dir === s.tree);
|
|
93
93
|
const started = ago(Date.now() - Date.parse(s.created));
|
|
94
94
|
const last = Number.isFinite(s.lastOutputAt) ? ago(Date.now() - s.lastOutputAt) : null;
|
|
@@ -103,7 +103,7 @@ export function createDock(opts) {
|
|
|
103
103
|
el('div', { class: 'chips2' }, el('b', { style: 'font-size:var(--fs-3)' }, s.name), el('span', { class: 'chips2 dim mono', style: 'font-size:var(--fs-1)' }, c3ic(PI[s.agent] || 'term', 'sm'), (PN[s.agent] || s.agent) + ' · ' + ((t && t.branch) || 'no branch'))),
|
|
104
104
|
el('div', { class: 'dim mono', style: 'font-size:var(--fs-1)' }, facts),
|
|
105
105
|
s.shown === 'needs' ? el('div', { class: 'fold a' }, c3ic('alert', 'sm'), 'needs you') : null,
|
|
106
|
-
el('a', { class: 'chip2 lk', href: '/', 'data-c3pg': 'dock-open-session', title: TIPS.terminalOpen, 'aria-label': 'Open the session ' + s.name,
|
|
106
|
+
el('a', { class: 'chip2 lk', href: '/classic.html', 'data-c3pg': 'dock-open-session', title: TIPS.terminalOpen, 'aria-label': 'Open the session ' + s.name,
|
|
107
107
|
onclick: (e) => { if (e.metaKey || e.ctrlKey || e.shiftKey || e.button > 0) return; e.preventDefault(); import('./c3-session-term.js').then((m) => m.openSessionTerm({ id: s.id, name: s.name })); } }, ic('term', 'sm'), 'Open the session'),
|
|
108
108
|
term,
|
|
109
109
|
el('div', { class: 'form' },
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Nothing is invented: a board with no linked documents says so. */
|
|
11
11
|
import { el } from './dom.js';
|
|
12
12
|
import { registerBoardTab } from './c3-initiatives.js';
|
|
13
|
-
import { icon, pbtn, lk, chip, tag, st, note, sect, list, empty, nrow, plural, day, ago, parseDay } from './c3-ini-ui.js';
|
|
13
|
+
import { icon, pbtn, lk, chip, tag, st, note, sect, list, empty, nrow, plural, day, ago, parseDay, put } from './c3-ini-ui.js';
|
|
14
14
|
import { loadItems } from './c3-ini-work.js';
|
|
15
15
|
|
|
16
16
|
const STALE_DAYS = 30;
|
|
@@ -50,30 +50,30 @@ export function renderDocs(host, ctx, docs, order, titles, grouped) {
|
|
|
50
50
|
const q = input.value.trim().toLowerCase();
|
|
51
51
|
const shown = docs.filter((d) => !q || (d.name + ' ' + d.path + ' ' + d.ty + ' ' + d.ini + ' ' + (titles.get(d.ini) || '')).toLowerCase().includes(q));
|
|
52
52
|
count.textContent = q ? plural(shown.length, 'document') + ' match' + (shown.length === 1 ? 'es' : '') : plural(docs.length, 'document');
|
|
53
|
-
if (!shown.length) { results
|
|
54
|
-
if (!grouped) { results
|
|
55
|
-
results
|
|
53
|
+
if (!shown.length) { put(results, empty(q ? 'No document matches "' + input.value.trim() + '".' : 'No documents.', { 'data-inb': 'docs-none' })); return; }
|
|
54
|
+
if (!grouped) { put(results, sect({ ic: 'doc', title: 'Documents', count: shown.length }, list(shown.map(row)))); return; }
|
|
55
|
+
put(results, ...order.filter((id) => shown.some((d) => d.ini === id)).map((id) => {
|
|
56
56
|
const g = shown.filter((d) => d.ini === id);
|
|
57
57
|
return sect({ ic: 'init', title: id + ' ' + (titles.get(id) || ''), count: g.length, attrs: { 'data-inb-docgrp': id }, right: lk({ onclick: () => ctx.openInitiative(id, 'docs'), 'aria-label': 'Open ' + id }, 'Open initiative') }, list(g.map(row)));
|
|
58
58
|
}));
|
|
59
59
|
};
|
|
60
60
|
input.addEventListener('input', draw);
|
|
61
|
-
host
|
|
61
|
+
put(host, el('div', { class: 'calbar' }, el('div', { class: 'form ini-grow' }, input), count), results);
|
|
62
62
|
draw();
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
registerBoardTab('docs', async (host, ctx) => {
|
|
66
66
|
let dead = false;
|
|
67
|
-
host
|
|
67
|
+
put(host, note('Reading the documents'));
|
|
68
68
|
const items = await loadItems(ctx);
|
|
69
69
|
if (dead) return () => {};
|
|
70
|
-
if (!items.ok) { host
|
|
70
|
+
if (!items.ok) { put(host, el('div', { class: 'fold w', role: 'alert' }, icon('alert', 'sm'), 'The initiatives of this board could not be read: ' + items.error + '.')); return () => {}; }
|
|
71
71
|
const titles = new Map(items.initiatives.map((r) => [r.id, r.title]));
|
|
72
72
|
const lib = await loadDocs(ctx, new Set(titles.keys()));
|
|
73
73
|
if (dead) return () => {};
|
|
74
|
-
if (!lib.ok) { host
|
|
74
|
+
if (!lib.ok) { put(host, el('div', { class: 'fold w', role: 'alert' }, icon('alert', 'sm'), 'The documents could not be read: ' + lib.error + '.')); return () => {}; }
|
|
75
75
|
if (!lib.docs.length) {
|
|
76
|
-
host
|
|
76
|
+
put(host, empty(items.initiatives.length ? 'No documents are linked to the initiatives of ' + ctx.board.name + ' yet.' : ctx.board.name + ' has no initiatives, so there are no documents.', { 'data-inb': 'docs-empty' }));
|
|
77
77
|
return () => { dead = true; };
|
|
78
78
|
}
|
|
79
79
|
const order = [...new Set(lib.docs.map((d) => d.ini))];
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* host.dataset.ibbSeq counts the answers drawn (the browser suite polls both). */
|
|
13
13
|
import { el } from './dom.js';
|
|
14
14
|
import { registerBoardTab } from './c3-initiatives.js';
|
|
15
|
-
import { icon, pill, lk, ib, pbtn, chip, st, note, lbl, sect, list, empty, nrow, statusChip, ownerChip, stepsMini, progChip, dateChip, daysBetween, plural, openMenu, sw, CATS } from './c3-ini-ui.js';
|
|
15
|
+
import { icon, pill, lk, ib, pbtn, chip, st, note, lbl, sect, list, empty, nrow, statusChip, ownerChip, stepsMini, progChip, dateChip, daysBetween, plural, openMenu, sw, CATS, put } from './c3-ini-ui.js';
|
|
16
16
|
import { wfOf, catOf, tplName } from './c3-ini-work.js';
|
|
17
17
|
|
|
18
18
|
const PAGE = 25;
|
|
@@ -44,20 +44,20 @@ registerBoardTab('initiatives', (host, ctx) => {
|
|
|
44
44
|
const err = el('div', { class: 'fold w', role: 'alert', 'data-ibb': 'error', hidden: '' });
|
|
45
45
|
const body = el('div', { 'data-ibb': 'body' });
|
|
46
46
|
const pager = el('div', { class: 'chips2 ini-pager', 'data-ibb': 'pager' });
|
|
47
|
-
host
|
|
47
|
+
put(host, el('div', { class: 'calbody', 'data-ibb': 'initiatives' }, top, filters, count, err, body, pager));
|
|
48
48
|
host.dataset.ibbState = 'loading'; host.dataset.ibbSeq = '0';
|
|
49
49
|
|
|
50
50
|
const sel = (k, label, opts) => el('label', { class: 'chips2' }, lbl(label),
|
|
51
51
|
el('select', { 'data-ibb-filter': k, 'aria-label': label, onchange: (e) => { Q[k] = e.target.value; Q.offset = 0; request(); } }, ...opts.map(([v, l]) => el('option', { value: v, selected: v === Q[k] }, l))));
|
|
52
52
|
function drawControls() {
|
|
53
|
-
views
|
|
53
|
+
put(views, ...VIEWS.map(([k, l]) => pill({ 'data-ibb-view': k, 'aria-pressed': String(Q.view === k), onclick: () => { Q.view = k; Q.offset = 0; request(); } }, l)));
|
|
54
54
|
const fa = (S.data && S.data.facets) || {};
|
|
55
55
|
const keep = (k, keys) => (Q[k] && !keys.includes(Q[k]) ? [Q[k], ...keys] : keys);
|
|
56
56
|
const order = (b.workflow || []).map((s) => s.name);
|
|
57
57
|
const stages = keep('stage', Object.keys(fa.stage || {}).sort((x, y) => ((order.indexOf(x) + 1 || 99) - (order.indexOf(y) + 1 || 99)) || x.localeCompare(y)));
|
|
58
58
|
const owners = keep('owner', Object.keys(fa.owner || {}).filter((o) => o !== 'none').sort((x, y) => (x === 'You' ? -1 : y === 'You' ? 1 : x.localeCompare(y))));
|
|
59
59
|
const n = (k, v) => (fa[k] && fa[k][v] != null ? ' (' + fa[k][v] + ')' : '');
|
|
60
|
-
filters
|
|
60
|
+
put(filters,
|
|
61
61
|
sel('cat', 'Category', [['', 'Any'], ...CATS.map((c) => [c, c + n('cat', c)])]),
|
|
62
62
|
sel('stage', 'Stage', [['', 'Any'], ...stages.map((s) => [s, s + n('stage', s)])]),
|
|
63
63
|
sel('status', 'Status', STATUS),
|
|
@@ -90,7 +90,7 @@ registerBoardTab('initiatives', (host, ctx) => {
|
|
|
90
90
|
if (pages < 2) { pager.replaceChildren(); return; }
|
|
91
91
|
const nums = []; for (let p = 0; p < pages; p++) if (p < 2 || p >= pages - 1 || Math.abs(p - cur) <= 1) nums.push(p); else if (nums[nums.length - 1] !== '…') nums.push('…');
|
|
92
92
|
const to = (p) => { Q.offset = p * PAGE; request(true); };
|
|
93
|
-
pager
|
|
93
|
+
put(pager, el('span', { class: 'dim mono ini-meta' }, (d.offset + 1) + '–' + Math.min(d.total, d.offset + d.rows.length) + ' of ' + d.total),
|
|
94
94
|
pill({ 'data-ibb': 'prev', disabled: cur === 0, onclick: () => to(cur - 1) }, 'Previous'),
|
|
95
95
|
...nums.map((p) => (p === '…' ? el('span', { class: 'dim' }, '…') : pill({ 'data-ibb-page': String(p + 1), 'aria-pressed': String(p === cur), 'aria-label': 'Page ' + (p + 1), onclick: () => to(p) }, String(p + 1)))),
|
|
96
96
|
pill({ 'data-ibb': 'next', disabled: cur >= pages - 1, onclick: () => to(cur + 1) }, 'Next'));
|
|
@@ -101,8 +101,8 @@ registerBoardTab('initiatives', (host, ctx) => {
|
|
|
101
101
|
const d = S.data;
|
|
102
102
|
if (!d) return;
|
|
103
103
|
const label = (VIEWS.find((v) => v[0] === Q.view) || [])[1];
|
|
104
|
-
count
|
|
105
|
-
body
|
|
104
|
+
put(count, plural(d.total, 'initiative') + ' · ' + label + (active() ? ' · ' : ''), active() ? lk({ 'data-ibb': 'clear', onclick: clearFilters }, 'Clear filters') : '', ' · each row shows its own workflow and stage');
|
|
105
|
+
put(body, sect({ ic: 'init', title: 'Initiatives', count: d.total },
|
|
106
106
|
d.rows.length ? list(d.rows.map(rowOf), { 'data-ibb': 'rows' }) : empty(active() || Q.view !== 'all' ? 'No initiatives match. Clear the filters or pick All.' : b.name + ' has no initiatives yet. Press New initiative, or accept an ask in Triage.', { 'data-ibb': 'empty' })));
|
|
107
107
|
drawPager(d);
|
|
108
108
|
host.dataset.ibbSeq = String(++S.seq);
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Every control is a button; the drag handlers are drag events on the cards and columns (pointer handlers stay on the document). */
|
|
15
15
|
import { el } from './dom.js';
|
|
16
16
|
import { registerBoardTab } from './c3-initiatives.js';
|
|
17
|
-
import { icon, pill, lk, ib, pbtn, note, lbl, sect, card, list, empty, stepsMini, jiraLabel, plural, trunc, openMenu, mono } from './c3-ini-ui.js';
|
|
17
|
+
import { icon, pill, lk, ib, pbtn, note, lbl, sect, card, list, empty, stepsMini, jiraLabel, plural, trunc, openMenu, mono, put } from './c3-ini-ui.js';
|
|
18
18
|
import { STATUSES, wfOf, tplName, loadItems, dropItems, workRow, workTlRows, iniTlRow, rescheduleTask, lockedToast } from './c3-ini-work.js';
|
|
19
19
|
import { timeline, tlInit, tlRange, tlStill } from './c3-ini-timeline.js';
|
|
20
20
|
|
|
@@ -40,7 +40,7 @@ function mountItems(tab) {
|
|
|
40
40
|
if (ctx.filter && ctx.filter.ini) st.ini = ctx.filter.ini;
|
|
41
41
|
if (!b.build && st.src === 'jira') st.src = '';
|
|
42
42
|
let dead = false; let R = null;
|
|
43
|
-
host
|
|
43
|
+
put(host, note('Reading the work items'));
|
|
44
44
|
async function reload(force) {
|
|
45
45
|
R = await loadItems(ctx, force);
|
|
46
46
|
if (!dead) draw();
|
|
@@ -183,10 +183,10 @@ function mountItems(tab) {
|
|
|
183
183
|
|
|
184
184
|
function draw(focusSel) {
|
|
185
185
|
if (dead) return;
|
|
186
|
-
if (!R.ok) { host
|
|
186
|
+
if (!R.ok) { put(host, el('div', { class: 'fold w', role: 'alert', 'data-ini': 'error' }, icon('alert', 'sm'), 'The work items could not be read: ' + R.error + '.')); return; }
|
|
187
187
|
const all = R.items; const shown = all.filter((w) => matches(st, w));
|
|
188
188
|
const body = tab === 'backlog' ? backlogBody(shown, all) : tab === 'board' ? kanbanBody(shown, all) : timelineBody(shown);
|
|
189
|
-
host
|
|
189
|
+
put(host, el('div', { class: 'calbody', 'data-wk-tab': tab }, ...toolbar(all, shown), ...body));
|
|
190
190
|
if (focusSel) { const f = host.querySelector(focusSel); if (f) { f.focus(); if (f.setSelectionRange && f.value) f.setSelectionRange(f.value.length, f.value.length); } }
|
|
191
191
|
}
|
|
192
192
|
|