@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.
- package/CHANGELOG.md +7 -0
- package/README.md +107 -0
- package/dist/auth.js +20 -0
- package/dist/auth.js.map +1 -0
- package/dist/crypto.js +42 -0
- package/dist/crypto.js.map +1 -0
- package/dist/db.js +111 -0
- package/dist/db.js.map +1 -0
- package/dist/index.js +88 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.js +16 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/routes/admin.js +207 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/auth.js +163 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/github.js +354 -0
- package/dist/routes/github.js.map +1 -0
- package/dist/routes/groups.js +180 -0
- package/dist/routes/groups.js.map +1 -0
- package/dist/routes/pm.js +2446 -0
- package/dist/routes/pm.js.map +1 -0
- package/dist/routes/projects.js +151 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/sharing.js +155 -0
- package/dist/routes/sharing.js.map +1 -0
- package/dist/server.js +64 -0
- package/dist/server.js.map +1 -0
- package/dist/services/pm-runner.js +190 -0
- package/dist/services/pm-runner.js.map +1 -0
- package/dist/services/sse.js +111 -0
- package/dist/services/sse.js.map +1 -0
- package/manifest.json +15 -0
- package/package.json +111 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/index.html +265 -0
- package/public/manifest.json +66 -0
- package/public/src/api.js +28 -0
- package/public/src/api.js.map +1 -0
- package/public/src/api.ts +29 -0
- package/public/src/app.js +926 -0
- package/public/src/app.js.map +1 -0
- package/public/src/app.ts +929 -0
- package/public/src/components/modals.js +62 -0
- package/public/src/components/modals.js.map +1 -0
- package/public/src/components/modals.ts +73 -0
- package/public/src/components/toast.js +10 -0
- package/public/src/components/toast.js.map +1 -0
- package/public/src/components/toast.ts +13 -0
- package/public/src/constants.js +30 -0
- package/public/src/constants.js.map +1 -0
- package/public/src/constants.ts +41 -0
- package/public/src/state.js +15 -0
- package/public/src/state.js.map +1 -0
- package/public/src/state.ts +19 -0
- package/public/src/types.js +5 -0
- package/public/src/types.js.map +1 -0
- package/public/src/types.ts +253 -0
- package/public/src/utils.js +57 -0
- package/public/src/utils.js.map +1 -0
- package/public/src/utils.ts +56 -0
- package/public/src/views/activity.js +47 -0
- package/public/src/views/activity.js.map +1 -0
- package/public/src/views/activity.ts +41 -0
- package/public/src/views/admin.js +435 -0
- package/public/src/views/admin.js.map +1 -0
- package/public/src/views/admin.ts +504 -0
- package/public/src/views/auth.js +81 -0
- package/public/src/views/auth.js.map +1 -0
- package/public/src/views/auth.ts +74 -0
- package/public/src/views/calendar.js +133 -0
- package/public/src/views/calendar.js.map +1 -0
- package/public/src/views/calendar.ts +129 -0
- package/public/src/views/comments-audit.js +109 -0
- package/public/src/views/comments-audit.js.map +1 -0
- package/public/src/views/comments-audit.ts +108 -0
- package/public/src/views/config.js +322 -0
- package/public/src/views/config.js.map +1 -0
- package/public/src/views/config.ts +344 -0
- package/public/src/views/context.js +98 -0
- package/public/src/views/context.js.map +1 -0
- package/public/src/views/context.ts +100 -0
- package/public/src/views/create.js +293 -0
- package/public/src/views/create.js.map +1 -0
- package/public/src/views/create.ts +246 -0
- package/public/src/views/dedupe.js +51 -0
- package/public/src/views/dedupe.js.map +1 -0
- package/public/src/views/dedupe.ts +43 -0
- package/public/src/views/export.js +300 -0
- package/public/src/views/export.js.map +1 -0
- package/public/src/views/export.ts +274 -0
- package/public/src/views/github.js +360 -0
- package/public/src/views/github.js.map +1 -0
- package/public/src/views/github.ts +308 -0
- package/public/src/views/graph-canvas.js +1986 -0
- package/public/src/views/graph-canvas.js.map +1 -0
- package/public/src/views/graph-canvas.ts +2218 -0
- package/public/src/views/graph.js +1824 -0
- package/public/src/views/graph.js.map +1 -0
- package/public/src/views/graph.ts +1891 -0
- package/public/src/views/groups.js +186 -0
- package/public/src/views/groups.js.map +1 -0
- package/public/src/views/groups.ts +172 -0
- package/public/src/views/guide.js +151 -0
- package/public/src/views/guide.js.map +1 -0
- package/public/src/views/guide.ts +162 -0
- package/public/src/views/health.js +105 -0
- package/public/src/views/health.js.map +1 -0
- package/public/src/views/health.ts +102 -0
- package/public/src/views/items.js +1306 -0
- package/public/src/views/items.js.map +1 -0
- package/public/src/views/items.ts +1196 -0
- package/public/src/views/normalize.js +67 -0
- package/public/src/views/normalize.js.map +1 -0
- package/public/src/views/normalize.ts +58 -0
- package/public/src/views/plan.js +454 -0
- package/public/src/views/plan.js.map +1 -0
- package/public/src/views/plan.ts +496 -0
- package/public/src/views/projects.js +204 -0
- package/public/src/views/projects.js.map +1 -0
- package/public/src/views/projects.ts +196 -0
- package/public/src/views/router.js +227 -0
- package/public/src/views/router.js.map +1 -0
- package/public/src/views/router.ts +188 -0
- package/public/src/views/search.js +103 -0
- package/public/src/views/search.js.map +1 -0
- package/public/src/views/search.ts +94 -0
- package/public/src/views/settings.js +272 -0
- package/public/src/views/settings.js.map +1 -0
- package/public/src/views/settings.ts +190 -0
- package/public/src/views/shared.js +49 -0
- package/public/src/views/shared.js.map +1 -0
- package/public/src/views/shared.ts +49 -0
- package/public/src/views/sharing.js +152 -0
- package/public/src/views/sharing.js.map +1 -0
- package/public/src/views/sharing.ts +139 -0
- package/public/src/views/stats.js +92 -0
- package/public/src/views/stats.js.map +1 -0
- package/public/src/views/stats.ts +88 -0
- package/public/src/views/templates.js +117 -0
- package/public/src/views/templates.js.map +1 -0
- package/public/src/views/templates.ts +113 -0
- package/public/src/views/validate.js +54 -0
- package/public/src/views/validate.js.map +1 -0
- package/public/src/views/validate.ts +48 -0
- package/public/styles.css +2231 -0
- package/public/sw.js +318 -0
- package/public/tsconfig.json +20 -0
- package/sql/schema.sql +105 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// UTILITIES
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
|
+
import { PRIORITY_LABELS, TYPE_ICONS } from './constants.js';
|
|
5
|
+
|
|
6
|
+
export function escHtml(s: string | undefined | null): string {
|
|
7
|
+
if (!s) return '';
|
|
8
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function statusBadge(s: string): string {
|
|
12
|
+
const labels: Record<string, string> = { in_progress: 'In Progress' };
|
|
13
|
+
return `<span class="status-badge status-${s}">${labels[s] || s}</span>`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function priorityDot(p: number): string {
|
|
17
|
+
return `<span class="priority-dot priority-${p}" title="P${p}: ${PRIORITY_LABELS[p]||''}"></span>`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function typeIcon(t: string): string {
|
|
21
|
+
return `<span class="item-type-icon" title="${t}">${TYPE_ICONS[t]||'·'}</span>`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function relTime(ts: string | undefined | null): string {
|
|
25
|
+
if (!ts) return '';
|
|
26
|
+
const d = new Date(ts);
|
|
27
|
+
const diff = Date.now() - d.getTime();
|
|
28
|
+
const s = Math.floor(diff/1000);
|
|
29
|
+
if (s < 60) return 'just now';
|
|
30
|
+
if (s < 3600) return `${Math.floor(s/60)}m ago`;
|
|
31
|
+
if (s < 86400) return `${Math.floor(s/3600)}h ago`;
|
|
32
|
+
if (s < 604800) return `${Math.floor(s/86400)}d ago`;
|
|
33
|
+
return d.toLocaleDateString();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function fmtDate(ts: string | undefined | null): string {
|
|
37
|
+
if (!ts) return '';
|
|
38
|
+
return new Date(ts).toLocaleDateString('en-US', { year:'numeric', month:'short', day:'numeric' });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function setLoading(btn: HTMLButtonElement, yes: boolean, text?: string): void {
|
|
42
|
+
btn.disabled = yes;
|
|
43
|
+
if (text) {
|
|
44
|
+
const span = btn.querySelector('span');
|
|
45
|
+
if (span) span.textContent = text;
|
|
46
|
+
else btn.textContent = text;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function skeletonRows(n = 5): string {
|
|
51
|
+
return Array.from({length: n}, () => '<div class="skeleton skeleton-row"></div>').join('');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function skeletonCards(n = 3): string {
|
|
55
|
+
return Array.from({length: n}, () => '<div class="skeleton skeleton-card"></div>').join('');
|
|
56
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// ACTIVITY VIEW
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
|
+
import { state } from '../state.js';
|
|
5
|
+
import { api } from '../api.js';
|
|
6
|
+
import { escHtml, relTime } from '../utils.js';
|
|
7
|
+
import { TYPE_ICONS } from '../constants.js';
|
|
8
|
+
export async function renderActivityView() {
|
|
9
|
+
const el = document.getElementById('content-activity');
|
|
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">Activity</div><div class="page-subtitle">Recent changes in ${escHtml(state.currentProject.name)}</div></div>
|
|
19
|
+
<div class="page-actions"><button class="btn btn-secondary btn-sm" onclick="window.__app.renderActivityView()">↺ Refresh</button></div>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="card"><div class="card-body" id="activity-list"><div class="loading-state"><div class="loading-spinner"></div></div></div></div>`;
|
|
22
|
+
try {
|
|
23
|
+
const data = await api('GET', `/projects/${state.currentProject.id}/pm/activity?limit=50`);
|
|
24
|
+
const items = data.activity || data.items || [];
|
|
25
|
+
const listEl = document.getElementById('activity-list');
|
|
26
|
+
if (!listEl)
|
|
27
|
+
return;
|
|
28
|
+
if (items.length === 0) {
|
|
29
|
+
listEl.innerHTML = `<div class="empty-state"><div class="empty-state-icon">◎</div><div class="empty-state-text">No activity yet</div></div>`;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
listEl.innerHTML = items.map((a) => `
|
|
33
|
+
<div class="activity-item">
|
|
34
|
+
<div class="activity-icon">${TYPE_ICONS[a.type] || '◎'}</div>
|
|
35
|
+
<div class="activity-body">
|
|
36
|
+
<div class="activity-desc">${escHtml(a.message || a.title || a.action || JSON.stringify(a))}</div>
|
|
37
|
+
<div class="activity-time">${relTime(a.timestamp || a.created_at)} ${a.id ? `· <span class="mono" style="font-size:11px">${escHtml(a.id)}</span>` : ''}</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>`).join('');
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
const listEl = document.getElementById('activity-list');
|
|
43
|
+
if (listEl)
|
|
44
|
+
listEl.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=activity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activity.js","sourceRoot":"","sources":["activity.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,gBAAgB;AAChB,kEAAkE;AAClE,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAChC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACtC,MAAM,EAAE,GAAG,QAAQ,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC;IACvD,IAAI,CAAC,EAAE;QAAE,OAAO;IAChB,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC;QAAC,EAAE,CAAC,SAAS,GAAG,wFAAwF,CAAC;QAAC,OAAO;IAAC,CAAC;IAC/I,EAAE,CAAC,SAAS,GAAG;;gGAE+E,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC;;;iJAGe,CAAC;IAEhJ,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,KAAK,EAAC,aAAa,KAAK,CAAC,cAAc,CAAC,EAAE,uBAAuB,CAAC,CAAC;QAC1F,MAAM,KAAK,GAAI,IAAY,CAAC,QAAQ,IAAK,IAAY,CAAC,KAAK,IAAI,EAAE,CAAC;QAClE,MAAM,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC;QACxD,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,CAAC,SAAS,GAAG,yHAAyH,CAAC;YAC7I,OAAO;QACT,CAAC;QACD,MAAM,CAAC,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAM,EAAC,EAAE,CAAA;;qCAEN,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,IAAE,GAAG;;uCAErB,OAAO,CAAC,CAAC,CAAC,OAAO,IAAE,CAAC,CAAC,KAAK,IAAE,CAAC,CAAC,MAAM,IAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;uCACxD,OAAO,CAAC,CAAC,CAAC,SAAS,IAAE,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,CAAA,CAAC,CAAA,+CAA+C,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,SAAS,CAAA,CAAC,CAAA,EAAE;;aAE7I,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtB,CAAC;IAAC,OAAM,GAAY,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC;QACxD,IAAI,MAAM;YAAE,MAAM,CAAC,SAAS,GAAG,iEAAiE,OAAO,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,cAAc,CAAC;IAC1K,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// ACTIVITY VIEW
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
|
+
import { state } from '../state.js';
|
|
5
|
+
import { api } from '../api.js';
|
|
6
|
+
import { escHtml, relTime } from '../utils.js';
|
|
7
|
+
import { TYPE_ICONS } from '../constants.js';
|
|
8
|
+
|
|
9
|
+
export async function renderActivityView(): Promise<void> {
|
|
10
|
+
const el = document.getElementById('content-activity');
|
|
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">Activity</div><div class="page-subtitle">Recent changes in ${escHtml(state.currentProject.name)}</div></div>
|
|
16
|
+
<div class="page-actions"><button class="btn btn-secondary btn-sm" onclick="window.__app.renderActivityView()">↺ Refresh</button></div>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="card"><div class="card-body" id="activity-list"><div class="loading-state"><div class="loading-spinner"></div></div></div></div>`;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const data = await api('GET',`/projects/${state.currentProject.id}/pm/activity?limit=50`);
|
|
22
|
+
const items = (data as any).activity || (data as any).items || [];
|
|
23
|
+
const listEl = document.getElementById('activity-list');
|
|
24
|
+
if (!listEl) return;
|
|
25
|
+
if (items.length === 0) {
|
|
26
|
+
listEl.innerHTML = `<div class="empty-state"><div class="empty-state-icon">◎</div><div class="empty-state-text">No activity yet</div></div>`;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
listEl.innerHTML = items.map((a: any)=>`
|
|
30
|
+
<div class="activity-item">
|
|
31
|
+
<div class="activity-icon">${TYPE_ICONS[a.type]||'◎'}</div>
|
|
32
|
+
<div class="activity-body">
|
|
33
|
+
<div class="activity-desc">${escHtml(a.message||a.title||a.action||JSON.stringify(a))}</div>
|
|
34
|
+
<div class="activity-time">${relTime(a.timestamp||a.created_at)} ${a.id?`· <span class="mono" style="font-size:11px">${escHtml(a.id)}</span>`:''}</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>`).join('');
|
|
37
|
+
} catch(err: unknown) {
|
|
38
|
+
const listEl = document.getElementById('activity-list');
|
|
39
|
+
if (listEl) listEl.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { api } from '../api.js';
|
|
2
|
+
import { state } from '../state.js';
|
|
3
|
+
import { escHtml } from '../utils.js';
|
|
4
|
+
import { toast } from '../components/toast.js';
|
|
5
|
+
import { confirmDialog, createModal, showModal, hideModal } from '../components/modals.js';
|
|
6
|
+
function getAuditActor(entry) {
|
|
7
|
+
return entry.actor_name || entry.actor_email || entry.userId || entry.userEmail || '—';
|
|
8
|
+
}
|
|
9
|
+
function normalizeAuditEntries(entries) {
|
|
10
|
+
return entries.map((entry) => ({
|
|
11
|
+
id: entry.id,
|
|
12
|
+
action: entry.action,
|
|
13
|
+
actor_email: entry.actor_email,
|
|
14
|
+
actor_name: entry.actor_name,
|
|
15
|
+
description: entry.description,
|
|
16
|
+
created_at: entry.created_at,
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
function getAuditDescription(entry) {
|
|
20
|
+
if (entry.description)
|
|
21
|
+
return entry.description;
|
|
22
|
+
if (entry.userId && entry.action)
|
|
23
|
+
return `${entry.action} by ${entry.userId}`;
|
|
24
|
+
if (entry.action)
|
|
25
|
+
return entry.action;
|
|
26
|
+
return '—';
|
|
27
|
+
}
|
|
28
|
+
let adminData = null;
|
|
29
|
+
let adminTab = 'users';
|
|
30
|
+
let userFilter = '';
|
|
31
|
+
let projectFilter = '';
|
|
32
|
+
let auditEntries = [];
|
|
33
|
+
let auditFilter = '';
|
|
34
|
+
let currentPage = 1;
|
|
35
|
+
const PAGE_SIZE = 20;
|
|
36
|
+
let adminAuditTotal = 0;
|
|
37
|
+
async function loadAuditData(page = 1) {
|
|
38
|
+
if (!state.user?.is_admin)
|
|
39
|
+
return;
|
|
40
|
+
const safePage = Math.max(1, page);
|
|
41
|
+
const offset = (safePage - 1) * PAGE_SIZE;
|
|
42
|
+
const data = await api('GET', `/admin/audit?limit=${PAGE_SIZE}&offset=${offset}`);
|
|
43
|
+
auditEntries = normalizeAuditEntries(data.entries || []);
|
|
44
|
+
adminAuditTotal = data.total || 0;
|
|
45
|
+
currentPage = safePage;
|
|
46
|
+
}
|
|
47
|
+
function paginate(items, page) {
|
|
48
|
+
const start = (page - 1) * PAGE_SIZE;
|
|
49
|
+
return items.slice(start, start + PAGE_SIZE);
|
|
50
|
+
}
|
|
51
|
+
function renderPagination(totalItems, currentPg, hook) {
|
|
52
|
+
const totalPages = Math.max(1, Math.ceil(totalItems / PAGE_SIZE));
|
|
53
|
+
if (totalPages <= 1)
|
|
54
|
+
return '';
|
|
55
|
+
return `
|
|
56
|
+
<div class="admin-pagination" style="display:flex;align-items:center;justify-content:space-between;padding:10px 0;font-size:12px;color:var(--text-muted)">
|
|
57
|
+
<span>Page ${currentPg} of ${totalPages} (${totalItems} items)</span>
|
|
58
|
+
<div style="display:flex;gap:4px">
|
|
59
|
+
<button class="btn btn-sm btn-secondary" ${currentPg <= 1 ? 'disabled' : ''} onclick="window.__app.adminSetPage(${currentPg - 1})" aria-label="Previous page">← Prev</button>
|
|
60
|
+
<button class="btn btn-sm btn-secondary" ${currentPg >= totalPages ? 'disabled' : ''} onclick="window.__app.adminSetPage(${currentPg + 1})" aria-label="Next page">Next →</button>
|
|
61
|
+
</div>
|
|
62
|
+
</div>`;
|
|
63
|
+
}
|
|
64
|
+
function renderUserRow(user) {
|
|
65
|
+
return `
|
|
66
|
+
<tr>
|
|
67
|
+
<td>
|
|
68
|
+
<strong>${escHtml(user.display_name || user.email)}</strong>
|
|
69
|
+
<span>${escHtml(user.email)}</span>
|
|
70
|
+
</td>
|
|
71
|
+
<td>${user.is_admin ? '<span class="admin-pill admin-pill-strong">Admin</span>' : '<span class="admin-pill">User</span>'}</td>
|
|
72
|
+
<td>${user.has_github_token ? 'Connected' : 'Not connected'}</td>
|
|
73
|
+
<td>${new Date(user.created_at).toLocaleDateString()}</td>
|
|
74
|
+
<td>
|
|
75
|
+
<div style="display:flex;gap:4px;flex-wrap:wrap">
|
|
76
|
+
<button class="btn btn-secondary btn-sm" onclick="window.__app.setAdminRole('${escHtml(user.id)}', ${user.is_admin ? 'false' : 'true'})" aria-label="${user.is_admin ? 'Remove admin role' : 'Make admin'}">
|
|
77
|
+
${user.is_admin ? 'Remove Admin' : 'Make Admin'}
|
|
78
|
+
</button>
|
|
79
|
+
<button class="btn btn-danger btn-sm" onclick="window.__app.adminDeleteUser('${escHtml(user.id)}','${escHtml(user.display_name || user.email)}')" aria-label="Delete user ${escHtml(user.display_name || user.email)}">
|
|
80
|
+
Delete
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
</td>
|
|
84
|
+
</tr>`;
|
|
85
|
+
}
|
|
86
|
+
function renderProjectRow(project) {
|
|
87
|
+
const repo = project.github_owner && project.github_repo ? `${project.github_owner}/${project.github_repo}` : 'Not linked';
|
|
88
|
+
return `
|
|
89
|
+
<tr>
|
|
90
|
+
<td>
|
|
91
|
+
<strong>${escHtml(project.name)}</strong>
|
|
92
|
+
<span>${escHtml(project.slug)} · ${escHtml(project.prefix)}</span>
|
|
93
|
+
</td>
|
|
94
|
+
<td>${escHtml(project.owner_display_name || project.owner_email)}</td>
|
|
95
|
+
<td>${escHtml(repo)}</td>
|
|
96
|
+
<td>${project.github_sync_enabled ? '<span class="admin-pill admin-pill-strong">Sync on</span>' : '<span class="admin-pill">Sync off</span>'}</td>
|
|
97
|
+
<td>${new Date(project.created_at).toLocaleDateString()}</td>
|
|
98
|
+
<td>
|
|
99
|
+
<button class="btn btn-danger btn-sm" onclick="window.__app.adminDeleteProject('${escHtml(project.id)}','${escHtml(project.name)}')" aria-label="Delete project ${escHtml(project.name)}">
|
|
100
|
+
Delete
|
|
101
|
+
</button>
|
|
102
|
+
</td>
|
|
103
|
+
</tr>`;
|
|
104
|
+
}
|
|
105
|
+
function renderAuditRow(entry) {
|
|
106
|
+
const actor = getAuditActor(entry);
|
|
107
|
+
const details = getAuditDescription(entry);
|
|
108
|
+
return `
|
|
109
|
+
<tr>
|
|
110
|
+
<td style="white-space:nowrap">${escHtml(actor)}</td>
|
|
111
|
+
<td><span class="admin-pill">${escHtml(entry.action || '—')}</span></td>
|
|
112
|
+
<td>${escHtml(entry.target || '—')}</td>
|
|
113
|
+
<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis">${escHtml(details || '—')}</td>
|
|
114
|
+
<td style="white-space:nowrap">${entry.created_at ? new Date(entry.created_at).toLocaleString() : entry.timestamp ? new Date(entry.timestamp).toLocaleString() : '—'}</td>
|
|
115
|
+
</tr>`;
|
|
116
|
+
}
|
|
117
|
+
function renderGroupCard(group) {
|
|
118
|
+
return `
|
|
119
|
+
<div class="admin-group-card">
|
|
120
|
+
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px">
|
|
121
|
+
<strong>${escHtml(group.name)}</strong>
|
|
122
|
+
<button class="btn btn-danger btn-sm" onclick="window.__app.adminDeleteGroup('${escHtml(group.id)}','${escHtml(group.name)}')" aria-label="Delete group ${escHtml(group.name)}" style="flex-shrink:0">Delete</button>
|
|
123
|
+
</div>
|
|
124
|
+
<span>${escHtml(group.owner_email)} · ${group.member_count} members</span>
|
|
125
|
+
${group.description ? `<p>${escHtml(group.description)}</p>` : ''}
|
|
126
|
+
</div>`;
|
|
127
|
+
}
|
|
128
|
+
function formatUptime(seconds) {
|
|
129
|
+
const h = Math.floor(seconds / 3600);
|
|
130
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
131
|
+
if (h > 0)
|
|
132
|
+
return `${h}h ${m}m`;
|
|
133
|
+
return `${m}m`;
|
|
134
|
+
}
|
|
135
|
+
function renderAdmin(data) {
|
|
136
|
+
// Filter users
|
|
137
|
+
const filteredUsers = data.users.filter(u => !userFilter || u.email.toLowerCase().includes(userFilter.toLowerCase()) || (u.display_name || '').toLowerCase().includes(userFilter.toLowerCase()));
|
|
138
|
+
const pagedUsers = paginate(filteredUsers, adminTab === 'users' ? currentPage : 1);
|
|
139
|
+
// Filter projects
|
|
140
|
+
const filteredProjects = data.projects.filter(p => !projectFilter || p.name.toLowerCase().includes(projectFilter.toLowerCase()) || p.slug.toLowerCase().includes(projectFilter.toLowerCase()) || p.owner_email.toLowerCase().includes(projectFilter.toLowerCase()));
|
|
141
|
+
const pagedProjects = paginate(filteredProjects, adminTab === 'projects' ? currentPage : 1);
|
|
142
|
+
// Filter audit
|
|
143
|
+
const filteredAudit = auditEntries.filter(e => !auditFilter ||
|
|
144
|
+
(e.action || '').toLowerCase().includes(auditFilter.toLowerCase()) ||
|
|
145
|
+
(e.userEmail || '').toLowerCase().includes(auditFilter.toLowerCase()) ||
|
|
146
|
+
(e.target || '').toLowerCase().includes(auditFilter.toLowerCase()) ||
|
|
147
|
+
(e.details || '').toLowerCase().includes(auditFilter.toLowerCase()));
|
|
148
|
+
const pagedAudit = adminTab === 'audit' ? filteredAudit : [];
|
|
149
|
+
const tabs = [
|
|
150
|
+
{ id: 'users', label: 'Users', count: filteredUsers.length },
|
|
151
|
+
{ id: 'projects', label: 'Projects', count: filteredProjects.length },
|
|
152
|
+
{ id: 'groups', label: 'Groups', count: data.groups.length },
|
|
153
|
+
{ id: 'audit', label: 'Audit Log', count: adminAuditTotal },
|
|
154
|
+
];
|
|
155
|
+
return `
|
|
156
|
+
<div class="view-header">
|
|
157
|
+
<div>
|
|
158
|
+
<h1>Admin</h1>
|
|
159
|
+
<p class="view-subtitle">User, project, sharing, GitHub, and group oversight for pm-web.</p>
|
|
160
|
+
</div>
|
|
161
|
+
<div class="page-actions">
|
|
162
|
+
<button class="btn btn-secondary" onclick="window.__app.renderAdminView()" aria-label="Refresh admin data">Refresh</button>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div class="admin-stats">
|
|
167
|
+
<div class="stat-card"><div class="stat-icon">◉</div><div class="stat-value">${data.stats.users}</div><div class="stat-label">Users</div></div>
|
|
168
|
+
<div class="stat-card"><div class="stat-icon">◇</div><div class="stat-value">${data.stats.admins}</div><div class="stat-label">Admins</div></div>
|
|
169
|
+
<div class="stat-card"><div class="stat-icon">⊞</div><div class="stat-value">${data.stats.projects}</div><div class="stat-label">Projects</div></div>
|
|
170
|
+
<div class="stat-card"><div class="stat-icon">⇄</div><div class="stat-value">${data.stats.sharedProjects}</div><div class="stat-label">Shares</div></div>
|
|
171
|
+
<div class="stat-card"><div class="stat-icon">◈</div><div class="stat-value">${data.stats.groups}</div><div class="stat-label">Groups</div></div>
|
|
172
|
+
${data.uptimeSeconds !== undefined ? `<div class="stat-card"><div class="stat-icon">◎</div><div class="stat-value">${formatUptime(data.uptimeSeconds)}</div><div class="stat-label">Uptime</div></div>` : ''}
|
|
173
|
+
${data.serverVersion ? `<div class="stat-card"><div class="stat-icon">◫</div><div class="stat-value">v${escHtml(data.serverVersion)}</div><div class="stat-label">Version</div></div>` : ''}
|
|
174
|
+
<div class="stat-card"><div class="stat-icon">◷</div><div class="stat-value" style="font-size:12px">${new Date().toLocaleTimeString()}</div><div class="stat-label">Last Refreshed</div></div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div class="tabs" role="tablist">
|
|
178
|
+
${tabs.map(t => `<div class="tab${adminTab === t.id ? ' active' : ''}" role="tab" aria-selected="${adminTab === t.id}" tabindex="0" onclick="window.__app.adminSwitchTab('${t.id}')" onkeydown="if(event.key==='Enter')window.__app.adminSwitchTab('${t.id}')">${escHtml(t.label)} (${t.count})</div>`).join('')}
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
${adminTab === 'users' ? `
|
|
182
|
+
<section class="admin-panel" aria-label="Users management">
|
|
183
|
+
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:12px">
|
|
184
|
+
<div class="graph-panel-title" style="margin-bottom:0">Users</div>
|
|
185
|
+
<div class="search-box-wrap" style="margin-bottom:0;max-width:300px;flex:1">
|
|
186
|
+
<span class="search-icon">⌕</span>
|
|
187
|
+
<input class="search-input" type="text" placeholder="Filter users…" value="${escHtml(userFilter)}" oninput="window.__app.adminFilterUsers(this.value)" aria-label="Filter users">
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="admin-table-wrap">
|
|
191
|
+
<table class="admin-table" role="table">
|
|
192
|
+
<thead><tr><th>User</th><th>Role</th><th>GitHub</th><th>Created</th><th>Actions</th></tr></thead>
|
|
193
|
+
<tbody>${pagedUsers.map(renderUserRow).join('')}</tbody>
|
|
194
|
+
</table>
|
|
195
|
+
</div>
|
|
196
|
+
${renderPagination(filteredUsers.length, currentPage, 'users')}
|
|
197
|
+
</section>` : ''}
|
|
198
|
+
|
|
199
|
+
${adminTab === 'projects' ? `
|
|
200
|
+
<section class="admin-panel" aria-label="Projects management">
|
|
201
|
+
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:12px">
|
|
202
|
+
<div class="graph-panel-title" style="margin-bottom:0">Projects</div>
|
|
203
|
+
<div class="search-box-wrap" style="margin-bottom:0;max-width:300px;flex:1">
|
|
204
|
+
<span class="search-icon">⌕</span>
|
|
205
|
+
<input class="search-input" type="text" placeholder="Filter projects…" value="${escHtml(projectFilter)}" oninput="window.__app.adminFilterProjects(this.value)" aria-label="Filter projects">
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="admin-table-wrap">
|
|
209
|
+
<table class="admin-table" role="table">
|
|
210
|
+
<thead><tr><th>Project</th><th>Owner</th><th>GitHub Repo</th><th>Sync</th><th>Created</th><th>Actions</th></tr></thead>
|
|
211
|
+
<tbody>${pagedProjects.map(renderProjectRow).join('')}</tbody>
|
|
212
|
+
</table>
|
|
213
|
+
</div>
|
|
214
|
+
${renderPagination(filteredProjects.length, currentPage, 'projects')}
|
|
215
|
+
</section>` : ''}
|
|
216
|
+
|
|
217
|
+
${adminTab === 'groups' ? `
|
|
218
|
+
<section class="admin-panel" aria-label="Groups management">
|
|
219
|
+
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px">
|
|
220
|
+
<div class="graph-panel-title" style="margin-bottom:0">Groups</div>
|
|
221
|
+
<button class="btn btn-primary btn-sm" onclick="window.__app.adminCreateGroup()" aria-label="Create new group">+ New Group</button>
|
|
222
|
+
</div>
|
|
223
|
+
<div class="admin-grid-list">
|
|
224
|
+
${data.groups.length === 0
|
|
225
|
+
? '<div class="empty-state"><div class="empty-state-text">No groups yet.</div></div>'
|
|
226
|
+
: data.groups.map(renderGroupCard).join('')}
|
|
227
|
+
</div>
|
|
228
|
+
</section>` : ''}
|
|
229
|
+
|
|
230
|
+
${adminTab === 'audit' ? `
|
|
231
|
+
<section class="admin-panel" aria-label="Audit log">
|
|
232
|
+
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:12px">
|
|
233
|
+
<div class="graph-panel-title" style="margin-bottom:0">Audit Log</div>
|
|
234
|
+
<div class="search-box-wrap" style="margin-bottom:0;max-width:300px;flex:1">
|
|
235
|
+
<span class="search-icon">⌕</span>
|
|
236
|
+
<input class="search-input" type="text" placeholder="Filter audit log…" value="${escHtml(auditFilter)}" oninput="window.__app.adminFilterAudit(this.value)" aria-label="Filter audit log">
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
${filteredAudit.length === 0
|
|
240
|
+
? '<div class="empty-state"><div class="empty-state-text">No audit entries found.</div></div>'
|
|
241
|
+
: `<div class="admin-table-wrap">
|
|
242
|
+
<table class="admin-table" role="table">
|
|
243
|
+
<thead><tr><th>User</th><th>Action</th><th>Target</th><th>Details</th><th>Time</th></tr></thead>
|
|
244
|
+
<tbody>${pagedAudit.map(renderAuditRow).join('')}</tbody>
|
|
245
|
+
</table>
|
|
246
|
+
</div>
|
|
247
|
+
${renderPagination(adminAuditTotal, currentPage, 'audit')}`}
|
|
248
|
+
</section>` : ''}`;
|
|
249
|
+
}
|
|
250
|
+
export async function renderAdminView() {
|
|
251
|
+
const el = document.getElementById('content-admin');
|
|
252
|
+
if (!el)
|
|
253
|
+
return;
|
|
254
|
+
if (!state.user?.is_admin) {
|
|
255
|
+
el.innerHTML = `<div class="empty-state"><div class="empty-state-text">Admin access is required to view this page.</div></div>`;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
el.innerHTML = '<div class="loading-state"><div class="loading-spinner"></div></div>';
|
|
259
|
+
try {
|
|
260
|
+
adminData = await api('GET', '/admin/overview');
|
|
261
|
+
if (adminTab === 'audit') {
|
|
262
|
+
await loadAuditData(currentPage);
|
|
263
|
+
}
|
|
264
|
+
el.innerHTML = renderAdmin(adminData);
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
el.innerHTML = `<div class="empty-state"><div class="empty-state-text">Admin failed: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
export async function setAdminRole(userId, isAdmin) {
|
|
271
|
+
try {
|
|
272
|
+
await api('PATCH', `/admin/users/${encodeURIComponent(userId)}`, { isAdmin });
|
|
273
|
+
toast('Admin role updated', 'success');
|
|
274
|
+
await renderAdminView();
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
toast(err instanceof Error ? err.message : String(err), 'error');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
export function adminSwitchTab(tab) {
|
|
281
|
+
adminTab = tab;
|
|
282
|
+
currentPage = 1;
|
|
283
|
+
if (tab === 'audit') {
|
|
284
|
+
void loadAuditData(1).then(() => {
|
|
285
|
+
const el = document.getElementById('content-admin');
|
|
286
|
+
if (adminData && el)
|
|
287
|
+
el.innerHTML = renderAdmin(adminData);
|
|
288
|
+
}).catch((err) => {
|
|
289
|
+
const el = document.getElementById('content-admin');
|
|
290
|
+
if (el)
|
|
291
|
+
el.innerHTML = `<div class="empty-state"><div class="empty-state-text">Failed to load audit log: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const el = document.getElementById('content-admin');
|
|
296
|
+
if (el && adminData) {
|
|
297
|
+
el.innerHTML = renderAdmin(adminData);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
export function adminFilterUsers(filter) {
|
|
301
|
+
userFilter = filter;
|
|
302
|
+
currentPage = 1;
|
|
303
|
+
if (adminData) {
|
|
304
|
+
const el = document.getElementById('content-admin');
|
|
305
|
+
if (el)
|
|
306
|
+
el.innerHTML = renderAdmin(adminData);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
export function adminFilterProjects(filter) {
|
|
310
|
+
projectFilter = filter;
|
|
311
|
+
currentPage = 1;
|
|
312
|
+
if (adminData) {
|
|
313
|
+
const el = document.getElementById('content-admin');
|
|
314
|
+
if (el)
|
|
315
|
+
el.innerHTML = renderAdmin(adminData);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
export function adminFilterAudit(filter) {
|
|
319
|
+
auditFilter = filter;
|
|
320
|
+
currentPage = 1;
|
|
321
|
+
if (adminData) {
|
|
322
|
+
const el = document.getElementById('content-admin');
|
|
323
|
+
if (adminTab === 'audit') {
|
|
324
|
+
void loadAuditData(1).then(() => {
|
|
325
|
+
if (el)
|
|
326
|
+
el.innerHTML = renderAdmin(adminData);
|
|
327
|
+
}).catch((err) => {
|
|
328
|
+
if (el)
|
|
329
|
+
el.innerHTML = `<div class="empty-state"><div class="empty-state-text">Failed to reload audit log: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
|
|
330
|
+
});
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (el)
|
|
334
|
+
el.innerHTML = renderAdmin(adminData);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
export async function adminSetPage(page) {
|
|
338
|
+
if (page < 1)
|
|
339
|
+
return;
|
|
340
|
+
if (!adminData)
|
|
341
|
+
return;
|
|
342
|
+
if (adminTab === 'audit') {
|
|
343
|
+
await loadAuditData(page);
|
|
344
|
+
const el = document.getElementById('content-admin');
|
|
345
|
+
if (el)
|
|
346
|
+
el.innerHTML = renderAdmin(adminData);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const userFilterLower = userFilter.toLowerCase();
|
|
350
|
+
const projectFilterLower = projectFilter.toLowerCase();
|
|
351
|
+
const filtered = adminTab === 'users'
|
|
352
|
+
? adminData.users.filter((u) => !userFilter || u.email.toLowerCase().includes(userFilterLower) || (u.display_name || '').toLowerCase().includes(userFilterLower))
|
|
353
|
+
: adminData.projects.filter((p) => !projectFilter || p.name.toLowerCase().includes(projectFilterLower) || p.slug.toLowerCase().includes(projectFilterLower) || p.owner_email.toLowerCase().includes(projectFilterLower));
|
|
354
|
+
const totalPages = Math.max(1, Math.ceil((filtered?.length || 0) / PAGE_SIZE));
|
|
355
|
+
currentPage = Math.min(page, totalPages);
|
|
356
|
+
const el = document.getElementById('content-admin');
|
|
357
|
+
if (el)
|
|
358
|
+
el.innerHTML = renderAdmin(adminData);
|
|
359
|
+
}
|
|
360
|
+
export async function adminDeleteUser(userId, userName) {
|
|
361
|
+
confirmDialog(`Delete user "${userName}"?`, 'This action cannot be undone. The user will be permanently removed.', async () => {
|
|
362
|
+
try {
|
|
363
|
+
await api('DELETE', `/admin/users/${encodeURIComponent(userId)}`);
|
|
364
|
+
toast('User deleted', 'success');
|
|
365
|
+
await renderAdminView();
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
toast(err instanceof Error ? err.message : String(err), 'error');
|
|
369
|
+
}
|
|
370
|
+
}, true);
|
|
371
|
+
}
|
|
372
|
+
export async function adminDeleteProject(projectId, projectName) {
|
|
373
|
+
confirmDialog(`Delete project "${projectName}"?`, 'This will permanently delete the project and all its items. This action cannot be undone.', async () => {
|
|
374
|
+
try {
|
|
375
|
+
await api('DELETE', `/admin/projects/${encodeURIComponent(projectId)}`);
|
|
376
|
+
toast('Project deleted', 'success');
|
|
377
|
+
await renderAdminView();
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
toast(err instanceof Error ? err.message : String(err), 'error');
|
|
381
|
+
}
|
|
382
|
+
}, true);
|
|
383
|
+
}
|
|
384
|
+
export async function adminDeleteGroup(groupId, groupName) {
|
|
385
|
+
confirmDialog(`Delete group "${groupName}"?`, 'This will permanently remove the group and all its memberships.', async () => {
|
|
386
|
+
try {
|
|
387
|
+
await api('DELETE', `/admin/groups/${encodeURIComponent(groupId)}`);
|
|
388
|
+
toast('Group deleted', 'success');
|
|
389
|
+
await renderAdminView();
|
|
390
|
+
}
|
|
391
|
+
catch (err) {
|
|
392
|
+
toast(err instanceof Error ? err.message : String(err), 'error');
|
|
393
|
+
}
|
|
394
|
+
}, true);
|
|
395
|
+
}
|
|
396
|
+
export function adminCreateGroup() {
|
|
397
|
+
const id = 'admin-create-group-' + Date.now();
|
|
398
|
+
createModal(id, 'Create Group', `
|
|
399
|
+
<div class="form-group">
|
|
400
|
+
<label class="form-label" for="admin-group-name">Group Name</label>
|
|
401
|
+
<input class="form-input" id="admin-group-name" type="text" placeholder="e.g. Engineering" required aria-required="true">
|
|
402
|
+
</div>
|
|
403
|
+
<div class="form-group">
|
|
404
|
+
<label class="form-label" for="admin-group-desc">Description</label>
|
|
405
|
+
<input class="form-input" id="admin-group-desc" type="text" placeholder="Optional description">
|
|
406
|
+
</div>
|
|
407
|
+
<div class="form-error" id="admin-group-error" style="display:none"></div>
|
|
408
|
+
`, `<button class="btn btn-primary" id="${id}-submit">Create Group</button>`);
|
|
409
|
+
showModal(id);
|
|
410
|
+
document.getElementById(`${id}-submit`)?.addEventListener('click', async () => {
|
|
411
|
+
const name = document.getElementById('admin-group-name')?.value?.trim();
|
|
412
|
+
const desc = document.getElementById('admin-group-desc')?.value?.trim();
|
|
413
|
+
const errEl = document.getElementById('admin-group-error');
|
|
414
|
+
if (!name) {
|
|
415
|
+
if (errEl) {
|
|
416
|
+
errEl.textContent = 'Group name is required';
|
|
417
|
+
errEl.style.display = 'block';
|
|
418
|
+
}
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
await api('POST', '/admin/groups', { name, description: desc });
|
|
423
|
+
toast('Group created', 'success');
|
|
424
|
+
hideModal(id);
|
|
425
|
+
await renderAdminView();
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
if (errEl) {
|
|
429
|
+
errEl.textContent = err instanceof Error ? err.message : String(err);
|
|
430
|
+
errEl.style.display = 'block';
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
//# sourceMappingURL=admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin.js","sourceRoot":"","sources":["admin.ts"],"names":[],"mappings":"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"}
|