project-mgr 0.4.0 → 0.5.1

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/dist/domain.d.ts CHANGED
@@ -18,6 +18,12 @@ export type ProjectMetadataInput = {
18
18
  description?: string | null;
19
19
  notes?: string | null;
20
20
  };
21
+ export type NextTodoRef = {
22
+ uid: string;
23
+ title: string;
24
+ status: "open" | "completed" | "cancelled";
25
+ project: string | null;
26
+ };
21
27
  export type StatusEntryInput = {
22
28
  uid?: string;
23
29
  project: string;
@@ -26,6 +32,7 @@ export type StatusEntryInput = {
26
32
  progress?: string;
27
33
  blockers?: string[];
28
34
  nextSteps?: string[];
35
+ nextTodoUids?: string[];
29
36
  nextStepsSourceUid?: string;
30
37
  decisions?: string[];
31
38
  tags?: string[];
@@ -33,8 +40,11 @@ export type StatusEntryInput = {
33
40
  source?: Source;
34
41
  externalRef?: string;
35
42
  };
36
- export type StatusEntry = Required<Omit<StatusEntryInput, "progress" | "externalRef" | "nextStepsSourceUid">> & {
43
+ export type StatusEntry = Required<Omit<StatusEntryInput, "progress" | "externalRef" | "nextStepsSourceUid" | "nextTodoUids">> & {
37
44
  nextStepsSourceUid?: string;
45
+ nextTodoUids?: string[];
46
+ /** Live Todo rows for display. Not a historical as-of view. */
47
+ nextTodos?: NextTodoRef[];
38
48
  id: number;
39
49
  progress?: string;
40
50
  externalRef?: string;
@@ -49,12 +59,12 @@ export type StatusEntry = Required<Omit<StatusEntryInput, "progress" | "external
49
59
  recordedAt: string;
50
60
  };
51
61
  };
52
- export type PortableStatusEntry = Omit<StatusEntry, "id" | "recordedAt" | "effectiveStatus" | "isCurrent" | "completedBy">;
62
+ export type PortableStatusEntry = Omit<StatusEntry, "id" | "recordedAt" | "effectiveStatus" | "isCurrent" | "completedBy" | "nextTodos">;
53
63
  export declare class ValidationError extends Error {
54
64
  constructor(message: string);
55
65
  }
56
66
  export declare function parseTime(value: unknown, field: string): string;
57
67
  export declare function validateProjectMetadataInput(value: unknown): ProjectMetadataInput;
58
- export declare function validateStatusEntry(value: unknown, defaultSource?: Source): StatusEntryInput & Required<Pick<StatusEntryInput, "occurredAt" | "source">>;
68
+ export declare function validateStatusEntry(value: unknown, defaultSource?: Source): StatusEntryInput & Required<Pick<StatusEntryInput, "occurredAt" | "source" | "nextSteps" | "blockers" | "decisions" | "tags">>;
59
69
  export declare function toPortableStatusEntry(entry: StatusEntry): PortableStatusEntry;
60
70
  export declare function validateUid(value: unknown): string;
package/dist/domain.js CHANGED
@@ -16,6 +16,7 @@ const keys = new Set([
16
16
  "progress",
17
17
  "blockers",
18
18
  "nextSteps",
19
+ "nextTodoUids",
19
20
  "nextStepsSourceUid",
20
21
  "decisions",
21
22
  "tags",
@@ -67,6 +68,14 @@ function textList(value, field) {
67
68
  throw new ValidationError(`${field} must be an array of at most 100 strings`);
68
69
  return value.map((item) => text(item, field, true));
69
70
  }
71
+ function uidList(value, field) {
72
+ if (!Array.isArray(value) || value.length > 100)
73
+ throw new ValidationError(`${field} must be an array of at most 100 uuids`);
74
+ const uids = value.map((item) => validateUid(item));
75
+ if (new Set(uids).size !== uids.length)
76
+ throw new ValidationError(`${field} contains a duplicate uid`);
77
+ return uids;
78
+ }
70
79
  export function parseTime(value, field) {
71
80
  if (typeof value !== "string" ||
72
81
  !/^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(value) ||
@@ -121,6 +130,18 @@ export function validateStatusEntry(value, defaultSource = "human") {
121
130
  const externalRef = text(input.externalRef, "externalRef");
122
131
  if (externalRef && externalRef.length > 200)
123
132
  throw new ValidationError("externalRef must be at most 200 characters");
133
+ const nextSteps = textList(input.nextSteps, "nextSteps");
134
+ const nextStepsSourceUid = input.nextStepsSourceUid === undefined
135
+ ? undefined
136
+ : validateUid(input.nextStepsSourceUid);
137
+ // null and omission both mean legacy rows: the refs field is absent, not [].
138
+ const nextTodoUids = input.nextTodoUids === undefined || input.nextTodoUids === null
139
+ ? undefined
140
+ : uidList(input.nextTodoUids, "nextTodoUids");
141
+ if (nextTodoUids !== undefined && nextSteps.length > 0)
142
+ throw new ValidationError("cannot combine nextSteps text with nextTodoUids");
143
+ if (nextTodoUids !== undefined && nextStepsSourceUid !== undefined)
144
+ throw new ValidationError("invalid next step provenance");
124
145
  return {
125
146
  ...(input.uid === undefined ? {} : { uid: validateUid(input.uid) }),
126
147
  project,
@@ -128,10 +149,9 @@ export function validateStatusEntry(value, defaultSource = "human") {
128
149
  summary: text(input.summary, "summary", true),
129
150
  progress: text(input.progress, "progress"),
130
151
  blockers: textList(input.blockers, "blockers"),
131
- nextSteps: textList(input.nextSteps, "nextSteps"),
132
- ...(input.nextStepsSourceUid === undefined
133
- ? {}
134
- : { nextStepsSourceUid: validateUid(input.nextStepsSourceUid) }),
152
+ nextSteps,
153
+ ...(nextTodoUids !== undefined ? { nextTodoUids } : {}),
154
+ ...(nextStepsSourceUid !== undefined ? { nextStepsSourceUid } : {}),
135
155
  decisions: textList(input.decisions, "decisions"),
136
156
  tags: textList(input.tags, "tags"),
137
157
  occurredAt: input.occurredAt === undefined
@@ -156,6 +176,8 @@ export function toPortableStatusEntry(entry) {
156
176
  };
157
177
  if (entry.nextStepsSourceUid !== undefined)
158
178
  result.nextStepsSourceUid = entry.nextStepsSourceUid;
179
+ if (entry.nextTodoUids !== undefined)
180
+ result.nextTodoUids = entry.nextTodoUids;
159
181
  if (entry.progress !== undefined)
160
182
  result.progress = entry.progress;
161
183
  if (entry.externalRef !== undefined)
package/dist/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { exec } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
  import { createServer, } from "node:http";
3
3
  import { URL } from "node:url";
4
4
  import { countEntries, createTodo, listTodos, updateTodo, convertNextStep, completeActivity, getEntry, history, listProjects, openLedger, } from "./db.js";
@@ -6,19 +6,18 @@ import { ValidationError } from "./domain.js";
6
6
  import { getWebUIHTML } from "./web/ui.js";
7
7
  export function openBrowser(url) {
8
8
  const platform = process.platform;
9
- let command = "";
10
- if (platform === "darwin") {
11
- command = `open "${url}"`;
12
- }
13
- else if (platform === "win32") {
14
- command = `start "" "${url}"`;
15
- }
16
- else {
17
- command = `xdg-open "${url}"`;
18
- }
19
- exec(command, () => {
20
- /* ignore background exec errors */
9
+ const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
10
+ const args = platform === "darwin"
11
+ ? [url]
12
+ : platform === "win32"
13
+ ? ["/c", "start", "", url]
14
+ : [url];
15
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
16
+ child.on("error", () => {
17
+ /* ignore background launch errors */
21
18
  });
19
+ // Detach so a long-lived browser does not keep this process alive.
20
+ child.unref();
22
21
  }
23
22
  function parseQueryParams(url) {
24
23
  const params = {};
@@ -68,7 +67,17 @@ export function handleRequest(req, res, db) {
68
67
  return;
69
68
  }
70
69
  if (pathname === "/api/todos") {
71
- sendJson(res, 200, listTodos(db, query.project));
70
+ const status = query.status;
71
+ if (status !== undefined &&
72
+ status !== "open" &&
73
+ status !== "completed" &&
74
+ status !== "cancelled") {
75
+ sendJson(res, 400, { error: "invalid todo status" });
76
+ return;
77
+ }
78
+ sendJson(res, 200, listTodos(db, query.project, status === undefined
79
+ ? {}
80
+ : { status: status }));
72
81
  return;
73
82
  }
74
83
  if (pathname === "/api/projects") {
@@ -184,7 +184,16 @@ export function getWebUIScript(initialLang) {
184
184
  </div>
185
185
  \` : ''}
186
186
 
187
- \${item.nextSteps && item.nextSteps.length ? \`
187
+ \${pinnedTodos(item).length ? \`
188
+ <div class="section-box">
189
+ <div class="section-title next-steps">\${escapeHtml(t('nextStepsTitle'))}</div>
190
+ <ul class="bullet-list">
191
+ \${pinnedTodos(item).map(todo => \`<li><strong>\${escapeHtml(todo.title)}</strong> <span class="status-badge status-\${escapeHtml(todo.status)}">\${escapeHtml(t('todo_' + todo.status))}</span> <code>\${escapeHtml(todo.uid)}</code></li>\`).join('')}
192
+ </ul>
193
+ </div>
194
+ \` : ''}
195
+
196
+ \${!item.nextTodoUids && item.nextSteps && item.nextSteps.length ? \`
188
197
  <div class="section-box">
189
198
  <div class="section-title next-steps">\${escapeHtml(t('nextStepsTitle'))}</div>
190
199
  <ul class="bullet-list">
@@ -211,7 +220,7 @@ export function getWebUIScript(initialLang) {
211
220
  <div class="section-box">
212
221
  <button class="btn" data-action="add-todo" data-entry="\${escapeHtml(item.uid)}">\${escapeHtml(t('addTodo'))}</button>
213
222
  \${status !== 'completed' ? \`<button class="btn" data-action="complete-entry" data-entry="\${escapeHtml(item.uid)}">\${escapeHtml(t('complete'))}</button>\` : ''}
214
- \${todosData.filter(todo => todo.entryUid === item.uid || todo.entryUid === item.nextStepsSourceUid).map(renderTodo).join('')}
223
+ \${todosData.filter(todo => (todo.entryUid === item.uid || todo.entryUid === item.nextStepsSourceUid) && !(item.nextTodoUids || []).includes(todo.uid)).map(renderTodo).join('')}
215
224
  </div>
216
225
  </div>
217
226
  \`;
@@ -283,6 +292,7 @@ export function getWebUIScript(initialLang) {
283
292
  <div class="latest-summary">
284
293
  <div class="latest-summary-title">\${escapeHtml(t('latestUpdate'))} (\${formatTime(p.latestStatus.occurredAt)})</div>
285
294
  <div>\${escapeHtml(p.latestStatus.summary)}</div>
295
+ \${Array.isArray(p.latestStatus.nextTodos) && p.latestStatus.nextTodos.length ? \`<ul class="bullet-list">\${p.latestStatus.nextTodos.map(todo => \`<li>\${escapeHtml(todo.title)} <span class="status-badge status-\${escapeHtml(todo.status)}">\${escapeHtml(t('todo_' + todo.status))}</span></li>\`).join('')}</ul>\` : ''}
286
296
  </div>
287
297
  \` : ''}
288
298
  </div>
@@ -376,6 +386,15 @@ export function getWebUIScript(initialLang) {
376
386
  return '<option value="">' + escapeHtml(t('noProject')) + '</option>' + projectsData.map(p =>
377
387
  '<option value="' + escapeHtml(p.project) + '"' + (selected === p.project ? ' selected' : '') + '>' + escapeHtml(p.displayName) + '</option>').join('');
378
388
  }
389
+ function pinnedTodos(item) {
390
+ if (!Array.isArray(item.nextTodoUids)) return [];
391
+ const embedded = Array.isArray(item.nextTodos) ? item.nextTodos : [];
392
+ return item.nextTodoUids.map((uid) => {
393
+ const live = todosData.find((todo) => todo.uid === uid);
394
+ const snapshot = embedded.find((todo) => todo.uid === uid);
395
+ return live || snapshot || { uid, title: uid, status: 'open', project: null };
396
+ });
397
+ }
379
398
  function renderTodo(todo) {
380
399
  return '<div class="section-box"><strong>' + escapeHtml(todo.title) + '</strong> · ' + escapeHtml(t('todo_' + todo.status)) +
381
400
  (todo.project ? ' · ' + escapeHtml(todo.project) : '') +
@@ -1 +1 @@
1
- export declare const WEB_UI_STYLES = " :root {\n --bg: #0d1117;\n --card-bg: #161b22;\n --border: #30363d;\n --text: #c9d1d9;\n --text-muted: #8b949e;\n --heading: #f0f6fc;\n --accent: #58a6ff;\n --accent-hover: #79c0ff;\n --success: #3fb950;\n --warning: #d29922;\n --danger: #f85149;\n --purple: #bc8cff;\n --cyan: #39c5cf;\n --radius: 8px;\n }\n\n * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n }\n\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n background-color: var(--bg);\n color: var(--text);\n line-height: 1.5;\n padding: 0;\n margin: 0;\n min-height: 100vh;\n }\n\n header {\n background-color: var(--card-bg);\n border-bottom: 1px solid var(--border);\n position: sticky;\n top: 0;\n z-index: 100;\n padding: 1rem 1.5rem;\n }\n\n .header-container {\n max-width: 1200px;\n margin: 0 auto;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n }\n\n .brand {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n }\n\n .logo-icon {\n width: 32px;\n height: 32px;\n background: linear-gradient(135deg, var(--accent), var(--purple));\n border-radius: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #fff;\n font-weight: bold;\n font-size: 1.1rem;\n }\n\n h1 {\n font-size: 1.25rem;\n font-weight: 600;\n color: var(--heading);\n }\n\n .header-right {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n flex-wrap: wrap;\n }\n\n .stats-bar {\n display: flex;\n gap: 0.75rem;\n flex-wrap: wrap;\n }\n\n .lang-switch {\n display: inline-flex;\n gap: 0.2rem;\n background: var(--bg);\n padding: 0.2rem;\n border-radius: 6px;\n border: 1px solid var(--border);\n }\n\n .lang-btn {\n background: transparent;\n border: none;\n color: var(--text-muted);\n padding: 0.25rem 0.65rem;\n font-size: 0.8rem;\n font-weight: 500;\n border-radius: 4px;\n cursor: pointer;\n transition: all 0.2s;\n }\n\n .lang-btn:hover {\n color: var(--heading);\n }\n\n .lang-btn.active {\n background: var(--accent);\n color: #ffffff;\n font-weight: 600;\n }\n\n .stat-pill {\n background: rgba(255, 255, 255, 0.05);\n border: 1px solid var(--border);\n border-radius: 20px;\n padding: 0.25rem 0.75rem;\n font-size: 0.85rem;\n display: flex;\n align-items: center;\n gap: 0.35rem;\n }\n\n .stat-val {\n font-weight: 600;\n color: var(--heading);\n }\n\n .controls {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n flex-wrap: wrap;\n width: 100%;\n max-width: 1200px;\n margin: 1rem auto 0 auto;\n padding: 0 1.5rem;\n }\n\n .search-box {\n flex: 1;\n min-width: 240px;\n position: relative;\n }\n\n .search-input {\n width: 100%;\n background: var(--card-bg);\n border: 1px solid var(--border);\n color: var(--text);\n padding: 0.5rem 0.75rem 0.5rem 2.25rem;\n border-radius: var(--radius);\n font-size: 0.9rem;\n outline: none;\n transition: border-color 0.2s;\n }\n\n .search-input:focus {\n border-color: var(--accent);\n }\n\n .search-icon {\n position: absolute;\n left: 0.75rem;\n top: 50%;\n transform: translateY(-50%);\n color: var(--text-muted);\n pointer-events: none;\n font-size: 0.85rem;\n }\n\n .nav-tabs {\n display: flex;\n gap: 0.5rem;\n background: var(--card-bg);\n padding: 0.25rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n }\n\n .tab-btn {\n background: transparent;\n border: none;\n color: var(--text-muted);\n padding: 0.4rem 1rem;\n font-size: 0.875rem;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.2s;\n }\n\n .tab-btn:hover {\n color: var(--heading);\n }\n\n .tab-btn.active {\n background: var(--accent);\n color: #ffffff;\n }\n\n .btn[hidden] { display: none !important; }\n .btn {\n background: var(--card-bg);\n border: 1px solid var(--border);\n color: var(--text);\n padding: 0.45rem 0.85rem;\n border-radius: var(--radius);\n font-size: 0.875rem;\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 0.35rem;\n transition: all 0.2s;\n }\n\n .btn:hover {\n border-color: var(--text-muted);\n color: var(--heading);\n }\n\n .btn-sm {\n padding: 0.3rem 0.6rem;\n font-size: 0.8rem;\n }\n\n .btn-primary {\n background: var(--accent);\n color: #ffffff;\n border-color: var(--accent);\n margin-top: 0.75rem;\n }\n\n .btn-primary:hover {\n background: var(--accent-hover);\n color: #ffffff;\n }\n\n main {\n max-width: 1200px;\n margin: 1.5rem auto;\n padding: 0 1.5rem;\n }\n\n .view-panel {\n display: none;\n }\n\n .view-panel.active {\n display: block;\n }\n\n /* Activity Feed Toolbar & Grouping Toggle */\n .activity-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n background: var(--card-bg);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 0.75rem 1rem;\n margin-bottom: 1.25rem;\n flex-wrap: wrap;\n }\n\n .date-picker-wrapper {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n font-size: 0.9rem;\n color: var(--heading);\n font-weight: 500;\n flex-wrap: wrap;\n }\n\n .date-input {\n background: var(--bg);\n border: 1px solid var(--border);\n color: var(--heading);\n padding: 0.35rem 0.65rem;\n border-radius: 6px;\n font-size: 0.875rem;\n outline: none;\n font-family: inherit;\n }\n\n .date-input:focus {\n border-color: var(--accent);\n }\n\n .view-mode-toggle {\n display: flex;\n gap: 0.25rem;\n background: var(--bg);\n padding: 0.2rem;\n border-radius: 6px;\n border: 1px solid var(--border);\n }\n\n .mode-btn {\n background: transparent;\n border: none;\n color: var(--text-muted);\n padding: 0.3rem 0.75rem;\n font-size: 0.8rem;\n font-weight: 500;\n border-radius: 4px;\n cursor: pointer;\n transition: all 0.2s;\n }\n\n .mode-btn:hover {\n color: var(--heading);\n }\n\n .mode-btn.active {\n background: var(--accent);\n color: #ffffff;\n font-weight: 600;\n }\n\n /* Project Grouping Container */\n .project-group-card {\n background: var(--card-bg);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 1.25rem;\n margin-bottom: 1.25rem;\n }\n\n .project-group-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n border-bottom: 1px solid var(--border);\n padding-bottom: 0.75rem;\n margin-bottom: 1rem;\n }\n\n .project-group-title {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n font-size: 1.1rem;\n font-weight: 600;\n color: var(--heading);\n }\n\n .project-group-count {\n font-size: 0.8rem;\n color: var(--text-muted);\n background: rgba(255, 255, 255, 0.06);\n padding: 0.15rem 0.55rem;\n border-radius: 12px;\n }\n\n /* Projects Grid */\n .projects-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));\n gap: 1.25rem;\n }\n\n .project-card {\n background: var(--card-bg);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 1.25rem;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n transition: transform 0.15s, border-color 0.15s;\n }\n\n .project-card:hover {\n border-color: var(--text-muted);\n transform: translateY(-2px);\n }\n\n .card-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 0.75rem;\n margin-bottom: 0.75rem;\n }\n\n .project-title {\n font-size: 1.1rem;\n font-weight: 600;\n color: var(--heading);\n }\n\n .project-slug {\n font-size: 0.8rem;\n color: var(--text-muted);\n font-family: monospace;\n }\n\n .completion-evidence { margin: 12px 0; line-height: 1.7; }\n details > summary { cursor: pointer; margin: 12px 0; }\n .status-badge {\n font-size: 0.75rem;\n font-weight: 600;\n text-transform: uppercase;\n padding: 0.2rem 0.55rem;\n border-radius: 12px;\n display: inline-block;\n white-space: nowrap;\n }\n\n .status-active { background: rgba(63, 185, 80, 0.15); color: var(--success); border: 1px solid rgba(63, 185, 80, 0.3); }\n .status-planned { background: rgba(88, 166, 255, 0.15); color: var(--accent); border: 1px solid rgba(88, 166, 255, 0.3); }\n .status-idea { background: rgba(188, 140, 255, 0.15); color: var(--purple); border: 1px solid rgba(188, 140, 255, 0.3); }\n .status-blocked { background: rgba(248, 81, 73, 0.15); color: var(--danger); border: 1px solid rgba(248, 81, 73, 0.3); }\n .status-paused { background: rgba(210, 153, 34, 0.15); color: var(--warning); border: 1px solid rgba(210, 153, 34, 0.3); }\n .status-completed { background: rgba(57, 197, 207, 0.15); color: var(--cyan); border: 1px solid rgba(57, 197, 207, 0.3); }\n .status-archived { background: rgba(139, 148, 158, 0.15); color: var(--text-muted); border: 1px solid rgba(139, 148, 158, 0.3); }\n\n .card-body {\n margin-bottom: 1rem;\n font-size: 0.9rem;\n }\n\n .card-description {\n color: var(--text);\n margin-bottom: 0.5rem;\n }\n\n .card-notes {\n color: var(--text-muted);\n font-size: 0.85rem;\n font-style: italic;\n border-left: 2px solid var(--border);\n padding-left: 0.5rem;\n margin-top: 0.5rem;\n }\n\n .latest-summary {\n background: rgba(0, 0, 0, 0.2);\n padding: 0.6rem;\n border-radius: 6px;\n margin-top: 0.75rem;\n font-size: 0.85rem;\n }\n\n .latest-summary-title {\n font-size: 0.75rem;\n color: var(--text-muted);\n text-transform: uppercase;\n margin-bottom: 0.25rem;\n }\n\n .card-footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n font-size: 0.85rem;\n color: var(--text-muted);\n border-top: 1px solid var(--border);\n padding-top: 0.75rem;\n margin-top: auto;\n }\n\n .link-btn {\n color: var(--accent);\n text-decoration: none;\n cursor: pointer;\n font-weight: 500;\n }\n\n .link-btn:hover {\n text-decoration: underline;\n }\n\n /* Timeline View */\n .timeline {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n position: relative;\n }\n\n .timeline-item {\n background: var(--card-bg);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 1.25rem;\n position: relative;\n }\n\n .timeline-header {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: 0.5rem;\n margin-bottom: 0.75rem;\n }\n\n .timeline-meta {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n flex-wrap: wrap;\n }\n\n .project-tag {\n background: rgba(88, 166, 255, 0.1);\n color: var(--accent);\n border: 1px solid rgba(88, 166, 255, 0.2);\n padding: 0.15rem 0.5rem;\n border-radius: 4px;\n font-family: monospace;\n font-size: 0.85rem;\n cursor: pointer;\n }\n\n .time-stamp {\n font-size: 0.8rem;\n color: var(--text-muted);\n }\n\n .source-badge {\n font-size: 0.75rem;\n color: var(--text-muted);\n background: rgba(255, 255, 255, 0.05);\n padding: 0.1rem 0.4rem;\n border-radius: 4px;\n }\n\n .entry-summary {\n font-size: 1rem;\n font-weight: 500;\n color: var(--heading);\n margin-bottom: 0.5rem;\n }\n\n .entry-progress {\n font-size: 0.9rem;\n color: var(--text);\n margin-bottom: 0.75rem;\n }\n\n .section-box {\n margin-top: 0.5rem;\n font-size: 0.875rem;\n }\n\n .section-title {\n font-weight: 600;\n font-size: 0.8rem;\n text-transform: uppercase;\n margin-bottom: 0.25rem;\n display: flex;\n align-items: center;\n gap: 0.25rem;\n }\n\n .section-title.blockers { color: var(--danger); }\n .section-title.next-steps { color: var(--accent); }\n .section-title.decisions { color: var(--purple); }\n\n .bullet-list {\n list-style: disc inside;\n color: var(--text);\n padding-left: 0.25rem;\n }\n\n .bullet-list li {\n margin-bottom: 0.2rem;\n }\n\n .tags-container {\n display: flex;\n flex-wrap: wrap;\n gap: 0.35rem;\n margin-top: 0.75rem;\n }\n\n .pill-tag {\n background: rgba(255, 255, 255, 0.06);\n border: 1px solid var(--border);\n color: var(--text-muted);\n font-size: 0.75rem;\n padding: 0.1rem 0.45rem;\n border-radius: 12px;\n }\n\n .empty-state {\n text-align: center;\n padding: 3rem 1rem;\n color: var(--text-muted);\n }\n\n .empty-state h3 {\n color: var(--heading);\n margin-bottom: 0.5rem;\n }\n\n .filter-bar {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n margin-bottom: 1rem;\n font-size: 0.875rem;\n color: var(--text-muted);\n }\n\n .filter-chip {\n background: rgba(88, 166, 255, 0.15);\n color: var(--accent);\n padding: 0.2rem 0.6rem;\n border-radius: 12px;\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n }\n\n .filter-chip button {\n background: none;\n border: none;\n color: var(--accent);\n cursor: pointer;\n font-weight: bold;\n }";
1
+ export declare const WEB_UI_STYLES = " :root {\n --bg: #0d1117;\n --card-bg: #161b22;\n --border: #30363d;\n --text: #c9d1d9;\n --text-muted: #8b949e;\n --heading: #f0f6fc;\n --accent: #58a6ff;\n --accent-hover: #79c0ff;\n --success: #3fb950;\n --warning: #d29922;\n --danger: #f85149;\n --purple: #bc8cff;\n --cyan: #39c5cf;\n --radius: 8px;\n }\n\n * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n }\n\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n background-color: var(--bg);\n color: var(--text);\n line-height: 1.5;\n padding: 0;\n margin: 0;\n min-height: 100vh;\n }\n\n header {\n background-color: var(--card-bg);\n border-bottom: 1px solid var(--border);\n position: sticky;\n top: 0;\n z-index: 100;\n padding: 1rem 1.5rem;\n }\n\n .header-container {\n max-width: 1200px;\n margin: 0 auto;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n }\n\n .brand {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n }\n\n .logo-icon {\n width: 32px;\n height: 32px;\n background: linear-gradient(135deg, var(--accent), var(--purple));\n border-radius: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #fff;\n font-weight: bold;\n font-size: 1.1rem;\n }\n\n h1 {\n font-size: 1.25rem;\n font-weight: 600;\n color: var(--heading);\n }\n\n .header-right {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n flex-wrap: wrap;\n }\n\n .stats-bar {\n display: flex;\n gap: 0.75rem;\n flex-wrap: wrap;\n }\n\n .lang-switch {\n display: inline-flex;\n gap: 0.2rem;\n background: var(--bg);\n padding: 0.2rem;\n border-radius: 6px;\n border: 1px solid var(--border);\n }\n\n .lang-btn {\n background: transparent;\n border: none;\n color: var(--text-muted);\n padding: 0.25rem 0.65rem;\n font-size: 0.8rem;\n font-weight: 500;\n border-radius: 4px;\n cursor: pointer;\n transition: all 0.2s;\n }\n\n .lang-btn:hover {\n color: var(--heading);\n }\n\n .lang-btn.active {\n background: var(--accent);\n color: #ffffff;\n font-weight: 600;\n }\n\n .stat-pill {\n background: rgba(255, 255, 255, 0.05);\n border: 1px solid var(--border);\n border-radius: 20px;\n padding: 0.25rem 0.75rem;\n font-size: 0.85rem;\n display: flex;\n align-items: center;\n gap: 0.35rem;\n }\n\n .stat-val {\n font-weight: 600;\n color: var(--heading);\n }\n\n .controls {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n flex-wrap: wrap;\n width: 100%;\n max-width: 1200px;\n margin: 1rem auto 0 auto;\n padding: 0 1.5rem;\n }\n\n .search-box {\n flex: 1;\n min-width: 240px;\n position: relative;\n }\n\n .search-input {\n width: 100%;\n background: var(--card-bg);\n border: 1px solid var(--border);\n color: var(--text);\n padding: 0.5rem 0.75rem 0.5rem 2.25rem;\n border-radius: var(--radius);\n font-size: 0.9rem;\n outline: none;\n transition: border-color 0.2s;\n }\n\n .search-input:focus {\n border-color: var(--accent);\n }\n\n .search-icon {\n position: absolute;\n left: 0.75rem;\n top: 50%;\n transform: translateY(-50%);\n color: var(--text-muted);\n pointer-events: none;\n font-size: 0.85rem;\n }\n\n .nav-tabs {\n display: flex;\n gap: 0.5rem;\n background: var(--card-bg);\n padding: 0.25rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n }\n\n .tab-btn {\n background: transparent;\n border: none;\n color: var(--text-muted);\n padding: 0.4rem 1rem;\n font-size: 0.875rem;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.2s;\n }\n\n .tab-btn:hover {\n color: var(--heading);\n }\n\n .tab-btn.active {\n background: var(--accent);\n color: #ffffff;\n }\n\n .btn[hidden] { display: none !important; }\n .btn {\n background: var(--card-bg);\n border: 1px solid var(--border);\n color: var(--text);\n padding: 0.45rem 0.85rem;\n border-radius: var(--radius);\n font-size: 0.875rem;\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 0.35rem;\n transition: all 0.2s;\n }\n\n .btn:hover {\n border-color: var(--text-muted);\n color: var(--heading);\n }\n\n .btn-sm {\n padding: 0.3rem 0.6rem;\n font-size: 0.8rem;\n }\n\n .btn-primary {\n background: var(--accent);\n color: #ffffff;\n border-color: var(--accent);\n margin-top: 0.75rem;\n }\n\n .btn-primary:hover {\n background: var(--accent-hover);\n color: #ffffff;\n }\n\n main {\n max-width: 1200px;\n margin: 1.5rem auto;\n padding: 0 1.5rem;\n }\n\n .view-panel {\n display: none;\n }\n\n .view-panel.active {\n display: block;\n }\n\n /* Activity Feed Toolbar & Grouping Toggle */\n .activity-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n background: var(--card-bg);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 0.75rem 1rem;\n margin-bottom: 1.25rem;\n flex-wrap: wrap;\n }\n\n .date-picker-wrapper {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n font-size: 0.9rem;\n color: var(--heading);\n font-weight: 500;\n flex-wrap: wrap;\n }\n\n .date-input {\n background: var(--bg);\n border: 1px solid var(--border);\n color: var(--heading);\n padding: 0.35rem 0.65rem;\n border-radius: 6px;\n font-size: 0.875rem;\n outline: none;\n font-family: inherit;\n }\n\n .date-input:focus {\n border-color: var(--accent);\n }\n\n .view-mode-toggle {\n display: flex;\n gap: 0.25rem;\n background: var(--bg);\n padding: 0.2rem;\n border-radius: 6px;\n border: 1px solid var(--border);\n }\n\n .mode-btn {\n background: transparent;\n border: none;\n color: var(--text-muted);\n padding: 0.3rem 0.75rem;\n font-size: 0.8rem;\n font-weight: 500;\n border-radius: 4px;\n cursor: pointer;\n transition: all 0.2s;\n }\n\n .mode-btn:hover {\n color: var(--heading);\n }\n\n .mode-btn.active {\n background: var(--accent);\n color: #ffffff;\n font-weight: 600;\n }\n\n /* Project Grouping Container */\n .project-group-card {\n background: var(--card-bg);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 1.25rem;\n margin-bottom: 1.25rem;\n }\n\n .project-group-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n border-bottom: 1px solid var(--border);\n padding-bottom: 0.75rem;\n margin-bottom: 1rem;\n }\n\n .project-group-title {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n font-size: 1.1rem;\n font-weight: 600;\n color: var(--heading);\n }\n\n .project-group-count {\n font-size: 0.8rem;\n color: var(--text-muted);\n background: rgba(255, 255, 255, 0.06);\n padding: 0.15rem 0.55rem;\n border-radius: 12px;\n }\n\n /* Projects Grid */\n .projects-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));\n gap: 1.25rem;\n }\n\n .project-card {\n background: var(--card-bg);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 1.25rem;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n transition: transform 0.15s, border-color 0.15s;\n }\n\n .project-card:hover {\n border-color: var(--text-muted);\n transform: translateY(-2px);\n }\n\n .card-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 0.75rem;\n margin-bottom: 0.75rem;\n }\n\n .project-title {\n font-size: 1.1rem;\n font-weight: 600;\n color: var(--heading);\n }\n\n .project-slug {\n font-size: 0.8rem;\n color: var(--text-muted);\n font-family: monospace;\n }\n\n .completion-evidence { margin: 12px 0; line-height: 1.7; }\n details > summary { cursor: pointer; margin: 12px 0; }\n .status-badge {\n font-size: 0.75rem;\n font-weight: 600;\n text-transform: uppercase;\n padding: 0.2rem 0.55rem;\n border-radius: 12px;\n display: inline-block;\n white-space: nowrap;\n }\n\n .status-active { background: rgba(63, 185, 80, 0.15); color: var(--success); border: 1px solid rgba(63, 185, 80, 0.3); }\n .status-planned { background: rgba(88, 166, 255, 0.15); color: var(--accent); border: 1px solid rgba(88, 166, 255, 0.3); }\n .status-idea { background: rgba(188, 140, 255, 0.15); color: var(--purple); border: 1px solid rgba(188, 140, 255, 0.3); }\n .status-blocked { background: rgba(248, 81, 73, 0.15); color: var(--danger); border: 1px solid rgba(248, 81, 73, 0.3); }\n .status-paused { background: rgba(210, 153, 34, 0.15); color: var(--warning); border: 1px solid rgba(210, 153, 34, 0.3); }\n .status-completed { background: rgba(57, 197, 207, 0.15); color: var(--cyan); border: 1px solid rgba(57, 197, 207, 0.3); }\n .status-archived { background: rgba(139, 148, 158, 0.15); color: var(--text-muted); border: 1px solid rgba(139, 148, 158, 0.3); }\n .status-open { background: rgba(63, 185, 80, 0.15); color: var(--success); border: 1px solid rgba(63, 185, 80, 0.3); }\n .status-cancelled { background: rgba(139, 148, 158, 0.15); color: var(--text-muted); border: 1px solid rgba(139, 148, 158, 0.3); }\n\n .card-body {\n margin-bottom: 1rem;\n font-size: 0.9rem;\n }\n\n .card-description {\n color: var(--text);\n margin-bottom: 0.5rem;\n }\n\n .card-notes {\n color: var(--text-muted);\n font-size: 0.85rem;\n font-style: italic;\n border-left: 2px solid var(--border);\n padding-left: 0.5rem;\n margin-top: 0.5rem;\n }\n\n .latest-summary {\n background: rgba(0, 0, 0, 0.2);\n padding: 0.6rem;\n border-radius: 6px;\n margin-top: 0.75rem;\n font-size: 0.85rem;\n }\n\n .latest-summary-title {\n font-size: 0.75rem;\n color: var(--text-muted);\n text-transform: uppercase;\n margin-bottom: 0.25rem;\n }\n\n .card-footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n font-size: 0.85rem;\n color: var(--text-muted);\n border-top: 1px solid var(--border);\n padding-top: 0.75rem;\n margin-top: auto;\n }\n\n .link-btn {\n color: var(--accent);\n text-decoration: none;\n cursor: pointer;\n font-weight: 500;\n }\n\n .link-btn:hover {\n text-decoration: underline;\n }\n\n /* Timeline View */\n .timeline {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n position: relative;\n }\n\n .timeline-item {\n background: var(--card-bg);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 1.25rem;\n position: relative;\n }\n\n .timeline-header {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: 0.5rem;\n margin-bottom: 0.75rem;\n }\n\n .timeline-meta {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n flex-wrap: wrap;\n }\n\n .project-tag {\n background: rgba(88, 166, 255, 0.1);\n color: var(--accent);\n border: 1px solid rgba(88, 166, 255, 0.2);\n padding: 0.15rem 0.5rem;\n border-radius: 4px;\n font-family: monospace;\n font-size: 0.85rem;\n cursor: pointer;\n }\n\n .time-stamp {\n font-size: 0.8rem;\n color: var(--text-muted);\n }\n\n .source-badge {\n font-size: 0.75rem;\n color: var(--text-muted);\n background: rgba(255, 255, 255, 0.05);\n padding: 0.1rem 0.4rem;\n border-radius: 4px;\n }\n\n .entry-summary {\n font-size: 1rem;\n font-weight: 500;\n color: var(--heading);\n margin-bottom: 0.5rem;\n }\n\n .entry-progress {\n font-size: 0.9rem;\n color: var(--text);\n margin-bottom: 0.75rem;\n }\n\n .section-box {\n margin-top: 0.5rem;\n font-size: 0.875rem;\n }\n\n .section-title {\n font-weight: 600;\n font-size: 0.8rem;\n text-transform: uppercase;\n margin-bottom: 0.25rem;\n display: flex;\n align-items: center;\n gap: 0.25rem;\n }\n\n .section-title.blockers { color: var(--danger); }\n .section-title.next-steps { color: var(--accent); }\n .section-title.decisions { color: var(--purple); }\n\n .bullet-list {\n list-style: disc inside;\n color: var(--text);\n padding-left: 0.25rem;\n }\n\n .bullet-list li {\n margin-bottom: 0.2rem;\n }\n\n .tags-container {\n display: flex;\n flex-wrap: wrap;\n gap: 0.35rem;\n margin-top: 0.75rem;\n }\n\n .pill-tag {\n background: rgba(255, 255, 255, 0.06);\n border: 1px solid var(--border);\n color: var(--text-muted);\n font-size: 0.75rem;\n padding: 0.1rem 0.45rem;\n border-radius: 12px;\n }\n\n .empty-state {\n text-align: center;\n padding: 3rem 1rem;\n color: var(--text-muted);\n }\n\n .empty-state h3 {\n color: var(--heading);\n margin-bottom: 0.5rem;\n }\n\n .filter-bar {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n margin-bottom: 1rem;\n font-size: 0.875rem;\n color: var(--text-muted);\n }\n\n .filter-chip {\n background: rgba(88, 166, 255, 0.15);\n color: var(--accent);\n padding: 0.2rem 0.6rem;\n border-radius: 12px;\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n }\n\n .filter-chip button {\n background: none;\n border: none;\n color: var(--accent);\n cursor: pointer;\n font-weight: bold;\n }";
@@ -426,6 +426,8 @@ export const WEB_UI_STYLES = ` :root {
426
426
  .status-paused { background: rgba(210, 153, 34, 0.15); color: var(--warning); border: 1px solid rgba(210, 153, 34, 0.3); }
427
427
  .status-completed { background: rgba(57, 197, 207, 0.15); color: var(--cyan); border: 1px solid rgba(57, 197, 207, 0.3); }
428
428
  .status-archived { background: rgba(139, 148, 158, 0.15); color: var(--text-muted); border: 1px solid rgba(139, 148, 158, 0.3); }
429
+ .status-open { background: rgba(63, 185, 80, 0.15); color: var(--success); border: 1px solid rgba(63, 185, 80, 0.3); }
430
+ .status-cancelled { background: rgba(139, 148, 158, 0.15); color: var(--text-muted); border: 1px solid rgba(139, 148, 158, 0.3); }
429
431
 
430
432
  .card-body {
431
433
  margin-bottom: 1rem;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-mgr",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Local-first append-only project status ledger",
5
5
  "type": "module",
6
6
  "bin": {
@@ -64,8 +64,12 @@ pmgr list --query "keyword" --json
64
64
  pmgr serve --port 3000 --no-open --json
65
65
 
66
66
  # Record a completed milestone while the project remains active.
67
+ # Inspect open work before every add. Existing open pins carry automatically.
68
+ pmgr next --project my-project --json
69
+ # Use --next for new actions; exact prior open titles reuse their Todo UID.
67
70
  pmgr add --project my-project --status active --summary "Implemented the CLI" \
68
71
  --progress "Tests pass" --next "Publish the package" --source agent --json
72
+ pmgr next --project my-project --json
69
73
 
70
74
  # Same project, with a tag naming the sub-project this snapshot covers.
71
75
  pmgr add --project acme --status active --summary "Shipped the web dashboard" \
@@ -92,4 +96,37 @@ When this update resolves all commitments of earlier entries (including blockers
92
96
 
93
97
  Use `complete --entry <id-or-uid> --by <id-or-uid> --source agent` for an existing follow-up. Use `revoke-completion --entry <id-or-uid> --by <id-or-uid> --reason "..." --source agent` to correct a mistaken link. Links require the same project and strictly increasing occurrence times. Query the affected history afterward to verify effective completion. Do not bulk-close old entries without evidence.
94
98
 
95
- Export produces version-2 bundles (one bundle per line for JSONL), preserving entry/event identities and audit history; legacy imports remain supported. `at` has no completion annotations. Project current state still comes from the latest snapshot.
99
+ ## Daily open-work checklist
100
+
101
+ Use `pmgr next --json` as the canonical daily checklist. Do not combine
102
+ `nextSteps`, `nextTodos`, and `todos` by hand. Before every `add`, run
103
+ `pmgr next --project P --json`. Decide each item from evidence: keep it (do
104
+ nothing; open pins carry), finish it (`--done-todo <uid>`), or retire superseded
105
+ work (`--cancel-todo <uid>`). Close flags are repeatable and atomic with `add`.
106
+ Unpinned Todos remain visible as backlog; pin them explicitly with `--next-todo`.
107
+ Use `--next` only for new actions, not status descriptions. Put blockers in
108
+ `--blocker`. Never cancel older or similar work automatically.
109
+
110
+ CLI behavior change: plain `add` keeps open pins instead of clearing them;
111
+ `--next` appends. Exact trimmed, case-sensitive titles reuse an existing open
112
+ Todo of the same project, or an unassigned Todo already pinned on it, preferring
113
+ a pin, then the oldest by creation time and UID. Its provenance stays unchanged. Duplicate new titles in one write still
114
+ create separate Todos. Do not combine `--next` and `--next-todo`.
115
+ `--replace-next` only replaces pins (or clears them with no next work); it does
116
+ not close Todos. The domain API defaults to replace; imports remain strict.
117
+
118
+ Legacy strings appear as `legacy-step` items until explicitly converted with
119
+ `pmgr todo-convert --project P --json`, or `--entry <id-or-uid> --index <n>`.
120
+ `--all` converts every current legacy source only when the user requests it;
121
+ back up first with `pmgr export --format json > backup.json`. Conversion is
122
+ idempotent and appends no snapshot. A plain `add` preserves partially converted
123
+ legacy strings; new next work requires conversion or `--replace-next`.
124
+ Once fully converted, the next carry-mode `add` pins the open converted Todos
125
+ in index order.
126
+
127
+ `current --json` adds live `openWork`, and text `current` marks pinned work.
128
+ Raw `nextSteps`, `nextTodoUids`, and `nextTodos` retain their existing meaning.
129
+ `history`, `at`, and `getEntry` do not gain `openWork`; `at` does not resolve
130
+ live Todo status. Project current state still comes from the latest snapshot.
131
+ Export emits version 2 without Todos/refs, version 3 with Todos but no refs,
132
+ and version 4 with refs. Import/export never carries, reuses, or closes work.