@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,113 @@
1
+ // ═══════════════════════════════════════════════════════════════
2
+ // TEMPLATES VIEW
3
+ // ═══════════════════════════════════════════════════════════════
4
+ import { state } from '../state.js';
5
+ import { api } from '../api.js';
6
+ import { escHtml, typeIcon } from '../utils.js';
7
+ import { toast } from '../components/toast.js';
8
+ import { showView } from './router.js';
9
+
10
+ export async function renderTemplatesView(): Promise<void> {
11
+ const el = document.getElementById('content-templates');
12
+ if (!el) return;
13
+ if (!state.currentProject) {
14
+ el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>';
15
+ return;
16
+ }
17
+ el.innerHTML = `
18
+ <div class="page-header">
19
+ <div>
20
+ <div class="page-title">Templates</div>
21
+ <div class="page-subtitle">Reusable item templates for ${escHtml(state.currentProject.name)}</div>
22
+ </div>
23
+ <div class="page-actions">
24
+ <button class="btn btn-secondary btn-sm" onclick="window.__app.renderTemplatesView()">↺ Refresh</button>
25
+ </div>
26
+ </div>
27
+ <div id="templates-content"><div class="loading-state"><div class="loading-spinner"></div></div></div>`;
28
+ await fetchAndRenderTemplates();
29
+ }
30
+
31
+ async function fetchAndRenderTemplates(): Promise<void> {
32
+ const pid = state.currentProject?.id;
33
+ if (!pid) return;
34
+ try {
35
+ const data = await api('GET', `/projects/${pid}/pm/templates`);
36
+ const templates: any[] = (data as any).templates || [];
37
+ const el = document.getElementById('templates-content');
38
+ if (!el) return;
39
+ if (templates.length === 0) {
40
+ el.innerHTML = `
41
+ <div class="card">
42
+ <div class="card-body">
43
+ <div style="color:var(--text-muted);font-size:13px;margin-bottom:16px">
44
+ No templates defined yet. Create templates using the <code style="font-family:var(--font-mono);background:var(--bg-input);padding:2px 6px;border-radius:4px">pm templates create</code> CLI command.
45
+ </div>
46
+ <div style="font-size:12px;color:var(--text-secondary)">
47
+ Templates allow you to pre-fill item fields (type, priority, tags, description, etc.) when creating new items.
48
+ </div>
49
+ </div>
50
+ </div>`;
51
+ return;
52
+ }
53
+ el.innerHTML = `
54
+ <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px">
55
+ ${templates.map((t: any) => renderTemplateCard(t)).join('')}
56
+ </div>`;
57
+ } catch(err: unknown) {
58
+ const el = document.getElementById('templates-content');
59
+ if (el) el.innerHTML = `<div class="empty-state"><div class="empty-state-text">Failed to load templates: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
60
+ }
61
+ }
62
+
63
+ function renderTemplateCard(t: any): string {
64
+ const name = t.name || t.id || 'Unnamed';
65
+ const type = t.type || t.defaults?.type || '';
66
+ const priority = t.priority || t.defaults?.priority || '';
67
+ const tags = (t.tags || t.defaults?.tags || []).join(', ');
68
+ const desc = t.description || t.defaults?.description || '';
69
+ return `
70
+ <div class="card" style="cursor:default">
71
+ <div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
72
+ <div class="card-title" style="display:flex;align-items:center;gap:6px">
73
+ ${type ? typeIcon(type) : ''}
74
+ <span>${escHtml(name)}</span>
75
+ </div>
76
+ ${priority ? `<span style="font-size:11px;color:var(--text-muted);background:var(--bg-input);padding:2px 8px;border-radius:4px">P${priority}</span>` : ''}
77
+ </div>
78
+ <div class="card-body" style="padding-top:0">
79
+ ${type ? `<div style="font-size:12px;color:var(--text-secondary);margin-bottom:6px">Type: <strong>${escHtml(type)}</strong></div>` : ''}
80
+ ${tags ? `<div style="font-size:12px;color:var(--text-secondary);margin-bottom:6px">Tags: ${escHtml(tags)}</div>` : ''}
81
+ ${desc ? `<div style="font-size:12px;color:var(--text-muted);margin-bottom:10px;line-height:1.4">${escHtml(desc)}</div>` : ''}
82
+ <button class="btn btn-primary btn-sm" style="width:100%" onclick="window.__app.createFromTemplate(${JSON.stringify(escHtml(name))}, ${JSON.stringify(t)})">
83
+ + Create from Template
84
+ </button>
85
+ </div>
86
+ </div>`;
87
+ }
88
+
89
+ export function createFromTemplate(name: string, template: any): void {
90
+ // Navigate to create view and pre-fill from template
91
+ showView('create');
92
+ // Give the create view time to render, then fill fields
93
+ setTimeout(() => {
94
+ const defaults = template.defaults || template;
95
+ const setVal = (id: string, val: string | undefined) => {
96
+ if (!val) return;
97
+ const el = document.getElementById(id) as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | null;
98
+ if (el) el.value = val;
99
+ };
100
+ setVal('ci-type', defaults.type || template.type);
101
+ setVal('ci-priority', String(defaults.priority || template.priority || ''));
102
+ setVal('ci-tags', (defaults.tags || template.tags || []).join(', '));
103
+ setVal('ci-desc', defaults.description || template.description || '');
104
+ setVal('ci-sprint', defaults.sprint || template.sprint || '');
105
+ setVal('ci-release', defaults.release || template.release || '');
106
+ setVal('ci-assignee', defaults.assignee || template.assignee || '');
107
+ if (defaults.acceptance_criteria || defaults.acceptanceCriteria) {
108
+ setVal('ci-acceptance-criteria', defaults.acceptance_criteria || defaults.acceptanceCriteria);
109
+ }
110
+ toast(`Template "${name}" applied`, 'success');
111
+ document.getElementById('ci-title')?.focus();
112
+ }, 100);
113
+ }
@@ -0,0 +1,54 @@
1
+ // ═══════════════════════════════════════════════════════════════
2
+ // VALIDATE VIEW
3
+ // ═══════════════════════════════════════════════════════════════
4
+ import { state } from '../state.js';
5
+ import { api } from '../api.js';
6
+ import { escHtml } from '../utils.js';
7
+ export async function renderValidateView() {
8
+ const el = document.getElementById('content-validate');
9
+ if (!el)
10
+ return;
11
+ if (!state.currentProject) {
12
+ el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>';
13
+ return;
14
+ }
15
+ el.innerHTML = `
16
+ <div class="page-header">
17
+ <div><div class="page-title">Validate</div><div class="page-subtitle">Run metadata & lifecycle validation on ${escHtml(state.currentProject.name)}</div></div>
18
+ <div class="page-actions"><button class="btn btn-secondary btn-sm" onclick="window.__app.renderValidateView()">↺ Refresh</button></div>
19
+ </div>
20
+ <div id="validate-content"><div class="loading-state"><div class="loading-spinner"></div></div></div>`;
21
+ try {
22
+ const data = await api('GET', `/projects/${state.currentProject.id}/pm/validate`);
23
+ const issues = data.issues || data.errors || data.violations || [];
24
+ const warnings = data.warnings || [];
25
+ const el2 = document.getElementById('validate-content');
26
+ if (!el2)
27
+ return;
28
+ const allIssues = [...issues.map((i) => ({ ...i, level: 'error' })), ...warnings.map((w) => ({ ...w, level: 'warning' }))];
29
+ el2.innerHTML = `
30
+ <div class="card" style="margin-bottom:12px">
31
+ <div class="card-header"><div class="card-title">Validation Results</div></div>
32
+ <div class="card-body">
33
+ ${allIssues.length === 0
34
+ ? '<div style="color:var(--status-closed);font-size:13px">✓ All checks passed — no issues found!</div>'
35
+ : allIssues.map((i) => `
36
+ <div style="display:flex;gap:8px;align-items:flex-start;padding:8px 0;border-bottom:1px solid var(--border)">
37
+ <span style="color:${i.level === 'error' ? 'var(--status-blocked)' : 'var(--priority-3)'};flex-shrink:0">${i.level === 'error' ? '✗' : '⚠'}</span>
38
+ <div>
39
+ <div style="font-size:13px">${escHtml(i.message || i.description || JSON.stringify(i))}</div>
40
+ ${i.id ? `<div style="font-size:11px;color:var(--text-muted);margin-top:2px"><a href="#" onclick="window.__app.openItemDetail('${escHtml(i.id)}');return false" style="color:var(--accent)">${escHtml(i.id)}</a></div>` : ''}
41
+ </div>
42
+ </div>`).join('')}
43
+ </div>
44
+ </div>
45
+ ${data.summary ? `<div class="card"><div class="card-header"><div class="card-title">Summary</div></div><div class="card-body"><div class="item-detail-desc">${escHtml(data.summary)}</div></div></div>` : ''}
46
+ ${data.ok !== undefined ? `<div style="margin-top:8px;font-size:12px;color:var(--text-muted)">Status: <span style="color:${data.ok ? 'var(--status-closed)' : 'var(--status-blocked)'}">${data.ok ? 'PASS' : 'FAIL'}</span></div>` : ''}`;
47
+ }
48
+ catch (err) {
49
+ const el2 = document.getElementById('validate-content');
50
+ if (el2)
51
+ el2.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
52
+ }
53
+ }
54
+ //# sourceMappingURL=validate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.js","sourceRoot":"","sources":["validate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ // ═══════════════════════════════════════════════════════════════
2
+ // VALIDATE VIEW
3
+ // ═══════════════════════════════════════════════════════════════
4
+ import { state } from '../state.js';
5
+ import { api } from '../api.js';
6
+ import { escHtml } from '../utils.js';
7
+
8
+ export async function renderValidateView(): Promise<void> {
9
+ const el = document.getElementById('content-validate');
10
+ if (!el) return;
11
+ if (!state.currentProject) { el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>'; return; }
12
+ el.innerHTML = `
13
+ <div class="page-header">
14
+ <div><div class="page-title">Validate</div><div class="page-subtitle">Run metadata & lifecycle validation on ${escHtml(state.currentProject.name)}</div></div>
15
+ <div class="page-actions"><button class="btn btn-secondary btn-sm" onclick="window.__app.renderValidateView()">↺ Refresh</button></div>
16
+ </div>
17
+ <div id="validate-content"><div class="loading-state"><div class="loading-spinner"></div></div></div>`;
18
+ try {
19
+ const data = await api('GET', `/projects/${state.currentProject.id}/pm/validate`) as any;
20
+ const issues = data.issues || data.errors || data.violations || [];
21
+ const warnings = data.warnings || [];
22
+ const el2 = document.getElementById('validate-content');
23
+ if (!el2) return;
24
+ const allIssues = [...issues.map((i: any)=>({...i,level:'error'})), ...warnings.map((w: any)=>({...w,level:'warning'}))];
25
+ el2.innerHTML = `
26
+ <div class="card" style="margin-bottom:12px">
27
+ <div class="card-header"><div class="card-title">Validation Results</div></div>
28
+ <div class="card-body">
29
+ ${allIssues.length === 0
30
+ ? '<div style="color:var(--status-closed);font-size:13px">✓ All checks passed — no issues found!</div>'
31
+ : allIssues.map((i: any)=>`
32
+ <div style="display:flex;gap:8px;align-items:flex-start;padding:8px 0;border-bottom:1px solid var(--border)">
33
+ <span style="color:${i.level==='error'?'var(--status-blocked)':'var(--priority-3)'};flex-shrink:0">${i.level==='error'?'✗':'⚠'}</span>
34
+ <div>
35
+ <div style="font-size:13px">${escHtml(i.message||i.description||JSON.stringify(i))}</div>
36
+ ${i.id?`<div style="font-size:11px;color:var(--text-muted);margin-top:2px"><a href="#" onclick="window.__app.openItemDetail('${escHtml(i.id)}');return false" style="color:var(--accent)">${escHtml(i.id)}</a></div>`:''}
37
+ </div>
38
+ </div>`).join('')
39
+ }
40
+ </div>
41
+ </div>
42
+ ${data.summary ? `<div class="card"><div class="card-header"><div class="card-title">Summary</div></div><div class="card-body"><div class="item-detail-desc">${escHtml(data.summary)}</div></div></div>` : ''}
43
+ ${data.ok !== undefined ? `<div style="margin-top:8px;font-size:12px;color:var(--text-muted)">Status: <span style="color:${data.ok?'var(--status-closed)':'var(--status-blocked)'}">${data.ok?'PASS':'FAIL'}</span></div>` : ''}`;
44
+ } catch(err: unknown) {
45
+ const el2 = document.getElementById('validate-content');
46
+ if (el2) el2.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
47
+ }
48
+ }