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.
Files changed (221) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +398 -0
  3. package/package.json +73 -0
  4. package/src/adapters/google-auth.js +148 -0
  5. package/src/adapters/google-drive.js +209 -0
  6. package/src/app/api/[entity]/[[...path]]/route.js +33 -0
  7. package/src/app/api/audit/dashboard/route.js +77 -0
  8. package/src/app/api/audit/logs/route.js +46 -0
  9. package/src/app/api/audit/permissions/[id]/route.js +37 -0
  10. package/src/app/api/audit/permissions/route.js +93 -0
  11. package/src/app/api/audit/permissions/stats/route.js +29 -0
  12. package/src/app/api/audit/route.js +79 -0
  13. package/src/app/api/audit/stats/route.js +18 -0
  14. package/src/app/api/auth/google/callback/route.js +94 -0
  15. package/src/app/api/auth/google/route.js +58 -0
  16. package/src/app/api/auth/login/route.js +121 -0
  17. package/src/app/api/auth/logout/route.js +52 -0
  18. package/src/app/api/auth/me/route.js +16 -0
  19. package/src/app/api/auth/mwr-bridge/route.js +77 -0
  20. package/src/app/api/auth/password-reset/route.js +65 -0
  21. package/src/app/api/cron/trigger/route.js +58 -0
  22. package/src/app/api/csrf-token/route.js +8 -0
  23. package/src/app/api/debug/config/route.js +22 -0
  24. package/src/app/api/debug/hooks/route.js +16 -0
  25. package/src/app/api/debug/plugins/route.js +20 -0
  26. package/src/app/api/debug/sqlite/route.js +21 -0
  27. package/src/app/api/debug/sync/route.js +29 -0
  28. package/src/app/api/debug/workflow/route.js +20 -0
  29. package/src/app/api/domains/[domain]/route.js +26 -0
  30. package/src/app/api/domains/route.js +21 -0
  31. package/src/app/api/email/allocate/batch/route.js +106 -0
  32. package/src/app/api/email/allocate/route.js +141 -0
  33. package/src/app/api/email/receive/route.js +158 -0
  34. package/src/app/api/email/route.js +3 -0
  35. package/src/app/api/email/send/route.js +77 -0
  36. package/src/app/api/email/unallocated/route.js +47 -0
  37. package/src/app/api/files/[id]/route.js +38 -0
  38. package/src/app/api/health/route.js +95 -0
  39. package/src/app/api/metrics/route.js +73 -0
  40. package/src/app/api/monitoring/dashboard/route.js +18 -0
  41. package/src/cli.js +243 -0
  42. package/src/config/config-loader.js +112 -0
  43. package/src/config/constants.js +127 -0
  44. package/src/config/env.js +164 -0
  45. package/src/config/spec-helpers.js +232 -0
  46. package/src/engine.server.js +212 -0
  47. package/src/index.js +368 -0
  48. package/src/lib/accessibility.js +162 -0
  49. package/src/lib/action-factory.js +34 -0
  50. package/src/lib/action-utils.js +21 -0
  51. package/src/lib/alert-manager.js +189 -0
  52. package/src/lib/api-error-wrapper.js +125 -0
  53. package/src/lib/api-helpers.js +53 -0
  54. package/src/lib/api.js +82 -0
  55. package/src/lib/audit-logger-enhanced.js +117 -0
  56. package/src/lib/audit-logger.js +193 -0
  57. package/src/lib/auth-middleware.js +102 -0
  58. package/src/lib/auth-route-helpers.js +83 -0
  59. package/src/lib/business-rules-engine.js +86 -0
  60. package/src/lib/compression.js +44 -0
  61. package/src/lib/config-field-helpers.js +91 -0
  62. package/src/lib/config-generator-engine.js +445 -0
  63. package/src/lib/config-helpers.js +120 -0
  64. package/src/lib/connection-guard.js +79 -0
  65. package/src/lib/crud-action-helpers.js +34 -0
  66. package/src/lib/crud-factory.js +83 -0
  67. package/src/lib/crud-handlers.js +244 -0
  68. package/src/lib/csrf-protection.js +63 -0
  69. package/src/lib/database-core.js +258 -0
  70. package/src/lib/database-migrations.js +96 -0
  71. package/src/lib/date-utils.js +159 -0
  72. package/src/lib/db-backup.js +97 -0
  73. package/src/lib/db-monitor.js +127 -0
  74. package/src/lib/domain-loader.js +82 -0
  75. package/src/lib/email-sender.js +100 -0
  76. package/src/lib/error-boundary.js +134 -0
  77. package/src/lib/error-handler.js +84 -0
  78. package/src/lib/error-recovery.js +190 -0
  79. package/src/lib/error-resilience.js +130 -0
  80. package/src/lib/errors.js +69 -0
  81. package/src/lib/events-engine.js +182 -0
  82. package/src/lib/field-iterator.js +50 -0
  83. package/src/lib/field-registry.js +68 -0
  84. package/src/lib/field-types.js +154 -0
  85. package/src/lib/generic-crud-handler.js +32 -0
  86. package/src/lib/health-monitor.js +134 -0
  87. package/src/lib/hook-engine.js +169 -0
  88. package/src/lib/hot-reload/cache-invalidator.js +115 -0
  89. package/src/lib/hot-reload/checkpoint.js +95 -0
  90. package/src/lib/hot-reload/debug-exposure.js +67 -0
  91. package/src/lib/hot-reload/directory-watcher.js +96 -0
  92. package/src/lib/hot-reload/index.js +50 -0
  93. package/src/lib/hot-reload/mutex.js +75 -0
  94. package/src/lib/hot-reload/promise-container.js +66 -0
  95. package/src/lib/hot-reload/route-wrapper.js +46 -0
  96. package/src/lib/hot-reload/safe-error.js +51 -0
  97. package/src/lib/hot-reload/supervisor.js +161 -0
  98. package/src/lib/hot-reload/timeout-wrapper.js +52 -0
  99. package/src/lib/http-methods-factory.js +25 -0
  100. package/src/lib/index-optimizer.js +96 -0
  101. package/src/lib/index.js +35 -0
  102. package/src/lib/list-data-transform.js +39 -0
  103. package/src/lib/log-aggregator.js +116 -0
  104. package/src/lib/logger.js +55 -0
  105. package/src/lib/metrics-collector.js +102 -0
  106. package/src/lib/minifier.js +19 -0
  107. package/src/lib/monitoring-init.js +67 -0
  108. package/src/lib/next-compat.js +80 -0
  109. package/src/lib/next-polyfills.js +135 -0
  110. package/src/lib/perf-monitor.js +91 -0
  111. package/src/lib/progress-components.js +181 -0
  112. package/src/lib/query-cache.js +126 -0
  113. package/src/lib/query-engine-write.js +221 -0
  114. package/src/lib/query-engine.js +399 -0
  115. package/src/lib/query-perf.js +117 -0
  116. package/src/lib/query-string-adapter.js +75 -0
  117. package/src/lib/realtime-server.js +67 -0
  118. package/src/lib/render-cache.js +61 -0
  119. package/src/lib/request-tracker.js +43 -0
  120. package/src/lib/resource-hints.js +29 -0
  121. package/src/lib/resource-monitor.js +117 -0
  122. package/src/lib/response-formatter.js +80 -0
  123. package/src/lib/route-helpers.js +33 -0
  124. package/src/lib/route-resolver.js +142 -0
  125. package/src/lib/safe-json.js +8 -0
  126. package/src/lib/server-bootstrap.js +71 -0
  127. package/src/lib/stage-pipeline.js +153 -0
  128. package/src/lib/state-protocol.js +171 -0
  129. package/src/lib/state-transport-client.js +169 -0
  130. package/src/lib/state-transport-reconnect.js +121 -0
  131. package/src/lib/state-transport-server.js +181 -0
  132. package/src/lib/static-server.js +97 -0
  133. package/src/lib/status-helpers.js +98 -0
  134. package/src/lib/universal-handler.js +7 -0
  135. package/src/lib/utils.js +93 -0
  136. package/src/lib/validate.js +197 -0
  137. package/src/lib/validation/business-validators.js +61 -0
  138. package/src/lib/validation/csrf.js +51 -0
  139. package/src/lib/validation/file-validators.js +34 -0
  140. package/src/lib/validation/format-validators.js +106 -0
  141. package/src/lib/validation/index.js +19 -0
  142. package/src/lib/validation/rate-limit.js +31 -0
  143. package/src/lib/validation/security-validators.js +78 -0
  144. package/src/lib/validation-middleware.js +133 -0
  145. package/src/lib/validators.js +105 -0
  146. package/src/lib/with-audit-logging.js +63 -0
  147. package/src/lib/with-error-handler.js +31 -0
  148. package/src/lib/workflow-engine.js +250 -0
  149. package/src/server/server.js +305 -0
  150. package/src/services/collaborator-role.service.js +205 -0
  151. package/src/services/email-sender.js +105 -0
  152. package/src/services/notification-engine.js +110 -0
  153. package/src/services/permission.service.js +181 -0
  154. package/src/ui/advanced-search-renderer.js +42 -0
  155. package/src/ui/advanced-widgets.js +47 -0
  156. package/src/ui/auth-pages.js +114 -0
  157. package/src/ui/auth-styles.js +53 -0
  158. package/src/ui/client.js +99 -0
  159. package/src/ui/collaboration-dialogs.js +31 -0
  160. package/src/ui/common-handlers.js +156 -0
  161. package/src/ui/component-engine.js +103 -0
  162. package/src/ui/dashboard-renderer.js +150 -0
  163. package/src/ui/dialog-engine.js +147 -0
  164. package/src/ui/dialog-factory.js +38 -0
  165. package/src/ui/engagement-cards.js +76 -0
  166. package/src/ui/engagement-dialogs.js +100 -0
  167. package/src/ui/engagement-grid-renderer.js +109 -0
  168. package/src/ui/entity-renderer.js +176 -0
  169. package/src/ui/event-delegation.js +108 -0
  170. package/src/ui/fetch-json.js +24 -0
  171. package/src/ui/file-dialogs.js +81 -0
  172. package/src/ui/flexup-report-renderer.js +173 -0
  173. package/src/ui/format-helpers.js +102 -0
  174. package/src/ui/global-tags.js +144 -0
  175. package/src/ui/highlight-threading-renderer.js +176 -0
  176. package/src/ui/idle-logout.js +156 -0
  177. package/src/ui/job-management-renderer.js +61 -0
  178. package/src/ui/layout.js +219 -0
  179. package/src/ui/letter-dialogs.js +37 -0
  180. package/src/ui/ml-console-renderer.js +108 -0
  181. package/src/ui/monitoring-dashboard-client.js +136 -0
  182. package/src/ui/monitoring-dashboard.js +134 -0
  183. package/src/ui/notifications-renderer.js +51 -0
  184. package/src/ui/page-handler-admin.js +121 -0
  185. package/src/ui/page-handler-helpers.js +111 -0
  186. package/src/ui/page-handler-reviews.js +165 -0
  187. package/src/ui/page-handler-rfi.js +42 -0
  188. package/src/ui/page-handler.js +210 -0
  189. package/src/ui/password-reset-page.js +146 -0
  190. package/src/ui/perf-helpers.js +96 -0
  191. package/src/ui/perf-renderer.js +71 -0
  192. package/src/ui/permissions-ui.js +163 -0
  193. package/src/ui/picker-dialogs.js +100 -0
  194. package/src/ui/render-helpers.js +124 -0
  195. package/src/ui/renderer.js +35 -0
  196. package/src/ui/review-comparison-renderer.js +58 -0
  197. package/src/ui/review-detail-panels.js +71 -0
  198. package/src/ui/review-detail-renderer.js +170 -0
  199. package/src/ui/review-detail-script.js +95 -0
  200. package/src/ui/review-mwr-renderer.js +113 -0
  201. package/src/ui/review-renderer.js +202 -0
  202. package/src/ui/review-widgets.js +88 -0
  203. package/src/ui/review-zone-nav.js +12 -0
  204. package/src/ui/rfi-detail-renderer.js +191 -0
  205. package/src/ui/rfi-renderer.js +194 -0
  206. package/src/ui/rfi-report-renderer.js +56 -0
  207. package/src/ui/rippleui.css +1 -0
  208. package/src/ui/settings-renderer-advanced.js +195 -0
  209. package/src/ui/settings-renderer-advanced2.js +158 -0
  210. package/src/ui/settings-renderer-teams.js +112 -0
  211. package/src/ui/settings-renderer.js +166 -0
  212. package/src/ui/spacing-system.js +155 -0
  213. package/src/ui/standalone-login.js +109 -0
  214. package/src/ui/styles.css +2530 -0
  215. package/src/ui/styles2.css +1602 -0
  216. package/src/ui/test-page.js +23 -0
  217. package/src/ui/validation-rules.js +73 -0
  218. package/src/ui/validation-ui.js +147 -0
  219. package/src/ui/virtual-scroll.js +107 -0
  220. package/src/ui/webjsx.js +61 -0
  221. 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,8 @@
1
+ export const safeJsonParse = (str, fallback = null) => {
2
+ try {
3
+ return JSON.parse(str || 'null') ?? fallback;
4
+ } catch {
5
+ console.warn('[SafeJSON] Parse failed:', str?.substring(0, 50));
6
+ return fallback;
7
+ }
8
+ };
@@ -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
+ }