create-pmos 0.55.4 → 0.55.5
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/guide/today.md +15 -1
- package/workspace/lib/goals.js +44 -4
- package/workspace/lib/proactive.js +20 -2
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/components.css +3 -0
- package/workspace/public/pm/dest-today.js +4 -1
- package/workspace/public/pm/today-copy.js +5 -0
- package/workspace/public/pm/today-day.js +57 -17
- package/workspace/public/pm/today-goals.js +13 -7
- package/workspace/server.js +7 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.55.
|
|
3
|
+
"version": "0.55.5",
|
|
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-06T10:22:39Z"}
|
|
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-06T10:22:39Z"}
|
|
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-06T10:22:39Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
package/workspace/guide/today.md
CHANGED
|
@@ -20,7 +20,21 @@ If the morning-brief session stops before it submits a brief, the Read step says
|
|
|
20
20
|
|
|
21
21
|
Each agent job runs in its own session, so you can open any job and follow or steer it. The workspace types the job's prompt into the session when the agent is ready to read, and presses Enter for you. When you approve or revise a job and its session is still open, the workspace uses that same session again. Prepare again works the same way: when the morning-brief session is still open and has finished its read, or has printed nothing for two minutes, the workspace types the read into that session and runs it again.
|
|
22
22
|
|
|
23
|
-
**Agent results**, at the top of the left column, counts the jobs and lists each finished one with its draft summary or the reason it failed. The other things that wait for you, such as outcome checks, report drafts and sessions, are in **Also waiting for you** at the end of the left column. When the day is folded, they show under it.
|
|
23
|
+
**Agent results**, at the top of the left column, counts the jobs and lists each finished one with its draft summary or the reason it failed. Your goals for today come under it, above the calendar. The other things that wait for you, such as outcome checks, report drafts and sessions, are in **Also waiting for you** at the end of the left column. When the day is folded, they show under it.
|
|
24
|
+
|
|
25
|
+
### One day for every repository
|
|
26
|
+
|
|
27
|
+
The day is yours, not a repository's. Every repository shows the same calendar, goals, jobs and mentions. To narrow the day, use the **Show** switch at the top of Today:
|
|
28
|
+
|
|
29
|
+
- **All repositories** shows the whole day. A job, mention, goal or watched place that belongs to a repository carries its name.
|
|
30
|
+
- **Repository** shows the items of the repository you opened Today from, and the items that belong to no repository. The calendar always shows.
|
|
31
|
+
|
|
32
|
+
To tie an item to a repository:
|
|
33
|
+
|
|
34
|
+
1. For a goal, open it and pick a **Repository**. A goal you add while Today shows one repository belongs to that repository.
|
|
35
|
+
2. For a Slack channel, Jira project or Confluence space, open **Sources**, open the service, and pick a repository next to the place.
|
|
36
|
+
|
|
37
|
+
A job takes the repository of its goal. A job with no goal takes the repository of the place it came from. A job with neither belongs to no repository.
|
|
24
38
|
|
|
25
39
|
Each job has one of three kinds.
|
|
26
40
|
|
package/workspace/lib/goals.js
CHANGED
|
@@ -7,9 +7,16 @@
|
|
|
7
7
|
*
|
|
8
8
|
* emptyDay(date) → { date, goals: [] }
|
|
9
9
|
* edit(day, op, { now }) → { ok:true, day } | { ok:false, error }
|
|
10
|
-
* op: { action: 'add', text, initiative_id? } | { action: 'done'|'drop'|'reopen'|'remove', id }
|
|
10
|
+
* op: { action: 'add', text, initiative_id?, repo? } | { action: 'done'|'drop'|'reopen'|'remove', id }
|
|
11
|
+
* | { action: 'tag', id, repo } (repo: a workspace id, or null for none)
|
|
11
12
|
* readDay(raw, date) → a checked day record, or an empty day when `raw` is not one
|
|
12
13
|
* planDay({ day, brief, cards, now, endOfDay }) → the plan Today and `pmos proactive goals` show
|
|
14
|
+
* itemRepos({ day, brief, placeRepos }) → { itemId: repo } for the brief items that have a repository
|
|
15
|
+
*
|
|
16
|
+
* ONE DAY, REPOSITORY TAGS. The day is the PM's, not a repository's: every repository shows the same goals, brief and
|
|
17
|
+
* jobs. A goal can carry a repository (`repo`, a workspace id), and so can a channel, project or space (the settings'
|
|
18
|
+
* `placeRepos`). An item takes the repository of the open goal it is under; failing that, of the place it came from
|
|
19
|
+
* (its `where`). An item with neither is personal and shows under every repository.
|
|
13
20
|
*
|
|
14
21
|
* THE JOIN IS CHECKABLE. A brief item or calendar event belongs to a goal when it names the goal's id (the morning-brief
|
|
15
22
|
* session sets it). Failing that, an item or Today card belongs to the first open goal tied to the same initiative. There
|
|
@@ -25,7 +32,9 @@ const crypto = require('crypto');
|
|
|
25
32
|
const MAX_GOALS = 7;
|
|
26
33
|
const MAX_TEXT = 140;
|
|
27
34
|
const STATUSES = ['open', 'done', 'dropped'];
|
|
28
|
-
const ACTIONS = ['add', 'done', 'drop', 'reopen', 'remove'];
|
|
35
|
+
const ACTIONS = ['add', 'done', 'drop', 'reopen', 'remove', 'tag'];
|
|
36
|
+
const REPO_RE = /^[A-Za-z0-9._-]{1,80}$/;
|
|
37
|
+
const repoOrNull = (r) => (typeof r === 'string' && REPO_RE.test(r) ? r : null);
|
|
29
38
|
const INI_RE = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
30
39
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
31
40
|
const GOAL_ID_RE = /^g-[0-9a-f]{8}$/;
|
|
@@ -42,6 +51,7 @@ function readDay(raw, date) {
|
|
|
42
51
|
&& STATUSES.includes(g.status)).slice(0, MAX_GOALS).map((g) => ({
|
|
43
52
|
id: g.id, text: clip(g.text, MAX_TEXT), initiative_id: typeof g.initiative_id === 'string' && INI_RE.test(g.initiative_id) ? g.initiative_id : null,
|
|
44
53
|
status: g.status, added_at: typeof g.added_at === 'string' ? g.added_at : null, closed_at: typeof g.closed_at === 'string' ? g.closed_at : null,
|
|
54
|
+
repo: repoOrNull(g.repo),
|
|
45
55
|
}));
|
|
46
56
|
return { date, goals };
|
|
47
57
|
}
|
|
@@ -58,16 +68,22 @@ function edit(day, op, { now = Date.now() } = {}) {
|
|
|
58
68
|
if (goals.length >= MAX_GOALS) return { ok: false, error: `a day has at most ${MAX_GOALS} goals. Drop or remove one first.` };
|
|
59
69
|
const ini = op.initiative_id == null || op.initiative_id === '' ? null : op.initiative_id;
|
|
60
70
|
if (ini !== null && (typeof ini !== 'string' || !INI_RE.test(ini))) return { ok: false, error: 'initiative_id is not an initiative id' };
|
|
71
|
+
if (op.repo != null && op.repo !== '' && !repoOrNull(op.repo)) return { ok: false, error: 'repo is not a workspace id' };
|
|
61
72
|
const text = clip(op.text, MAX_TEXT);
|
|
62
73
|
if (goals.some((g) => g.text.toLowerCase() === text.toLowerCase())) return { ok: false, error: 'that goal is already on the list' };
|
|
63
74
|
const id = 'g-' + crypto.createHash('sha1').update(`${day.date}|${text}|${at}|${goals.length}`).digest('hex').slice(0, 8);
|
|
64
|
-
goals.push({ id, text, initiative_id: ini, status: 'open', added_at: at, closed_at: null });
|
|
75
|
+
goals.push({ id, text, initiative_id: ini, status: 'open', added_at: at, closed_at: null, repo: repoOrNull(op.repo) });
|
|
65
76
|
return { ok: true, day: { date: day.date, goals }, goal: goals[goals.length - 1] };
|
|
66
77
|
}
|
|
67
78
|
const i = goals.findIndex((g) => g.id === op.id);
|
|
68
79
|
if (i < 0) return { ok: false, error: 'no goal with that id today' };
|
|
69
80
|
const g = goals[i];
|
|
70
81
|
if (op.action === 'remove') { goals.splice(i, 1); return { ok: true, day: { date: day.date, goals }, goal: g }; }
|
|
82
|
+
if (op.action === 'tag') {
|
|
83
|
+
if (op.repo != null && op.repo !== '' && !repoOrNull(op.repo)) return { ok: false, error: 'repo is not a workspace id' };
|
|
84
|
+
g.repo = repoOrNull(op.repo);
|
|
85
|
+
return { ok: true, day: { date: day.date, goals }, goal: g };
|
|
86
|
+
}
|
|
71
87
|
const to = { done: 'done', drop: 'dropped', reopen: 'open' }[op.action];
|
|
72
88
|
if (g.status === to) return { ok: false, error: `that goal is already ${to}` };
|
|
73
89
|
g.status = to; g.closed_at = to === 'open' ? null : at;
|
|
@@ -184,4 +200,28 @@ function planDay({ day, brief = null, cards = [], now = Date.now(), endOfDay = E
|
|
|
184
200
|
};
|
|
185
201
|
}
|
|
186
202
|
|
|
187
|
-
|
|
203
|
+
/** The place an item came from, as a key that a place name can match: lower case, no leading '#', and a Jira issue key
|
|
204
|
+
* (PAY-412) reduced to its project key (pay). A place named "PAY · Payments" matches by its first word. */
|
|
205
|
+
const placeKey = (s) => { const t = String(s || '').trim().toLowerCase().replace(/^#/, ''); const k = /^([a-z][a-z0-9]*)-\d+$/.exec(t); return k ? k[1] : t; };
|
|
206
|
+
const placeNameKeys = (n) => { const t = String(n || '').trim().toLowerCase().replace(/^#/, ''); return [t, t.split(/[\s·:|]+/)[0]].filter(Boolean); };
|
|
207
|
+
|
|
208
|
+
/** { itemId: repo } for today's brief items: the repository of the open goal an item is under, else of its place. */
|
|
209
|
+
function itemRepos({ day, brief, placeRepos = {} } = {}) {
|
|
210
|
+
const out = {};
|
|
211
|
+
if (!brief || !day || brief.date !== day.date) return out;
|
|
212
|
+
const open = (day.goals || []).filter((g) => g.status === 'open');
|
|
213
|
+
const byId = new Map(open.map((g) => [g.id, g]));
|
|
214
|
+
const byIni = (ini) => (ini ? open.find((g) => g.initiative_id === ini) || null : null);
|
|
215
|
+
for (const x of [...(brief.priorities || []), ...(brief.worth_asking || [])]) {
|
|
216
|
+
const g = (x.goal && byId.get(x.goal)) || byIni(x.initiative_id);
|
|
217
|
+
let r = g ? repoOrNull(g.repo) : null;
|
|
218
|
+
if (!r && x.where) {
|
|
219
|
+
const want = placeKey(x.where);
|
|
220
|
+
for (const [name, repo] of Object.entries(placeRepos[x.source] || {})) if (placeNameKeys(name).includes(want) && repoOrNull(repo)) { r = repo; break; }
|
|
221
|
+
}
|
|
222
|
+
if (r) out[x.id] = r;
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = { MAX_GOALS, MAX_TEXT, STATUSES, ACTIONS, GOAL_ID_RE, START_OF_DAY, END_OF_DAY, emptyDay, readDay, edit, freeTime, planDay, itemRepos };
|
|
@@ -44,7 +44,7 @@ const MAX_WEBSITES = 10;
|
|
|
44
44
|
const DAY_CHECK_EVERY = [0, 30, 60];
|
|
45
45
|
const DAY_CHECK_DEFAULT = Object.freeze({ every: 60, start: '09:00', end: '18:00' });
|
|
46
46
|
const DEFAULTS = Object.freeze({ name: '', briefSources: BRIEF_DEFAULT, briefScoring: false, backgroundPrepare: false, enabled: true, triggers: Object.freeze(Object.fromEntries(TRIGGERS.map((t) => [t, true]))), stalledDays: 5, quiet: null, pauseLearning: false,
|
|
47
|
-
scopes: Object.freeze({ slack: Object.freeze({}), jira: Object.freeze({}), confluence: Object.freeze({}) }), dayCheck: DAY_CHECK_DEFAULT, websites: Object.freeze([]) });
|
|
47
|
+
scopes: Object.freeze({ slack: Object.freeze({}), jira: Object.freeze({}), confluence: Object.freeze({}) }), placeRepos: Object.freeze({ slack: Object.freeze({}), jira: Object.freeze({}), confluence: Object.freeze({}) }), dayCheck: DAY_CHECK_DEFAULT, websites: Object.freeze([]) });
|
|
48
48
|
|
|
49
49
|
const idOf = (key) => crypto.createHash('sha1').update(String(key)).digest('hex').slice(0, 12);
|
|
50
50
|
const clip = (s, n) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, n);
|
|
@@ -178,7 +178,7 @@ const HHMM = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
|
|
178
178
|
function normalizeSettings(raw) {
|
|
179
179
|
const r = raw && typeof raw === 'object' ? raw : {};
|
|
180
180
|
const s = { name: DEFAULTS.name, briefSources: { ...BRIEF_DEFAULT }, briefScoring: DEFAULTS.briefScoring, backgroundPrepare: DEFAULTS.backgroundPrepare, enabled: DEFAULTS.enabled, triggers: { ...DEFAULTS.triggers }, stalledDays: DEFAULTS.stalledDays, quiet: DEFAULTS.quiet, pauseLearning: DEFAULTS.pauseLearning,
|
|
181
|
-
scopes: { slack: {}, jira: {}, confluence: {} }, dayCheck: { ...DAY_CHECK_DEFAULT }, websites: [] };
|
|
181
|
+
scopes: { slack: {}, jira: {}, confluence: {} }, placeRepos: { slack: {}, jira: {}, confluence: {} }, dayCheck: { ...DAY_CHECK_DEFAULT }, websites: [] };
|
|
182
182
|
if (r.enabled !== undefined) { if (typeof r.enabled !== 'boolean') return { ok: false, error: 'enabled must be true or false' }; s.enabled = r.enabled; }
|
|
183
183
|
if (r.name !== undefined) { if (typeof r.name !== 'string' || r.name.length > 40) return { ok: false, error: 'name must be text of at most 40 characters' }; s.name = r.name.replace(/\s+/g, ' ').trim(); }
|
|
184
184
|
if (r.briefScoring !== undefined) { if (typeof r.briefScoring !== 'boolean') return { ok: false, error: 'briefScoring must be true or false' }; s.briefScoring = r.briefScoring; }
|
|
@@ -228,6 +228,24 @@ function normalizeSettings(raw) {
|
|
|
228
228
|
s.scopes[svc] = out;
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
|
+
// placeRepos: the repository a channel, project or space belongs to (a workspace id). Today shows its mentions and jobs
|
|
232
|
+
// under that repository; a place with none is personal. null clears it; the route merges a patch over what is stored.
|
|
233
|
+
if (r.placeRepos !== undefined) {
|
|
234
|
+
if (!r.placeRepos || typeof r.placeRepos !== 'object' || Array.isArray(r.placeRepos)) return { ok: false, error: `placeRepos must be an object of ${SCOPE_SERVICES.join(', ')} → {name: workspace id}` };
|
|
235
|
+
for (const [svc, names] of Object.entries(r.placeRepos)) {
|
|
236
|
+
if (!SCOPE_SERVICES.includes(svc)) return { ok: false, error: `unknown placeRepos service "${svc}" — one of ${SCOPE_SERVICES.join(', ')}` };
|
|
237
|
+
if (!names || typeof names !== 'object' || Array.isArray(names)) return { ok: false, error: `placeRepos.${svc} must be an object of name → workspace id` };
|
|
238
|
+
const out = {};
|
|
239
|
+
for (const [name, repo] of Object.entries(names)) {
|
|
240
|
+
if (repo === null || repo === '') continue;
|
|
241
|
+
if (!name.trim() || name.length > 120) return { ok: false, error: `a ${svc} name is 1 to 120 characters` };
|
|
242
|
+
if (typeof repo !== 'string' || !/^[A-Za-z0-9._-]{1,80}$/.test(repo)) return { ok: false, error: `the repository of ${svc} "${name}" must be a workspace id` };
|
|
243
|
+
out[name] = repo;
|
|
244
|
+
}
|
|
245
|
+
if (Object.keys(out).length > MAX_SCOPE_NAMES) return { ok: false, error: `placeRepos.${svc} has ${Object.keys(out).length} names — at most ${MAX_SCOPE_NAMES}` };
|
|
246
|
+
s.placeRepos[svc] = out;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
231
249
|
if (r.dayCheck !== undefined) {
|
|
232
250
|
const d = r.dayCheck;
|
|
233
251
|
if (!d || typeof d !== 'object' || Array.isArray(d)) return { ok: false, error: 'dayCheck must be {every, start, end}' };
|
package/workspace/package.json
CHANGED
|
@@ -2362,6 +2362,9 @@
|
|
|
2362
2362
|
.td-day__r[data-goal-tie="true"] { border-left: 3px solid var(--foreground); }
|
|
2363
2363
|
.td-day__lead { min-width: 44px; font: 12px var(--mono); color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
|
|
2364
2364
|
.td-day__chips { display: inline-flex; gap: 4px; }
|
|
2365
|
+
.td-day__chips { align-items: center; flex-wrap: wrap; justify-content: flex-end; }
|
|
2366
|
+
.td-day__repo { font: inherit; font-size: 12px; color: var(--foreground); background: var(--background); border: 1px solid var(--border); border-radius: 0; padding: 2px 6px; max-width: 160px; }
|
|
2367
|
+
.td-day__repo:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 2px; }
|
|
2365
2368
|
.td-day__nowrow { display: flex; align-items: center; gap: 8px; padding: 0 8px; font: 11px var(--mono); color: var(--destructive); }
|
|
2366
2369
|
.td-day__nowrow::after { content: ""; flex: 1; border-top: 2px solid var(--destructive); }
|
|
2367
2370
|
.td-day__gh { display: flex; align-items: center; gap: 6px; margin: 4px 8px 2px; font-size: 12px; font-weight: 600; color: var(--muted-foreground); }
|
|
@@ -125,7 +125,10 @@ function build(ctx0) {
|
|
|
125
125
|
// the open day takes the list into its left column ("Also waiting for you"), so Today shows one layout; a folded day
|
|
126
126
|
// leaves it below, as before
|
|
127
127
|
const owed = { list, more, count: n };
|
|
128
|
-
|
|
128
|
+
// one day for every repository: the day is the same everywhere; the Show switch narrows it to the repository Today
|
|
129
|
+
// was opened from (`here`) or shows all of it
|
|
130
|
+
const here = sc.repo ? wsOf(sc.repo) : null;
|
|
131
|
+
const day = daySection({ ws: dayWs, setState: ctx0.setState || setState, openIni: dayRepo ? (id) => openInitiative(dayRepo, id) : null, owed, here, all: sc.all });
|
|
129
132
|
const inDay = !!(day && day.contains(list));
|
|
130
133
|
return { children: inDay ? [head, day, health] : [head, day, health, list, more], sig: JSON.stringify([sc.all, sc.repo, n, ready, expanded, banner]) + daySig(dayWs) + nodes.map((x) => x.outerHTML).join('') };
|
|
131
134
|
}
|
|
@@ -133,6 +133,11 @@ export const COPY = {
|
|
|
133
133
|
c.working ? `${c.working} working` : null, c.failed ? `${c.failed} failed` : null].filter(Boolean).join(' · ') || 'No agent job yet.',
|
|
134
134
|
summaryAll: (n) => `All ${n} agent ${n === 1 ? 'job has' : 'jobs have'} a result.`,
|
|
135
135
|
waitingHint: 'Outcome checks, reports and sessions from your repositories, outside today\'s jobs.',
|
|
136
|
+
// one day for every repository: a goal or a place can carry a repository; the Show switch narrows the day to it
|
|
137
|
+
repo: { label: 'Repository', none: 'No repository',
|
|
138
|
+
goalHint: 'Its jobs show under this repository. With no repository, the goal is yours and shows under every repository.',
|
|
139
|
+
placeHint: 'Repository: its mentions and jobs show under that repository. With none, they show under every repository.',
|
|
140
|
+
only: (name) => `Showing ${name} and your items with no repository. The calendar always shows.` },
|
|
136
141
|
now: (at) => `now ${at}`,
|
|
137
142
|
noCalendar: 'No events read for today.',
|
|
138
143
|
noJobs: 'No jobs yet. Press Prepare my day. Each job shows here; select one to see what it needs.',
|
|
@@ -130,7 +130,10 @@ function deriveStage(run, brief) {
|
|
|
130
130
|
return 'done';
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
/* One day for every repository. `view` is { here: the repository Today was opened from (a workspace id), all: the
|
|
134
|
+
* Show switch is on All repositories }. An item, a goal or a place carries a repository or none; with the switch on one
|
|
135
|
+
* repository, the day keeps that repository's items and the ones with none. The calendar is never narrowed. */
|
|
136
|
+
function model(d, view) {
|
|
134
137
|
const today = d && d.today;
|
|
135
138
|
const brief = d && d.brief && (!today || d.brief.date === today) ? d.brief : null;
|
|
136
139
|
const plan = (d && d.plan) || null;
|
|
@@ -156,17 +159,31 @@ function model(d) {
|
|
|
156
159
|
const level = (svc, name) => (scopes[svc] && scopes[svc][name]) || 'search';
|
|
157
160
|
const digests = ((brief && Array.isArray(brief.digests)) ? brief.digests : []).map((g) => ({ ...g, id: g.source + '|' + g.name, level: level(g.source, g.name) }));
|
|
158
161
|
const places = (brief && Array.isArray(brief.places)) ? brief.places : [];
|
|
162
|
+
const placeRepos = (settings && settings.placeRepos) || {};
|
|
163
|
+
const placeRepo = (svc, name) => (placeRepos[svc] && placeRepos[svc][name]) || null;
|
|
164
|
+
const itemRepos = (d && d.item_repos) || {};
|
|
165
|
+
const repos = (d && Array.isArray(d.repos)) ? d.repos : [];
|
|
166
|
+
const repoName = (id) => { const r = repos.find((x) => x.id === id); return r ? r.name : id; };
|
|
167
|
+
for (const j of jobs) j.repo = itemRepos[j.id] || null;
|
|
168
|
+
for (const g of digests) g.repo = placeRepo(g.source, g.name);
|
|
169
|
+
const v = view || {};
|
|
170
|
+
const narrow = !v.all && !!v.here;
|
|
171
|
+
const inView = (r) => !narrow || !r || r === v.here;
|
|
172
|
+
const shownJobs = jobs.filter((j) => inView(j.repo));
|
|
173
|
+
const shownIds = new Set(shownJobs.map((j) => j.task && j.task.id).filter(Boolean));
|
|
174
|
+
const shownTasks = narrow ? tasks.filter((t) => shownIds.has(t.id)) : tasks;
|
|
159
175
|
return {
|
|
160
|
-
d, brief, plan, goals, goalNo, run, tasks, jobs, events, now, settings, scopes, level,
|
|
176
|
+
d, brief, plan, goals: goals.filter((g) => inView(g.repo)), allGoals: goals, goalNo, run, tasks: shownTasks, jobs: shownJobs, events, now, settings, scopes, level,
|
|
177
|
+
digests: digests.filter((g) => inView(g.repo)), places, placeRepo, repos, repoName, here: v.here || null, narrow,
|
|
161
178
|
stage: run ? (d.stage || deriveStage(run, brief)) : null,
|
|
162
|
-
mentions:
|
|
179
|
+
mentions: shownJobs.filter((j) => j.item && j.item.mention),
|
|
163
180
|
sources: (d && Array.isArray(d.sources)) ? d.sources : [],
|
|
164
181
|
check: (d && d.check) || null,
|
|
165
182
|
readFailed: (d && d.read_failed) || null,
|
|
166
183
|
free: plan && plan.time ? plan.time.free_minutes : null,
|
|
167
184
|
maxGoals: (d && d.max_goals) || 7,
|
|
168
|
-
needs:
|
|
169
|
-
done:
|
|
185
|
+
needs: shownJobs.filter((j) => j.group === 'needs').length,
|
|
186
|
+
done: shownJobs.filter((j) => j.group === 'done').length,
|
|
170
187
|
};
|
|
171
188
|
}
|
|
172
189
|
|
|
@@ -179,6 +196,20 @@ const TONE = { '': 'muted', ok: 'outline', info: 'outline', goal: 'outline', war
|
|
|
179
196
|
function pill(text, tone, attrs) {
|
|
180
197
|
return badge({ text, tone: TONE[tone || ''] || 'muted', attrs });
|
|
181
198
|
}
|
|
199
|
+
/** The repository a row belongs to, as a small tag. A row with none (yours) carries no tag. */
|
|
200
|
+
function repoTag(m, r) {
|
|
201
|
+
return r ? badge({ text: m.repoName(r), tone: 'outline', attrs: { 'data-day-repo': r, title: COPY.day.repo.label } }) : null;
|
|
202
|
+
}
|
|
203
|
+
/** A picker of the repository: none, or one of the registered repositories. `onPick(id|null)`. */
|
|
204
|
+
export function repoPicker(m, cur, onPick, attrs) {
|
|
205
|
+
const sel = el('select', { class: 'td-day__repo', 'aria-label': COPY.day.repo.label, ...(attrs || {}) },
|
|
206
|
+
el('option', { value: '' }, COPY.day.repo.none), ...m.repos.map((r) => el('option', { value: r.id }, r.name)),
|
|
207
|
+
// a repository that is no longer registered stays shown, so the picker never hides what is stored
|
|
208
|
+
cur && !m.repos.some((r) => r.id === cur) ? el('option', { value: cur }, cur) : null);
|
|
209
|
+
sel.value = cur || '';
|
|
210
|
+
sel.addEventListener('change', () => onPick(sel.value || null));
|
|
211
|
+
return sel;
|
|
212
|
+
}
|
|
182
213
|
function statusPill(j) {
|
|
183
214
|
const s = D.status2;
|
|
184
215
|
const map = {
|
|
@@ -365,7 +396,7 @@ function jobRow(ctx, j) {
|
|
|
365
396
|
const n = j.goal ? ctx.m.goalNo.get(j.goal) : null;
|
|
366
397
|
const sub = j.status === 'failed' && j.task && j.task.error ? j.task.error
|
|
367
398
|
: [j.item && j.item.where, srcName(j.source)].filter(Boolean).join(' · ');
|
|
368
|
-
return rowBtn(ctx, 'job', j.id, { lead: n ? D.pill.goal(n) : '·', title: j.title, meta: sub, trail: statusPill(j),
|
|
399
|
+
return rowBtn(ctx, 'job', j.id, { lead: n ? D.pill.goal(n) : '·', title: j.title, meta: sub, trail: el('span', { class: 'td-day__chips' }, repoTag(ctx.m, j.repo), statusPill(j)),
|
|
369
400
|
dot: j.isNew && !seen.has(j.id) ? el('span', { class: 'td-day__newdot', 'data-day': 'new', role: 'img', 'aria-label': D.newItem }) : null });
|
|
370
401
|
}
|
|
371
402
|
|
|
@@ -412,7 +443,7 @@ function waitingPanel(owed) {
|
|
|
412
443
|
|
|
413
444
|
function goalsPanel(ctx) {
|
|
414
445
|
const { m } = ctx;
|
|
415
|
-
return panel('goals', D.goalsCount(m.
|
|
446
|
+
return panel('goals', D.goalsCount(m.allGoals.filter((g) => g.status === 'open').length, m.maxGoals), goalRows(ctx), goalAdd(ctx));
|
|
416
447
|
}
|
|
417
448
|
|
|
418
449
|
function mentionStatus(j) {
|
|
@@ -423,7 +454,7 @@ function mentionStatus(j) {
|
|
|
423
454
|
|
|
424
455
|
function digestRow(ctx, g) {
|
|
425
456
|
const sub = D.digestLine(D.levelWord[g.level] || g.level, (g.decisions || []).length, (g.open || []).length);
|
|
426
|
-
return rowBtn(ctx, 'channel', g.id, { lead: el('span', { class: 'td-day__chip' }, srcName(g.source)), title: g.name, meta: sub });
|
|
457
|
+
return rowBtn(ctx, 'channel', g.id, { lead: el('span', { class: 'td-day__chip' }, srcName(g.source)), title: g.name, meta: sub, trail: repoTag(ctx.m, g.repo) });
|
|
427
458
|
}
|
|
428
459
|
|
|
429
460
|
function mentionsPanel(ctx) {
|
|
@@ -438,7 +469,7 @@ function mentionsPanel(ctx) {
|
|
|
438
469
|
const rows = shown.length ? el('ul', { class: 'td-day__rows', 'data-day-list': 'mentions' }, ...shown.map((j) => {
|
|
439
470
|
const it = j.item;
|
|
440
471
|
return rowBtn(ctx, 'mention', j.id, { lead: el('span', { class: 'td-day__chip' }, srcName(j.source)),
|
|
441
|
-
title: it.about && it.ask ? `${it.about}: ${it.ask}` : it.ask || j.title, meta: D.fromWho(it.who, it.where) || it.why_now || '', trail: mentionStatus(j) });
|
|
472
|
+
title: it.about && it.ask ? `${it.about}: ${it.ask}` : it.ask || j.title, meta: D.fromWho(it.who, it.where) || it.why_now || '', trail: el('span', { class: 'td-day__chips' }, repoTag(ctx.m, j.repo), mentionStatus(j)) });
|
|
442
473
|
})) : note(D.noMentions);
|
|
443
474
|
const live = m.digests.filter((g) => g.level !== 'ignore');
|
|
444
475
|
const ign = m.digests.filter((g) => g.level === 'ignore');
|
|
@@ -469,7 +500,7 @@ function stepItem(st) {
|
|
|
469
500
|
function eventDetail(ctx, e) {
|
|
470
501
|
const { m } = ctx;
|
|
471
502
|
const n = e.goal ? m.goalNo.get(e.goal) : null;
|
|
472
|
-
const g = n ? m.
|
|
503
|
+
const g = n ? m.allGoals[n - 1] : null;
|
|
473
504
|
const ini = e.initiative && e.initiative.id ? e.initiative : null;
|
|
474
505
|
const steps = e.steps || {};
|
|
475
506
|
const notes = e.notes || {};
|
|
@@ -484,7 +515,7 @@ function eventDetail(ctx, e) {
|
|
|
484
515
|
list(D.actions, (w.actions || []).map((a) => (typeof a === 'string' ? a : stepItem(a))))] : note(D.noWispr),
|
|
485
516
|
mailBlock || note(D.noMail)) : null;
|
|
486
517
|
const tie = g ? el('p', { class: 'td-day__goalline', 'data-day': 'tie' }, D.countsToward(n, g.text))
|
|
487
|
-
: [note(D.tieHow, { 'data-day': 'tie' }), m.
|
|
518
|
+
: [note(D.tieHow, { 'data-day': 'tie' }), m.allGoals.length < m.maxGoals ? el('div', { class: 'td-day__acts' }, button({ label: D.addAsGoal, size: 'sm', attrs: { 'data-day-act': 'add-goal' },
|
|
488
519
|
onClick: async () => { if (await addGoal(ctx, e.title, ini ? ini.id : null)) toast({ text: D.goalAdded }); } })) : null];
|
|
489
520
|
return [
|
|
490
521
|
dhead(D.kind.meeting, e.title),
|
|
@@ -557,6 +588,7 @@ function jobDetail(ctx, j) {
|
|
|
557
588
|
const mention = j.item && j.item.mention;
|
|
558
589
|
return [
|
|
559
590
|
dhead(n ? D.kind.jobGoal(n) : D.kind.job, null),
|
|
591
|
+
j.repo ? el('div', { class: 'td-day__acts' }, repoTag(m, j.repo)) : null,
|
|
560
592
|
jobCard(ctx, j),
|
|
561
593
|
meets.length || mention ? el('div', { class: 'td-day__rel' }, el('h4', null, D.related), el('ul', { class: 'td-day__rows' },
|
|
562
594
|
...meets.map((e) => relRow(ctx, 'event', e.id, e.title, e.time)),
|
|
@@ -586,6 +618,10 @@ function setLevel(ctx, svc, name, lvl) {
|
|
|
586
618
|
return setSettings(ctx.ws, { scopes });
|
|
587
619
|
}
|
|
588
620
|
|
|
621
|
+
function setPlaceRepo(ctx, svc, name, repo) {
|
|
622
|
+
return setSettings(ctx.ws, { placeRepos: { [svc]: { [name]: repo } } });
|
|
623
|
+
}
|
|
624
|
+
|
|
589
625
|
function channelDetail(ctx, id) {
|
|
590
626
|
const g = ctx.m.digests.find((x) => x.id === id);
|
|
591
627
|
if (!g) return null;
|
|
@@ -594,6 +630,7 @@ function channelDetail(ctx, id) {
|
|
|
594
630
|
dhead(D.kind.place(srcName(g.source), D.place[g.source] || '', D.levelWord[g.level] || g.level), g.name),
|
|
595
631
|
sec(D.decisions, g.decisions), sec(D.open, g.open), sec(D.forGoals, g.goals),
|
|
596
632
|
el('div', { class: 'td-day__ch' }, el('span', null, D.changeLevel), levelSeg(ctx, g.source, g.name)),
|
|
633
|
+
el('div', { class: 'td-day__ch' }, el('span', null, D.repo.label), repoPicker(ctx.m, g.repo, (r) => setPlaceRepo(ctx, g.source, g.name, r), { 'data-day-place-repo': g.source + ':' + g.name })),
|
|
597
634
|
note(D.levelHelp),
|
|
598
635
|
el('div', { class: 'td-day__acts' }, extLink(D.openIn(srcName(g.source)), g.link, { 'data-day-link': 'place' }),
|
|
599
636
|
button({ label: D.manageSources, size: 'sm', variant: 'ghost', onClick: () => select('sources', 'all') })),
|
|
@@ -619,7 +656,7 @@ function stepDetail(ctx, i) {
|
|
|
619
656
|
if (i === 1) {
|
|
620
657
|
const live = m.jobs.filter((j) => !j.worth);
|
|
621
658
|
return [head, el('p', null, D.madeJobs(live.length, live.filter((j) => j.kind === 'you').length, live.filter((j) => j.kind !== 'you').length)),
|
|
622
|
-
m.goals.length ? el('ul', { class: 'td-day__rows' }, ...m.goals.map((g
|
|
659
|
+
m.goals.length ? el('ul', { class: 'td-day__rows' }, ...m.goals.map((g) => relRow(ctx, 'goal', g.id, g.text, String(m.goalNo.get(g.id))))) : null];
|
|
623
660
|
}
|
|
624
661
|
if (i === 2) {
|
|
625
662
|
const ts = m.jobs.filter((j) => j.task);
|
|
@@ -641,7 +678,8 @@ function placeControls(ctx, svc) {
|
|
|
641
678
|
// every place the last read found you in, then any you set a level for or that has a digest
|
|
642
679
|
const names = [...new Set([...m.places.filter((x) => x.source === svc).map((x) => x.name), ...Object.keys(m.scopes[svc] || {}),
|
|
643
680
|
...m.digests.filter((g) => g.source === svc).map((g) => g.name)])];
|
|
644
|
-
const row = (name) => el('div', { class: 'td-day__ch', 'data-day-place': svc + ':' + name }, el('span', null, name),
|
|
681
|
+
const row = (name) => el('div', { class: 'td-day__ch', 'data-day-place': svc + ':' + name }, el('span', null, name),
|
|
682
|
+
el('span', { class: 'td-day__chips' }, repoPicker(m, m.placeRepo(svc, name), (r) => setPlaceRepo(ctx, svc, name, r), { 'data-day-place-repo': svc + ':' + name }), levelSeg(ctx, svc, name)));
|
|
645
683
|
const live = names.filter((x) => m.level(svc, x) !== 'ignore');
|
|
646
684
|
const ign = names.filter((x) => m.level(svc, x) === 'ignore');
|
|
647
685
|
const input = field({ label: D.placeAdd[svc], value: '', placeholder: D.placeHint, hideLabel: true, attrs: { maxlength: '120', 'data-day-input': 'place-' + svc },
|
|
@@ -663,6 +701,7 @@ function placeControls(ctx, svc) {
|
|
|
663
701
|
ui.showIgn[svc] ? ign.map(row) : null,
|
|
664
702
|
input,
|
|
665
703
|
note(D.levelHelp),
|
|
704
|
+
note(D.repo.placeHint),
|
|
666
705
|
];
|
|
667
706
|
}
|
|
668
707
|
|
|
@@ -753,14 +792,14 @@ function fold(open) {
|
|
|
753
792
|
}
|
|
754
793
|
|
|
755
794
|
/** The section. `ws` is the repository the day run belongs to; `openIni(id)` opens an initiative of it. */
|
|
756
|
-
export function daySection({ ws, setState, openIni, owed } = {}) {
|
|
795
|
+
export function daySection({ ws, setState, openIni, owed, here, all } = {}) {
|
|
757
796
|
if (FIXTURE) return null;
|
|
758
797
|
// like Today's own feeds, a read that lands re-renders only while the section is on the page: a hidden Today must not
|
|
759
798
|
// cause a whole-page render under whatever the PM is doing elsewhere
|
|
760
799
|
if (setState) rerender = () => { if (document.querySelector('[data-today="day"]')) setState({}); };
|
|
761
800
|
ensure(ws);
|
|
762
801
|
const c = cache.get(ws) || {};
|
|
763
|
-
const m = model(c.data);
|
|
802
|
+
const m = model(c.data, { here: here || null, all: !!all });
|
|
764
803
|
const open = ui.open !== null ? ui.open : !!m.run;
|
|
765
804
|
if (!open) {
|
|
766
805
|
return el('section', { class: 'td-day', 'data-today': 'day', 'data-open': 'false', 'aria-label': D.title },
|
|
@@ -768,7 +807,7 @@ export function daySection({ ws, setState, openIni, owed } = {}) {
|
|
|
768
807
|
}
|
|
769
808
|
const sel = ui.sel;
|
|
770
809
|
const ctx = {
|
|
771
|
-
ws, m, sel, openIni, reload: () => load(ws), rerender: () => rerender(), select,
|
|
810
|
+
ws, m, sel, openIni, reload: () => load(ws), rerender: () => rerender(), select, repoPicker,
|
|
772
811
|
cur: (type, id) => { const s = sel || defaultSel(m); return s.type === type && s.id === String(id) ? 'true' : 'false'; },
|
|
773
812
|
};
|
|
774
813
|
ctx.sel = sel || defaultSel(m);
|
|
@@ -784,10 +823,11 @@ export function daySection({ ws, setState, openIni, owed } = {}) {
|
|
|
784
823
|
return el('section', { class: 'td-day', 'data-today': 'day', 'data-open': 'true', 'aria-label': D.title },
|
|
785
824
|
topBar(ctx),
|
|
786
825
|
runLine(ctx),
|
|
826
|
+
m.narrow && m.repos.length > 1 ? note(D.repo.only(m.repoName(m.here)), { 'data-day': 'repo-view' }) : null,
|
|
787
827
|
c.error ? el('p', { class: 'td-day__run td-day__run--failed', role: 'status', 'data-day': 'error' }, D.refused(c.error)) : null,
|
|
788
828
|
m.d && m.d.legacy ? note(D.oldServer) : null,
|
|
789
829
|
el('div', { class: 'td-day__cols' },
|
|
790
|
-
el('div', { class: 'td-day__left' }, summaryPanel(ctx),
|
|
830
|
+
el('div', { class: 'td-day__left' }, summaryPanel(ctx), goalsPanel(ctx), calendar(ctx), jobsPanel(ctx), mentionsPanel(ctx), waitingPanel(owed)),
|
|
791
831
|
detail(ctx)));
|
|
792
832
|
}
|
|
793
833
|
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* goalDetail(ctx, goalId) → the right column's detail of one goal: its meetings, its jobs, the steps found, and the
|
|
6
6
|
* presses done, drop, reopen and remove; on an agent step that names an initiative, Prepare
|
|
7
7
|
* then Queue it (the shipped background prepare: one approval is one prompt hash)
|
|
8
|
-
* addGoal(ctx, text, ini) → POST the add; true when the goal is on the list
|
|
8
|
+
* addGoal(ctx, text, ini) → POST the add; true when the goal is on the list. While Today shows one repository, the
|
|
9
|
+
* goal belongs to it (repo); the goal's detail can change that.
|
|
9
10
|
* goalsSig() → a string that changes whenever what this module draws changes on its own
|
|
10
11
|
*
|
|
11
12
|
* `ctx` is the day section's (today-day.js): { ws, m (the day model), select(type, id), reload(), rerender(), cur(type, id) }.
|
|
@@ -33,6 +34,8 @@ async function press(ctx, body) {
|
|
|
33
34
|
export function addGoal(ctx, text, ini) {
|
|
34
35
|
const body = { action: 'add', text: String(text || '').trim().slice(0, 140) };
|
|
35
36
|
if (ini) body.initiative_id = ini;
|
|
37
|
+
// one day for every repository: a goal added while Today shows one repository belongs to it; otherwise to none
|
|
38
|
+
if (ctx.m && ctx.m.narrow && ctx.m.here) body.repo = ctx.m.here;
|
|
36
39
|
return press(ctx, body);
|
|
37
40
|
}
|
|
38
41
|
|
|
@@ -42,7 +45,7 @@ const jobsOf = (m, id) => m.jobs.filter((j) => j.goal === id && !j.worth);
|
|
|
42
45
|
export function goalRows(ctx) {
|
|
43
46
|
const { m } = ctx;
|
|
44
47
|
if (!m.goals.length) return null;
|
|
45
|
-
return el('ul', { class: 'td-day__rows', 'data-day-list': 'goals' }, ...m.goals.map((g
|
|
48
|
+
return el('ul', { class: 'td-day__rows', 'data-day-list': 'goals' }, ...m.goals.map((g) => {
|
|
46
49
|
const js = jobsOf(m, g.id);
|
|
47
50
|
const done = js.filter((j) => j.group === 'done').length;
|
|
48
51
|
const need = js.filter((j) => j.group === 'needs').length;
|
|
@@ -51,8 +54,8 @@ export function goalRows(ctx) {
|
|
|
51
54
|
const cur = ctx.cur('goal', g.id);
|
|
52
55
|
return el('li', null, row({
|
|
53
56
|
title: g.text, meta: [sub], done: closed, onOpen: () => ctx.select('goal', g.id),
|
|
54
|
-
lead: [el('span', { class: 'td-day__lead' }, String(
|
|
55
|
-
trail: [meter({ done: js.length ? done : g.status === 'done' ? 1 : 0, total: js.length || 1, label: '' })],
|
|
57
|
+
lead: [el('span', { class: 'td-day__lead' }, String(m.goalNo.get(g.id)))],
|
|
58
|
+
trail: [el('span', { class: 'td-day__chips' }, g.repo ? badge({ text: m.repoName(g.repo), tone: 'outline', attrs: { 'data-day-repo': g.repo } }) : null), meter({ done: js.length ? done : g.status === 'done' ? 1 : 0, total: js.length || 1, label: '' })],
|
|
56
59
|
attrs: { class: 'td-day__r', 'data-day-sel': 'goal:' + g.id, 'data-goal': g.id, 'data-goal-coverage': g.coverage, 'data-cur': cur },
|
|
57
60
|
openAttrs: { 'aria-current': cur },
|
|
58
61
|
}));
|
|
@@ -62,7 +65,7 @@ export function goalRows(ctx) {
|
|
|
62
65
|
export function goalAdd(ctx) {
|
|
63
66
|
const { m } = ctx;
|
|
64
67
|
const max = m.maxGoals;
|
|
65
|
-
if (m.
|
|
68
|
+
if (m.allGoals.length >= max) return el('p', { class: 'td-day__note' }, G.full(max));
|
|
66
69
|
const f = field({ label: G.inputLabel, value: '', placeholder: G.inputHint, hideLabel: true, attrs: { 'data-goals': 'input', 'data-day-input': 'goal', maxlength: '140' },
|
|
67
70
|
onCommit: async (v) => {
|
|
68
71
|
if (!String(v).trim()) { draft = v; return; }
|
|
@@ -111,9 +114,9 @@ function stepNode(ctx, st) {
|
|
|
111
114
|
/** The detail of one goal. `row(type, id, label, lead, trail)` draws a selectable row (today-day.js). */
|
|
112
115
|
export function goalDetail(ctx, id, row) {
|
|
113
116
|
const { m } = ctx;
|
|
114
|
-
const i = m.
|
|
117
|
+
const i = m.allGoals.findIndex((g) => g.id === id);
|
|
115
118
|
if (i < 0) return null;
|
|
116
|
-
const g = m.
|
|
119
|
+
const g = m.allGoals[i];
|
|
117
120
|
const closed = g.status !== 'open';
|
|
118
121
|
const js = jobsOf(m, g.id);
|
|
119
122
|
const meets = m.events.filter((e) => e.goal === g.id);
|
|
@@ -128,6 +131,9 @@ export function goalDetail(ctx, id, row) {
|
|
|
128
131
|
el('div', { class: 'td-day__dhead' }, el('span', { class: 'td-day__kind' }, D.kind.goal(i + 1)), el('h3', null, g.text)),
|
|
129
132
|
el('div', { class: 'td-day__acts' }, badge({ text: G.coverage[g.coverage] || g.coverage, tone: g.coverage === 'needs-plan' ? 'warning' : 'outline' }),
|
|
130
133
|
g.initiative_id ? el('span', { class: 'td-day__mono' }, g.initiative_id) : null),
|
|
134
|
+
ctx.repoPicker && m.repos.length ? el('div', { class: 'td-day__ch', 'data-goals': 'repo' }, el('span', null, D.repo.label),
|
|
135
|
+
ctx.repoPicker(m, g.repo, (r) => press(ctx, { action: 'tag', id: g.id, repo: r }), { 'data-goal-repo': g.id })) : null,
|
|
136
|
+
ctx.repoPicker && m.repos.length ? el('p', { class: 'td-day__note' }, D.repo.goalHint) : null,
|
|
131
137
|
js.length ? el('p', null, D.goalJobs(js.filter((j) => j.group === 'done').length, js.length)) : el('p', { class: 'td-day__note' }, G.noStep),
|
|
132
138
|
meets.length ? el('div', { class: 'td-day__rel' }, el('h4', null, D.meetings), el('ul', { class: 'td-day__rows' }, ...meets.map((e) => row('event', e.id, e.title, e.time)))) : null,
|
|
133
139
|
js.length ? el('div', { class: 'td-day__rel' }, el('h4', null, D.jobs), el('ul', { class: 'td-day__rows' }, ...js.map((j) => row('job', j.id, j.title, '', j)))) : null,
|
package/workspace/server.js
CHANGED
|
@@ -7257,7 +7257,10 @@ async function handleRequest(req, res) {
|
|
|
7257
7257
|
: (!!bs.ended_at || !bsess || bsess.state === 'exited') ? { session: bs.id, why: (bsess && bsess.note) || 'the session ended without submitting a brief.' }
|
|
7258
7258
|
: briefSessionDone(run, date, now) === 'idle' ? { session: bs.id, idle: true, why: 'the session has printed nothing for two minutes and has not submitted a brief.' } : null;
|
|
7259
7259
|
return json(res, { ok: true, ws: w ? w.id : null, today: date, now: new Date(now).toISOString(), run, stage: dayRunLib.stage(run, brief), brief, plan, cards_error: cardsError, read_failed: readFailed,
|
|
7260
|
-
settings: { briefSources: st.briefSources, scopes: st.scopes, dayCheck: st.dayCheck, websites: st.websites }, sources,
|
|
7260
|
+
settings: { briefSources: st.briefSources, scopes: st.scopes, placeRepos: st.placeRepos, dayCheck: st.dayCheck, websites: st.websites }, sources,
|
|
7261
|
+
// one day for every repository: the repositories it can be narrowed to, and the repository of each tagged item
|
|
7262
|
+
repos: [...workspaces.values()].map((x) => ({ id: x.id, name: x.name || path.basename(x.root) })),
|
|
7263
|
+
item_repos: goalsLib.itemRepos({ day: readGoalsDay(date), brief, placeRepos: st.placeRepos || {} }),
|
|
7261
7264
|
check: { every: st.dayCheck.every, last: run ? run.checks.last : null, next: due.next, running: !!(run && run.checks.session_id), log: run ? run.checks.log : [] } });
|
|
7262
7265
|
}
|
|
7263
7266
|
if (p.startsWith('/api/proactive/day/') && req.method === 'POST') {
|
|
@@ -7396,7 +7399,9 @@ async function handleRequest(req, res) {
|
|
|
7396
7399
|
const bs = body.scopes && typeof body.scopes === 'object' && !Array.isArray(body.scopes) ? body.scopes : null;
|
|
7397
7400
|
const scopes = bs ? { ...cur.scopes, ...Object.fromEntries(Object.entries(bs).map(([k, v]) => [k, v && typeof v === 'object' && !Array.isArray(v) ? { ...(cur.scopes[k] || {}), ...v } : v])) } : cur.scopes;
|
|
7398
7401
|
const dayCheck = body.dayCheck && typeof body.dayCheck === 'object' && !Array.isArray(body.dayCheck) ? { ...cur.dayCheck, ...body.dayCheck } : (body.dayCheck === undefined ? cur.dayCheck : body.dayCheck);
|
|
7399
|
-
const
|
|
7402
|
+
const bpr = body.placeRepos && typeof body.placeRepos === 'object' && !Array.isArray(body.placeRepos) ? body.placeRepos : null;
|
|
7403
|
+
const placeRepos = bpr ? { ...cur.placeRepos, ...Object.fromEntries(Object.entries(bpr).map(([k, v]) => [k, v && typeof v === 'object' && !Array.isArray(v) ? { ...((cur.placeRepos || {})[k] || {}), ...v } : v])) } : cur.placeRepos;
|
|
7404
|
+
const n = proactiveLib.normalizeSettings({ ...cur, ...body, placeRepos, triggers: { ...cur.triggers, ...(body.triggers || {}) }, briefSources: { ...cur.briefSources, ...(body.briefSources || {}) }, scopes, dayCheck });
|
|
7400
7405
|
if (!n.ok) return json(res, { ok: false, error: n.error }, 400);
|
|
7401
7406
|
try { fs.mkdirSync(STATE, { recursive: true }); fs.writeFileSync(PRO_SETTINGS(), JSON.stringify(n.settings) + '\n'); }
|
|
7402
7407
|
catch (e) { warnWriteFail('proactive-settings.json', e); return json(res, { ok: false, error: 'could not save — nothing changed' }, 500); }
|