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,43 @@
|
|
|
1
|
+
import { recordRequest, recordError } from '@/lib/metrics-collector.js'
|
|
2
|
+
|
|
3
|
+
function wrapHandler(handler, metadata = {}) {
|
|
4
|
+
return async (request, context) => {
|
|
5
|
+
const start = process.hrtime.bigint()
|
|
6
|
+
const method = request.method
|
|
7
|
+
const path = new URL(request.url).pathname
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const response = await handler(request, context)
|
|
11
|
+
const duration = Number(process.hrtime.bigint() - start) / 1000000
|
|
12
|
+
const status = response.status || 200
|
|
13
|
+
|
|
14
|
+
recordRequest(path, method, duration, status)
|
|
15
|
+
|
|
16
|
+
return response
|
|
17
|
+
} catch (err) {
|
|
18
|
+
const duration = Number(process.hrtime.bigint() - start) / 1000000
|
|
19
|
+
|
|
20
|
+
recordRequest(path, method, duration, 500)
|
|
21
|
+
recordError(path, method, err.message, err.stack)
|
|
22
|
+
|
|
23
|
+
throw err
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function trackRequest(path, method, duration, status) {
|
|
29
|
+
recordRequest(path, method, duration, status)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function trackError(path, method, error, stack) {
|
|
33
|
+
recordError(path, method, error, stack)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { wrapHandler, trackRequest, trackError }
|
|
37
|
+
|
|
38
|
+
if (typeof globalThis !== 'undefined') {
|
|
39
|
+
globalThis.__requestTracker = {
|
|
40
|
+
trackRequest,
|
|
41
|
+
trackError
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function generateResourceHints(pathname) {
|
|
2
|
+
const hints = [];
|
|
3
|
+
|
|
4
|
+
hints.push({ rel: 'preload', href: '/ui/client.js', as: 'script' });
|
|
5
|
+
hints.push({ rel: 'preload', href: '/ui/styles.css', as: 'style' });
|
|
6
|
+
hints.push({ rel: 'preload', href: '/lib/webjsx/runtime.js', as: 'script' });
|
|
7
|
+
|
|
8
|
+
hints.push({ rel: 'dns-prefetch', href: '//fonts.googleapis.com' });
|
|
9
|
+
|
|
10
|
+
if (pathname === '/' || pathname === '/login') {
|
|
11
|
+
hints.push({ rel: 'prefetch', href: '/api/friday/engagement' });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (pathname.includes('/engagement')) {
|
|
15
|
+
hints.push({ rel: 'prefetch', href: '/api/friday/rfi' });
|
|
16
|
+
hints.push({ rel: 'prefetch', href: '/api/mwr/review' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return hints;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function renderResourceHints(hints) {
|
|
23
|
+
return hints.map(h => {
|
|
24
|
+
const attrs = Object.entries(h)
|
|
25
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
26
|
+
.join(' ');
|
|
27
|
+
return `<link ${attrs}>`;
|
|
28
|
+
}).join('\n ');
|
|
29
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import os from 'os'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import { recordResource } from '@/lib/metrics-collector.js'
|
|
4
|
+
|
|
5
|
+
let monitoringInterval = null
|
|
6
|
+
let dbPath = null
|
|
7
|
+
|
|
8
|
+
function getCPUUsage() {
|
|
9
|
+
const cpus = os.cpus()
|
|
10
|
+
let totalIdle = 0
|
|
11
|
+
let totalTick = 0
|
|
12
|
+
|
|
13
|
+
cpus.forEach(cpu => {
|
|
14
|
+
for (const type in cpu.times) {
|
|
15
|
+
totalTick += cpu.times[type]
|
|
16
|
+
}
|
|
17
|
+
totalIdle += cpu.times.idle
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const idle = totalIdle / cpus.length
|
|
21
|
+
const total = totalTick / cpus.length
|
|
22
|
+
const usage = 1 - (idle / total)
|
|
23
|
+
|
|
24
|
+
return Math.max(0, Math.min(1, usage))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getMemoryUsage() {
|
|
28
|
+
const totalMem = os.totalmem()
|
|
29
|
+
const freeMem = os.freemem()
|
|
30
|
+
const usedMem = totalMem - freeMem
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
total: totalMem,
|
|
34
|
+
used: usedMem,
|
|
35
|
+
free: freeMem,
|
|
36
|
+
percentage: usedMem / totalMem
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getDiskUsage() {
|
|
41
|
+
if (!dbPath) return null
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const stats = fs.statSync(dbPath)
|
|
45
|
+
const totalSpace = 1024 * 1024 * 1024 * 100
|
|
46
|
+
const usedSpace = stats.size
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
total: totalSpace,
|
|
50
|
+
used: usedSpace,
|
|
51
|
+
percentage: usedSpace / totalSpace
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function collectResources() {
|
|
59
|
+
try {
|
|
60
|
+
const cpu = getCPUUsage()
|
|
61
|
+
const memory = getMemoryUsage()
|
|
62
|
+
const disk = getDiskUsage()
|
|
63
|
+
|
|
64
|
+
recordResource(
|
|
65
|
+
cpu,
|
|
66
|
+
memory.percentage,
|
|
67
|
+
disk?.percentage
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return { cpu, memory, disk }
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error('[ResourceMonitor] Collection error:', err)
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function startMonitoring(interval = 5000, databasePath = null) {
|
|
78
|
+
if (monitoringInterval) {
|
|
79
|
+
stopMonitoring()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
dbPath = databasePath
|
|
83
|
+
|
|
84
|
+
monitoringInterval = setInterval(() => {
|
|
85
|
+
collectResources()
|
|
86
|
+
}, interval)
|
|
87
|
+
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function stopMonitoring() {
|
|
91
|
+
if (monitoringInterval) {
|
|
92
|
+
clearInterval(monitoringInterval)
|
|
93
|
+
monitoringInterval = null
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getCurrentResources() {
|
|
98
|
+
return collectResources()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export {
|
|
102
|
+
getCPUUsage,
|
|
103
|
+
getMemoryUsage,
|
|
104
|
+
getDiskUsage,
|
|
105
|
+
collectResources,
|
|
106
|
+
startMonitoring,
|
|
107
|
+
stopMonitoring,
|
|
108
|
+
getCurrentResources
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (typeof globalThis !== 'undefined') {
|
|
112
|
+
globalThis.__resources = {
|
|
113
|
+
getCurrentResources,
|
|
114
|
+
startMonitoring,
|
|
115
|
+
stopMonitoring
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Formatter - Standardized API response helpers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { HTTP } from '../config/constants.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Successful response with data
|
|
9
|
+
* @param {any} data
|
|
10
|
+
* @returns {Response} NextResponse-compatible
|
|
11
|
+
*/
|
|
12
|
+
export function ok(data) {
|
|
13
|
+
return new Response(JSON.stringify(data), {
|
|
14
|
+
status: HTTP.OK,
|
|
15
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Created response (201)
|
|
21
|
+
* @param {any} data
|
|
22
|
+
* @returns {Response}
|
|
23
|
+
*/
|
|
24
|
+
export function created(data) {
|
|
25
|
+
return new Response(JSON.stringify(data), {
|
|
26
|
+
status: HTTP.CREATED,
|
|
27
|
+
headers: { 'Content-Type': 'application/json' },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Paginated response
|
|
33
|
+
* @param {Array} items
|
|
34
|
+
* @param {object} pagination
|
|
35
|
+
* @returns {Response}
|
|
36
|
+
*/
|
|
37
|
+
export function paginated(items, pagination) {
|
|
38
|
+
return new Response(JSON.stringify({
|
|
39
|
+
items,
|
|
40
|
+
pagination,
|
|
41
|
+
}), {
|
|
42
|
+
status: HTTP.OK,
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* No content response (204)
|
|
49
|
+
* @returns {Response}
|
|
50
|
+
*/
|
|
51
|
+
export function noContent() {
|
|
52
|
+
return new Response(null, { status: HTTP.NO_CONTENT });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Error response
|
|
57
|
+
* @param {string} message
|
|
58
|
+
* @param {number} status
|
|
59
|
+
* @param {string} code
|
|
60
|
+
* @returns {Response}
|
|
61
|
+
*/
|
|
62
|
+
export function error(message, status = HTTP.INTERNAL_ERROR, code = 'ERROR') {
|
|
63
|
+
return new Response(JSON.stringify({ error: message, code }), {
|
|
64
|
+
status,
|
|
65
|
+
headers: { 'Content-Type': 'application/json' },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* With metadata wrapper
|
|
71
|
+
* @param {any} data
|
|
72
|
+
* @param {number} status
|
|
73
|
+
* @param {string} type
|
|
74
|
+
* @returns {object}
|
|
75
|
+
*/
|
|
76
|
+
export function withMetadata(data, status, type = 'success') {
|
|
77
|
+
return { ...data, _meta: { status, type, timestamp: Date.now() } };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
import { HTTP } from '../config/constants.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getSpec, getNavItems } from '@/config/spec-helpers';
|
|
2
|
+
import { get, getChildren } from '@/engine';
|
|
3
|
+
import { can } from '@/services/permission.service';
|
|
4
|
+
import { withPageAuth } from '@/lib/auth-middleware';
|
|
5
|
+
import { notFound } from '@/lib/next-polyfills';
|
|
6
|
+
import { loadFormOptions } from '@/lib/utils';
|
|
7
|
+
|
|
8
|
+
export { withPageAuth as requireEntityAccess };
|
|
9
|
+
|
|
10
|
+
export async function getEntityData(entityName, id, options = {}) {
|
|
11
|
+
const { user, spec } = await withPageAuth(entityName, 'view', options);
|
|
12
|
+
const data = get(entityName, id);
|
|
13
|
+
if (!data) notFound();
|
|
14
|
+
const children = spec.children ? Object.fromEntries(Object.entries(spec.children).map(([k, def]) => [k, getChildren(entityName, id, def)])) : {};
|
|
15
|
+
return { user, spec, data, children };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function getFormContext(entityName, id = null, action = 'create') {
|
|
19
|
+
const { user, spec } = await withPageAuth(entityName, action, { redirectTo: id ? `/${entityName}/${id}` : `/${entityName}` });
|
|
20
|
+
const data = id ? get(entityName, id) : {};
|
|
21
|
+
if (id && !data) notFound();
|
|
22
|
+
const options = await loadFormOptions(spec);
|
|
23
|
+
return { user, spec, data, options };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const getPageProps = (user, spec) => ({ user, nav: getNavItems(), canCreate: can(user, spec, 'create'), canEdit: can(user, spec, 'edit'), canDelete: can(user, spec, 'delete') });
|
|
27
|
+
|
|
28
|
+
const createMeta = (fn) => (...args) => { try { return fn(...args); } catch { return { title: 'Not Found' }; } };
|
|
29
|
+
|
|
30
|
+
export const listMetadata = createMeta((entity) => ({ title: getSpec(entity).labelPlural }));
|
|
31
|
+
export const detailMetadata = createMeta((entity, id) => { const spec = getSpec(entity), data = get(entity, id); return { title: data?.name || data?.email || spec.label }; });
|
|
32
|
+
export const newMetadata = createMeta((entity) => ({ title: `New ${getSpec(entity).label}` }));
|
|
33
|
+
export const editMetadata = createMeta((entity, id) => { const spec = getSpec(entity), data = get(entity, id); return { title: `Edit ${data?.name || spec.label}` }; });
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export function resolveParamRoute(baseDir, segments) {
|
|
5
|
+
if (!fs.existsSync(baseDir)) return null;
|
|
6
|
+
if (segments.length === 0) {
|
|
7
|
+
const r = path.join(baseDir, 'route.js');
|
|
8
|
+
return fs.existsSync(r) ? r : null;
|
|
9
|
+
}
|
|
10
|
+
const [seg, ...rest] = segments;
|
|
11
|
+
const entries = fs.readdirSync(baseDir, { withFileTypes: true }).filter(e => e.isDirectory());
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
const name = entry.name;
|
|
14
|
+
if (name === seg || name.startsWith('[')) {
|
|
15
|
+
const found = resolveParamRoute(path.join(baseDir, name), rest);
|
|
16
|
+
if (found) return found;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveSpecificParams(routeFile, pathParts) {
|
|
23
|
+
const result = {};
|
|
24
|
+
const routeRelative = routeFile.replace(/.*src\/app\/api\//, '').replace(/\/route\.js$/, '');
|
|
25
|
+
const routeSegments = routeRelative.split('/');
|
|
26
|
+
const urlSegments = pathParts.slice(0);
|
|
27
|
+
for (let i = 0; i < routeSegments.length && i < urlSegments.length; i++) {
|
|
28
|
+
const seg = routeSegments[i];
|
|
29
|
+
if (seg.startsWith('[') && seg.endsWith(']')) {
|
|
30
|
+
const paramName = seg.replace(/^\[\.\.\./, '').replace(/[\[\]]/g, '');
|
|
31
|
+
result[paramName] = urlSegments[i];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function singularize(name) {
|
|
38
|
+
if (name.endsWith('ies')) return name.slice(0, -3) + 'y';
|
|
39
|
+
if (name.endsWith('ses') || name.endsWith('xes') || name.endsWith('zes')) return name.slice(0, -2);
|
|
40
|
+
if (name.endsWith('s') && !name.endsWith('ss')) return name.slice(0, -1);
|
|
41
|
+
return name;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildNestedRoutePath(baseDir, domain, parentEntity, childParts) {
|
|
45
|
+
if (childParts.length === 0) return null;
|
|
46
|
+
|
|
47
|
+
function buildSegments(parentParam, childParamFn) {
|
|
48
|
+
const segs = [domain, parentEntity, parentParam];
|
|
49
|
+
for (let i = 0; i < childParts.length; i++) {
|
|
50
|
+
if (i % 2 === 0) segs.push(childParts[i]);
|
|
51
|
+
else segs.push(childParamFn(i));
|
|
52
|
+
}
|
|
53
|
+
return path.join(baseDir, 'src/app/api', ...segs, 'route.js');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const variants = [
|
|
57
|
+
() => buildSegments('[id]', (i) => `[${childParts[i - 1]}Id]`),
|
|
58
|
+
() => buildSegments('[id]', (i) => `[${singularize(childParts[i - 1])}Id]`),
|
|
59
|
+
() => buildSegments(`[${parentEntity}Id]`, (i) => `[${childParts[i - 1]}Id]`),
|
|
60
|
+
() => buildSegments(`[${parentEntity}Id]`, (i) => `[${singularize(childParts[i - 1])}Id]`),
|
|
61
|
+
() => buildSegments('[id]', () => `[${childParts[0]}Id]`),
|
|
62
|
+
() => buildSegments('[id]', () => `[${singularize(childParts[0])}Id]`),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
for (const variant of variants) {
|
|
66
|
+
const candidate = variant();
|
|
67
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
68
|
+
}
|
|
69
|
+
return variants[0]();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const DOMAINS = ['friday', 'mwr'];
|
|
73
|
+
|
|
74
|
+
export function resolveRoute(__dirname, pathname, url) {
|
|
75
|
+
const pathParts = pathname.slice(5).split('/').filter(Boolean);
|
|
76
|
+
const firstPart = pathParts[0];
|
|
77
|
+
const isDomain = DOMAINS.includes(firstPart);
|
|
78
|
+
let routeFile = null;
|
|
79
|
+
let params = {};
|
|
80
|
+
|
|
81
|
+
if (isDomain) {
|
|
82
|
+
const domain = firstPart;
|
|
83
|
+
const domainParts = pathParts.slice(1);
|
|
84
|
+
|
|
85
|
+
const specificCheck = path.join(__dirname, `src/app/api/${domain}/${domainParts.join('/')}/route.js`);
|
|
86
|
+
if (fs.existsSync(specificCheck)) {
|
|
87
|
+
routeFile = specificCheck;
|
|
88
|
+
params = resolveSpecificParams(specificCheck, pathParts);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!routeFile && domainParts.length >= 3) {
|
|
92
|
+
const parentEntity = domainParts[0];
|
|
93
|
+
const childParts = domainParts.slice(2);
|
|
94
|
+
const parentId = domainParts[1];
|
|
95
|
+
const childEntity = childParts[0];
|
|
96
|
+
const childId = childParts[1] || null;
|
|
97
|
+
|
|
98
|
+
const nestedSpecific = buildNestedRoutePath(__dirname, domain, parentEntity, childParts);
|
|
99
|
+
if (nestedSpecific && fs.existsSync(nestedSpecific)) {
|
|
100
|
+
routeFile = nestedSpecific;
|
|
101
|
+
params = resolveSpecificParams(nestedSpecific, pathParts);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!routeFile) {
|
|
105
|
+
routeFile = path.join(__dirname, 'src/app/api/[entity]/[[...path]]/route.js');
|
|
106
|
+
url.searchParams.set('domain', domain);
|
|
107
|
+
params = { entity: childEntity, path: childId ? [childId] : [], parentEntity, parentId };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!routeFile && domainParts.length >= 1) {
|
|
112
|
+
const entity = domainParts[0];
|
|
113
|
+
const entityPath = domainParts.slice(1);
|
|
114
|
+
routeFile = path.join(__dirname, 'src/app/api/[entity]/[[...path]]/route.js');
|
|
115
|
+
url.searchParams.set('domain', domain);
|
|
116
|
+
params = { entity, path: entityPath };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!routeFile && firstPart) {
|
|
121
|
+
const exactRoute = path.join(__dirname, `src/app/api/${pathParts.join('/')}/route.js`);
|
|
122
|
+
if (fs.existsSync(exactRoute)) {
|
|
123
|
+
routeFile = exactRoute;
|
|
124
|
+
params = resolveSpecificParams(exactRoute, pathParts);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!routeFile && firstPart) {
|
|
129
|
+
const paramRouteFile = resolveParamRoute(path.join(__dirname, 'src/app/api', firstPart), pathParts.slice(1));
|
|
130
|
+
if (paramRouteFile) {
|
|
131
|
+
routeFile = paramRouteFile;
|
|
132
|
+
params = resolveSpecificParams(paramRouteFile, pathParts);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!routeFile) {
|
|
137
|
+
routeFile = path.join(__dirname, 'src/app/api/[entity]/[[...path]]/route.js');
|
|
138
|
+
params = { entity: firstPart, path: pathParts.slice(1) };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { routeFile, params, isDomain, firstPart, pathParts };
|
|
142
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export function loadEnv(filePath) {
|
|
5
|
+
try {
|
|
6
|
+
const envFile = fs.readFileSync(filePath, 'utf-8');
|
|
7
|
+
envFile.split('\n').forEach(line => {
|
|
8
|
+
const trimmed = line.trim();
|
|
9
|
+
if (!trimmed || trimmed.startsWith('#')) return;
|
|
10
|
+
const [key, ...valueParts] = trimmed.split('=');
|
|
11
|
+
const value = valueParts.join('=').replace(/^['"]|['"]$/g, '');
|
|
12
|
+
if (key) process.env[key] = value;
|
|
13
|
+
});
|
|
14
|
+
} catch (e) {
|
|
15
|
+
console.warn('[Server] .env file not loaded:', e.message);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function setupProcessGuards() {
|
|
20
|
+
process.on('uncaughtException', (err) => {
|
|
21
|
+
console.error('[FATAL] Uncaught exception (process kept alive):', err?.message || err);
|
|
22
|
+
if (err?.stack) console.error(err.stack);
|
|
23
|
+
});
|
|
24
|
+
process.on('unhandledRejection', (reason) => {
|
|
25
|
+
console.error('[FATAL] Unhandled rejection (process kept alive):', reason?.message || reason);
|
|
26
|
+
if (reason?.stack) console.error(reason.stack);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function setupHotReload(__dirname, moduleCache, onConfigReset) {
|
|
31
|
+
const watchedDirs = ['src/config', 'src/app/api', 'src/ui', 'src/plugins'].map(d => path.join(__dirname, d));
|
|
32
|
+
watchedDirs.forEach((dir) => {
|
|
33
|
+
if (!fs.existsSync(dir)) return;
|
|
34
|
+
try {
|
|
35
|
+
const watcher = fs.watch(dir, { recursive: true }, async (eventType, filename) => {
|
|
36
|
+
if (filename && (filename.endsWith('.js') || filename.endsWith('.jsx') || filename.endsWith('.yml'))) {
|
|
37
|
+
moduleCache.clear();
|
|
38
|
+
globalThis.__reloadTs__ = Date.now();
|
|
39
|
+
console.log(`[Hot] Invalidated: ${filename}`);
|
|
40
|
+
if (filename.endsWith('master-config.yml') || filename.includes('master-config')) {
|
|
41
|
+
try {
|
|
42
|
+
const { resetConfigEngine } = await import('./config-generator-engine.js');
|
|
43
|
+
resetConfigEngine();
|
|
44
|
+
onConfigReset?.();
|
|
45
|
+
} catch (e) { console.log('[Hot] Could not reset config engine:', e.message); }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
watcher.on('error', (err) => console.error(`[Hot] Watcher error on ${dir}:`, err.message));
|
|
50
|
+
} catch (err) { console.error(`[Hot] Failed to watch ${dir}:`, err.message); }
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function loadModule(moduleCache, filePath) {
|
|
55
|
+
const cached = moduleCache.get(filePath);
|
|
56
|
+
if (cached) return cached;
|
|
57
|
+
const module = await import(`file://${filePath}?t=${Date.now()}`);
|
|
58
|
+
moduleCache.set(filePath, module);
|
|
59
|
+
return module;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function setSecurityHeaders(res, nodeEnv) {
|
|
63
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
64
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
65
|
+
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
66
|
+
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
67
|
+
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
|
68
|
+
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
|
69
|
+
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com data:; connect-src 'self' wss: ws:; frame-ancestors 'none'");
|
|
70
|
+
if (nodeEnv === 'production') res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
71
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
export function createStagePipeline(options = {}) {
|
|
2
|
+
const {
|
|
3
|
+
stages = [],
|
|
4
|
+
currentStageIndex = 0,
|
|
5
|
+
onStageClick = null,
|
|
6
|
+
readOnly = false
|
|
7
|
+
} = options;
|
|
8
|
+
|
|
9
|
+
const container = document.createElement('div');
|
|
10
|
+
container.className = 'stage-pipeline';
|
|
11
|
+
container.style.cssText = `
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
gap: 1rem;
|
|
15
|
+
padding: 1rem;
|
|
16
|
+
background: white;
|
|
17
|
+
border-radius: 8px;
|
|
18
|
+
border: 1px solid #eee;
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
stages.forEach((stage, idx) => {
|
|
22
|
+
const isComplete = idx < currentStageIndex;
|
|
23
|
+
const isCurrent = idx === currentStageIndex;
|
|
24
|
+
const isUpcoming = idx > currentStageIndex;
|
|
25
|
+
|
|
26
|
+
const stageItem = document.createElement('div');
|
|
27
|
+
stageItem.className = `stage-item stage-${isComplete ? 'complete' : isCurrent ? 'current' : 'upcoming'}`;
|
|
28
|
+
stageItem.style.cssText = `
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: 0.5rem;
|
|
33
|
+
flex: 1;
|
|
34
|
+
cursor: ${!readOnly && onStageClick ? 'pointer' : 'default'};
|
|
35
|
+
transition: all 0.2s;
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const circle = document.createElement('div');
|
|
39
|
+
circle.style.cssText = `
|
|
40
|
+
width: 40px;
|
|
41
|
+
height: 40px;
|
|
42
|
+
border-radius: 50%;
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
font-weight: 600;
|
|
47
|
+
font-size: 0.875rem;
|
|
48
|
+
color: white;
|
|
49
|
+
background: ${isComplete ? '#28a745' : isCurrent ? '#007bff' : '#e0e0e0'};
|
|
50
|
+
border: 2px solid ${isCurrent ? '#0056b3' : 'transparent'};
|
|
51
|
+
transition: all 0.2s;
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
if (isComplete) {
|
|
55
|
+
circle.textContent = '✓';
|
|
56
|
+
} else {
|
|
57
|
+
circle.textContent = idx + 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const label = document.createElement('div');
|
|
61
|
+
label.style.cssText = `
|
|
62
|
+
text-align: center;
|
|
63
|
+
font-size: 0.75rem;
|
|
64
|
+
font-weight: ${isCurrent ? '600' : '500'};
|
|
65
|
+
color: ${isCurrent ? '#007bff' : isComplete ? '#28a745' : '#999'};
|
|
66
|
+
max-width: 80px;
|
|
67
|
+
overflow: hidden;
|
|
68
|
+
text-overflow: ellipsis;
|
|
69
|
+
white-space: nowrap;
|
|
70
|
+
`;
|
|
71
|
+
label.textContent = stage.label || stage.name || `Stage ${idx + 1}`;
|
|
72
|
+
|
|
73
|
+
if (idx < stages.length - 1) {
|
|
74
|
+
const connector = document.createElement('div');
|
|
75
|
+
connector.style.cssText = `
|
|
76
|
+
position: absolute;
|
|
77
|
+
width: calc(100% / ${stages.length} - 1rem);
|
|
78
|
+
height: 2px;
|
|
79
|
+
top: 20px;
|
|
80
|
+
left: calc(${idx * (100 / stages.length) + 50 / stages.length}%);
|
|
81
|
+
background: ${isComplete || (isCurrent && idx < currentStageIndex) ? '#28a745' : '#e0e0e0'};
|
|
82
|
+
transition: all 0.2s;
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
stageItem.appendChild(circle);
|
|
87
|
+
stageItem.appendChild(label);
|
|
88
|
+
|
|
89
|
+
if (!readOnly && onStageClick) {
|
|
90
|
+
stageItem.onmouseover = () => {
|
|
91
|
+
circle.style.transform = 'scale(1.1)';
|
|
92
|
+
};
|
|
93
|
+
stageItem.onmouseout = () => {
|
|
94
|
+
circle.style.transform = 'scale(1)';
|
|
95
|
+
};
|
|
96
|
+
stageItem.onclick = () => {
|
|
97
|
+
onStageClick(idx, stage);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
container.appendChild(stageItem);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
element: container,
|
|
106
|
+
setCurrentStage: (idx) => {
|
|
107
|
+
container.querySelectorAll('.stage-item').forEach((el, i) => {
|
|
108
|
+
const circle = el.querySelector('div:first-child');
|
|
109
|
+
const label = el.querySelector('div:last-child');
|
|
110
|
+
|
|
111
|
+
if (i < idx) {
|
|
112
|
+
circle.style.background = '#28a745';
|
|
113
|
+
circle.textContent = '✓';
|
|
114
|
+
label.style.color = '#28a745';
|
|
115
|
+
} else if (i === idx) {
|
|
116
|
+
circle.style.background = '#007bff';
|
|
117
|
+
label.style.color = '#007bff';
|
|
118
|
+
label.style.fontWeight = '600';
|
|
119
|
+
} else {
|
|
120
|
+
circle.style.background = '#e0e0e0';
|
|
121
|
+
label.style.color = '#999';
|
|
122
|
+
label.style.fontWeight = '500';
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function renderStageSelector(stages, currentIndex, onChange) {
|
|
130
|
+
const select = document.createElement('select');
|
|
131
|
+
select.style.cssText = `
|
|
132
|
+
padding: 0.5rem;
|
|
133
|
+
border: 1px solid #ddd;
|
|
134
|
+
border-radius: 4px;
|
|
135
|
+
font-size: 1rem;
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
`;
|
|
138
|
+
|
|
139
|
+
stages.forEach((stage, idx) => {
|
|
140
|
+
const option = document.createElement('option');
|
|
141
|
+
option.value = idx;
|
|
142
|
+
option.textContent = stage.label || stage.name || `Stage ${idx + 1}`;
|
|
143
|
+
if (idx === currentIndex) option.selected = true;
|
|
144
|
+
select.appendChild(option);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
select.onchange = () => {
|
|
148
|
+
const newIdx = parseInt(select.value);
|
|
149
|
+
if (onChange) onChange(newIdx, stages[newIdx]);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return select;
|
|
153
|
+
}
|