@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,152 @@
1
+ // ═══════════════════════════════════════════════════════════════
2
+ // SHARING VIEW
3
+ // ═══════════════════════════════════════════════════════════════
4
+ import { state } from '../state.js';
5
+ import { api } from '../api.js';
6
+ import { escHtml } from '../utils.js';
7
+ import { showModal, hideModal, createModal, confirmDialog } from '../components/modals.js';
8
+ import { toast } from '../components/toast.js';
9
+ function shareDisplay(share) {
10
+ const isGroup = Boolean(share.group_id || share.groupId);
11
+ const name = isGroup
12
+ ? (share.group_name || share.groupName || 'Unknown group')
13
+ : (share.user_display_name || share.userDisplayName || share.user_email || share.email || share.user_id || share.userId || 'Unknown user');
14
+ const detail = isGroup
15
+ ? 'Group'
16
+ : (share.user_email || share.email || '');
17
+ return { name, detail, avatar: name.slice(0, 2).toUpperCase(), isGroup };
18
+ }
19
+ export async function renderSharingView() {
20
+ const el = document.getElementById('content-sharing');
21
+ if (!el)
22
+ return;
23
+ if (!state.currentProject) {
24
+ el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>';
25
+ return;
26
+ }
27
+ el.innerHTML = `
28
+ <div class="page-header">
29
+ <div>
30
+ <div class="page-title">Project Sharing</div>
31
+ <div class="page-subtitle">Manage access to ${escHtml(state.currentProject.name)}</div>
32
+ </div>
33
+ <div class="page-actions">
34
+ <button class="btn btn-secondary btn-sm" onclick="window.__app.renderSharingView()">↺ Refresh</button>
35
+ <button class="btn btn-primary" onclick="window.__app.openShareModal()">+ Invite</button>
36
+ </div>
37
+ </div>
38
+ <div id="shares-list"><div class="loading-state"><div class="loading-spinner"></div></div></div>`;
39
+ await loadShares();
40
+ }
41
+ async function loadShares() {
42
+ const el = document.getElementById('shares-list');
43
+ if (!el)
44
+ return;
45
+ try {
46
+ const data = await api('GET', `/projects/${state.currentProject.id}/shares`);
47
+ const shares = data.shares || [];
48
+ if (shares.length === 0) {
49
+ el.innerHTML = `
50
+ <div class="card">
51
+ <div class="card-body">
52
+ <div class="empty-state" style="padding:32px">
53
+ <div class="empty-state-icon">⇄</div>
54
+ <div class="empty-state-text">Not shared with anyone yet</div>
55
+ <div class="empty-state-sub">Invite teammates by email to collaborate</div>
56
+ </div>
57
+ </div>
58
+ </div>`;
59
+ return;
60
+ }
61
+ el.innerHTML = `
62
+ <div class="card">
63
+ <div class="card-header"><div class="card-title">Shared with</div></div>
64
+ <div class="card-body">
65
+ ${shares.map((s) => `
66
+ ${(() => {
67
+ const display = shareDisplay(s);
68
+ return `
69
+ <div class="share-row">
70
+ <div class="member-avatar">${escHtml(display.avatar)}</div>
71
+ <div style="flex:1">
72
+ <div style="font-size:13px;font-weight:500">${escHtml(display.name)}</div>
73
+ <div class="group-desc">${escHtml(display.detail)}</div>
74
+ </div>
75
+ <span class="share-perm">${escHtml(s.permission || 'view')}</span>
76
+ <button class="btn btn-danger btn-sm" onclick="window.__app.removeShare('${escHtml(s.id || s.shareId || '')}')">Remove</button>
77
+ </div>`;
78
+ })()}`).join('')}
79
+ </div>
80
+ </div>`;
81
+ }
82
+ catch (err) {
83
+ if (el)
84
+ el.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
85
+ }
86
+ }
87
+ export function openShareModal() {
88
+ createModal('share-modal', 'Invite to Project', `
89
+ <div class="form-group">
90
+ <label class="form-label">Email Address</label>
91
+ <input class="form-input" id="share-email" type="email" placeholder="colleague@example.com">
92
+ </div>
93
+ <div class="form-group">
94
+ <label class="form-label">Or Group ID</label>
95
+ <input class="form-input" id="share-group-id" type="text" placeholder="Leave empty to invite by email">
96
+ </div>
97
+ <div class="form-group">
98
+ <label class="form-label">Permission</label>
99
+ <select class="form-select" id="share-permission">
100
+ <option value="view">View — read-only access</option>
101
+ <option value="edit">Edit — can create and modify items</option>
102
+ </select>
103
+ </div>
104
+ <div class="form-error" id="share-error" style="display:none"></div>`, `<button class="btn btn-ghost" onclick="window.__app.hideModal('share-modal')">Cancel</button>
105
+ <button class="btn btn-primary" onclick="window.__app.submitShare()"><span>Send Invite</span></button>`);
106
+ showModal('share-modal');
107
+ }
108
+ export async function submitShare() {
109
+ const email = document.getElementById('share-email')?.value?.trim() || '';
110
+ const groupId = document.getElementById('share-group-id')?.value?.trim() || '';
111
+ const permission = document.getElementById('share-permission')?.value || 'view';
112
+ const errEl = document.getElementById('share-error');
113
+ if (errEl)
114
+ errEl.style.display = 'none';
115
+ if (!email && !groupId) {
116
+ if (errEl) {
117
+ errEl.textContent = 'Email or Group ID is required';
118
+ errEl.style.display = 'block';
119
+ }
120
+ return;
121
+ }
122
+ const body = { permission };
123
+ if (email)
124
+ body.email = email;
125
+ if (groupId)
126
+ body.groupId = groupId;
127
+ try {
128
+ await api('POST', `/projects/${state.currentProject.id}/shares`, body);
129
+ toast('Project shared successfully', 'success');
130
+ hideModal('share-modal');
131
+ await loadShares();
132
+ }
133
+ catch (err) {
134
+ if (errEl) {
135
+ errEl.textContent = err instanceof Error ? err.message : String(err);
136
+ errEl.style.display = 'block';
137
+ }
138
+ }
139
+ }
140
+ export function removeShare(shareId) {
141
+ confirmDialog('Remove Access?', 'The user will lose access to this project.', async () => {
142
+ try {
143
+ await api('DELETE', `/projects/${state.currentProject.id}/shares/${shareId}`);
144
+ toast('Share removed', 'success');
145
+ await loadShares();
146
+ }
147
+ catch (err) {
148
+ toast(err instanceof Error ? err.message : String(err), 'error');
149
+ }
150
+ });
151
+ }
152
+ //# sourceMappingURL=sharing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharing.js","sourceRoot":"","sources":["sharing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,139 @@
1
+ // ═══════════════════════════════════════════════════════════════
2
+ // SHARING VIEW
3
+ // ═══════════════════════════════════════════════════════════════
4
+ import { state } from '../state.js';
5
+ import { api } from '../api.js';
6
+ import { escHtml } from '../utils.js';
7
+ import { showModal, hideModal, createModal, confirmDialog } from '../components/modals.js';
8
+ import { toast } from '../components/toast.js';
9
+
10
+ function shareDisplay(share: any): { name: string; detail: string; avatar: string; isGroup: boolean } {
11
+ const isGroup = Boolean(share.group_id || share.groupId);
12
+ const name = isGroup
13
+ ? (share.group_name || share.groupName || 'Unknown group')
14
+ : (share.user_display_name || share.userDisplayName || share.user_email || share.email || share.user_id || share.userId || 'Unknown user');
15
+ const detail = isGroup
16
+ ? 'Group'
17
+ : (share.user_email || share.email || '');
18
+ return { name, detail, avatar: name.slice(0, 2).toUpperCase(), isGroup };
19
+ }
20
+
21
+ export async function renderSharingView(): Promise<void> {
22
+ const el = document.getElementById('content-sharing');
23
+ if (!el) return;
24
+ if (!state.currentProject) { el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>'; return; }
25
+ el.innerHTML = `
26
+ <div class="page-header">
27
+ <div>
28
+ <div class="page-title">Project Sharing</div>
29
+ <div class="page-subtitle">Manage access to ${escHtml(state.currentProject.name)}</div>
30
+ </div>
31
+ <div class="page-actions">
32
+ <button class="btn btn-secondary btn-sm" onclick="window.__app.renderSharingView()">↺ Refresh</button>
33
+ <button class="btn btn-primary" onclick="window.__app.openShareModal()">+ Invite</button>
34
+ </div>
35
+ </div>
36
+ <div id="shares-list"><div class="loading-state"><div class="loading-spinner"></div></div></div>`;
37
+ await loadShares();
38
+ }
39
+
40
+ async function loadShares(): Promise<void> {
41
+ const el = document.getElementById('shares-list');
42
+ if (!el) return;
43
+ try {
44
+ const data = await api('GET',`/projects/${state.currentProject!.id}/shares`);
45
+ const shares = (data as any).shares || [];
46
+ if (shares.length === 0) {
47
+ el.innerHTML = `
48
+ <div class="card">
49
+ <div class="card-body">
50
+ <div class="empty-state" style="padding:32px">
51
+ <div class="empty-state-icon">⇄</div>
52
+ <div class="empty-state-text">Not shared with anyone yet</div>
53
+ <div class="empty-state-sub">Invite teammates by email to collaborate</div>
54
+ </div>
55
+ </div>
56
+ </div>`;
57
+ return;
58
+ }
59
+ el.innerHTML = `
60
+ <div class="card">
61
+ <div class="card-header"><div class="card-title">Shared with</div></div>
62
+ <div class="card-body">
63
+ ${shares.map((s: any)=>`
64
+ ${(() => {
65
+ const display = shareDisplay(s);
66
+ return `
67
+ <div class="share-row">
68
+ <div class="member-avatar">${escHtml(display.avatar)}</div>
69
+ <div style="flex:1">
70
+ <div style="font-size:13px;font-weight:500">${escHtml(display.name)}</div>
71
+ <div class="group-desc">${escHtml(display.detail)}</div>
72
+ </div>
73
+ <span class="share-perm">${escHtml(s.permission||'view')}</span>
74
+ <button class="btn btn-danger btn-sm" onclick="window.__app.removeShare('${escHtml(s.id||s.shareId||'')}')">Remove</button>
75
+ </div>`;
76
+ })()}`).join('')}
77
+ </div>
78
+ </div>`;
79
+ } catch(err: unknown) {
80
+ if (el) el.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
81
+ }
82
+ }
83
+
84
+ export function openShareModal(): void {
85
+ createModal('share-modal','Invite to Project',`
86
+ <div class="form-group">
87
+ <label class="form-label">Email Address</label>
88
+ <input class="form-input" id="share-email" type="email" placeholder="colleague@example.com">
89
+ </div>
90
+ <div class="form-group">
91
+ <label class="form-label">Or Group ID</label>
92
+ <input class="form-input" id="share-group-id" type="text" placeholder="Leave empty to invite by email">
93
+ </div>
94
+ <div class="form-group">
95
+ <label class="form-label">Permission</label>
96
+ <select class="form-select" id="share-permission">
97
+ <option value="view">View — read-only access</option>
98
+ <option value="edit">Edit — can create and modify items</option>
99
+ </select>
100
+ </div>
101
+ <div class="form-error" id="share-error" style="display:none"></div>`,
102
+ `<button class="btn btn-ghost" onclick="window.__app.hideModal('share-modal')">Cancel</button>
103
+ <button class="btn btn-primary" onclick="window.__app.submitShare()"><span>Send Invite</span></button>`
104
+ );
105
+ showModal('share-modal');
106
+ }
107
+
108
+ export async function submitShare(): Promise<void> {
109
+ const email = (document.getElementById('share-email') as HTMLInputElement | null)?.value?.trim() || '';
110
+ const groupId = (document.getElementById('share-group-id') as HTMLInputElement | null)?.value?.trim() || '';
111
+ const permission = (document.getElementById('share-permission') as HTMLSelectElement | null)?.value || 'view';
112
+ const errEl = document.getElementById('share-error') as HTMLElement | null;
113
+ if (errEl) errEl.style.display = 'none';
114
+ if (!email && !groupId) {
115
+ if (errEl) { errEl.textContent = 'Email or Group ID is required'; errEl.style.display = 'block'; }
116
+ return;
117
+ }
118
+ const body: Record<string, string> = { permission };
119
+ if (email) body.email = email;
120
+ if (groupId) body.groupId = groupId;
121
+ try {
122
+ await api('POST',`/projects/${state.currentProject!.id}/shares`,body);
123
+ toast('Project shared successfully','success');
124
+ hideModal('share-modal');
125
+ await loadShares();
126
+ } catch(err: unknown) {
127
+ if (errEl) { errEl.textContent = err instanceof Error ? err.message : String(err); errEl.style.display = 'block'; }
128
+ }
129
+ }
130
+
131
+ export function removeShare(shareId: string): void {
132
+ confirmDialog('Remove Access?', 'The user will lose access to this project.', async () => {
133
+ try {
134
+ await api('DELETE',`/projects/${state.currentProject!.id}/shares/${shareId}`);
135
+ toast('Share removed','success');
136
+ await loadShares();
137
+ } catch(err: unknown) { toast(err instanceof Error ? err.message : String(err),'error'); }
138
+ });
139
+ }
@@ -0,0 +1,92 @@
1
+ // ═══════════════════════════════════════════════════════════════
2
+ // STATS VIEW
3
+ // ═══════════════════════════════════════════════════════════════
4
+ import { state } from '../state.js';
5
+ import { api } from '../api.js';
6
+ import { escHtml, statusBadge, typeIcon } from '../utils.js';
7
+ export async function renderStatsView() {
8
+ const el = document.getElementById('content-stats');
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">Stats</div><div class="page-subtitle">${escHtml(state.currentProject.name)} statistics</div></div>
18
+ <div class="page-actions"><button class="btn btn-secondary btn-sm" onclick="window.__app.renderStatsView()">↺ Refresh</button></div>
19
+ </div>
20
+ <div id="stats-content"><div class="loading-state"><div class="loading-spinner"></div></div></div>`;
21
+ try {
22
+ const [statsData, aggData, healthData] = await Promise.all([
23
+ api('GET', `/projects/${state.currentProject.id}/pm/stats`),
24
+ api('GET', `/projects/${state.currentProject.id}/pm/aggregate`).catch(() => ({})),
25
+ api('GET', `/projects/${state.currentProject.id}/pm/health`).catch(() => ({})),
26
+ ]);
27
+ const s = statsData.stats || statsData;
28
+ const health = healthData.health || healthData;
29
+ const byStatus = s.byStatus || {};
30
+ const byType = s.byType || {};
31
+ const total = s.total || Object.values(byStatus).reduce((a, b) => a + b, 0) || 0;
32
+ const openCount = (s.byStatus?.open || 0) + (s.byStatus?.in_progress || 0);
33
+ const closedCount = s.byStatus?.closed || 0;
34
+ const blockedCount = s.byStatus?.blocked || 0;
35
+ const maxStatus = Math.max(...Object.values(byStatus), 1);
36
+ const maxType = Math.max(...Object.values(byType), 1);
37
+ const contentEl = document.getElementById('stats-content');
38
+ if (!contentEl)
39
+ return;
40
+ contentEl.innerHTML = `
41
+ <div class="stats-grid">
42
+ <div class="stat-card"><div class="stat-value">${total}</div><div class="stat-label">Total Items</div></div>
43
+ <div class="stat-card"><div class="stat-value" style="color:var(--status-open)">${openCount}</div><div class="stat-label">Open / In Progress</div></div>
44
+ <div class="stat-card"><div class="stat-value" style="color:var(--status-closed)">${closedCount}</div><div class="stat-label">Closed</div></div>
45
+ <div class="stat-card"><div class="stat-value" style="color:var(--status-blocked)">${blockedCount}</div><div class="stat-label">Blocked</div></div>
46
+ ${total > 0 ? `<div class="stat-card"><div class="stat-value">${Math.round((closedCount / total) * 100)}%</div><div class="stat-label">Completion</div></div>` : ''}
47
+ </div>
48
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
49
+ <div class="card">
50
+ <div class="card-header"><div class="card-title">By Status</div></div>
51
+ <div class="card-body">
52
+ ${Object.entries(byStatus).sort((a, b) => b[1] - a[1]).map(([k, v]) => `
53
+ <div class="breakdown-row">
54
+ <span class="breakdown-label">${statusBadge(k)}</span>
55
+ <div class="breakdown-bar-wrap"><div class="breakdown-bar" style="width:${Math.round((v / maxStatus) * 100)}%"></div></div>
56
+ <span class="breakdown-count">${v}</span>
57
+ </div>`).join('') || '<div style="color:var(--text-muted);font-size:13px">No data</div>'}
58
+ </div>
59
+ </div>
60
+ <div class="card">
61
+ <div class="card-header"><div class="card-title">By Type</div></div>
62
+ <div class="card-body">
63
+ ${Object.entries(byType).sort((a, b) => b[1] - a[1]).map(([k, v]) => `
64
+ <div class="breakdown-row">
65
+ <span class="breakdown-label">${typeIcon(k)} ${k}</span>
66
+ <div class="breakdown-bar-wrap"><div class="breakdown-bar" style="width:${Math.round((v / maxType) * 100)}%"></div></div>
67
+ <span class="breakdown-count">${v}</span>
68
+ </div>`).join('') || '<div style="color:var(--text-muted);font-size:13px">No data</div>'}
69
+ </div>
70
+ </div>
71
+ </div>
72
+ ${health && (health.issues || health.score !== undefined) ? `
73
+ <div class="card" style="margin-top:16px">
74
+ <div class="card-header">
75
+ <div class="card-title">Project Health</div>
76
+ ${health.score !== undefined ? `<div class="health-indicator">
77
+ <div class="health-dot ${health.score >= 80 ? 'health-good' : health.score >= 50 ? 'health-warn' : 'health-bad'}"></div>
78
+ <span style="font-size:13px;color:var(--text-secondary)">${health.score}/100</span>
79
+ </div>` : ''}
80
+ </div>
81
+ <div class="card-body">
82
+ ${(health.issues || []).map((i) => `<div style="padding:6px 0;border-bottom:1px solid var(--border);font-size:13px;color:var(--text-secondary)">⚠ ${escHtml(i.message || i)}</div>`).join('') || '<div style="color:var(--status-closed);font-size:13px">✓ No issues found</div>'}
83
+ </div>
84
+ </div>` : ''}`;
85
+ }
86
+ catch (err) {
87
+ const contentEl = document.getElementById('stats-content');
88
+ if (contentEl)
89
+ contentEl.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
90
+ }
91
+ }
92
+ //# sourceMappingURL=stats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stats.js","sourceRoot":"","sources":["stats.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,88 @@
1
+ // ═══════════════════════════════════════════════════════════════
2
+ // STATS VIEW
3
+ // ═══════════════════════════════════════════════════════════════
4
+ import { state } from '../state.js';
5
+ import { api } from '../api.js';
6
+ import { escHtml, statusBadge, typeIcon } from '../utils.js';
7
+
8
+ export async function renderStatsView(): Promise<void> {
9
+ const el = document.getElementById('content-stats');
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">Stats</div><div class="page-subtitle">${escHtml(state.currentProject.name)} statistics</div></div>
15
+ <div class="page-actions"><button class="btn btn-secondary btn-sm" onclick="window.__app.renderStatsView()">↺ Refresh</button></div>
16
+ </div>
17
+ <div id="stats-content"><div class="loading-state"><div class="loading-spinner"></div></div></div>`;
18
+
19
+ try {
20
+ const [statsData, aggData, healthData] = await Promise.all([
21
+ api('GET',`/projects/${state.currentProject.id}/pm/stats`),
22
+ api('GET',`/projects/${state.currentProject.id}/pm/aggregate`).catch(()=>({})),
23
+ api('GET',`/projects/${state.currentProject.id}/pm/health`).catch(()=>({})),
24
+ ]);
25
+
26
+ const s = (statsData as any).stats || statsData;
27
+ const health = (healthData as any).health || healthData;
28
+ const byStatus = s.byStatus || {};
29
+ const byType = s.byType || {};
30
+ const total = s.total || (Object.values(byStatus) as number[]).reduce((a,b)=>a+b,0) || 0;
31
+ const openCount = (s.byStatus?.open||0) + (s.byStatus?.in_progress||0);
32
+ const closedCount = s.byStatus?.closed||0;
33
+ const blockedCount = s.byStatus?.blocked||0;
34
+ const maxStatus = Math.max(...(Object.values(byStatus) as number[]),1);
35
+ const maxType = Math.max(...(Object.values(byType) as number[]),1);
36
+
37
+ const contentEl = document.getElementById('stats-content');
38
+ if (!contentEl) return;
39
+ contentEl.innerHTML = `
40
+ <div class="stats-grid">
41
+ <div class="stat-card"><div class="stat-value">${total}</div><div class="stat-label">Total Items</div></div>
42
+ <div class="stat-card"><div class="stat-value" style="color:var(--status-open)">${openCount}</div><div class="stat-label">Open / In Progress</div></div>
43
+ <div class="stat-card"><div class="stat-value" style="color:var(--status-closed)">${closedCount}</div><div class="stat-label">Closed</div></div>
44
+ <div class="stat-card"><div class="stat-value" style="color:var(--status-blocked)">${blockedCount}</div><div class="stat-label">Blocked</div></div>
45
+ ${total > 0 ? `<div class="stat-card"><div class="stat-value">${Math.round((closedCount/total)*100)}%</div><div class="stat-label">Completion</div></div>` : ''}
46
+ </div>
47
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
48
+ <div class="card">
49
+ <div class="card-header"><div class="card-title">By Status</div></div>
50
+ <div class="card-body">
51
+ ${Object.entries(byStatus).sort((a,b)=>(b[1] as number)-(a[1] as number)).map(([k,v])=>`
52
+ <div class="breakdown-row">
53
+ <span class="breakdown-label">${statusBadge(k)}</span>
54
+ <div class="breakdown-bar-wrap"><div class="breakdown-bar" style="width:${Math.round(((v as number)/maxStatus)*100)}%"></div></div>
55
+ <span class="breakdown-count">${v}</span>
56
+ </div>`).join('') || '<div style="color:var(--text-muted);font-size:13px">No data</div>'}
57
+ </div>
58
+ </div>
59
+ <div class="card">
60
+ <div class="card-header"><div class="card-title">By Type</div></div>
61
+ <div class="card-body">
62
+ ${Object.entries(byType).sort((a,b)=>(b[1] as number)-(a[1] as number)).map(([k,v])=>`
63
+ <div class="breakdown-row">
64
+ <span class="breakdown-label">${typeIcon(k)} ${k}</span>
65
+ <div class="breakdown-bar-wrap"><div class="breakdown-bar" style="width:${Math.round(((v as number)/maxType)*100)}%"></div></div>
66
+ <span class="breakdown-count">${v}</span>
67
+ </div>`).join('') || '<div style="color:var(--text-muted);font-size:13px">No data</div>'}
68
+ </div>
69
+ </div>
70
+ </div>
71
+ ${health && ((health as any).issues||(health as any).score!==undefined) ? `
72
+ <div class="card" style="margin-top:16px">
73
+ <div class="card-header">
74
+ <div class="card-title">Project Health</div>
75
+ ${(health as any).score !== undefined ? `<div class="health-indicator">
76
+ <div class="health-dot ${(health as any).score>=80?'health-good':(health as any).score>=50?'health-warn':'health-bad'}"></div>
77
+ <span style="font-size:13px;color:var(--text-secondary)">${(health as any).score}/100</span>
78
+ </div>` : ''}
79
+ </div>
80
+ <div class="card-body">
81
+ ${((health as any).issues||[]).map((i: any)=>`<div style="padding:6px 0;border-bottom:1px solid var(--border);font-size:13px;color:var(--text-secondary)">⚠ ${escHtml(i.message||i)}</div>`).join('') || '<div style="color:var(--status-closed);font-size:13px">✓ No issues found</div>'}
82
+ </div>
83
+ </div>` : ''}`;
84
+ } catch(err: unknown) {
85
+ const contentEl = document.getElementById('stats-content');
86
+ if (contentEl) contentEl.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
87
+ }
88
+ }
@@ -0,0 +1,117 @@
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
+ export async function renderTemplatesView() {
10
+ const el = document.getElementById('content-templates');
11
+ if (!el)
12
+ 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
+ async function fetchAndRenderTemplates() {
31
+ const pid = state.currentProject?.id;
32
+ if (!pid)
33
+ return;
34
+ try {
35
+ const data = await api('GET', `/projects/${pid}/pm/templates`);
36
+ const templates = data.templates || [];
37
+ const el = document.getElementById('templates-content');
38
+ if (!el)
39
+ return;
40
+ if (templates.length === 0) {
41
+ el.innerHTML = `
42
+ <div class="card">
43
+ <div class="card-body">
44
+ <div style="color:var(--text-muted);font-size:13px;margin-bottom:16px">
45
+ 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.
46
+ </div>
47
+ <div style="font-size:12px;color:var(--text-secondary)">
48
+ Templates allow you to pre-fill item fields (type, priority, tags, description, etc.) when creating new items.
49
+ </div>
50
+ </div>
51
+ </div>`;
52
+ return;
53
+ }
54
+ el.innerHTML = `
55
+ <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px">
56
+ ${templates.map((t) => renderTemplateCard(t)).join('')}
57
+ </div>`;
58
+ }
59
+ catch (err) {
60
+ const el = document.getElementById('templates-content');
61
+ if (el)
62
+ 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>`;
63
+ }
64
+ }
65
+ function renderTemplateCard(t) {
66
+ const name = t.name || t.id || 'Unnamed';
67
+ const type = t.type || t.defaults?.type || '';
68
+ const priority = t.priority || t.defaults?.priority || '';
69
+ const tags = (t.tags || t.defaults?.tags || []).join(', ');
70
+ const desc = t.description || t.defaults?.description || '';
71
+ return `
72
+ <div class="card" style="cursor:default">
73
+ <div class="card-header" style="display:flex;align-items:center;justify-content:space-between">
74
+ <div class="card-title" style="display:flex;align-items:center;gap:6px">
75
+ ${type ? typeIcon(type) : ''}
76
+ <span>${escHtml(name)}</span>
77
+ </div>
78
+ ${priority ? `<span style="font-size:11px;color:var(--text-muted);background:var(--bg-input);padding:2px 8px;border-radius:4px">P${priority}</span>` : ''}
79
+ </div>
80
+ <div class="card-body" style="padding-top:0">
81
+ ${type ? `<div style="font-size:12px;color:var(--text-secondary);margin-bottom:6px">Type: <strong>${escHtml(type)}</strong></div>` : ''}
82
+ ${tags ? `<div style="font-size:12px;color:var(--text-secondary);margin-bottom:6px">Tags: ${escHtml(tags)}</div>` : ''}
83
+ ${desc ? `<div style="font-size:12px;color:var(--text-muted);margin-bottom:10px;line-height:1.4">${escHtml(desc)}</div>` : ''}
84
+ <button class="btn btn-primary btn-sm" style="width:100%" onclick="window.__app.createFromTemplate(${JSON.stringify(escHtml(name))}, ${JSON.stringify(t)})">
85
+ + Create from Template
86
+ </button>
87
+ </div>
88
+ </div>`;
89
+ }
90
+ export function createFromTemplate(name, template) {
91
+ // Navigate to create view and pre-fill from template
92
+ showView('create');
93
+ // Give the create view time to render, then fill fields
94
+ setTimeout(() => {
95
+ const defaults = template.defaults || template;
96
+ const setVal = (id, val) => {
97
+ if (!val)
98
+ return;
99
+ const el = document.getElementById(id);
100
+ if (el)
101
+ el.value = val;
102
+ };
103
+ setVal('ci-type', defaults.type || template.type);
104
+ setVal('ci-priority', String(defaults.priority || template.priority || ''));
105
+ setVal('ci-tags', (defaults.tags || template.tags || []).join(', '));
106
+ setVal('ci-desc', defaults.description || template.description || '');
107
+ setVal('ci-sprint', defaults.sprint || template.sprint || '');
108
+ setVal('ci-release', defaults.release || template.release || '');
109
+ setVal('ci-assignee', defaults.assignee || template.assignee || '');
110
+ if (defaults.acceptance_criteria || defaults.acceptanceCriteria) {
111
+ setVal('ci-acceptance-criteria', defaults.acceptance_criteria || defaults.acceptanceCriteria);
112
+ }
113
+ toast(`Template "${name}" applied`, 'success');
114
+ document.getElementById('ci-title')?.focus();
115
+ }, 100);
116
+ }
117
+ //# sourceMappingURL=templates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.js","sourceRoot":"","sources":["templates.ts"],"names":[],"mappings":"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"}