thatcher 1.0.4
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/LICENSE +21 -0
- package/README.md +398 -0
- package/package.json +73 -0
- package/src/adapters/google-auth.js +148 -0
- package/src/adapters/google-drive.js +209 -0
- package/src/app/api/[entity]/[[...path]]/route.js +33 -0
- package/src/app/api/audit/dashboard/route.js +77 -0
- package/src/app/api/audit/logs/route.js +46 -0
- package/src/app/api/audit/permissions/[id]/route.js +37 -0
- package/src/app/api/audit/permissions/route.js +93 -0
- package/src/app/api/audit/permissions/stats/route.js +29 -0
- package/src/app/api/audit/route.js +79 -0
- package/src/app/api/audit/stats/route.js +18 -0
- package/src/app/api/auth/google/callback/route.js +94 -0
- package/src/app/api/auth/google/route.js +58 -0
- package/src/app/api/auth/login/route.js +121 -0
- package/src/app/api/auth/logout/route.js +52 -0
- package/src/app/api/auth/me/route.js +16 -0
- package/src/app/api/auth/mwr-bridge/route.js +77 -0
- package/src/app/api/auth/password-reset/route.js +65 -0
- package/src/app/api/cron/trigger/route.js +58 -0
- package/src/app/api/csrf-token/route.js +8 -0
- package/src/app/api/debug/config/route.js +22 -0
- package/src/app/api/debug/hooks/route.js +16 -0
- package/src/app/api/debug/plugins/route.js +20 -0
- package/src/app/api/debug/sqlite/route.js +21 -0
- package/src/app/api/debug/sync/route.js +29 -0
- package/src/app/api/debug/workflow/route.js +20 -0
- package/src/app/api/domains/[domain]/route.js +26 -0
- package/src/app/api/domains/route.js +21 -0
- package/src/app/api/email/allocate/batch/route.js +106 -0
- package/src/app/api/email/allocate/route.js +141 -0
- package/src/app/api/email/receive/route.js +158 -0
- package/src/app/api/email/route.js +3 -0
- package/src/app/api/email/send/route.js +77 -0
- package/src/app/api/email/unallocated/route.js +47 -0
- package/src/app/api/files/[id]/route.js +38 -0
- package/src/app/api/health/route.js +95 -0
- package/src/app/api/metrics/route.js +73 -0
- package/src/app/api/monitoring/dashboard/route.js +18 -0
- package/src/cli.js +243 -0
- package/src/config/config-loader.js +112 -0
- package/src/config/constants.js +127 -0
- package/src/config/env.js +164 -0
- package/src/config/spec-helpers.js +232 -0
- package/src/engine.server.js +212 -0
- package/src/index.js +368 -0
- package/src/lib/accessibility.js +162 -0
- package/src/lib/action-factory.js +34 -0
- package/src/lib/action-utils.js +21 -0
- package/src/lib/alert-manager.js +189 -0
- package/src/lib/api-error-wrapper.js +125 -0
- package/src/lib/api-helpers.js +53 -0
- package/src/lib/api.js +82 -0
- package/src/lib/audit-logger-enhanced.js +117 -0
- package/src/lib/audit-logger.js +193 -0
- package/src/lib/auth-middleware.js +102 -0
- package/src/lib/auth-route-helpers.js +83 -0
- package/src/lib/business-rules-engine.js +86 -0
- package/src/lib/compression.js +44 -0
- package/src/lib/config-field-helpers.js +91 -0
- package/src/lib/config-generator-engine.js +445 -0
- package/src/lib/config-helpers.js +120 -0
- package/src/lib/connection-guard.js +79 -0
- package/src/lib/crud-action-helpers.js +34 -0
- package/src/lib/crud-factory.js +83 -0
- package/src/lib/crud-handlers.js +244 -0
- package/src/lib/csrf-protection.js +63 -0
- package/src/lib/database-core.js +258 -0
- package/src/lib/database-migrations.js +96 -0
- package/src/lib/date-utils.js +159 -0
- package/src/lib/db-backup.js +97 -0
- package/src/lib/db-monitor.js +127 -0
- package/src/lib/domain-loader.js +82 -0
- package/src/lib/email-sender.js +100 -0
- package/src/lib/error-boundary.js +134 -0
- package/src/lib/error-handler.js +84 -0
- package/src/lib/error-recovery.js +190 -0
- package/src/lib/error-resilience.js +130 -0
- package/src/lib/errors.js +69 -0
- package/src/lib/events-engine.js +182 -0
- package/src/lib/field-iterator.js +50 -0
- package/src/lib/field-registry.js +68 -0
- package/src/lib/field-types.js +154 -0
- package/src/lib/generic-crud-handler.js +32 -0
- package/src/lib/health-monitor.js +134 -0
- package/src/lib/hook-engine.js +169 -0
- package/src/lib/hot-reload/cache-invalidator.js +115 -0
- package/src/lib/hot-reload/checkpoint.js +95 -0
- package/src/lib/hot-reload/debug-exposure.js +67 -0
- package/src/lib/hot-reload/directory-watcher.js +96 -0
- package/src/lib/hot-reload/index.js +50 -0
- package/src/lib/hot-reload/mutex.js +75 -0
- package/src/lib/hot-reload/promise-container.js +66 -0
- package/src/lib/hot-reload/route-wrapper.js +46 -0
- package/src/lib/hot-reload/safe-error.js +51 -0
- package/src/lib/hot-reload/supervisor.js +161 -0
- package/src/lib/hot-reload/timeout-wrapper.js +52 -0
- package/src/lib/http-methods-factory.js +25 -0
- package/src/lib/index-optimizer.js +96 -0
- package/src/lib/index.js +35 -0
- package/src/lib/list-data-transform.js +39 -0
- package/src/lib/log-aggregator.js +116 -0
- package/src/lib/logger.js +55 -0
- package/src/lib/metrics-collector.js +102 -0
- package/src/lib/minifier.js +19 -0
- package/src/lib/monitoring-init.js +67 -0
- package/src/lib/next-compat.js +80 -0
- package/src/lib/next-polyfills.js +135 -0
- package/src/lib/perf-monitor.js +91 -0
- package/src/lib/progress-components.js +181 -0
- package/src/lib/query-cache.js +126 -0
- package/src/lib/query-engine-write.js +221 -0
- package/src/lib/query-engine.js +399 -0
- package/src/lib/query-perf.js +117 -0
- package/src/lib/query-string-adapter.js +75 -0
- package/src/lib/realtime-server.js +67 -0
- package/src/lib/render-cache.js +61 -0
- package/src/lib/request-tracker.js +43 -0
- package/src/lib/resource-hints.js +29 -0
- package/src/lib/resource-monitor.js +117 -0
- package/src/lib/response-formatter.js +80 -0
- package/src/lib/route-helpers.js +33 -0
- package/src/lib/route-resolver.js +142 -0
- package/src/lib/safe-json.js +8 -0
- package/src/lib/server-bootstrap.js +71 -0
- package/src/lib/stage-pipeline.js +153 -0
- package/src/lib/state-protocol.js +171 -0
- package/src/lib/state-transport-client.js +169 -0
- package/src/lib/state-transport-reconnect.js +121 -0
- package/src/lib/state-transport-server.js +181 -0
- package/src/lib/static-server.js +97 -0
- package/src/lib/status-helpers.js +98 -0
- package/src/lib/universal-handler.js +7 -0
- package/src/lib/utils.js +93 -0
- package/src/lib/validate.js +197 -0
- package/src/lib/validation/business-validators.js +61 -0
- package/src/lib/validation/csrf.js +51 -0
- package/src/lib/validation/file-validators.js +34 -0
- package/src/lib/validation/format-validators.js +106 -0
- package/src/lib/validation/index.js +19 -0
- package/src/lib/validation/rate-limit.js +31 -0
- package/src/lib/validation/security-validators.js +78 -0
- package/src/lib/validation-middleware.js +133 -0
- package/src/lib/validators.js +105 -0
- package/src/lib/with-audit-logging.js +63 -0
- package/src/lib/with-error-handler.js +31 -0
- package/src/lib/workflow-engine.js +250 -0
- package/src/server/server.js +305 -0
- package/src/services/collaborator-role.service.js +205 -0
- package/src/services/email-sender.js +105 -0
- package/src/services/notification-engine.js +110 -0
- package/src/services/permission.service.js +181 -0
- package/src/ui/advanced-search-renderer.js +42 -0
- package/src/ui/advanced-widgets.js +47 -0
- package/src/ui/auth-pages.js +114 -0
- package/src/ui/auth-styles.js +53 -0
- package/src/ui/client.js +99 -0
- package/src/ui/collaboration-dialogs.js +31 -0
- package/src/ui/common-handlers.js +156 -0
- package/src/ui/component-engine.js +103 -0
- package/src/ui/dashboard-renderer.js +150 -0
- package/src/ui/dialog-engine.js +147 -0
- package/src/ui/dialog-factory.js +38 -0
- package/src/ui/engagement-cards.js +76 -0
- package/src/ui/engagement-dialogs.js +100 -0
- package/src/ui/engagement-grid-renderer.js +109 -0
- package/src/ui/entity-renderer.js +176 -0
- package/src/ui/event-delegation.js +108 -0
- package/src/ui/fetch-json.js +24 -0
- package/src/ui/file-dialogs.js +81 -0
- package/src/ui/flexup-report-renderer.js +173 -0
- package/src/ui/format-helpers.js +102 -0
- package/src/ui/global-tags.js +144 -0
- package/src/ui/highlight-threading-renderer.js +176 -0
- package/src/ui/idle-logout.js +156 -0
- package/src/ui/job-management-renderer.js +61 -0
- package/src/ui/layout.js +219 -0
- package/src/ui/letter-dialogs.js +37 -0
- package/src/ui/ml-console-renderer.js +108 -0
- package/src/ui/monitoring-dashboard-client.js +136 -0
- package/src/ui/monitoring-dashboard.js +134 -0
- package/src/ui/notifications-renderer.js +51 -0
- package/src/ui/page-handler-admin.js +121 -0
- package/src/ui/page-handler-helpers.js +111 -0
- package/src/ui/page-handler-reviews.js +165 -0
- package/src/ui/page-handler-rfi.js +42 -0
- package/src/ui/page-handler.js +210 -0
- package/src/ui/password-reset-page.js +146 -0
- package/src/ui/perf-helpers.js +96 -0
- package/src/ui/perf-renderer.js +71 -0
- package/src/ui/permissions-ui.js +163 -0
- package/src/ui/picker-dialogs.js +100 -0
- package/src/ui/render-helpers.js +124 -0
- package/src/ui/renderer.js +35 -0
- package/src/ui/review-comparison-renderer.js +58 -0
- package/src/ui/review-detail-panels.js +71 -0
- package/src/ui/review-detail-renderer.js +170 -0
- package/src/ui/review-detail-script.js +95 -0
- package/src/ui/review-mwr-renderer.js +113 -0
- package/src/ui/review-renderer.js +202 -0
- package/src/ui/review-widgets.js +88 -0
- package/src/ui/review-zone-nav.js +12 -0
- package/src/ui/rfi-detail-renderer.js +191 -0
- package/src/ui/rfi-renderer.js +194 -0
- package/src/ui/rfi-report-renderer.js +56 -0
- package/src/ui/rippleui.css +1 -0
- package/src/ui/settings-renderer-advanced.js +195 -0
- package/src/ui/settings-renderer-advanced2.js +158 -0
- package/src/ui/settings-renderer-teams.js +112 -0
- package/src/ui/settings-renderer.js +166 -0
- package/src/ui/spacing-system.js +155 -0
- package/src/ui/standalone-login.js +109 -0
- package/src/ui/styles.css +2530 -0
- package/src/ui/styles2.css +1602 -0
- package/src/ui/test-page.js +23 -0
- package/src/ui/validation-rules.js +73 -0
- package/src/ui/validation-ui.js +147 -0
- package/src/ui/virtual-scroll.js +107 -0
- package/src/ui/webjsx.js +61 -0
- package/src/ui/widgets.js +152 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export function monitoringDashboardScript() {
|
|
2
|
+
return `
|
|
3
|
+
<script>
|
|
4
|
+
let currentTab = 'requests'
|
|
5
|
+
|
|
6
|
+
function showTab(name) {
|
|
7
|
+
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'))
|
|
8
|
+
document.getElementById('tab-' + name).classList.remove('hidden')
|
|
9
|
+
|
|
10
|
+
document.querySelectorAll('.tab').forEach(el => el.classList.remove('tab-active'))
|
|
11
|
+
event.target.classList.add('tab-active')
|
|
12
|
+
|
|
13
|
+
currentTab = name
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function fetchMetrics() {
|
|
17
|
+
try {
|
|
18
|
+
const [health, metrics] = await Promise.all([
|
|
19
|
+
fetch('/api/health?detailed=true').then(r => r.json()),
|
|
20
|
+
fetch('/api/metrics').then(r => r.json())
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
document.getElementById('health-status').textContent = health.status.toUpperCase()
|
|
24
|
+
document.getElementById('health-status').className =
|
|
25
|
+
'metric-value ' + (health.status === 'ok' ? 'text-success' : 'text-error')
|
|
26
|
+
|
|
27
|
+
if (metrics.metrics?.requests) {
|
|
28
|
+
const allP95 = Object.values(metrics.metrics.requests).map(m => m.p95).filter(v => v > 0)
|
|
29
|
+
const avgP95 = allP95.length ? (allP95.reduce((a,b) => a+b, 0) / allP95.length).toFixed(1) : '-'
|
|
30
|
+
document.getElementById('p95-latency').textContent = avgP95
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (metrics.metrics?.errors) {
|
|
34
|
+
const totalErrors = Object.values(metrics.metrics.errors.byEndpoint)
|
|
35
|
+
.reduce((sum, ep) => sum + ep.count, 0)
|
|
36
|
+
document.getElementById('error-count').textContent = totalErrors
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (health.resources?.memory) {
|
|
40
|
+
const memPct = (health.resources.memory.percentage * 100).toFixed(1)
|
|
41
|
+
document.getElementById('memory-usage').textContent = memPct
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (currentTab === 'requests') updateRequests(metrics.metrics?.requests || {})
|
|
45
|
+
if (currentTab === 'alerts') updateAlerts(metrics.alerts || [])
|
|
46
|
+
if (currentTab === 'database') updateDatabase(metrics.database || {})
|
|
47
|
+
if (currentTab === 'resources') updateResources(health.resources || {})
|
|
48
|
+
if (currentTab === 'logs') updateLogs(metrics.logs || [])
|
|
49
|
+
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error('Fetch error:', err)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function updateRequests(requests) {
|
|
56
|
+
const html = Object.entries(requests).map(([endpoint, stats]) => \`
|
|
57
|
+
<div class="mb-4 p-3 bg-base-100 rounded">
|
|
58
|
+
<div class="font-bold">\${endpoint}</div>
|
|
59
|
+
<div class="grid grid-cols-7 gap-2 text-sm mt-2">
|
|
60
|
+
<div>Count: \${stats.count}</div>
|
|
61
|
+
<div>Min: \${stats.min.toFixed(1)}ms</div>
|
|
62
|
+
<div>Max: \${stats.max.toFixed(1)}ms</div>
|
|
63
|
+
<div>Avg: \${stats.avg.toFixed(1)}ms</div>
|
|
64
|
+
<div>P50: \${stats.p50.toFixed(1)}ms</div>
|
|
65
|
+
<div>P95: \${stats.p95.toFixed(1)}ms</div>
|
|
66
|
+
<div>P99: \${stats.p99.toFixed(1)}ms</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
\`).join('')
|
|
70
|
+
document.getElementById('request-metrics').innerHTML = html || '<p>No data</p>'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function updateAlerts(alerts) {
|
|
74
|
+
const html = alerts.map(alert => \`
|
|
75
|
+
<div class="p-3 rounded alert-\${alert.severity}">
|
|
76
|
+
<div class="font-bold">[\${alert.severity.toUpperCase()}] \${alert.type}</div>
|
|
77
|
+
<div>\${alert.message}</div>
|
|
78
|
+
<div class="text-sm text-gray-600">\${new Date(alert.timestamp).toLocaleString()}</div>
|
|
79
|
+
</div>
|
|
80
|
+
\`).join('')
|
|
81
|
+
document.getElementById('alert-list').innerHTML = html || '<p>No alerts</p>'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function updateDatabase(db) {
|
|
85
|
+
const html = \`
|
|
86
|
+
<div class="space-y-2">
|
|
87
|
+
<div>Active Queries: \${db.activeQueries || 0}</div>
|
|
88
|
+
<div>Connections: \${db.connections || 0}</div>
|
|
89
|
+
<div>Locks: \${db.locks || 0}</div>
|
|
90
|
+
<div class="mt-4 font-bold">Slow Queries:</div>
|
|
91
|
+
\${(db.slowQueries || []).map(q => \`
|
|
92
|
+
<div class="p-2 bg-base-100 rounded text-sm">
|
|
93
|
+
<div class="font-mono">\${q.sql}</div>
|
|
94
|
+
<div>Duration: \${q.duration.toFixed(2)}ms</div>
|
|
95
|
+
</div>
|
|
96
|
+
\`).join('') || '<p>None</p>'}
|
|
97
|
+
</div>
|
|
98
|
+
\`
|
|
99
|
+
document.getElementById('database-metrics').innerHTML = html
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function updateResources(resources) {
|
|
103
|
+
const html = \`
|
|
104
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
105
|
+
<div class="p-4 bg-base-100 rounded">
|
|
106
|
+
<div class="font-bold">CPU</div>
|
|
107
|
+
<div>\${resources.cpu ? (resources.cpu * 100).toFixed(1) + '%' : 'N/A'}</div>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="p-4 bg-base-100 rounded">
|
|
110
|
+
<div class="font-bold">Memory</div>
|
|
111
|
+
<div>\${resources.memory ? (resources.memory.percentage * 100).toFixed(1) + '%' : 'N/A'}</div>
|
|
112
|
+
<div class="text-sm">\${resources.memory ? (resources.memory.used / 1024 / 1024 / 1024).toFixed(2) + ' GB used' : ''}</div>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="p-4 bg-base-100 rounded">
|
|
115
|
+
<div class="font-bold">Disk</div>
|
|
116
|
+
<div>\${resources.disk ? (resources.disk.percentage * 100).toFixed(1) + '%' : 'N/A'}</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
\`
|
|
120
|
+
document.getElementById('resource-metrics').innerHTML = html
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function updateLogs(logs) {
|
|
124
|
+
const html = logs.map(log => \`
|
|
125
|
+
<div class="log-\${log.level} mb-1">
|
|
126
|
+
[\${log.iso}] [\${log.level.toUpperCase()}] \${log.message}
|
|
127
|
+
\${Object.keys(log.metadata).length ? ' ' + JSON.stringify(log.metadata) : ''}
|
|
128
|
+
</div>
|
|
129
|
+
\`).join('')
|
|
130
|
+
document.getElementById('log-list').innerHTML = html || '<p>No logs</p>'
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fetchMetrics()
|
|
134
|
+
setInterval(fetchMetrics, 5000)
|
|
135
|
+
</script>`
|
|
136
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { monitoringDashboardScript } from '@/ui/monitoring-dashboard-client.js'
|
|
2
|
+
import { SPACING } from '@/ui/spacing-system.js'
|
|
3
|
+
|
|
4
|
+
const html = (strings, ...values) => strings.reduce((r, s, i) => r + s + (values[i] ?? ''), '')
|
|
5
|
+
|
|
6
|
+
export function renderMonitoringDashboard() {
|
|
7
|
+
return html`
|
|
8
|
+
<!DOCTYPE html>
|
|
9
|
+
<html lang="en">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="UTF-8">
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
13
|
+
<title>Monitoring Dashboard</title>
|
|
14
|
+
<link rel="stylesheet" href="/ui/styles.css">
|
|
15
|
+
<style>
|
|
16
|
+
.metric-card { min-height: 120px; padding: ${SPACING.md}; }
|
|
17
|
+
.metric-value { font-size: 2rem; font-weight: bold; margin-bottom: ${SPACING.sm}; }
|
|
18
|
+
.metric-label { font-size: 0.875rem; color: #666; }
|
|
19
|
+
.alert-critical { background-color: #fee; border-left: ${SPACING.sm} solid #f00; padding: ${SPACING.md}; margin-bottom: ${SPACING.md}; }
|
|
20
|
+
.alert-warning { background-color: #ffa; border-left: ${SPACING.sm} solid #fa0; padding: ${SPACING.md}; margin-bottom: ${SPACING.md}; }
|
|
21
|
+
.alert-info { background-color: #eff; border-left: ${SPACING.sm} solid #0af; padding: ${SPACING.md}; margin-bottom: ${SPACING.md}; }
|
|
22
|
+
.log-error { color: #f00; }
|
|
23
|
+
.log-warn { color: #fa0; }
|
|
24
|
+
.log-info { color: #0af; }
|
|
25
|
+
.log-debug { color: #888; }
|
|
26
|
+
</style>
|
|
27
|
+
</head>
|
|
28
|
+
<body class="bg-base-100">
|
|
29
|
+
<main id="main-content" role="main">
|
|
30
|
+
<div class="container mx-auto p-4">
|
|
31
|
+
<h1 class="text-3xl font-bold mb-6">System Monitoring Dashboard</h1>
|
|
32
|
+
|
|
33
|
+
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
|
34
|
+
<div class="card metric-card bg-base-200">
|
|
35
|
+
<div class="card-clean-body">
|
|
36
|
+
<div class="metric-value text-success" id="health-status">OK</div>
|
|
37
|
+
<div class="metric-label">System Health</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="card metric-card bg-base-200">
|
|
42
|
+
<div class="card-clean-body">
|
|
43
|
+
<div class="metric-value" id="p95-latency">-</div>
|
|
44
|
+
<div class="metric-label">P95 Latency (ms)</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="card metric-card bg-base-200">
|
|
49
|
+
<div class="card-clean-body">
|
|
50
|
+
<div class="metric-value" id="error-count">0</div>
|
|
51
|
+
<div class="metric-label">Error Count</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="card metric-card bg-base-200">
|
|
56
|
+
<div class="card-clean-body">
|
|
57
|
+
<div class="metric-value" id="memory-usage">-</div>
|
|
58
|
+
<div class="metric-label">Memory Usage (%)</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div class="tabs mb-4">
|
|
64
|
+
<button class="tab tab-lifted tab-active" data-action="showTab" data-args='["requests"]'>Requests</button>
|
|
65
|
+
<button class="tab tab-lifted" data-action="showTab" data-args='["alerts"]'>Alerts</button>
|
|
66
|
+
<button class="tab tab-lifted" data-action="showTab" data-args='["database"]'>Database</button>
|
|
67
|
+
<button class="tab tab-lifted" data-action="showTab" data-args='["resources"]'>Resources</button>
|
|
68
|
+
<button class="tab tab-lifted" data-action="showTab" data-args='["logs"]'>Logs</button>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div id="tab-requests" class="tab-content">
|
|
72
|
+
<div class="card bg-base-200">
|
|
73
|
+
<div class="card-clean-body">
|
|
74
|
+
<h2 style="font-size:1rem;font-weight:600">Request Metrics</h2>
|
|
75
|
+
<div id="request-metrics" class="overflow-auto max-h-96"></div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div id="tab-alerts" class="tab-content hidden">
|
|
81
|
+
<div class="card bg-base-200">
|
|
82
|
+
<div class="card-clean-body">
|
|
83
|
+
<h2 style="font-size:1rem;font-weight:600">Recent Alerts</h2>
|
|
84
|
+
<div id="alert-list" class="space-y-2"></div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div id="tab-database" class="tab-content hidden">
|
|
90
|
+
<div class="card bg-base-200">
|
|
91
|
+
<div class="card-clean-body">
|
|
92
|
+
<h2 style="font-size:1rem;font-weight:600">Database Metrics</h2>
|
|
93
|
+
<div id="database-metrics"></div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div id="tab-resources" class="tab-content hidden">
|
|
99
|
+
<div class="card bg-base-200">
|
|
100
|
+
<div class="card-clean-body">
|
|
101
|
+
<h2 style="font-size:1rem;font-weight:600">Resource Usage</h2>
|
|
102
|
+
<div id="resource-metrics"></div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div id="tab-logs" class="tab-content hidden">
|
|
108
|
+
<div class="card bg-base-200">
|
|
109
|
+
<div class="card-clean-body">
|
|
110
|
+
<h2 style="font-size:1rem;font-weight:600">System Logs</h2>
|
|
111
|
+
<div class="mb-4 flex gap-2">
|
|
112
|
+
<label for="log-level" class="sr-only">Log level filter</label>
|
|
113
|
+
<select id="log-level" class="select select-bordered" aria-label="Log level filter">
|
|
114
|
+
<option value="">All Levels</option>
|
|
115
|
+
<option value="debug">Debug</option>
|
|
116
|
+
<option value="info">Info</option>
|
|
117
|
+
<option value="warn">Warning</option>
|
|
118
|
+
<option value="error">Error</option>
|
|
119
|
+
</select>
|
|
120
|
+
<label for="log-search" class="sr-only">Search logs</label>
|
|
121
|
+
<input id="log-search" type="text" placeholder="Search logs..." class="input input-bordered flex-1" aria-label="Search logs">
|
|
122
|
+
</div>
|
|
123
|
+
<div id="log-list" class="overflow-auto max-h-96 font-mono text-sm"></div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
</main>
|
|
130
|
+
${monitoringDashboardScript()}
|
|
131
|
+
</body>
|
|
132
|
+
</html>
|
|
133
|
+
`
|
|
134
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { page } from '@/ui/layout.js';
|
|
2
|
+
import { esc, TOAST_SCRIPT } from '@/ui/render-helpers.js';
|
|
3
|
+
|
|
4
|
+
function fmtDate(ts) {
|
|
5
|
+
if (!ts) return '-';
|
|
6
|
+
return new Date(typeof ts === 'number' && ts < 2e10 ? ts * 1000 : ts).toLocaleString();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function notifRow(n) {
|
|
10
|
+
const isRead = !!n.read_at;
|
|
11
|
+
const typeLabels = { review_notification: 'Review', highlight_response: 'Highlight', rfi_response: 'RFI', general: 'General' };
|
|
12
|
+
const typeLabel = typeLabels[n.type] || n.type || '-';
|
|
13
|
+
const entityLink = n.entity_type === 'review' && n.entity_id
|
|
14
|
+
? `<a href="/review/${esc(n.entity_id)}" class="text-primary text-sm" style="text-decoration:underline">View</a>`
|
|
15
|
+
: '';
|
|
16
|
+
return `<tr class="hover${isRead ? ' opacity-60' : ''}" data-notif-id="${esc(n.id)}">
|
|
17
|
+
<td class="text-xs text-base-content/40 w-32">${fmtDate(n.created_at)}</td>
|
|
18
|
+
<td class="text-xs"><span class="badge badge-flat-secondary">${esc(typeLabel)}</span></td>
|
|
19
|
+
<td class="text-sm max-w-md">${esc(n.message || '-')}</td>
|
|
20
|
+
<td>${entityLink}</td>
|
|
21
|
+
<td>${isRead ? '<span class="text-xs text-base-content/30">Read</span>' : `<button onclick="markRead('${esc(n.id)}')" class="btn btn-ghost btn-xs">Mark read</button>`}</td>
|
|
22
|
+
</tr>`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function renderNotificationsPage(user, notifs = []) {
|
|
26
|
+
const rows = notifs.map(notifRow).join('') ||
|
|
27
|
+
`<tr><td colspan="5" class="text-center py-8 text-base-content/40 text-sm">No notifications</td></tr>`;
|
|
28
|
+
|
|
29
|
+
const content = `
|
|
30
|
+
<div class="page-header">
|
|
31
|
+
<h1 class="page-title">Notifications</h1>
|
|
32
|
+
<button onclick="markAllRead()" class="btn btn-ghost btn-sm">Mark All Read</button>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="card-clean">
|
|
35
|
+
<div class="card-clean-body">
|
|
36
|
+
<div class="table-wrap">
|
|
37
|
+
<table class="data-table">
|
|
38
|
+
<thead><tr><th>Time</th><th>Type</th><th>Message</th><th></th><th></th></tr></thead>
|
|
39
|
+
<tbody>${rows}</tbody>
|
|
40
|
+
</table>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const script = `${TOAST_SCRIPT}
|
|
47
|
+
async function markRead(id){try{await fetch('/api/notifications',{method:'PATCH',headers:{'Content-Type':'application/json'},credentials:'include',body:JSON.stringify({id})});var row=document.querySelector('[data-notif-id="'+id+'"]');if(row){row.classList.add('opacity-60');var btn=row.querySelector('button');if(btn)btn.replaceWith(document.createTextNode('Read'))}var count=document.getElementById('notif-count');if(count){var c=parseInt(count.textContent||'0',10)-1;if(c<=0)count.style.display='none';else count.textContent=c}}catch(e){showToast('Error','error')}}
|
|
48
|
+
async function markAllRead(){try{await fetch('/api/notifications',{method:'PATCH',headers:{'Content-Type':'application/json'},credentials:'include',body:JSON.stringify({})});document.querySelectorAll('[data-notif-id]').forEach(function(row){row.classList.add('opacity-60');var btn=row.querySelector('button');if(btn)btn.replaceWith(document.createTextNode('Read'))});var count=document.getElementById('notif-count');if(count)count.style.display='none';showToast('All marked read','success')}catch(e){showToast('Error','error')}}`;
|
|
49
|
+
|
|
50
|
+
return page(user, 'Notifications | Moonlanding', null, content, [script]);
|
|
51
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { list, get } from '@/lib/query-engine.js';
|
|
2
|
+
import { renderAuditDashboard, renderSystemHealth, renderAccessDenied, generateHtml, REDIRECT } from '@/ui/renderer.js';
|
|
3
|
+
import { renderSettingsHome, renderSettingsSystem, renderSettingsUsers, renderSettingsTeams, renderSettingsRfiSections } from '@/ui/settings-renderer.js';
|
|
4
|
+
import { renderSettingsTemplates, renderSettingsNotifications, renderSettingsIntegrations, renderSettingsChecklists, renderSettingsRecreation, renderSettingsEntityTypes, renderSettingsEngagementTypes } from '@/ui/settings-renderer-advanced.js';
|
|
5
|
+
import { renderSettingsReviewSettings, renderSettingsFileReview, renderSettingsTemplateManage, renderSettingsMwrPermissions } from '@/ui/settings-renderer-advanced2.js';
|
|
6
|
+
import { renderChecklistsManagement } from '@/ui/checklist-renderer.js';
|
|
7
|
+
import { renderJobManagement } from '@/ui/job-management-renderer.js';
|
|
8
|
+
import { isPartner, isManager } from '@/ui/permissions-ui.js';
|
|
9
|
+
import { getSystemConfig, getSettingsCounts, getAuditData, getSystemHealth, renderBuildLogsContent } from '@/ui/page-handler-helpers.js';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
const __dirname_adm = fileURLToPath(new URL('.', import.meta.url));
|
|
12
|
+
|
|
13
|
+
async function lazyRenderer(name) {
|
|
14
|
+
const t = globalThis.__reloadTs__ || Date.now();
|
|
15
|
+
return import(`file://${__dirname_adm}${name}?t=${t}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function handleAdminPage(normalized, segments, user) {
|
|
19
|
+
if (normalized === '/admin/audit') {
|
|
20
|
+
if (!isPartner(user) && !isManager(user)) return renderAccessDenied(user, 'admin', 'view');
|
|
21
|
+
const auditData = await getAuditData();
|
|
22
|
+
return renderAuditDashboard(user, auditData);
|
|
23
|
+
}
|
|
24
|
+
if (!isPartner(user)) return renderAccessDenied(user, 'admin', 'view');
|
|
25
|
+
|
|
26
|
+
if (normalized === '/admin/settings') {
|
|
27
|
+
const config = await getSystemConfig();
|
|
28
|
+
const counts = await getSettingsCounts();
|
|
29
|
+
return renderSettingsHome(user, config, counts);
|
|
30
|
+
}
|
|
31
|
+
if (normalized === '/admin/settings/system') { const config = await getSystemConfig(); return renderSettingsSystem(user, config); }
|
|
32
|
+
if (normalized === '/admin/settings/users') { return renderSettingsUsers(user, list('user', {})); }
|
|
33
|
+
if (normalized === '/admin/settings/teams') {
|
|
34
|
+
const { renderSettingsTeams: _rt } = await lazyRenderer('settings-renderer.js');
|
|
35
|
+
return _rt(user, list('team', {}), list('user', {}));
|
|
36
|
+
}
|
|
37
|
+
if (normalized === '/admin/settings/rfi-sections') {
|
|
38
|
+
let sections = []; try { sections = list('rfi_section', {}); } catch {}
|
|
39
|
+
return renderSettingsRfiSections(user, sections);
|
|
40
|
+
}
|
|
41
|
+
if (normalized === '/admin/settings/rfi-templates') {
|
|
42
|
+
const { renderSettingsRfiTemplates } = await lazyRenderer('settings-renderer-rfi-templates.js');
|
|
43
|
+
let templates = []; try { templates = list('rfi_template', {}); } catch {}
|
|
44
|
+
return renderSettingsRfiTemplates(user, templates);
|
|
45
|
+
}
|
|
46
|
+
if (normalized === '/admin/settings/templates') {
|
|
47
|
+
let templates = []; try { templates = list('review_template', {}); } catch {}
|
|
48
|
+
return renderSettingsTemplates(user, templates);
|
|
49
|
+
}
|
|
50
|
+
if (normalized === '/admin/settings/notifications') { return renderSettingsNotifications(user, await getSystemConfig()); }
|
|
51
|
+
if (normalized === '/admin/settings/integrations') {
|
|
52
|
+
let intSettings = {};
|
|
53
|
+
try { const { getDatabase } = await import('@/lib/database-core.js'); const row = getDatabase().prepare('SELECT value FROM system_settings WHERE key = ?').get('integration_settings'); intSettings = row ? JSON.parse(row.value) : {}; } catch {}
|
|
54
|
+
return renderSettingsIntegrations(user, intSettings);
|
|
55
|
+
}
|
|
56
|
+
if (normalized === '/admin/settings/checklists') {
|
|
57
|
+
let checklists = []; try { checklists = list('checklist', {}); } catch {}
|
|
58
|
+
return renderSettingsChecklists(user, checklists);
|
|
59
|
+
}
|
|
60
|
+
if (normalized === '/admin/settings/recreation') {
|
|
61
|
+
return renderSettingsRecreation(user);
|
|
62
|
+
}
|
|
63
|
+
if (normalized === '/admin/settings/entity-types') {
|
|
64
|
+
let items = []; try { items = list('entity_type', {}); } catch {}
|
|
65
|
+
return renderSettingsEntityTypes(user, items);
|
|
66
|
+
}
|
|
67
|
+
if (normalized === '/admin/settings/engagement-types') {
|
|
68
|
+
let items = []; try { items = list('engagement_type', {}); } catch {}
|
|
69
|
+
return renderSettingsEngagementTypes(user, items);
|
|
70
|
+
}
|
|
71
|
+
if (normalized === '/admin/settings/permissions') {
|
|
72
|
+
let permissions = []; try { permissions = list('permission', {}); } catch {}
|
|
73
|
+
return renderSettingsMwrPermissions(user, permissions);
|
|
74
|
+
}
|
|
75
|
+
if (normalized === '/admin/settings/review') { return renderSettingsReviewSettings(user, await getSystemConfig()); }
|
|
76
|
+
if (normalized === '/admin/settings/file-review') {
|
|
77
|
+
let frSettings = {};
|
|
78
|
+
try { const { getDatabase } = await import('@/lib/database-core.js'); const row = getDatabase().prepare('SELECT value FROM system_settings WHERE key = ?').get('file_review_settings'); frSettings = row ? JSON.parse(row.value) : {}; } catch {}
|
|
79
|
+
return renderSettingsFileReview(user, await getSystemConfig(), frSettings);
|
|
80
|
+
}
|
|
81
|
+
if (normalized.startsWith('/admin/settings/templates/') && segments.length === 4) {
|
|
82
|
+
const templateId = segments[3];
|
|
83
|
+
let template = {}, sections = [];
|
|
84
|
+
try { template = get('review_template', templateId) || {}; sections = list('review_template_section', {}).filter(s => s.review_template_id === templateId); } catch {}
|
|
85
|
+
return renderSettingsTemplateManage(user, template, sections);
|
|
86
|
+
}
|
|
87
|
+
if (normalized === '/admin/settings/checklists/manage') {
|
|
88
|
+
let checklists = [];
|
|
89
|
+
try { checklists = list('checklist', {}).map(c => { let items = []; try { items = list('checklist_item', {}).filter(i => i.checklist_id === c.id); } catch {} return { ...c, total_items: items.length }; }); } catch {}
|
|
90
|
+
return renderChecklistsManagement(user, checklists);
|
|
91
|
+
}
|
|
92
|
+
if (normalized === '/admin/build-logs') {
|
|
93
|
+
let logs = []; try { logs = list('build_log', {}); } catch {}
|
|
94
|
+
const content = renderBuildLogsContent(logs);
|
|
95
|
+
return generateHtml('Build Logs', content, []);
|
|
96
|
+
}
|
|
97
|
+
if (normalized === '/admin/health') { return renderSystemHealth(user, await getSystemHealth()); }
|
|
98
|
+
if (normalized === '/admin/settings/users/new') {
|
|
99
|
+
const { renderSettingsUserDetail } = await lazyRenderer('settings-user-team-renderer.js');
|
|
100
|
+
return renderSettingsUserDetail(user, {}, list('team', {}));
|
|
101
|
+
}
|
|
102
|
+
if (segments.length === 4 && segments[2] === 'users' && segments[3] !== 'new') {
|
|
103
|
+
const { renderSettingsUserDetail: _rUD } = await lazyRenderer('settings-user-team-renderer.js');
|
|
104
|
+
return _rUD(user, get('user', segments[3]) || {}, list('team', {}));
|
|
105
|
+
}
|
|
106
|
+
if (normalized === '/admin/settings/teams/new') {
|
|
107
|
+
const { renderSettingsTeamDetail } = await lazyRenderer('settings-user-team-renderer.js');
|
|
108
|
+
return renderSettingsTeamDetail(user, {}, list('user', {}));
|
|
109
|
+
}
|
|
110
|
+
if (segments.length === 4 && segments[2] === 'teams' && segments[3] !== 'new') {
|
|
111
|
+
const { renderSettingsTeamDetail: _rTD } = await lazyRenderer('settings-user-team-renderer.js');
|
|
112
|
+
return _rTD(user, get('team', segments[3]) || {}, list('user', {}));
|
|
113
|
+
}
|
|
114
|
+
if (normalized === '/admin/jobs') {
|
|
115
|
+
let jobs = [];
|
|
116
|
+
try { const { getConfigEngineSync } = await import('@/lib/config-generator-engine.js'); const engine = getConfigEngineSync(); const config = engine.getConfig(); jobs = (config.jobs || []).map(j => ({ ...j, status: j.enabled === false ? 'disabled' : 'scheduled' })); } catch {}
|
|
117
|
+
let logs = []; try { logs = list('job_log', {}).slice(0, 20); } catch {}
|
|
118
|
+
return renderJobManagement(user, jobs, logs);
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { list, get, count } from '@/lib/query-engine.js';
|
|
2
|
+
import { getConfigEngineSync } from '@/lib/config-generator-engine.js';
|
|
3
|
+
|
|
4
|
+
export function resolveEnumOptions(spec) {
|
|
5
|
+
const resolvedSpec = { ...spec, fields: { ...spec.fields } };
|
|
6
|
+
const engine = getConfigEngineSync();
|
|
7
|
+
const config = engine.getConfig();
|
|
8
|
+
for (const [fieldKey, field] of Object.entries(spec.fields || {})) {
|
|
9
|
+
if (field.type === 'enum' && typeof field.options === 'string' && field.options.includes('engagement_lifecycle.stages')) {
|
|
10
|
+
const stages = config.workflows?.engagement_lifecycle?.stages || [];
|
|
11
|
+
resolvedSpec.fields[fieldKey] = { ...field, options: stages.map(s => ({ value: s.name, label: s.label || s.name })) };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return resolvedSpec;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getRefOptions(spec) {
|
|
18
|
+
const refOptions = {};
|
|
19
|
+
for (const [fieldKey, field] of Object.entries(spec.fields || {}).filter(([k, f]) => f.type === 'ref' && f.ref)) {
|
|
20
|
+
try { refOptions[fieldKey] = list(field.ref, {}).map(r => ({ value: r.id, label: r.name || r.title || r.label || r.email || r.id })); }
|
|
21
|
+
catch { refOptions[fieldKey] = []; }
|
|
22
|
+
}
|
|
23
|
+
return refOptions;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolveRefFields(items, spec) {
|
|
27
|
+
try {
|
|
28
|
+
const refFields = Object.entries(spec.fields || {}).filter(([k, f]) => f.type === 'ref' && f.ref);
|
|
29
|
+
if (!refFields.length) return items;
|
|
30
|
+
const refCaches = {};
|
|
31
|
+
for (const [fieldKey, field] of refFields) {
|
|
32
|
+
const refIds = [...new Set(items.map(i => i[fieldKey]).filter(Boolean))];
|
|
33
|
+
if (refIds.length) try { refCaches[fieldKey] = Object.fromEntries(list(field.ref, {}).map(r => [r.id, r.name || r.title || r.label || r.id])); } catch {}
|
|
34
|
+
}
|
|
35
|
+
return items.map(item => {
|
|
36
|
+
const resolved = { ...item };
|
|
37
|
+
for (const [fieldKey] of refFields) {
|
|
38
|
+
if (item[fieldKey] && refCaches[fieldKey]) resolved[`${fieldKey}_display`] = refCaches[fieldKey][item[fieldKey]] || item[fieldKey];
|
|
39
|
+
}
|
|
40
|
+
return resolved;
|
|
41
|
+
});
|
|
42
|
+
} catch { return items; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function getDashboardStats(user) {
|
|
46
|
+
try {
|
|
47
|
+
const engagements = list('engagement', {});
|
|
48
|
+
const clients = list('client', {});
|
|
49
|
+
const clientMap = Object.fromEntries(clients.map(c => [c.id, c.name]));
|
|
50
|
+
const rfis = list('rfi', {});
|
|
51
|
+
const reviews = list('review', {});
|
|
52
|
+
const myRfis = user ? rfis.filter(r => r.assigned_to === user.id) : [];
|
|
53
|
+
const now = Math.floor(Date.now() / 1000);
|
|
54
|
+
const overdueRfis = rfis.filter(r => r.status !== 'closed' && r.due_date && r.due_date < now).map(r => ({ ...r, daysOverdue: Math.floor((now - r.due_date) / 86400) }));
|
|
55
|
+
const recentEngagements = [...engagements].sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0)).slice(0, 8).map(e => ({ ...e, client_name: clientMap[e.client_id] || e.client_name || '-' }));
|
|
56
|
+
return { engagements: engagements.length, clients: clients.length, rfis: rfis.length, reviews: reviews.length, myRfis, overdueRfis, recentEngagements };
|
|
57
|
+
} catch { return { engagements: 0, clients: 0, rfis: 0, reviews: 0, myRfis: [], overdueRfis: [], recentEngagements: [] }; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function getSettingsCounts() {
|
|
61
|
+
const counts = {};
|
|
62
|
+
const entities = { users: 'user', teams: 'team', rfiSections: 'rfi_section', templates: 'review_template', checklists: 'checklist', recreation: 'permission_audit', rfiTemplates: 'rfi_template' };
|
|
63
|
+
for (const [key, entity] of Object.entries(entities)) { try { counts[key] = count(entity); } catch { counts[key] = 0; } }
|
|
64
|
+
return counts;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function getSystemConfig() {
|
|
68
|
+
try {
|
|
69
|
+
const { getConfigEngineSync: gce } = await import('@/lib/config-generator-engine.js');
|
|
70
|
+
const engine = gce();
|
|
71
|
+
const config = engine.getConfig();
|
|
72
|
+
return { database: config.system?.database || {}, server: config.system?.server || {}, thresholds: config.thresholds || {} };
|
|
73
|
+
} catch { return {}; }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function getAuditData() {
|
|
77
|
+
try {
|
|
78
|
+
const audits = list('permission_audit', {});
|
|
79
|
+
const cutoff = Math.floor(Date.now() / 1000) - 30 * 86400;
|
|
80
|
+
const recent = audits.filter(a => (a.timestamp || a.created_at) > cutoff);
|
|
81
|
+
return { summary: { total_actions: recent.length, grants: recent.filter(a => a.action === 'grant').length, revokes: recent.filter(a => a.action === 'revoke').length, role_changes: recent.filter(a => a.action === 'role_change').length }, recentActivity: audits.slice(0, 50) };
|
|
82
|
+
} catch { return { summary: {}, recentActivity: [] }; }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getClientDashboardStats(clientId) {
|
|
86
|
+
try {
|
|
87
|
+
const engagements = list('engagement', {}).filter(e => e.client_id === clientId);
|
|
88
|
+
const rfis = list('rfi', {}).filter(r => engagements.some(e => e.id === r.engagement_id));
|
|
89
|
+
const reviews = list('review', {}).filter(r => engagements.some(e => e.id === r.engagement_id));
|
|
90
|
+
const users = list('user', {}).filter(u => u.client_id === clientId);
|
|
91
|
+
return { engagements: engagements.length, activeRfis: rfis.filter(r => r.status !== 'closed' && r.status !== 'completed').length, users: users.length, reviews: reviews.length, engagementList: engagements.slice(0, 10) };
|
|
92
|
+
} catch { return { engagements: 0, activeRfis: 0, users: 0, reviews: 0, engagementList: [] }; }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function getSystemHealth() {
|
|
96
|
+
try {
|
|
97
|
+
const entityCounts = {};
|
|
98
|
+
for (const e of ['user','client','engagement','rfi','review','team']) { try { entityCounts[e] = count(e); } catch { entityCounts[e] = 0; } }
|
|
99
|
+
const u = process.uptime();
|
|
100
|
+
return { database: { status: 'Connected', type: 'SQLite' }, server: { port: 3004, uptime: `${Math.floor(u/3600)}h ${Math.floor((u%3600)/60)}m` }, entities: entityCounts };
|
|
101
|
+
} catch { return {}; }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function renderBuildLogsContent(logs) {
|
|
105
|
+
const rows = (logs || []).map(l => {
|
|
106
|
+
const sts = l.status === 'success' ? '<span class="pill pill-success">Success</span>' : l.status === 'failed' ? '<span class="pill pill-danger">Failed</span>' : `<span class="pill pill-warning">${l.status || 'pending'}</span>`;
|
|
107
|
+
const ts = l.created_at ? new Date(l.created_at * 1000).toLocaleString() : '-';
|
|
108
|
+
return `<tr><td style="padding:8px 12px">${l.version||'-'}</td><td style="padding:8px 12px">${sts}</td><td style="padding:8px 12px;font-size:0.78rem;color:var(--color-text-muted)">${ts}</td><td style="padding:8px 12px">${l.duration?l.duration+'s':'-'}</td><td style="padding:8px 12px;font-size:0.78rem">${l.message||'-'}</td></tr>`;
|
|
109
|
+
}).join('');
|
|
110
|
+
return `<div style="margin-bottom:24px"><h1 class="page-title">Build Logs</h1></div><div class="table-wrap" style="overflow-x:auto"><table class="data-table"><thead><tr><th>Version</th><th>Status</th><th>Time</th><th>Duration</th><th>Message</th></tr></thead><tbody>${rows||'<tr><td colspan="5" style="padding:32px;text-align:center;color:var(--color-text-muted)">No build logs</td></tr>'}</tbody></table></div>`;
|
|
111
|
+
}
|