@unbrained/pm-web 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +107 -0
  3. package/dist/auth.js +20 -0
  4. package/dist/auth.js.map +1 -0
  5. package/dist/crypto.js +42 -0
  6. package/dist/crypto.js.map +1 -0
  7. package/dist/db.js +111 -0
  8. package/dist/db.js.map +1 -0
  9. package/dist/index.js +88 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/middleware/auth.js +16 -0
  12. package/dist/middleware/auth.js.map +1 -0
  13. package/dist/routes/admin.js +207 -0
  14. package/dist/routes/admin.js.map +1 -0
  15. package/dist/routes/auth.js +163 -0
  16. package/dist/routes/auth.js.map +1 -0
  17. package/dist/routes/github.js +354 -0
  18. package/dist/routes/github.js.map +1 -0
  19. package/dist/routes/groups.js +180 -0
  20. package/dist/routes/groups.js.map +1 -0
  21. package/dist/routes/pm.js +2446 -0
  22. package/dist/routes/pm.js.map +1 -0
  23. package/dist/routes/projects.js +151 -0
  24. package/dist/routes/projects.js.map +1 -0
  25. package/dist/routes/sharing.js +155 -0
  26. package/dist/routes/sharing.js.map +1 -0
  27. package/dist/server.js +64 -0
  28. package/dist/server.js.map +1 -0
  29. package/dist/services/pm-runner.js +190 -0
  30. package/dist/services/pm-runner.js.map +1 -0
  31. package/dist/services/sse.js +111 -0
  32. package/dist/services/sse.js.map +1 -0
  33. package/manifest.json +15 -0
  34. package/package.json +111 -0
  35. package/public/icons/icon-192.png +0 -0
  36. package/public/icons/icon-512.png +0 -0
  37. package/public/index.html +265 -0
  38. package/public/manifest.json +66 -0
  39. package/public/src/api.js +28 -0
  40. package/public/src/api.js.map +1 -0
  41. package/public/src/api.ts +29 -0
  42. package/public/src/app.js +926 -0
  43. package/public/src/app.js.map +1 -0
  44. package/public/src/app.ts +929 -0
  45. package/public/src/components/modals.js +62 -0
  46. package/public/src/components/modals.js.map +1 -0
  47. package/public/src/components/modals.ts +73 -0
  48. package/public/src/components/toast.js +10 -0
  49. package/public/src/components/toast.js.map +1 -0
  50. package/public/src/components/toast.ts +13 -0
  51. package/public/src/constants.js +30 -0
  52. package/public/src/constants.js.map +1 -0
  53. package/public/src/constants.ts +41 -0
  54. package/public/src/state.js +15 -0
  55. package/public/src/state.js.map +1 -0
  56. package/public/src/state.ts +19 -0
  57. package/public/src/types.js +5 -0
  58. package/public/src/types.js.map +1 -0
  59. package/public/src/types.ts +253 -0
  60. package/public/src/utils.js +57 -0
  61. package/public/src/utils.js.map +1 -0
  62. package/public/src/utils.ts +56 -0
  63. package/public/src/views/activity.js +47 -0
  64. package/public/src/views/activity.js.map +1 -0
  65. package/public/src/views/activity.ts +41 -0
  66. package/public/src/views/admin.js +435 -0
  67. package/public/src/views/admin.js.map +1 -0
  68. package/public/src/views/admin.ts +504 -0
  69. package/public/src/views/auth.js +81 -0
  70. package/public/src/views/auth.js.map +1 -0
  71. package/public/src/views/auth.ts +74 -0
  72. package/public/src/views/calendar.js +133 -0
  73. package/public/src/views/calendar.js.map +1 -0
  74. package/public/src/views/calendar.ts +129 -0
  75. package/public/src/views/comments-audit.js +109 -0
  76. package/public/src/views/comments-audit.js.map +1 -0
  77. package/public/src/views/comments-audit.ts +108 -0
  78. package/public/src/views/config.js +322 -0
  79. package/public/src/views/config.js.map +1 -0
  80. package/public/src/views/config.ts +344 -0
  81. package/public/src/views/context.js +98 -0
  82. package/public/src/views/context.js.map +1 -0
  83. package/public/src/views/context.ts +100 -0
  84. package/public/src/views/create.js +293 -0
  85. package/public/src/views/create.js.map +1 -0
  86. package/public/src/views/create.ts +246 -0
  87. package/public/src/views/dedupe.js +51 -0
  88. package/public/src/views/dedupe.js.map +1 -0
  89. package/public/src/views/dedupe.ts +43 -0
  90. package/public/src/views/export.js +300 -0
  91. package/public/src/views/export.js.map +1 -0
  92. package/public/src/views/export.ts +274 -0
  93. package/public/src/views/github.js +360 -0
  94. package/public/src/views/github.js.map +1 -0
  95. package/public/src/views/github.ts +308 -0
  96. package/public/src/views/graph-canvas.js +1986 -0
  97. package/public/src/views/graph-canvas.js.map +1 -0
  98. package/public/src/views/graph-canvas.ts +2218 -0
  99. package/public/src/views/graph.js +1824 -0
  100. package/public/src/views/graph.js.map +1 -0
  101. package/public/src/views/graph.ts +1891 -0
  102. package/public/src/views/groups.js +186 -0
  103. package/public/src/views/groups.js.map +1 -0
  104. package/public/src/views/groups.ts +172 -0
  105. package/public/src/views/guide.js +151 -0
  106. package/public/src/views/guide.js.map +1 -0
  107. package/public/src/views/guide.ts +162 -0
  108. package/public/src/views/health.js +105 -0
  109. package/public/src/views/health.js.map +1 -0
  110. package/public/src/views/health.ts +102 -0
  111. package/public/src/views/items.js +1306 -0
  112. package/public/src/views/items.js.map +1 -0
  113. package/public/src/views/items.ts +1196 -0
  114. package/public/src/views/normalize.js +67 -0
  115. package/public/src/views/normalize.js.map +1 -0
  116. package/public/src/views/normalize.ts +58 -0
  117. package/public/src/views/plan.js +454 -0
  118. package/public/src/views/plan.js.map +1 -0
  119. package/public/src/views/plan.ts +496 -0
  120. package/public/src/views/projects.js +204 -0
  121. package/public/src/views/projects.js.map +1 -0
  122. package/public/src/views/projects.ts +196 -0
  123. package/public/src/views/router.js +227 -0
  124. package/public/src/views/router.js.map +1 -0
  125. package/public/src/views/router.ts +188 -0
  126. package/public/src/views/search.js +103 -0
  127. package/public/src/views/search.js.map +1 -0
  128. package/public/src/views/search.ts +94 -0
  129. package/public/src/views/settings.js +272 -0
  130. package/public/src/views/settings.js.map +1 -0
  131. package/public/src/views/settings.ts +190 -0
  132. package/public/src/views/shared.js +49 -0
  133. package/public/src/views/shared.js.map +1 -0
  134. package/public/src/views/shared.ts +49 -0
  135. package/public/src/views/sharing.js +152 -0
  136. package/public/src/views/sharing.js.map +1 -0
  137. package/public/src/views/sharing.ts +139 -0
  138. package/public/src/views/stats.js +92 -0
  139. package/public/src/views/stats.js.map +1 -0
  140. package/public/src/views/stats.ts +88 -0
  141. package/public/src/views/templates.js +117 -0
  142. package/public/src/views/templates.js.map +1 -0
  143. package/public/src/views/templates.ts +113 -0
  144. package/public/src/views/validate.js +54 -0
  145. package/public/src/views/validate.js.map +1 -0
  146. package/public/src/views/validate.ts +48 -0
  147. package/public/styles.css +2231 -0
  148. package/public/sw.js +318 -0
  149. package/public/tsconfig.json +20 -0
  150. package/sql/schema.sql +105 -0
@@ -0,0 +1,162 @@
1
+ // ═══════════════════════════════════════════════════════════════
2
+ // GUIDE VIEW
3
+ // ═══════════════════════════════════════════════════════════════
4
+ import { state } from '../state.js';
5
+ import { getGuide, getGuideTopic } from '../api.js';
6
+ import { escHtml } from '../utils.js';
7
+ import { toast } from '../components/toast.js';
8
+
9
+ const TOPIC_ICONS: Record<string, string> = {
10
+ quickstart: '🚀',
11
+ commands: '⌨️',
12
+ workflows: '🔄',
13
+ sdk: '🛠️',
14
+ extensions: '🧩',
15
+ skills: '🤖',
16
+ harnesses: '⚙️',
17
+ release: '📦',
18
+ };
19
+
20
+ function topicIcon(id: string): string {
21
+ return TOPIC_ICONS[id] ?? '📖';
22
+ }
23
+
24
+ function renderTopicCards(topics: Array<{id: string; title: string; summary?: string; intent?: string}>): string {
25
+ if (topics.length === 0) {
26
+ return '<div class="empty-state"><div class="empty-state-text">No guide topics found.</div></div>';
27
+ }
28
+ return `<div class="guide-topic-grid">${topics.map(t => `
29
+ <div class="card guide-topic-card" style="cursor:pointer;margin-bottom:0" onclick="window.__app.renderGuideView('${escHtml(t.id)}')">
30
+ <div class="guide-topic-icon">${topicIcon(t.id)}</div>
31
+ <div class="guide-topic-body">
32
+ <div class="guide-topic-title">${escHtml(t.title)}</div>
33
+ <div class="guide-topic-summary">${escHtml(t.summary || t.intent || '')}</div>
34
+ </div>
35
+ </div>`).join('')}</div>`;
36
+ }
37
+
38
+ function renderTopicDetail(topic: {
39
+ id: string;
40
+ title: string;
41
+ summary?: string;
42
+ intent?: string;
43
+ quick_commands?: string[];
44
+ commands?: string[];
45
+ workflows?: Array<{name: string; goal?: string; commands?: string[]}>;
46
+ related?: string[];
47
+ }): string {
48
+ const commands: string[] = topic.commands ?? topic.quick_commands ?? [];
49
+ const related: string[] = topic.related ?? [];
50
+
51
+ return `
52
+ <div style="margin-bottom:16px">
53
+ <a href="#" class="guide-back-link" onclick="event.preventDefault();window.__app.renderGuideView()"
54
+ style="font-size:13px;color:var(--accent);text-decoration:none">← Back to topics</a>
55
+ </div>
56
+ <div class="page-header">
57
+ <div>
58
+ <div class="page-title">${topicIcon(topic.id)} ${escHtml(topic.title)}</div>
59
+ ${topic.intent ? `<div class="page-subtitle">${escHtml(topic.intent)}</div>` : ''}
60
+ </div>
61
+ </div>
62
+ ${topic.summary ? `
63
+ <div class="card" style="margin-bottom:16px">
64
+ <div class="card-body">
65
+ <div class="item-detail-desc">${escHtml(topic.summary)}</div>
66
+ </div>
67
+ </div>` : ''}
68
+ ${commands.length > 0 ? `
69
+ <div class="card" style="margin-bottom:16px">
70
+ <div class="card-header"><div class="card-title">Quick Commands</div></div>
71
+ <div class="card-body">
72
+ ${commands.map(cmd => `
73
+ <div class="guide-cmd-row">
74
+ <code class="guide-cmd-code">${escHtml(cmd)}</code>
75
+ <button class="btn btn-ghost btn-sm guide-copy-btn"
76
+ onclick="navigator.clipboard.writeText('${escHtml(cmd.replace(/'/g, "\\'"))}').then(()=>window.__app.toast('Copied!','success')).catch(()=>{})"
77
+ title="Copy to clipboard">⎘</button>
78
+ </div>`).join('')}
79
+ </div>
80
+ </div>` : ''}
81
+ ${topic.workflows && topic.workflows.length > 0 ? `
82
+ <div class="card" style="margin-bottom:16px">
83
+ <div class="card-header"><div class="card-title">Workflows</div></div>
84
+ <div class="card-body">
85
+ ${topic.workflows.map(wf => `
86
+ <div style="margin-bottom:16px">
87
+ <div style="font-weight:600;font-size:14px;margin-bottom:4px">${escHtml(wf.name)}</div>
88
+ ${wf.goal ? `<div style="font-size:13px;color:var(--text-muted);margin-bottom:8px">${escHtml(wf.goal)}</div>` : ''}
89
+ ${wf.commands && wf.commands.length > 0 ? wf.commands.map(cmd => `
90
+ <div class="guide-cmd-row">
91
+ <code class="guide-cmd-code">${escHtml(cmd)}</code>
92
+ <button class="btn btn-ghost btn-sm guide-copy-btn"
93
+ onclick="navigator.clipboard.writeText('${escHtml(cmd.replace(/'/g, "\\'"))}').then(()=>window.__app.toast('Copied!','success')).catch(()=>{})"
94
+ title="Copy to clipboard">⎘</button>
95
+ </div>`).join('') : ''}
96
+ </div>`).join('')}
97
+ </div>
98
+ </div>` : ''}
99
+ ${related.length > 0 ? `
100
+ <div class="card">
101
+ <div class="card-header"><div class="card-title">Related Topics</div></div>
102
+ <div class="card-body" style="display:flex;flex-wrap:wrap;gap:8px">
103
+ ${related.map(r => `
104
+ <a href="#" class="badge badge-secondary"
105
+ onclick="event.preventDefault();window.__app.renderGuideView('${escHtml(r)}')"
106
+ style="cursor:pointer;text-decoration:none">${escHtml(r)}</a>`).join('')}
107
+ </div>
108
+ </div>` : ''}`;
109
+ }
110
+
111
+ export async function renderGuideView(topicId?: string): Promise<void> {
112
+ const el = document.getElementById('content-guide');
113
+ if (!el) return;
114
+ if (!state.currentProject) {
115
+ el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>';
116
+ return;
117
+ }
118
+
119
+ if (topicId) {
120
+ // Show single topic detail
121
+ el.innerHTML = `
122
+ <div style="margin-bottom:16px">
123
+ <a href="#" onclick="event.preventDefault();window.__app.renderGuideView()"
124
+ style="font-size:13px;color:var(--accent);text-decoration:none">← Back to topics</a>
125
+ </div>
126
+ <div id="guide-content"><div class="loading-state"><div class="loading-spinner"></div></div></div>`;
127
+
128
+ try {
129
+ const data = await getGuideTopic(state.currentProject.id, topicId);
130
+ const topic = (data as {topic?: unknown}).topic || data;
131
+ const contentEl = document.getElementById('guide-content');
132
+ if (contentEl) contentEl.innerHTML = renderTopicDetail(topic as Parameters<typeof renderTopicDetail>[0]);
133
+ } catch (err: unknown) {
134
+ const contentEl = document.getElementById('guide-content');
135
+ if (contentEl) {
136
+ contentEl.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
137
+ }
138
+ toast(err instanceof Error ? err.message : 'Failed to load topic', 'error');
139
+ }
140
+ } else {
141
+ // Show topic list
142
+ el.innerHTML = `
143
+ <div class="page-header">
144
+ <div><div class="page-title">Guide</div><div class="page-subtitle">${escHtml(state.currentProject.name)}</div></div>
145
+ <div class="page-actions"><button class="btn btn-secondary btn-sm" onclick="window.__app.renderGuideView()">↺ Refresh</button></div>
146
+ </div>
147
+ <div id="guide-content"><div class="loading-state"><div class="loading-spinner"></div></div></div>`;
148
+
149
+ try {
150
+ const data = await getGuide(state.currentProject.id);
151
+ const topics = (data as {topics?: unknown[]}).topics || [];
152
+ const contentEl = document.getElementById('guide-content');
153
+ if (contentEl) contentEl.innerHTML = renderTopicCards(topics as Parameters<typeof renderTopicCards>[0]);
154
+ } catch (err: unknown) {
155
+ const contentEl = document.getElementById('guide-content');
156
+ if (contentEl) {
157
+ contentEl.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
158
+ }
159
+ toast(err instanceof Error ? err.message : 'Failed to load guide', 'error');
160
+ }
161
+ }
162
+ }
@@ -0,0 +1,105 @@
1
+ // ═══════════════════════════════════════════════════════════════
2
+ // HEALTH VIEW
3
+ // ═══════════════════════════════════════════════════════════════
4
+ import { state } from '../state.js';
5
+ import { api } from '../api.js';
6
+ import { escHtml } from '../utils.js';
7
+ import { toast } from '../components/toast.js';
8
+ // Detect item IDs mentioned in a history-drift issue message.
9
+ function extractItemIdFromIssue(msg) {
10
+ // Common patterns: "history drift for ABC-123", "ABC-123 has history drift", etc.
11
+ const match = msg.match(/\b([A-Z][A-Z0-9]+-\d+)\b/);
12
+ return match ? match[1] : null;
13
+ }
14
+ function isHistoryDriftIssue(issue) {
15
+ const msg = (issue.message || issue.description || issue.type || '').toLowerCase();
16
+ return msg.includes('history') && (msg.includes('drift') || msg.includes('repair') || msg.includes('mismatch'));
17
+ }
18
+ function renderIssueRow(issue, projectId) {
19
+ const msg = escHtml(issue.message || issue.description || String(issue));
20
+ if (isHistoryDriftIssue(issue)) {
21
+ const itemId = extractItemIdFromIssue(issue.message || issue.description || '');
22
+ const repairBtn = itemId
23
+ ? `<button class="btn btn-secondary btn-sm" style="margin-left:8px;flex-shrink:0" onclick="window.__app.repairItemHistory('${escHtml(projectId)}','${escHtml(itemId)}',false)">Repair</button>
24
+ <button class="btn btn-ghost btn-sm" style="flex-shrink:0" onclick="window.__app.repairItemHistory('${escHtml(projectId)}','${escHtml(itemId)}',true)" title="Preview without applying">Dry Run</button>`
25
+ : '';
26
+ return `<div class="health-issue-item" style="display:flex;align-items:center;gap:4px;flex-wrap:wrap">
27
+ <span style="flex:1">⚠ ${msg}</span>${repairBtn}</div>`;
28
+ }
29
+ return `<div class="health-issue-item">⚠ ${msg}</div>`;
30
+ }
31
+ export async function repairItemHistory(projectId, itemId, dryRun) {
32
+ try {
33
+ const data = await api('POST', `/projects/${projectId}/pm/items/${encodeURIComponent(itemId)}/history-repair`, { dryRun });
34
+ if (dryRun) {
35
+ toast(`Dry run for ${itemId}: ${data.message || JSON.stringify(data)}`, 'info');
36
+ }
37
+ else {
38
+ toast(`History repaired for ${itemId}`, 'success');
39
+ }
40
+ }
41
+ catch (err) {
42
+ toast(`Repair failed: ${err instanceof Error ? err.message : String(err)}`, 'error');
43
+ }
44
+ }
45
+ export async function renderHealthView() {
46
+ const el = document.getElementById('content-health');
47
+ if (!el)
48
+ return;
49
+ if (!state.currentProject) {
50
+ el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>';
51
+ return;
52
+ }
53
+ const projectId = state.currentProject.id;
54
+ el.innerHTML = `
55
+ <div class="page-header">
56
+ <div><div class="page-title">Project Health</div><div class="page-subtitle">${escHtml(state.currentProject.name)}</div></div>
57
+ <div class="page-actions"><button class="btn btn-secondary btn-sm" onclick="window.__app.renderHealthView()">↺ Refresh</button></div>
58
+ </div>
59
+ <div id="health-content"><div class="loading-state"><div class="loading-spinner"></div></div></div>`;
60
+ try {
61
+ const data = await api('GET', `/projects/${projectId}/pm/health`);
62
+ const health = data.health || data;
63
+ const score = health.score !== undefined ? health.score : null;
64
+ const issues = health.issues || [];
65
+ let scoreColor = '#4ade80';
66
+ if (score !== null) {
67
+ if (score < 50)
68
+ scoreColor = '#f87171';
69
+ else if (score < 80)
70
+ scoreColor = '#facc15';
71
+ }
72
+ const contentEl = document.getElementById('health-content');
73
+ if (!contentEl)
74
+ return;
75
+ contentEl.innerHTML = `
76
+ ${score !== null ? `
77
+ <div class="card" style="margin-bottom:16px">
78
+ <div class="health-score-display">
79
+ <div class="health-score-number" style="color:${scoreColor}">${score}</div>
80
+ <div class="health-score-label">Health Score / 100</div>
81
+ </div>
82
+ </div>` : ''}
83
+ <div class="card">
84
+ <div class="card-header">
85
+ <div class="card-title">Issues ${issues.length > 0 ? `(${issues.length})` : ''}</div>
86
+ </div>
87
+ <div class="card-body">
88
+ ${issues.length === 0
89
+ ? '<div style="color:var(--status-closed);font-size:13px">✓ No issues found — project looks healthy!</div>'
90
+ : issues.map((i) => renderIssueRow(i, projectId)).join('')}
91
+ </div>
92
+ </div>
93
+ ${health.summary ? `
94
+ <div class="card" style="margin-top:16px">
95
+ <div class="card-header"><div class="card-title">Summary</div></div>
96
+ <div class="card-body"><div class="item-detail-desc">${escHtml(health.summary)}</div></div>
97
+ </div>` : ''}`;
98
+ }
99
+ catch (err) {
100
+ const contentEl = document.getElementById('health-content');
101
+ if (contentEl)
102
+ contentEl.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
103
+ }
104
+ }
105
+ //# sourceMappingURL=health.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.js","sourceRoot":"","sources":["health.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,102 @@
1
+ // ═══════════════════════════════════════════════════════════════
2
+ // HEALTH VIEW
3
+ // ═══════════════════════════════════════════════════════════════
4
+ import { state } from '../state.js';
5
+ import { api } from '../api.js';
6
+ import { escHtml } from '../utils.js';
7
+ import { toast } from '../components/toast.js';
8
+
9
+ // Detect item IDs mentioned in a history-drift issue message.
10
+ function extractItemIdFromIssue(msg: string): string | null {
11
+ // Common patterns: "history drift for ABC-123", "ABC-123 has history drift", etc.
12
+ const match = msg.match(/\b([A-Z][A-Z0-9]+-\d+)\b/);
13
+ return match ? match[1] : null;
14
+ }
15
+
16
+ function isHistoryDriftIssue(issue: any): boolean {
17
+ const msg: string = (issue.message || issue.description || issue.type || '').toLowerCase();
18
+ return msg.includes('history') && (msg.includes('drift') || msg.includes('repair') || msg.includes('mismatch'));
19
+ }
20
+
21
+ function renderIssueRow(issue: any, projectId: string): string {
22
+ const msg = escHtml(issue.message || issue.description || String(issue));
23
+ if (isHistoryDriftIssue(issue)) {
24
+ const itemId = extractItemIdFromIssue(issue.message || issue.description || '');
25
+ const repairBtn = itemId
26
+ ? `<button class="btn btn-secondary btn-sm" style="margin-left:8px;flex-shrink:0" onclick="window.__app.repairItemHistory('${escHtml(projectId)}','${escHtml(itemId)}',false)">Repair</button>
27
+ <button class="btn btn-ghost btn-sm" style="flex-shrink:0" onclick="window.__app.repairItemHistory('${escHtml(projectId)}','${escHtml(itemId)}',true)" title="Preview without applying">Dry Run</button>`
28
+ : '';
29
+ return `<div class="health-issue-item" style="display:flex;align-items:center;gap:4px;flex-wrap:wrap">
30
+ <span style="flex:1">⚠ ${msg}</span>${repairBtn}</div>`;
31
+ }
32
+ return `<div class="health-issue-item">⚠ ${msg}</div>`;
33
+ }
34
+
35
+ export async function repairItemHistory(projectId: string, itemId: string, dryRun: boolean): Promise<void> {
36
+ try {
37
+ const data = await api('POST', `/projects/${projectId}/pm/items/${encodeURIComponent(itemId)}/history-repair`, { dryRun });
38
+ if (dryRun) {
39
+ toast(`Dry run for ${itemId}: ${(data as any).message || JSON.stringify(data)}`, 'info');
40
+ } else {
41
+ toast(`History repaired for ${itemId}`, 'success');
42
+ }
43
+ } catch(err: unknown) {
44
+ toast(`Repair failed: ${err instanceof Error ? err.message : String(err)}`, 'error');
45
+ }
46
+ }
47
+
48
+ export async function renderHealthView(): Promise<void> {
49
+ const el = document.getElementById('content-health');
50
+ if (!el) return;
51
+ if (!state.currentProject) { el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>'; return; }
52
+ const projectId = state.currentProject.id;
53
+ el.innerHTML = `
54
+ <div class="page-header">
55
+ <div><div class="page-title">Project Health</div><div class="page-subtitle">${escHtml(state.currentProject.name)}</div></div>
56
+ <div class="page-actions"><button class="btn btn-secondary btn-sm" onclick="window.__app.renderHealthView()">↺ Refresh</button></div>
57
+ </div>
58
+ <div id="health-content"><div class="loading-state"><div class="loading-spinner"></div></div></div>`;
59
+
60
+ try {
61
+ const data = await api('GET',`/projects/${projectId}/pm/health`);
62
+ const health = (data as any).health || data;
63
+ const score = health.score !== undefined ? health.score : null;
64
+ const issues = health.issues || [];
65
+
66
+ let scoreColor = '#4ade80';
67
+ if (score !== null) {
68
+ if (score < 50) scoreColor = '#f87171';
69
+ else if (score < 80) scoreColor = '#facc15';
70
+ }
71
+
72
+ const contentEl = document.getElementById('health-content');
73
+ if (!contentEl) return;
74
+ contentEl.innerHTML = `
75
+ ${score !== null ? `
76
+ <div class="card" style="margin-bottom:16px">
77
+ <div class="health-score-display">
78
+ <div class="health-score-number" style="color:${scoreColor}">${score}</div>
79
+ <div class="health-score-label">Health Score / 100</div>
80
+ </div>
81
+ </div>` : ''}
82
+ <div class="card">
83
+ <div class="card-header">
84
+ <div class="card-title">Issues ${issues.length > 0 ? `(${issues.length})` : ''}</div>
85
+ </div>
86
+ <div class="card-body">
87
+ ${issues.length === 0
88
+ ? '<div style="color:var(--status-closed);font-size:13px">✓ No issues found — project looks healthy!</div>'
89
+ : issues.map((i: any) => renderIssueRow(i, projectId)).join('')
90
+ }
91
+ </div>
92
+ </div>
93
+ ${health.summary ? `
94
+ <div class="card" style="margin-top:16px">
95
+ <div class="card-header"><div class="card-title">Summary</div></div>
96
+ <div class="card-body"><div class="item-detail-desc">${escHtml(health.summary)}</div></div>
97
+ </div>` : ''}`;
98
+ } catch(err: unknown) {
99
+ const contentEl = document.getElementById('health-content');
100
+ if (contentEl) contentEl.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
101
+ }
102
+ }