@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,344 @@
1
+ // ═══════════════════════════════════════════════════════════════
2
+ // CONFIG VIEW — Project configuration editor
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
+ interface ConfigKey {
10
+ key: string;
11
+ aliases: string[];
12
+ value_kind: 'string' | 'string_array' | 'enum' | 'object';
13
+ set_flags: string[];
14
+ summary: string;
15
+ value: unknown;
16
+ }
17
+
18
+ interface ConfigResponse {
19
+ scope: string;
20
+ keys: ConfigKey[];
21
+ count: number;
22
+ settings_path?: string;
23
+ changed?: boolean;
24
+ }
25
+
26
+ export async function renderConfigView(): Promise<void> {
27
+ const el = document.getElementById('content-config');
28
+ if (!el) return;
29
+ if (!state.currentProject) {
30
+ el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>';
31
+ return;
32
+ }
33
+
34
+ el.innerHTML = `
35
+ <div class="page-header">
36
+ <div>
37
+ <div class="page-title">Project Config</div>
38
+ <div class="page-subtitle">Configure pm CLI settings for this project</div>
39
+ </div>
40
+ <div class="page-actions">
41
+ <button class="btn btn-secondary btn-sm" onclick="window.__app.renderConfigView()">↺ Refresh</button>
42
+ </div>
43
+ </div>
44
+ <div class="loading-state"><div class="loading-spinner"></div></div>`;
45
+
46
+ try {
47
+ const data = await api('GET', `/projects/${state.currentProject.id}/pm/config`) as ConfigResponse;
48
+ renderConfigData(el, data);
49
+ } catch (err: unknown) {
50
+ el.innerHTML = `
51
+ <div class="page-header">
52
+ <div>
53
+ <div class="page-title">Project Config</div>
54
+ <div class="page-subtitle">Configure pm CLI settings for this project</div>
55
+ </div>
56
+ </div>
57
+ <div class="empty-state">
58
+ <div class="empty-state-text">Failed to load config</div>
59
+ <div class="empty-state-sub">${escHtml(err instanceof Error ? err.message : String(err))}</div>
60
+ </div>`;
61
+ }
62
+ }
63
+
64
+ function renderConfigData(el: HTMLElement, data: ConfigResponse): void {
65
+ const { keys } = data;
66
+
67
+ // Group keys into categories
68
+ const simpleKeys = keys.filter(k => k.value_kind === 'string' || k.value_kind === 'enum');
69
+ const arrayKeys = keys.filter(k => k.value_kind === 'string_array');
70
+ const objectKeys = keys.filter(k => k.value_kind === 'object');
71
+
72
+ function renderArrayField(k: ConfigKey): string {
73
+ const arr = Array.isArray(k.value) ? k.value as string[] : [];
74
+ const items = arr.map((item, i) => `
75
+ <div class="config-array-item" id="config-arr-${escHtml(k.key)}-${i}" style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
76
+ <input class="form-input" style="flex:1" type="text" value="${escHtml(item)}" data-key="${escHtml(k.key)}" data-idx="${i}">
77
+ <button class="btn btn-danger btn-sm" style="padding:4px 8px;flex-shrink:0" onclick="window.__app.configRemoveArrayItem('${escHtml(k.key)}',${i})">✕</button>
78
+ </div>`).join('');
79
+ return `
80
+ <div class="card" style="margin-bottom:16px">
81
+ <div class="card-header">
82
+ <div class="card-title">${escHtml(k.key.replace(/_/g,' '))}</div>
83
+ <div style="font-size:11px;color:var(--text-muted)">${escHtml(k.summary)}</div>
84
+ </div>
85
+ <div class="card-body">
86
+ <div id="config-arr-${escHtml(k.key)}-container">${items || '<div style="color:var(--text-muted);font-size:13px;margin-bottom:8px">No items yet</div>'}</div>
87
+ <div style="display:flex;gap:8px;margin-top:8px">
88
+ <input class="form-input" id="config-arr-${escHtml(k.key)}-new" type="text" placeholder="Add item…" style="flex:1"
89
+ onkeydown="if(event.key==='Enter'){event.preventDefault();window.__app.configAddArrayItem('${escHtml(k.key)}');}">
90
+ <button class="btn btn-secondary btn-sm" onclick="window.__app.configAddArrayItem('${escHtml(k.key)}')">Add</button>
91
+ <button class="btn btn-primary btn-sm" onclick="window.__app.configSaveArray('${escHtml(k.key)}')">Save</button>
92
+ </div>
93
+ </div>
94
+ </div>`;
95
+ }
96
+
97
+ function renderSimpleField(k: ConfigKey): string {
98
+ const val = k.value !== null && k.value !== undefined ? String(k.value) : '';
99
+ return `
100
+ <div class="form-group">
101
+ <label class="form-label" title="${escHtml(k.summary)}">${escHtml(k.key.replace(/_/g,' '))}</label>
102
+ <div style="display:flex;gap:8px;align-items:center">
103
+ <input class="form-input" id="config-field-${escHtml(k.key)}" type="text" value="${escHtml(val)}"
104
+ placeholder="${escHtml(k.summary)}" style="flex:1"
105
+ onkeydown="if(event.key==='Enter'){event.preventDefault();window.__app.configSaveSimple('${escHtml(k.key)}');}">
106
+ <button class="btn btn-primary btn-sm" onclick="window.__app.configSaveSimple('${escHtml(k.key)}')">Save</button>
107
+ </div>
108
+ <div style="font-size:11px;color:var(--text-muted);margin-top:3px">${escHtml(k.summary)}</div>
109
+ </div>`;
110
+ }
111
+
112
+ function renderObjectField(k: ConfigKey): string {
113
+ const val = k.value !== null && k.value !== undefined ? JSON.stringify(k.value, null, 2) : '{}';
114
+ return `
115
+ <div class="form-group">
116
+ <label class="form-label">${escHtml(k.key.replace(/_/g,' '))}</label>
117
+ <div style="font-size:11px;color:var(--text-muted);margin-bottom:4px">${escHtml(k.summary)}</div>
118
+ <textarea class="form-input" id="config-field-${escHtml(k.key)}" style="font-family:monospace;font-size:12px;min-height:120px;resize:vertical">${escHtml(val)}</textarea>
119
+ <div style="margin-top:6px">
120
+ <button class="btn btn-primary btn-sm" onclick="window.__app.configSaveObject('${escHtml(k.key)}')">Save</button>
121
+ </div>
122
+ </div>`;
123
+ }
124
+
125
+ el.innerHTML = `
126
+ <div class="page-header">
127
+ <div>
128
+ <div class="page-title">Project Config</div>
129
+ <div class="page-subtitle">Configure pm CLI settings for ${escHtml(state.currentProject?.name || '')}</div>
130
+ </div>
131
+ <div class="page-actions">
132
+ <button class="btn btn-secondary btn-sm" onclick="window.__app.renderConfigView()">↺ Refresh</button>
133
+ </div>
134
+ </div>
135
+
136
+ ${arrayKeys.length > 0 ? `
137
+ <div style="margin-bottom:24px">
138
+ <div style="font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:12px">List Settings</div>
139
+ ${arrayKeys.map(renderArrayField).join('')}
140
+ </div>` : ''}
141
+
142
+ ${simpleKeys.length > 0 ? `
143
+ <div class="card" style="margin-bottom:24px">
144
+ <div class="card-header"><div class="card-title">Simple Settings</div></div>
145
+ <div class="card-body">
146
+ ${simpleKeys.map(renderSimpleField).join('')}
147
+ </div>
148
+ </div>` : ''}
149
+
150
+ ${objectKeys.length > 0 ? `
151
+ <div class="card" style="margin-bottom:24px">
152
+ <div class="card-header"><div class="card-title">Object Settings</div></div>
153
+ <div class="card-body">
154
+ ${objectKeys.map(renderObjectField).join('')}
155
+ </div>
156
+ </div>` : ''}
157
+
158
+ ${data.settings_path ? `<div style="font-size:11px;color:var(--text-muted);margin-top:8px">Settings path: <code style="font-family:monospace;background:var(--bg-input);padding:1px 4px;border-radius:3px">${escHtml(data.settings_path)}</code></div>` : ''}
159
+
160
+ <div class="card" style="margin-top:24px">
161
+ <div class="card-header">
162
+ <div class="card-title">Custom Schema Types</div>
163
+ <div style="font-size:11px;color:var(--text-muted)">Add a new item type using <code style="font-family:monospace">pm schema add-type</code></div>
164
+ </div>
165
+ <div class="card-body">
166
+ <div class="two-col">
167
+ <div class="form-group">
168
+ <label class="form-label">Type Name *</label>
169
+ <input class="form-input" id="schema-type-name" type="text" placeholder="e.g. Bug, Story, Spike">
170
+ </div>
171
+ <div class="form-group">
172
+ <label class="form-label">Default Status</label>
173
+ <input class="form-input" id="schema-type-default-status" type="text" placeholder="e.g. open">
174
+ </div>
175
+ </div>
176
+ <div class="form-group">
177
+ <label class="form-label">Description</label>
178
+ <input class="form-input" id="schema-type-description" type="text" placeholder="What is this type for?">
179
+ </div>
180
+ <div class="two-col">
181
+ <div class="form-group">
182
+ <label class="form-label">Folder</label>
183
+ <input class="form-input" id="schema-type-folder" type="text" placeholder="Subfolder for this type (optional)">
184
+ </div>
185
+ <div class="form-group">
186
+ <label class="form-label">Aliases (comma-separated)</label>
187
+ <input class="form-input" id="schema-type-aliases" type="text" placeholder="e.g. b, defect">
188
+ </div>
189
+ </div>
190
+ <div id="schema-type-error" style="display:none;color:var(--status-blocked);font-size:13px;margin-bottom:8px"></div>
191
+ <button class="btn btn-primary btn-sm" onclick="window.__app.addSchemaType()">Add Custom Type</button>
192
+ </div>
193
+ </div>
194
+ `;
195
+
196
+ // Store the current keys in DOM for mutation use
197
+ (el as any).__configKeys = keys;
198
+ }
199
+
200
+ // ─── Array field helpers ───────────────────────────────────────
201
+
202
+ export function configAddArrayItem(key: string): void {
203
+ const inputEl = document.getElementById(`config-arr-${key}-new`) as HTMLInputElement | null;
204
+ const val = inputEl?.value?.trim();
205
+ if (!val) return;
206
+
207
+ const container = document.getElementById(`config-arr-${key}-container`);
208
+ if (!container) return;
209
+
210
+ // Count existing items
211
+ const existing = container.querySelectorAll('[data-key]');
212
+ const idx = existing.length;
213
+
214
+ // Remove "no items" message if present
215
+ const noItems = container.querySelector('div');
216
+ if (noItems && noItems.textContent?.includes('No items yet')) noItems.remove();
217
+
218
+ const div = document.createElement('div');
219
+ div.className = 'config-array-item';
220
+ div.id = `config-arr-${key}-${idx}`;
221
+ div.style.cssText = 'display:flex;align-items:center;gap:8px;margin-bottom:6px';
222
+ div.innerHTML = `
223
+ <input class="form-input" style="flex:1" type="text" value="${escHtml(val)}" data-key="${escHtml(key)}" data-idx="${idx}">
224
+ <button class="btn btn-danger btn-sm" style="padding:4px 8px;flex-shrink:0" onclick="window.__app.configRemoveArrayItem('${escHtml(key)}',${idx})">✕</button>
225
+ `;
226
+ container.appendChild(div);
227
+ if (inputEl) inputEl.value = '';
228
+ }
229
+
230
+ export function configRemoveArrayItem(key: string, idx: number): void {
231
+ const container = document.getElementById(`config-arr-${key}-container`);
232
+ if (!container) return;
233
+
234
+ const items = container.querySelectorAll('[data-key]');
235
+ if (items[idx]) {
236
+ (items[idx] as HTMLElement).closest('.config-array-item')?.remove();
237
+ }
238
+
239
+ // Show "no items" if empty
240
+ if (container.querySelectorAll('[data-key]').length === 0) {
241
+ container.innerHTML = '<div style="color:var(--text-muted);font-size:13px;margin-bottom:8px">No items yet</div>';
242
+ }
243
+ }
244
+
245
+ export async function configSaveArray(key: string): Promise<void> {
246
+ const pid = state.currentProject?.id;
247
+ if (!pid) return;
248
+
249
+ const container = document.getElementById(`config-arr-${key}-container`);
250
+ if (!container) return;
251
+
252
+ const inputs = container.querySelectorAll('input[data-key]');
253
+ const values = Array.from(inputs).map(inp => (inp as HTMLInputElement).value.trim()).filter(Boolean);
254
+
255
+ try {
256
+ await api('PATCH', `/projects/${pid}/pm/config/${encodeURIComponent(key)}`, { value: values });
257
+ toast(`Saved ${key.replace(/_/g, ' ')}`, 'success');
258
+ } catch (err: unknown) {
259
+ toast(`Error: ${err instanceof Error ? err.message : String(err)}`, 'error');
260
+ }
261
+ }
262
+
263
+ export async function configSaveSimple(key: string): Promise<void> {
264
+ const pid = state.currentProject?.id;
265
+ if (!pid) return;
266
+
267
+ const inputEl = document.getElementById(`config-field-${key}`) as HTMLInputElement | null;
268
+ const value = inputEl?.value?.trim() ?? '';
269
+
270
+ try {
271
+ await api('PATCH', `/projects/${pid}/pm/config/${encodeURIComponent(key)}`, { value });
272
+ toast(`Saved ${key.replace(/_/g, ' ')}`, 'success');
273
+ } catch (err: unknown) {
274
+ toast(`Error: ${err instanceof Error ? err.message : String(err)}`, 'error');
275
+ }
276
+ }
277
+
278
+ export async function addSchemaType(): Promise<void> {
279
+ const pid = state.currentProject?.id;
280
+ if (!pid) return;
281
+
282
+ const nameEl = document.getElementById('schema-type-name') as HTMLInputElement | null;
283
+ const descEl = document.getElementById('schema-type-description') as HTMLInputElement | null;
284
+ const defaultStatusEl = document.getElementById('schema-type-default-status') as HTMLInputElement | null;
285
+ const folderEl = document.getElementById('schema-type-folder') as HTMLInputElement | null;
286
+ const aliasesEl = document.getElementById('schema-type-aliases') as HTMLInputElement | null;
287
+ const errEl = document.getElementById('schema-type-error') as HTMLElement | null;
288
+
289
+ const name = nameEl?.value?.trim() ?? '';
290
+ if (!name) {
291
+ if (errEl) { errEl.textContent = 'Type name is required'; errEl.style.display = 'block'; }
292
+ return;
293
+ }
294
+ if (errEl) errEl.style.display = 'none';
295
+
296
+ const description = descEl?.value?.trim() ?? '';
297
+ const defaultStatus = defaultStatusEl?.value?.trim() ?? '';
298
+ const folder = folderEl?.value?.trim() ?? '';
299
+ const aliasesRaw = aliasesEl?.value?.trim() ?? '';
300
+ const aliases = aliasesRaw ? aliasesRaw.split(',').map(a => a.trim()).filter(Boolean) : [];
301
+
302
+ try {
303
+ await api('POST', `/projects/${pid}/pm/schema/add-type`, {
304
+ name,
305
+ ...(description ? { description } : {}),
306
+ ...(defaultStatus ? { defaultStatus } : {}),
307
+ ...(folder ? { folder } : {}),
308
+ ...(aliases.length ? { aliases } : {}),
309
+ });
310
+ toast(`Custom type "${name}" added`, 'success');
311
+ if (nameEl) nameEl.value = '';
312
+ if (descEl) descEl.value = '';
313
+ if (defaultStatusEl) defaultStatusEl.value = '';
314
+ if (folderEl) folderEl.value = '';
315
+ if (aliasesEl) aliasesEl.value = '';
316
+ } catch (err: unknown) {
317
+ const msg = err instanceof Error ? err.message : String(err);
318
+ if (errEl) { errEl.textContent = msg; errEl.style.display = 'block'; }
319
+ toast(`Error: ${msg}`, 'error');
320
+ }
321
+ }
322
+
323
+ export async function configSaveObject(key: string): Promise<void> {
324
+ const pid = state.currentProject?.id;
325
+ if (!pid) return;
326
+
327
+ const textEl = document.getElementById(`config-field-${key}`) as HTMLTextAreaElement | null;
328
+ const raw = textEl?.value?.trim() ?? '';
329
+
330
+ let parsed: unknown;
331
+ try {
332
+ parsed = JSON.parse(raw);
333
+ } catch {
334
+ toast('Invalid JSON — please check the format', 'error');
335
+ return;
336
+ }
337
+
338
+ try {
339
+ await api('PATCH', `/projects/${pid}/pm/config/${encodeURIComponent(key)}`, { value: parsed });
340
+ toast(`Saved ${key.replace(/_/g, ' ')}`, 'success');
341
+ } catch (err: unknown) {
342
+ toast(`Error: ${err instanceof Error ? err.message : String(err)}`, 'error');
343
+ }
344
+ }
@@ -0,0 +1,98 @@
1
+ // ═══════════════════════════════════════════════════════════════
2
+ // CONTEXT VIEW
3
+ // ═══════════════════════════════════════════════════════════════
4
+ import { state } from '../state.js';
5
+ import { api } from '../api.js';
6
+ import { escHtml, relTime, typeIcon, statusBadge } from '../utils.js';
7
+ import { TYPE_ICONS } from '../constants.js';
8
+ export async function renderContextView() {
9
+ const el = document.getElementById('content-context');
10
+ if (!el)
11
+ return;
12
+ if (!state.currentProject) {
13
+ el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>';
14
+ return;
15
+ }
16
+ el.innerHTML = `
17
+ <div class="page-header">
18
+ <div><div class="page-title">Context</div><div class="page-subtitle">Project snapshot for ${escHtml(state.currentProject.name)}</div></div>
19
+ <div class="page-actions"><button class="btn btn-secondary btn-sm" onclick="window.__app.renderContextView()">↺ Refresh</button></div>
20
+ </div>
21
+ <div id="context-content"><div class="loading-state"><div class="loading-spinner"></div></div></div>`;
22
+ try {
23
+ const data = await api('GET', `/projects/${state.currentProject.id}/pm/context`);
24
+ const ctx = data.context || data;
25
+ const contentEl = document.getElementById('context-content');
26
+ if (contentEl)
27
+ contentEl.innerHTML = renderContextData(ctx);
28
+ }
29
+ catch (err) {
30
+ const contentEl = document.getElementById('context-content');
31
+ if (contentEl)
32
+ contentEl.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
33
+ }
34
+ }
35
+ function renderContextData(ctx) {
36
+ if (!ctx || typeof ctx !== 'object') {
37
+ return `<div class="card"><div class="card-body"><div class="context-block">${escHtml(JSON.stringify(ctx, null, 2))}</div></div></div>`;
38
+ }
39
+ const sections = [];
40
+ if (ctx.summary || ctx.description) {
41
+ sections.push(`<div class="context-section">
42
+ <div class="context-section-title">◈ Summary</div>
43
+ <div class="item-detail-desc">${escHtml(ctx.summary || ctx.description)}</div>
44
+ </div>`);
45
+ }
46
+ const activeItems = ctx.activeItems || ctx.inProgress || ctx.open || [];
47
+ if (activeItems.length > 0) {
48
+ sections.push(`<div class="context-section">
49
+ <div class="context-section-title">⚡ Active Items (${activeItems.length})</div>
50
+ <div class="card"><div class="card-body">
51
+ ${activeItems.map((item) => `
52
+ <div class="context-item-row">
53
+ ${typeIcon(item.type)} <span class="mono" style="font-size:11px;color:var(--text-muted)">${escHtml(item.id || '')}</span>
54
+ <span style="flex:1">${escHtml(item.title || '')}</span>
55
+ ${statusBadge(item.status || 'open')}
56
+ </div>`).join('')}
57
+ </div></div>
58
+ </div>`);
59
+ }
60
+ const blockedItems = ctx.blockedItems || ctx.blocked || [];
61
+ if (blockedItems.length > 0) {
62
+ sections.push(`<div class="context-section">
63
+ <div class="context-section-title" style="color:var(--status-blocked)">⛔ Blocked (${blockedItems.length})</div>
64
+ <div class="card"><div class="card-body">
65
+ ${blockedItems.map((item) => `
66
+ <div class="context-item-row">
67
+ ${typeIcon(item.type)} <span class="mono" style="font-size:11px;color:var(--text-muted)">${escHtml(item.id || '')}</span>
68
+ <span style="flex:1">${escHtml(item.title || '')}</span>
69
+ ${statusBadge('blocked')}
70
+ </div>`).join('')}
71
+ </div></div>
72
+ </div>`);
73
+ }
74
+ const recentActivity = ctx.recentActivity || ctx.activity || [];
75
+ if (recentActivity.length > 0) {
76
+ sections.push(`<div class="context-section">
77
+ <div class="context-section-title">◎ Recent Activity</div>
78
+ <div class="card"><div class="card-body">
79
+ ${recentActivity.slice(0, 10).map((a) => `
80
+ <div class="activity-item">
81
+ <div class="activity-icon">${TYPE_ICONS[a.type] || '◎'}</div>
82
+ <div class="activity-body">
83
+ <div class="activity-desc">${escHtml(a.message || a.title || a.action || '')}</div>
84
+ <div class="activity-time">${relTime(a.timestamp || a.created_at)}</div>
85
+ </div>
86
+ </div>`).join('')}
87
+ </div></div>
88
+ </div>`);
89
+ }
90
+ if (sections.length === 0) {
91
+ sections.push(`<div class="card"><div class="card-body">
92
+ <div class="context-section-title">Raw Context</div>
93
+ <div class="context-block">${escHtml(JSON.stringify(ctx, null, 2))}</div>
94
+ </div></div>`);
95
+ }
96
+ return sections.join('');
97
+ }
98
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["context.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,100 @@
1
+ // ═══════════════════════════════════════════════════════════════
2
+ // CONTEXT VIEW
3
+ // ═══════════════════════════════════════════════════════════════
4
+ import { state } from '../state.js';
5
+ import { api } from '../api.js';
6
+ import { escHtml, relTime, typeIcon, statusBadge } from '../utils.js';
7
+ import { TYPE_ICONS } from '../constants.js';
8
+
9
+ export async function renderContextView(): Promise<void> {
10
+ const el = document.getElementById('content-context');
11
+ if (!el) return;
12
+ if (!state.currentProject) { el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>'; return; }
13
+ el.innerHTML = `
14
+ <div class="page-header">
15
+ <div><div class="page-title">Context</div><div class="page-subtitle">Project snapshot for ${escHtml(state.currentProject.name)}</div></div>
16
+ <div class="page-actions"><button class="btn btn-secondary btn-sm" onclick="window.__app.renderContextView()">↺ Refresh</button></div>
17
+ </div>
18
+ <div id="context-content"><div class="loading-state"><div class="loading-spinner"></div></div></div>`;
19
+
20
+ try {
21
+ const data = await api('GET',`/projects/${state.currentProject.id}/pm/context`);
22
+ const ctx = (data as any).context || data;
23
+ const contentEl = document.getElementById('context-content');
24
+ if (contentEl) contentEl.innerHTML = renderContextData(ctx);
25
+ } catch(err: unknown) {
26
+ const contentEl = document.getElementById('context-content');
27
+ if (contentEl) contentEl.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
28
+ }
29
+ }
30
+
31
+ function renderContextData(ctx: any): string {
32
+ if (!ctx || typeof ctx !== 'object') {
33
+ return `<div class="card"><div class="card-body"><div class="context-block">${escHtml(JSON.stringify(ctx,null,2))}</div></div></div>`;
34
+ }
35
+
36
+ const sections: string[] = [];
37
+
38
+ if (ctx.summary || ctx.description) {
39
+ sections.push(`<div class="context-section">
40
+ <div class="context-section-title">◈ Summary</div>
41
+ <div class="item-detail-desc">${escHtml(ctx.summary||ctx.description)}</div>
42
+ </div>`);
43
+ }
44
+
45
+ const activeItems = ctx.activeItems || ctx.inProgress || ctx.open || [];
46
+ if (activeItems.length > 0) {
47
+ sections.push(`<div class="context-section">
48
+ <div class="context-section-title">⚡ Active Items (${activeItems.length})</div>
49
+ <div class="card"><div class="card-body">
50
+ ${activeItems.map((item: any)=>`
51
+ <div class="context-item-row">
52
+ ${typeIcon(item.type)} <span class="mono" style="font-size:11px;color:var(--text-muted)">${escHtml(item.id||'')}</span>
53
+ <span style="flex:1">${escHtml(item.title||'')}</span>
54
+ ${statusBadge(item.status||'open')}
55
+ </div>`).join('')}
56
+ </div></div>
57
+ </div>`);
58
+ }
59
+
60
+ const blockedItems = ctx.blockedItems || ctx.blocked || [];
61
+ if (blockedItems.length > 0) {
62
+ sections.push(`<div class="context-section">
63
+ <div class="context-section-title" style="color:var(--status-blocked)">⛔ Blocked (${blockedItems.length})</div>
64
+ <div class="card"><div class="card-body">
65
+ ${blockedItems.map((item: any)=>`
66
+ <div class="context-item-row">
67
+ ${typeIcon(item.type)} <span class="mono" style="font-size:11px;color:var(--text-muted)">${escHtml(item.id||'')}</span>
68
+ <span style="flex:1">${escHtml(item.title||'')}</span>
69
+ ${statusBadge('blocked')}
70
+ </div>`).join('')}
71
+ </div></div>
72
+ </div>`);
73
+ }
74
+
75
+ const recentActivity = ctx.recentActivity || ctx.activity || [];
76
+ if (recentActivity.length > 0) {
77
+ sections.push(`<div class="context-section">
78
+ <div class="context-section-title">◎ Recent Activity</div>
79
+ <div class="card"><div class="card-body">
80
+ ${recentActivity.slice(0,10).map((a: any)=>`
81
+ <div class="activity-item">
82
+ <div class="activity-icon">${TYPE_ICONS[a.type]||'◎'}</div>
83
+ <div class="activity-body">
84
+ <div class="activity-desc">${escHtml(a.message||a.title||a.action||'')}</div>
85
+ <div class="activity-time">${relTime(a.timestamp||a.created_at)}</div>
86
+ </div>
87
+ </div>`).join('')}
88
+ </div></div>
89
+ </div>`);
90
+ }
91
+
92
+ if (sections.length === 0) {
93
+ sections.push(`<div class="card"><div class="card-body">
94
+ <div class="context-section-title">Raw Context</div>
95
+ <div class="context-block">${escHtml(JSON.stringify(ctx,null,2))}</div>
96
+ </div></div>`);
97
+ }
98
+
99
+ return sections.join('');
100
+ }