create-mercato-app 0.7.1-develop.7150.1.c1941e0c22 → 0.7.1-develop.7152.1.a69e92f9c9
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/agentic/shared/ai/harness/cases.json +4 -2
- package/dist/agentic/guides/module-facts.json +828 -149
- package/dist/agentic/guides/module-facts.v2.json +828 -149
- package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
- package/dist/agentic/guides/modules/api_docs/index.md +1 -1
- package/dist/agentic/guides/modules/api_keys/index.md +1 -1
- package/dist/agentic/guides/modules/attachments/index.md +1 -1
- package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
- package/dist/agentic/guides/modules/auth/index.md +1 -1
- package/dist/agentic/guides/modules/business_rules/index.md +1 -1
- package/dist/agentic/guides/modules/catalog/index.md +1 -1
- package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
- package/dist/agentic/guides/modules/channel_discord/index.md +1 -1
- package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
- package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
- package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
- package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
- package/dist/agentic/guides/modules/channel_resend/acl-features.md +12 -0
- package/dist/agentic/guides/modules/channel_resend/contribution-resolutions.md +11 -0
- package/dist/agentic/guides/modules/channel_resend/di-registrations-rich.md +12 -0
- package/dist/agentic/guides/modules/channel_resend/exact-override-targets.md +16 -0
- package/dist/agentic/guides/modules/channel_resend/index.md +18 -0
- package/dist/agentic/guides/modules/channel_resend/owned-contract-module-metadata.md +11 -0
- package/dist/agentic/guides/modules/channel_resend/setup.md +11 -0
- package/dist/agentic/guides/modules/channel_resend/umes-contributions.md +11 -0
- package/dist/agentic/guides/modules/channel_ses/acl-features.md +12 -0
- package/dist/agentic/guides/modules/channel_ses/contribution-resolutions.md +11 -0
- package/dist/agentic/guides/modules/channel_ses/di-registrations-rich.md +12 -0
- package/dist/agentic/guides/modules/channel_ses/exact-override-targets.md +16 -0
- package/dist/agentic/guides/modules/channel_ses/index.md +18 -0
- package/dist/agentic/guides/modules/channel_ses/owned-contract-module-metadata.md +11 -0
- package/dist/agentic/guides/modules/channel_ses/setup.md +11 -0
- package/dist/agentic/guides/modules/channel_ses/umes-contributions.md +11 -0
- package/dist/agentic/guides/modules/checkout/index.md +1 -1
- package/dist/agentic/guides/modules/communication_channels/di-registrations-rich.md +10 -9
- package/dist/agentic/guides/modules/communication_channels/exact-override-targets.md +12 -11
- package/dist/agentic/guides/modules/communication_channels/index.md +1 -1
- package/dist/agentic/guides/modules/communication_channels/setup.md +1 -1
- package/dist/agentic/guides/modules/configs/index.md +1 -1
- package/dist/agentic/guides/modules/content/index.md +1 -1
- package/dist/agentic/guides/modules/currencies/index.md +1 -1
- package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
- package/dist/agentic/guides/modules/customers/index.md +1 -1
- package/dist/agentic/guides/modules/dashboards/index.md +1 -1
- package/dist/agentic/guides/modules/data_sync/index.md +1 -1
- package/dist/agentic/guides/modules/design_system/index.md +1 -1
- package/dist/agentic/guides/modules/devices/index.md +1 -1
- package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
- package/dist/agentic/guides/modules/directory/index.md +1 -1
- package/dist/agentic/guides/modules/documents/index.md +1 -1
- package/dist/agentic/guides/modules/entities/index.md +1 -1
- package/dist/agentic/guides/modules/eudr/index.md +1 -1
- package/dist/agentic/guides/modules/events/index.md +1 -1
- package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
- package/dist/agentic/guides/modules/generators/index.md +1 -1
- package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
- package/dist/agentic/guides/modules/integrations/index.md +1 -1
- package/dist/agentic/guides/modules/messages/index.md +1 -1
- package/dist/agentic/guides/modules/notifications/index.md +1 -1
- package/dist/agentic/guides/modules/onboarding/index.md +1 -1
- package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
- package/dist/agentic/guides/modules/perspectives/index.md +1 -1
- package/dist/agentic/guides/modules/planner/index.md +1 -1
- package/dist/agentic/guides/modules/portal/index.md +1 -1
- package/dist/agentic/guides/modules/progress/index.md +1 -1
- package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
- package/dist/agentic/guides/modules/query_index/index.md +1 -1
- package/dist/agentic/guides/modules/record_locks/index.md +1 -1
- package/dist/agentic/guides/modules/resources/index.md +1 -1
- package/dist/agentic/guides/modules/sales/index.md +1 -1
- package/dist/agentic/guides/modules/scheduler/index.md +1 -1
- package/dist/agentic/guides/modules/search/index.md +1 -1
- package/dist/agentic/guides/modules/security/index.md +1 -1
- package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
- package/dist/agentic/guides/modules/sso/index.md +1 -1
- package/dist/agentic/guides/modules/staff/index.md +1 -1
- package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
- package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
- package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
- package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
- package/dist/agentic/guides/modules/translations/index.md +1 -1
- package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
- package/dist/agentic/guides/modules/webhooks/index.md +1 -1
- package/dist/agentic/guides/modules/wms/index.md +1 -1
- package/dist/agentic/guides/modules/workflows/index.md +1 -1
- package/dist/agentic/guides/reference-module-facts.json +1 -1
- package/dist/agentic/guides/upstream/manifest.json +1 -1
- package/dist/agentic/shared/ai/harness/cases.json +4 -2
- package/package.json +3 -3
- package/template/.env.example +46 -1
- package/template/package.json.template +2 -0
- package/template/scripts/dev-runtime-actions.mjs +99 -0
- package/template/scripts/dev-runtime-config.mjs +89 -0
- package/template/scripts/dev-runtime-diagnostics.mjs +292 -0
- package/template/scripts/dev-runtime-gateway.mjs +370 -0
- package/template/scripts/dev-runtime-probe.mjs +77 -0
- package/template/scripts/dev-runtime-state.mjs +571 -0
- package/template/scripts/dev-runtime-supervisor.mjs +268 -0
- package/template/scripts/dev-runtime.mjs +29 -0
- package/template/scripts/dev-splash-state.mjs +14 -0
- package/template/scripts/dev-splash.html +357 -2
- package/template/scripts/dev.mjs +396 -14
- package/template/src/app/__tests__/start-page-bootstrap.test.ts +34 -0
- package/template/src/app/api/dev-runtime/actions/[action]/route.ts +5 -0
- package/template/src/app/api/dev-runtime/diagnostics/route.ts +5 -0
- package/template/src/app/api/dev-runtime/logs/route.ts +5 -0
- package/template/src/app/api/dev-runtime/status/route.ts +5 -0
- package/template/src/app/api/healthz/__tests__/route.test.ts +3 -1
- package/template/src/app/global-error.tsx +7 -0
- package/template/src/app/layout.tsx +11 -0
- package/template/src/app/start/page.tsx +11 -1
- package/template/src/i18n/de.json +27 -0
- package/template/src/i18n/en.json +27 -0
- package/template/src/i18n/es.json +27 -0
- package/template/src/i18n/ko.json +27 -0
- package/template/src/i18n/pl.json +27 -0
- package/template/src/modules/example/__integration__/TC-START-001-onboarding-gate.spec.ts +33 -0
- package/template/src/modules.ts +2 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
browserReportToSignal,
|
|
6
|
+
createDevRuntimeToken,
|
|
7
|
+
createDiagnosticsSink,
|
|
8
|
+
validateActionRequest,
|
|
9
|
+
} from './dev-runtime-diagnostics.mjs'
|
|
10
|
+
import { createRuntimeProbe } from './dev-runtime-probe.mjs'
|
|
11
|
+
import { createRuntimeStateStore } from './dev-runtime-state.mjs'
|
|
12
|
+
|
|
13
|
+
export const RUNTIME_STATUS_FILE_NAME = 'dev-runtime-status.json'
|
|
14
|
+
export const RUNTIME_DIAGNOSTICS_FILE_NAME = 'dev-runtime-diagnostics.ndjson'
|
|
15
|
+
export const RUNTIME_ACTIONS_FILE_NAME = 'dev-runtime-actions.ndjson'
|
|
16
|
+
export const RUNTIME_LOGS_FILE_NAME = 'dev-runtime-logs.json'
|
|
17
|
+
|
|
18
|
+
const SYNC_INTERVAL_MS = 1000
|
|
19
|
+
const MAX_CHILD_SIGNALS_PER_SYNC = 50
|
|
20
|
+
|
|
21
|
+
// Owns the generation-aware runtime state for one supervisor process: it
|
|
22
|
+
// ingests app-runtime signals, drains browser reports, runs the continuous
|
|
23
|
+
// probe, and publishes an atomically written status file the dev-only app
|
|
24
|
+
// bridge can read.
|
|
25
|
+
export function createDevRuntimeSupervisor(options = {}) {
|
|
26
|
+
const enabled = options.enabled !== false
|
|
27
|
+
const stateDirectory = options.stateDirectory ?? path.join(process.cwd(), '.mercato')
|
|
28
|
+
const config = options.config ?? {}
|
|
29
|
+
const logger = options.logger ?? console
|
|
30
|
+
const resolveBaseUrl = typeof options.resolveBaseUrl === 'function' ? options.resolveBaseUrl : () => null
|
|
31
|
+
const readChildState = typeof options.readChildState === 'function' ? options.readChildState : () => null
|
|
32
|
+
|
|
33
|
+
const token = createDevRuntimeToken()
|
|
34
|
+
const statusFilePath = path.join(stateDirectory, RUNTIME_STATUS_FILE_NAME)
|
|
35
|
+
const diagnosticsFilePath = path.join(stateDirectory, RUNTIME_DIAGNOSTICS_FILE_NAME)
|
|
36
|
+
const actionsFilePath = path.join(stateDirectory, RUNTIME_ACTIONS_FILE_NAME)
|
|
37
|
+
const logsFilePath = path.join(stateDirectory, RUNTIME_LOGS_FILE_NAME)
|
|
38
|
+
|
|
39
|
+
const store = createRuntimeStateStore({
|
|
40
|
+
cwd: process.cwd(),
|
|
41
|
+
configuredPort: options.configuredPort,
|
|
42
|
+
publicUrl: options.publicUrl,
|
|
43
|
+
})
|
|
44
|
+
const sink = createDiagnosticsSink({ filePath: diagnosticsFilePath })
|
|
45
|
+
// The in-app banner cannot call the supervisor directly, so the dev-only app
|
|
46
|
+
// route queues an allowlisted action here and the supervisor drains it.
|
|
47
|
+
const actionSink = createDiagnosticsSink({ filePath: actionsFilePath, validate: validateActionRequest })
|
|
48
|
+
const runAction = typeof options.runAction === 'function' ? options.runAction : null
|
|
49
|
+
|
|
50
|
+
let lastIngestedChildSignalSeq = 0
|
|
51
|
+
let lastPublishedPayload = null
|
|
52
|
+
let lastPublishedLogsPayload = null
|
|
53
|
+
let syncTimer = null
|
|
54
|
+
|
|
55
|
+
const probe = createRuntimeProbe({
|
|
56
|
+
config: config.probe,
|
|
57
|
+
resolveBaseUrl,
|
|
58
|
+
shouldRun: () => store.isReadySeen() && !store.isRecoveryBusy(),
|
|
59
|
+
onDegraded: (result) => {
|
|
60
|
+
store.recordSignal({
|
|
61
|
+
source: 'probe',
|
|
62
|
+
generation: store.getGeneration(),
|
|
63
|
+
code: 'probe_unhealthy',
|
|
64
|
+
title: 'Runtime health check is failing',
|
|
65
|
+
detail: result?.status
|
|
66
|
+
? `The runtime answered /api/healthz with HTTP ${result.status}`
|
|
67
|
+
: 'The runtime stopped answering /api/healthz',
|
|
68
|
+
blocking: false,
|
|
69
|
+
})
|
|
70
|
+
publish()
|
|
71
|
+
},
|
|
72
|
+
onRecovered: () => {
|
|
73
|
+
store.clearIncidentsBySource('probe')
|
|
74
|
+
publish()
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
function writeStatusFile(status) {
|
|
79
|
+
if (!enabled) return
|
|
80
|
+
const payload = JSON.stringify({ token, status, pid: process.pid })
|
|
81
|
+
if (payload === lastPublishedPayload) return
|
|
82
|
+
lastPublishedPayload = payload
|
|
83
|
+
const temporaryPath = `${statusFilePath}.${process.pid}.tmp`
|
|
84
|
+
try {
|
|
85
|
+
fs.mkdirSync(stateDirectory, { recursive: true })
|
|
86
|
+
fs.writeFileSync(temporaryPath, payload, 'utf8')
|
|
87
|
+
fs.renameSync(temporaryPath, statusFilePath)
|
|
88
|
+
} catch {
|
|
89
|
+
try {
|
|
90
|
+
fs.rmSync(temporaryPath, { force: true })
|
|
91
|
+
} catch {
|
|
92
|
+
// Publishing status is best-effort; the terminal stays authoritative.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The bounded log tail lives in its own file so the 2s status poll stays small
|
|
98
|
+
// and the app only pays for lines when a developer opens the logs view.
|
|
99
|
+
function writeLogsFile() {
|
|
100
|
+
if (!enabled) return
|
|
101
|
+
const snapshot = store.getDiagnosticLines(0)
|
|
102
|
+
const payload = JSON.stringify({ token, generation: snapshot.generation, lines: snapshot.lines })
|
|
103
|
+
if (payload === lastPublishedLogsPayload) return
|
|
104
|
+
lastPublishedLogsPayload = payload
|
|
105
|
+
const temporaryPath = `${logsFilePath}.${process.pid}.tmp`
|
|
106
|
+
try {
|
|
107
|
+
fs.mkdirSync(stateDirectory, { recursive: true })
|
|
108
|
+
fs.writeFileSync(temporaryPath, payload, 'utf8')
|
|
109
|
+
fs.renameSync(temporaryPath, logsFilePath)
|
|
110
|
+
} catch {
|
|
111
|
+
try {
|
|
112
|
+
fs.rmSync(temporaryPath, { force: true })
|
|
113
|
+
} catch {
|
|
114
|
+
// Publishing logs is best-effort; the terminal stays authoritative.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function publish() {
|
|
120
|
+
const status = store.getStatus()
|
|
121
|
+
writeStatusFile(status)
|
|
122
|
+
writeLogsFile()
|
|
123
|
+
return status
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function drainBrowserReports() {
|
|
127
|
+
if (!enabled) return
|
|
128
|
+
for (const report of sink.drain()) {
|
|
129
|
+
store.recordSignal(browserReportToSignal(report, store.getGeneration()))
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Requests carrying a stale generation are dropped: the runtime they were
|
|
134
|
+
// raised against no longer exists.
|
|
135
|
+
function drainActionRequests() {
|
|
136
|
+
if (!enabled || !runAction) return
|
|
137
|
+
for (const request of actionSink.drain()) {
|
|
138
|
+
if (Number.isInteger(request.generation) && request.generation !== store.getGeneration()) continue
|
|
139
|
+
void runAction(request.action)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// The app runtime wrapper publishes classified signals through its splash
|
|
144
|
+
// child-state file. Sequence numbers make ingestion idempotent.
|
|
145
|
+
function ingestChildState(childState) {
|
|
146
|
+
if (!enabled || !childState || typeof childState !== 'object') return
|
|
147
|
+
const signals = Array.isArray(childState.runtimeSignals) ? childState.runtimeSignals : []
|
|
148
|
+
let ingested = 0
|
|
149
|
+
for (const signal of signals) {
|
|
150
|
+
if (!signal || typeof signal !== 'object') continue
|
|
151
|
+
if (!Number.isInteger(signal.seq) || signal.seq <= lastIngestedChildSignalSeq) continue
|
|
152
|
+
if (ingested >= MAX_CHILD_SIGNALS_PER_SYNC) break
|
|
153
|
+
lastIngestedChildSignalSeq = signal.seq
|
|
154
|
+
ingested += 1
|
|
155
|
+
store.recordSignal({ ...signal, generation: store.getGeneration() })
|
|
156
|
+
}
|
|
157
|
+
if (childState.ready === true && !store.isReadySeen()) {
|
|
158
|
+
store.markReady()
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// One collection cycle: pull the child's typed signals, drain browser
|
|
163
|
+
// reports, and republish. Runs on an interval and is safe to call directly.
|
|
164
|
+
function sync() {
|
|
165
|
+
ingestChildState(readChildState())
|
|
166
|
+
drainBrowserReports()
|
|
167
|
+
drainActionRequests()
|
|
168
|
+
return publish()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
enabled,
|
|
173
|
+
token,
|
|
174
|
+
statusFilePath,
|
|
175
|
+
diagnosticsFilePath,
|
|
176
|
+
actionsFilePath,
|
|
177
|
+
logsFilePath,
|
|
178
|
+
getStatus: store.getStatus,
|
|
179
|
+
getGeneration: store.getGeneration,
|
|
180
|
+
isReadySeen: store.isReadySeen,
|
|
181
|
+
ingestChildState,
|
|
182
|
+
publish,
|
|
183
|
+
sync,
|
|
184
|
+
|
|
185
|
+
// Env passed to the managed app process so its dev-only routes can find the
|
|
186
|
+
// status file, the diagnostic sink, and the per-run token.
|
|
187
|
+
childEnv() {
|
|
188
|
+
if (!enabled) return {}
|
|
189
|
+
return {
|
|
190
|
+
OM_DEV_RUNTIME_TOKEN: token,
|
|
191
|
+
OM_DEV_RUNTIME_STATUS_FILE: statusFilePath,
|
|
192
|
+
OM_DEV_RUNTIME_DIAGNOSTICS_FILE: diagnosticsFilePath,
|
|
193
|
+
OM_DEV_RUNTIME_ACTIONS_FILE: actionsFilePath,
|
|
194
|
+
OM_DEV_RUNTIME_LOGS_FILE: logsFilePath,
|
|
195
|
+
OM_DEV_RUNTIME_DIAGNOSTICS: '1',
|
|
196
|
+
OM_DEV_RUNTIME_BANNER: config.bannerEnabled === false ? '0' : '1',
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
recordSignal(signal) {
|
|
201
|
+
if (!enabled) return store.getStatus()
|
|
202
|
+
store.recordSignal({ generation: store.getGeneration(), ...signal })
|
|
203
|
+
return publish()
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
beginGeneration(reason) {
|
|
207
|
+
if (!enabled) return 0
|
|
208
|
+
lastIngestedChildSignalSeq = 0
|
|
209
|
+
probe.reset()
|
|
210
|
+
sink.clear()
|
|
211
|
+
actionSink.clear()
|
|
212
|
+
const generation = store.beginGeneration(reason)
|
|
213
|
+
publish()
|
|
214
|
+
return generation
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
setUpstream(patch) {
|
|
218
|
+
if (!enabled) return
|
|
219
|
+
store.setUpstream(patch)
|
|
220
|
+
publish()
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
beginRecovery(action) {
|
|
224
|
+
if (!enabled) return null
|
|
225
|
+
const status = store.beginRecovery(action)
|
|
226
|
+
publish()
|
|
227
|
+
return status
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
completeRecovery(exitCode, options) {
|
|
231
|
+
if (!enabled) return null
|
|
232
|
+
const status = store.completeRecovery(exitCode, options)
|
|
233
|
+
publish()
|
|
234
|
+
return status
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
getDiagnosticLines(cursor) {
|
|
238
|
+
return store.getDiagnosticLines(cursor)
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
start() {
|
|
242
|
+
if (!enabled || syncTimer) return
|
|
243
|
+
sink.clear()
|
|
244
|
+
actionSink.clear()
|
|
245
|
+
publish()
|
|
246
|
+
syncTimer = setInterval(sync, SYNC_INTERVAL_MS)
|
|
247
|
+
syncTimer.unref?.()
|
|
248
|
+
probe.start()
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
stop() {
|
|
252
|
+
if (syncTimer) {
|
|
253
|
+
clearInterval(syncTimer)
|
|
254
|
+
syncTimer = null
|
|
255
|
+
}
|
|
256
|
+
probe.stop()
|
|
257
|
+
if (!enabled) return
|
|
258
|
+
try {
|
|
259
|
+
fs.rmSync(statusFilePath, { force: true })
|
|
260
|
+
fs.rmSync(logsFilePath, { force: true })
|
|
261
|
+
} catch {
|
|
262
|
+
logger?.debug?.('[dev-runtime] Unable to remove the runtime status file during shutdown.')
|
|
263
|
+
}
|
|
264
|
+
sink.clear()
|
|
265
|
+
actionSink.clear()
|
|
266
|
+
},
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -187,7 +187,10 @@ const splashState = {
|
|
|
187
187
|
progressPercent: 0,
|
|
188
188
|
progressLabel: setupSplashMode ? 'Starting app runtime' : 'Preparing app runtime',
|
|
189
189
|
activities: [],
|
|
190
|
+
runtimeSignals: [],
|
|
190
191
|
}
|
|
192
|
+
const maxBufferedRuntimeSignals = 20
|
|
193
|
+
let runtimeSignalSequence = 0
|
|
191
194
|
const startupProgress = {
|
|
192
195
|
current: runtimeProgressCurrent,
|
|
193
196
|
total: runtimeProgressTotal,
|
|
@@ -559,6 +562,19 @@ function collectRuntimeFailureLines(maxLines = 10) {
|
|
|
559
562
|
return lines.slice(-maxLines)
|
|
560
563
|
}
|
|
561
564
|
|
|
565
|
+
// Typed hand-off to the root supervisor's incident collector. The wrapper only
|
|
566
|
+
// reports what it observed; classification, redaction, and state transitions
|
|
567
|
+
// stay in the supervisor so one generation owns them.
|
|
568
|
+
function emitRuntimeSignal(signal) {
|
|
569
|
+
if (!splashChildStateFile) return
|
|
570
|
+
runtimeSignalSequence += 1
|
|
571
|
+
splashState.runtimeSignals.push({ seq: runtimeSignalSequence, ...signal })
|
|
572
|
+
while (splashState.runtimeSignals.length > maxBufferedRuntimeSignals) {
|
|
573
|
+
splashState.runtimeSignals.shift()
|
|
574
|
+
}
|
|
575
|
+
persistSplashState()
|
|
576
|
+
}
|
|
577
|
+
|
|
562
578
|
function publishRuntimeFailure(detail, options = {}) {
|
|
563
579
|
const failureLines = Array.isArray(options.failureLines) && options.failureLines.length > 0
|
|
564
580
|
? options.failureLines
|
|
@@ -573,6 +589,15 @@ function publishRuntimeFailure(detail, options = {}) {
|
|
|
573
589
|
? options.progressLabel
|
|
574
590
|
: (startupProgress.current >= runtimeProgressCurrent ? startupProgress.label : 'Starting app server')
|
|
575
591
|
|
|
592
|
+
emitRuntimeSignal({
|
|
593
|
+
source: options.source ?? 'log',
|
|
594
|
+
message: failureLines.join('\n') || failureDetail,
|
|
595
|
+
detail: failureDetail,
|
|
596
|
+
failureLines,
|
|
597
|
+
failureCommand: 'yarn dev',
|
|
598
|
+
failureStage: options.failureStage,
|
|
599
|
+
})
|
|
600
|
+
|
|
576
601
|
updateSplashState({
|
|
577
602
|
phase: 'Runtime error detected',
|
|
578
603
|
detail: failureDetail,
|
|
@@ -668,6 +693,8 @@ function reportUnexpectedChildExit(result) {
|
|
|
668
693
|
// The banner was just printed, so buffer it without echoing it a second time.
|
|
669
694
|
bufferRawLog(report.banner)
|
|
670
695
|
publishRuntimeFailure(report.banner, {
|
|
696
|
+
source: 'process',
|
|
697
|
+
failureStage: result?.label,
|
|
671
698
|
progressCurrent: splashState.progressCurrent >= runtimeProgressCurrent ? splashState.progressCurrent : runtimeProgressCurrent,
|
|
672
699
|
progressLabel: splashState.progressLabel || startupProgress.label,
|
|
673
700
|
failureLines: report.failureLines,
|
|
@@ -1084,6 +1111,8 @@ async function runTargetedRouteWarmup() {
|
|
|
1084
1111
|
runtimeWarmupState.failed = true
|
|
1085
1112
|
const detail = `Warmup failed after ${attempt} retries: ${reason}`
|
|
1086
1113
|
publishRuntimeFailure(detail, {
|
|
1114
|
+
source: 'warmup',
|
|
1115
|
+
failureStage: 'Startup warmup',
|
|
1087
1116
|
progressCurrent: runtimeProgressCurrent,
|
|
1088
1117
|
progressLabel: progressLabel,
|
|
1089
1118
|
failureLines: [
|
|
@@ -4,9 +4,23 @@ function hasUsableReadyTarget(state) {
|
|
|
4
4
|
|| typeof state.readyUrl === 'string' && state.readyUrl.trim().length > 0
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
// The structured runtime payload is the authority for post-ready problems.
|
|
8
|
+
// `ready` is only a compatibility projection, so it must not erase an incident
|
|
9
|
+
// that the collector still considers active for the current generation.
|
|
10
|
+
export function hasActiveRuntimeIncident(state) {
|
|
11
|
+
const runtime = state?.runtime
|
|
12
|
+
if (!runtime || typeof runtime !== 'object') return false
|
|
13
|
+
if (runtime.health === 'degraded' || runtime.health === 'unavailable') return true
|
|
14
|
+
if (!Array.isArray(runtime.incidents)) return false
|
|
15
|
+
return runtime.incidents.some((incident) => (
|
|
16
|
+
incident && typeof incident === 'object' && incident.generation === runtime.generation
|
|
17
|
+
))
|
|
18
|
+
}
|
|
19
|
+
|
|
7
20
|
export function shouldPreferReadySplashState(state) {
|
|
8
21
|
if (!state || typeof state !== 'object') return false
|
|
9
22
|
if (state.ready !== true) return false
|
|
23
|
+
if (hasActiveRuntimeIncident(state)) return false
|
|
10
24
|
|
|
11
25
|
const phase = typeof state.phase === 'string' ? state.phase : ''
|
|
12
26
|
const detail = typeof state.detail === 'string' ? state.detail : ''
|