@shawnstack/quickforge 1.7.10 → 1.7.12
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/dist/assets/{AgentProfilesPage-Dw9orPNt.js → AgentProfilesPage-CApCpsDd.js} +1 -1
- package/dist/assets/ChatPanelHost-COTO7swS.js +48 -0
- package/dist/assets/CloudAccountSettingsPage-Be5r3dtv.js +1 -0
- package/dist/assets/{PluginsPage-Dj1bmsEf.js → PluginsPage-BIbIDfWY.js} +1 -1
- package/dist/assets/ScheduledTasksPage-BOqmBNCx.js +2 -0
- package/dist/assets/{SettingsWorkspacePage-CFuvSATi.js → SettingsWorkspacePage-BCRtycRN.js} +304 -304
- package/dist/assets/ShareLinksSettingsPage-BrxQacJn.js +1 -0
- package/dist/assets/{SharedConversationPage-DLo05j7c.js → SharedConversationPage-DJa0M0El.js} +1 -1
- package/dist/assets/TerminalDock-Cqz_9mTC.js +2 -0
- package/dist/assets/WorkspaceInspector-C-GOK81D.js +36 -0
- package/dist/assets/{abnfDiagram-VRR7QNED-CMhVoYUh.js → abnfDiagram-VRR7QNED-DGklo3PO.js} +1 -1
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-C52qL6Hg.js → architectureDiagram-ZJ3FMSHR-CioWLVTp.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-CM3XbLol.js → blockDiagram-677ZJIJ3-BJWBq5e9.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-D7Bh1ui1.js → c4Diagram-LMCZKHZV-BacRvqFK.js} +1 -1
- package/dist/assets/channel-CeXY-wZo.js +1 -0
- package/dist/assets/{chunk-32BRIVSS-xgrtbT8e.js → chunk-32BRIVSS-B53BNE2C.js} +1 -1
- package/dist/assets/{chunk-52WLFC77-CDbaHw80.js → chunk-52WLFC77-CYq4hT4f.js} +1 -1
- package/dist/assets/{chunk-C7G6YPKG-ZMLUvfLy.js → chunk-C7G6YPKG-KFaGpP_Z.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-BcQhaqt7.js → chunk-EX3LRPZG-DqVd_ihG.js} +1 -1
- package/dist/assets/{chunk-FWX5IMBZ-DrnrdrSu.js → chunk-FWX5IMBZ-BxRjZkOP.js} +2 -2
- package/dist/assets/{chunk-HOUHSVGY-tdc-Z9eW.js → chunk-HOUHSVGY-C9QO8omv.js} +1 -1
- package/dist/assets/{chunk-ICXQ74PX-Cd7igVGt.js → chunk-ICXQ74PX-ClbGPgLV.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-CLTjV5ov.js → chunk-MOJQB5TN-Ba4wlF1J.js} +1 -1
- package/dist/assets/{chunk-OGEWGWER-TgvkXkFX.js → chunk-OGEWGWER-DczhQmcx.js} +1 -1
- package/dist/assets/{chunk-PUDLZKDR-C5GMVYup.js → chunk-PUDLZKDR-CPWpcKUF.js} +1 -1
- package/dist/assets/{chunk-Q4XR5HBZ-fo5-HsqY.js → chunk-Q4XR5HBZ-CiF6T--X.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-CEDazV6w.js → chunk-V7JOEXUC-Dxv0zwmw.js} +1 -1
- package/dist/assets/{chunk-VAUOI2AC-CWYjg8zE.js → chunk-VAUOI2AC-D3J5bw1P.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-C_-eEj-9.js → chunk-VR4S4FIN-BXQSwwII.js} +1 -1
- package/dist/assets/{chunk-WYO6CB5R-NF-MsJJR.js → chunk-WYO6CB5R-C0azl-fk.js} +2 -2
- package/dist/assets/{chunk-ZGVPDNZ5-CRrAYi4g.js → chunk-ZGVPDNZ5-D_8F4Akx.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-B8M6sf49.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-BY7fzBDH.js +1 -0
- package/dist/assets/{cynefinDiagram-TSTJHNR4-D4sZ7LYI.js → cynefinDiagram-TSTJHNR4-BFlsWz0B.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB-BlsnSh-Q.js → dagre-VKFMJZFB-DXhpjlEs.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-DW1yQiRO.js → diagram-FQU43EPY-DRVVOD9k.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-BTUAsFjb.js → diagram-G47NLZAW-BV9Gw1tL.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-BcaomqSd.js → diagram-NH7WQ7WH-CrUkXfRY.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-DDLX7Xwr.js → diagram-OA4YK3LP-CuILjn7v.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-CIave0An.js → diagram-WEI45ONY-nlNN3tWB.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-Cr1Y6lPu.js → ebnfDiagram-CCIWWBDH-BuDgZt9n.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-D3EgiVVH.js → erDiagram-Q63AITRT-MEwxjmtP.js} +1 -1
- package/dist/assets/flowDiagram-23GEKE2U-BiUP-Scq.js +1 -0
- package/dist/assets/{ganttDiagram-NO4QXBWP-B0oyDIqK.js → ganttDiagram-NO4QXBWP-DNw5qghE.js} +1 -1
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-uQtr6IqK.js → gitGraphDiagram-IHSO6WYX-CaTcakAt.js} +1 -1
- package/dist/assets/index-BWf6y3Kh.css +3 -0
- package/dist/assets/index-D3GWkXJR.js +66 -0
- package/dist/assets/{infoDiagram-FWYZ7A6U-B_GzlLIM.js → infoDiagram-FWYZ7A6U-DwfyP7dU.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-CKQIsSdU.js → ishikawaDiagram-FXEZZL3T-CmrzbOCV.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-DrrvzZL1.js → journeyDiagram-5HDEW3XC-CYqe3sgr.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-CJQRTdXz.js → kanban-definition-HUTT4EX6-BMDO_mv0.js} +1 -1
- package/dist/assets/{line-B4DxX7tC.js → line-SbxJR8ZT.js} +1 -1
- package/dist/assets/lit-vendor-CinchGmI.js +2 -0
- package/dist/assets/local-tools-DY0yEU3T.js +350 -0
- package/dist/assets/{mcp-servers-dialog-CwHp0K4q.js → mcp-servers-dialog-7Xwu2RVW.js} +2 -2
- package/dist/assets/{mermaid.core-DWpShXCs.js → mermaid.core-CwXTzYjs.js} +3 -3
- package/dist/assets/{mindmap-definition-LN4V7U3C-CEVwZD78.js → mindmap-definition-LN4V7U3C-CLyJ78mO.js} +1 -1
- package/dist/assets/{pegDiagram-2B236MQR-CBLkBdnQ.js → pegDiagram-2B236MQR-4qFznrSw.js} +1 -1
- package/dist/assets/{pi-web-ui-DV5jKjSg.js → pi-web-ui-BNuvqKDT.js} +289 -289
- package/dist/assets/{pieDiagram-ENE6RG2P-8tCnwtWS.js → pieDiagram-ENE6RG2P-141sGxVe.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-CWd28cHF.js → quadrantDiagram-ABIIQ3AL-SUI44pAX.js} +1 -1
- package/dist/assets/{railroadDiagram-RFXS5EU6-CI4ouOBs.js → railroadDiagram-RFXS5EU6-1oiZuYBB.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-DtFz7sw4.js → requirementDiagram-TGXJPOKE-CfVishLi.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB--VeOwwk9.js → sankeyDiagram-HTMAVEWB-DVu7J_Z9.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-B8AHk5Oe.js → sequenceDiagram-DBY2YBRQ-D-Ip2yiS.js} +1 -1
- package/dist/assets/{skills-dialog-k9ycekIU.js → skills-dialog-C_JvWbAa.js} +3 -3
- package/dist/assets/{stateDiagram-2N3HPSRC-etXnJ8X8.js → stateDiagram-2N3HPSRC-Cg0hWwrp.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DhmzNQd9.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-pabIKAmn.js → swimlanes-5IMT3BWC-Co5D-cGU.js} +1 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-BEvomQNa.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-C55MKm3L.js → timeline-definition-FHXFAJF6-CBk4c_Ua.js} +1 -1
- package/dist/assets/{vennDiagram-L72KCM5P-CgNf1Q9W.js → vennDiagram-L72KCM5P-BII7wdRx.js} +1 -1
- package/dist/assets/{wardleyDiagram-EHGQE667-Drt5ytec.js → wardleyDiagram-EHGQE667-CkyJJIXP.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-hhoEo_Ar.js → xychartDiagram-FW5EYKEG-B1A66Ks0.js} +1 -1
- package/dist/index.html +6 -6
- package/package.json +1 -1
- package/server/acp/server.mjs +3 -6
- package/server/agent-manager.mjs +192 -48
- package/server/approval-store.mjs +1 -0
- package/server/ask-store.mjs +78 -0
- package/server/auto-archive.mjs +55 -8
- package/server/index.mjs +172 -51
- package/server/lan-access-cutover.mjs +21 -4
- package/server/maintenance/downgrade-session-state-v1.mjs +46 -85
- package/server/maintenance/export-session-state-v1.mjs +1 -11
- package/server/routes/agent.mjs +9 -0
- package/server/routes/backup.mjs +1 -1
- package/server/routes/storage.mjs +63 -49
- package/server/scheduled-runs-cutover.mjs +15 -1
- package/server/session-index-service.mjs +57 -391
- package/server/session-state-backup.mjs +56 -5
- package/server/session-state-import.mjs +174 -0
- package/server/session-state-maintenance.mjs +126 -0
- package/server/session-state-service.mjs +128 -210
- package/server/share-cutover.mjs +21 -4
- package/server/sqlite/database.mjs +142 -7
- package/server/sqlite/lan-access-repository.mjs +6 -4
- package/server/sqlite/migrations.mjs +109 -0
- package/server/sqlite/scheduled-task-runs-repository.mjs +6 -3
- package/server/sqlite/session-index-repository.mjs +59 -208
- package/server/sqlite/session-state-repository.mjs +337 -387
- package/server/sqlite/share-repository.mjs +56 -45
- package/server/startup-state.mjs +107 -0
- package/server/storage.mjs +218 -454
- package/server/tools/definitions.mjs +18 -0
- package/server/tools/index.mjs +14 -2
- package/server/utils/logger.mjs +15 -4
- package/dist/assets/ChatPanelHost-CW3-MQyL.js +0 -48
- package/dist/assets/CloudAccountSettingsPage-CDkKN1Sz.js +0 -1
- package/dist/assets/ScheduledTasksPage-C9lOTi75.js +0 -2
- package/dist/assets/ShareLinksSettingsPage-CH_t33A3.js +0 -1
- package/dist/assets/TerminalDock-CSOVub-m.js +0 -2
- package/dist/assets/WorkspaceInspector-DPXgLIum.js +0 -36
- package/dist/assets/channel-Duu1CuH9.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-Cc63fLWP.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-CoXU0qT6.js +0 -1
- package/dist/assets/flowDiagram-23GEKE2U-BS7KXcqn.js +0 -1
- package/dist/assets/index-DQBOm9gt.js +0 -66
- package/dist/assets/index-ps85guTs.css +0 -3
- package/dist/assets/lit-vendor-DvaQX_UQ.js +0 -2
- package/dist/assets/local-tools-BxfQeIUQ.js +0 -270
- package/dist/assets/stateDiagram-v2-6OUMAXLB-Dms3pICj.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-B14aWjv6.js +0 -8
- package/server/session-state-cutover.mjs +0 -563
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto'
|
|
2
|
-
import { getSqliteStorage } from './database.mjs'
|
|
2
|
+
import { getSqliteStorage, runSharedSqliteQuickCheck } from './database.mjs'
|
|
3
3
|
|
|
4
4
|
// F10 share storage: `share_sessions` (strictly mapped columns + `extra_json`
|
|
5
5
|
// for opaque/unknown fields) and `share_tokens` (independent token table with
|
|
@@ -301,7 +301,36 @@ function attachTokens(records, tokenRows) {
|
|
|
301
301
|
}))
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
-
|
|
304
|
+
const SHARE_SESSION_UPSERT_SQL = `
|
|
305
|
+
INSERT INTO share_sessions (${SHARE_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
306
|
+
ON CONFLICT(share_id) DO UPDATE SET
|
|
307
|
+
session_id = excluded.session_id,
|
|
308
|
+
permission = excluded.permission,
|
|
309
|
+
title_snapshot = excluded.title_snapshot,
|
|
310
|
+
scope = excluded.scope,
|
|
311
|
+
project_id = excluded.project_id,
|
|
312
|
+
password_hash = excluded.password_hash,
|
|
313
|
+
password_salt = excluded.password_salt,
|
|
314
|
+
password_version = excluded.password_version,
|
|
315
|
+
auth_version = excluded.auth_version,
|
|
316
|
+
allow_cloud_usage = excluded.allow_cloud_usage,
|
|
317
|
+
created_at = excluded.created_at,
|
|
318
|
+
updated_at = excluded.updated_at,
|
|
319
|
+
expires_at = excluded.expires_at,
|
|
320
|
+
revoked_at = excluded.revoked_at,
|
|
321
|
+
superseded_at = excluded.superseded_at,
|
|
322
|
+
access_count = excluded.access_count,
|
|
323
|
+
last_accessed_at = excluded.last_accessed_at,
|
|
324
|
+
created_from_host = excluded.created_from_host,
|
|
325
|
+
last_updated_from_host = excluded.last_updated_from_host,
|
|
326
|
+
revision = excluded.revision,
|
|
327
|
+
record_digest = excluded.record_digest,
|
|
328
|
+
deleted_at = excluded.deleted_at,
|
|
329
|
+
extra_json = excluded.extra_json
|
|
330
|
+
`
|
|
331
|
+
|
|
332
|
+
// The optional statement lets the cutover replaceAll reuse one precompiled upsert.
|
|
333
|
+
function insertShareRow(database, record, revision, createdAt, updatedAt, statement = null) {
|
|
305
334
|
const extra = {}
|
|
306
335
|
for (const [key, value] of Object.entries(record)) {
|
|
307
336
|
if (!KNOWN_FIELDS.has(key) && !['revision', 'deletedAt'].includes(key)) extra[key] = value
|
|
@@ -317,33 +346,8 @@ function insertShareRow(database, record, revision, createdAt, updatedAt) {
|
|
|
317
346
|
record.createdFromHost ?? null, record.lastUpdatedFromHost ?? null,
|
|
318
347
|
revision, shareRecordDigest(record), record.deletedAt ?? null, JSON.stringify(extra),
|
|
319
348
|
]
|
|
320
|
-
database.prepare(
|
|
321
|
-
|
|
322
|
-
ON CONFLICT(share_id) DO UPDATE SET
|
|
323
|
-
session_id = excluded.session_id,
|
|
324
|
-
permission = excluded.permission,
|
|
325
|
-
title_snapshot = excluded.title_snapshot,
|
|
326
|
-
scope = excluded.scope,
|
|
327
|
-
project_id = excluded.project_id,
|
|
328
|
-
password_hash = excluded.password_hash,
|
|
329
|
-
password_salt = excluded.password_salt,
|
|
330
|
-
password_version = excluded.password_version,
|
|
331
|
-
auth_version = excluded.auth_version,
|
|
332
|
-
allow_cloud_usage = excluded.allow_cloud_usage,
|
|
333
|
-
created_at = excluded.created_at,
|
|
334
|
-
updated_at = excluded.updated_at,
|
|
335
|
-
expires_at = excluded.expires_at,
|
|
336
|
-
revoked_at = excluded.revoked_at,
|
|
337
|
-
superseded_at = excluded.superseded_at,
|
|
338
|
-
access_count = excluded.access_count,
|
|
339
|
-
last_accessed_at = excluded.last_accessed_at,
|
|
340
|
-
created_from_host = excluded.created_from_host,
|
|
341
|
-
last_updated_from_host = excluded.last_updated_from_host,
|
|
342
|
-
revision = excluded.revision,
|
|
343
|
-
record_digest = excluded.record_digest,
|
|
344
|
-
deleted_at = excluded.deleted_at,
|
|
345
|
-
extra_json = excluded.extra_json
|
|
346
|
-
`).run(...values)
|
|
349
|
+
const insert = statement ?? database.prepare(SHARE_SESSION_UPSERT_SQL)
|
|
350
|
+
insert.run(...values)
|
|
347
351
|
}
|
|
348
352
|
|
|
349
353
|
function replaceTokens(database, shareId, tokens) {
|
|
@@ -354,18 +358,21 @@ function replaceTokens(database, shareId, tokens) {
|
|
|
354
358
|
}
|
|
355
359
|
}
|
|
356
360
|
|
|
357
|
-
|
|
361
|
+
const ENQUEUE_SHARE_MIRROR_SQL = `
|
|
362
|
+
INSERT INTO share_json_mirror_queue (share_id, operation, share_json, attempts, last_error, updated_at)
|
|
363
|
+
VALUES (?, ?, ?, 0, NULL, ?)
|
|
364
|
+
ON CONFLICT(share_id) DO UPDATE SET
|
|
365
|
+
operation = excluded.operation,
|
|
366
|
+
share_json = excluded.share_json,
|
|
367
|
+
attempts = 0,
|
|
368
|
+
last_error = NULL,
|
|
369
|
+
updated_at = excluded.updated_at
|
|
370
|
+
`
|
|
371
|
+
|
|
372
|
+
function enqueueShareMirror(database, record, operation, updatedAt, statement = null) {
|
|
358
373
|
const stored = record ? { ...record, revision: undefined, deletedAt: undefined } : null
|
|
359
|
-
database.prepare(
|
|
360
|
-
|
|
361
|
-
VALUES (?, ?, ?, 0, NULL, ?)
|
|
362
|
-
ON CONFLICT(share_id) DO UPDATE SET
|
|
363
|
-
operation = excluded.operation,
|
|
364
|
-
share_json = excluded.share_json,
|
|
365
|
-
attempts = 0,
|
|
366
|
-
last_error = NULL,
|
|
367
|
-
updated_at = excluded.updated_at
|
|
368
|
-
`).run(
|
|
374
|
+
const insert = statement ?? database.prepare(ENQUEUE_SHARE_MIRROR_SQL)
|
|
375
|
+
insert.run(
|
|
369
376
|
record?.id || record,
|
|
370
377
|
operation,
|
|
371
378
|
operation === 'upsert' ? (stored ? recordJson(stored) : null) : null,
|
|
@@ -716,10 +723,12 @@ export function createShareRepository(storageHandle, { now = () => new Date().to
|
|
|
716
723
|
const timestamp = now()
|
|
717
724
|
return storage.transaction((database) => {
|
|
718
725
|
database.exec('DELETE FROM share_sessions; DELETE FROM share_tokens; DELETE FROM share_json_mirror_queue;')
|
|
726
|
+
const insertSession = database.prepare(SHARE_SESSION_UPSERT_SQL)
|
|
727
|
+
const insertMirror = database.prepare(ENQUEUE_SHARE_MIRROR_SQL)
|
|
719
728
|
for (const record of records) {
|
|
720
|
-
insertShareRow(database, { ...record, updatedAt: record.updatedAt || timestamp }, 1, record.createdAt || timestamp, record.updatedAt || timestamp)
|
|
729
|
+
insertShareRow(database, { ...record, updatedAt: record.updatedAt || timestamp }, 1, record.createdAt || timestamp, record.updatedAt || timestamp, insertSession)
|
|
721
730
|
replaceTokens(database, record.id, record.tokens)
|
|
722
|
-
enqueueShareMirror(database, { ...record, revision: 1 }, 'upsert', timestamp)
|
|
731
|
+
enqueueShareMirror(database, { ...record, revision: 1 }, 'upsert', timestamp, insertMirror)
|
|
723
732
|
}
|
|
724
733
|
const digest = shareSnapshotDigest(records)
|
|
725
734
|
if (expectedCount !== undefined && records.length !== expectedCount) throw new Error('Share replace count verification failed')
|
|
@@ -750,10 +759,12 @@ export function createShareRepository(storageHandle, { now = () => new Date().to
|
|
|
750
759
|
))
|
|
751
760
|
}
|
|
752
761
|
|
|
753
|
-
function verifyIntegrity({ quickCheck = false } = {}) {
|
|
762
|
+
function verifyIntegrity({ quickCheck = false, forceQuickCheck = false } = {}) {
|
|
754
763
|
if (quickCheck) {
|
|
755
|
-
|
|
756
|
-
|
|
764
|
+
// Shared quick_check gate (process cache + marker cadence) — see
|
|
765
|
+
// runSharedSqliteQuickCheck in database.mjs; `forceQuickCheck` is the
|
|
766
|
+
// manual-maintenance escape hatch that always runs a real scan.
|
|
767
|
+
runSharedSqliteQuickCheck(storage, { force: forceQuickCheck === true })
|
|
757
768
|
}
|
|
758
769
|
const rows = storage.prepare(`SELECT ${SHARE_COLUMNS} FROM share_sessions`).all()
|
|
759
770
|
let invalidRecords = 0
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { readScheduledRunsState } from './scheduled-runs-cutover.mjs'
|
|
2
|
+
import { readSessionStorageState } from './session-state-service.mjs'
|
|
3
|
+
import { readShareStorageState } from './share-service.mjs'
|
|
4
|
+
import { readLanAccessStorageState } from './lan-access-service.mjs'
|
|
5
|
+
|
|
6
|
+
// P1 startup maintenance window: the HTTP server listens before the storage
|
|
7
|
+
// migration/initialization chain completes. This module owns the process-wide
|
|
8
|
+
// startup state shared by server/index.mjs (maintenance gate, /api/health and
|
|
9
|
+
// /api/migration-status) and its tests.
|
|
10
|
+
|
|
11
|
+
export const STARTUP_STATES = Object.freeze({
|
|
12
|
+
MIGRATING: 'migrating',
|
|
13
|
+
READY: 'ready',
|
|
14
|
+
FAILED: 'failed',
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
// Read-only endpoints that stay reachable while the maintenance gate refuses
|
|
18
|
+
// every other /api/* route, so CLI/Desktop clients can observe progress.
|
|
19
|
+
export const MAINTENANCE_API_WHITELIST = Object.freeze([
|
|
20
|
+
'/api/health',
|
|
21
|
+
'/api/migration-status',
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
// The server process starts in 'migrating' at module evaluation; the startup
|
|
25
|
+
// chain moves it to 'ready' (all domains initialized) or 'failed'.
|
|
26
|
+
let startupState = STARTUP_STATES.MIGRATING
|
|
27
|
+
let startupError = null
|
|
28
|
+
|
|
29
|
+
export function getStartupState() {
|
|
30
|
+
return startupState
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getStartupError() {
|
|
34
|
+
return startupError
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function setStartupState(state, errorMessage = null) {
|
|
38
|
+
startupState = state
|
|
39
|
+
startupError = state === STARTUP_STATES.FAILED
|
|
40
|
+
? (errorMessage || 'Startup failed')
|
|
41
|
+
: null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Actionable recovery guidance appended to startup errors (fail-closed
|
|
45
|
+
// startup, review §5.1). The original exception text stays intact; the
|
|
46
|
+
// guidance is an additional multi-line paragraph shown verbatim by the
|
|
47
|
+
// frontend failed page (whitespace-pre-wrap).
|
|
48
|
+
export const STARTUP_RECOVERY_GUIDANCE = [
|
|
49
|
+
'Recovery guidance:',
|
|
50
|
+
'1. Stop all QuickForge processes so the data files are not locked.',
|
|
51
|
+
'2. Diagnose offline: node server/maintenance/downgrade-session-state-v1.mjs --dry-run',
|
|
52
|
+
'3. Export a backup: node server/maintenance/export-session-state-v1.mjs <output-path>',
|
|
53
|
+
'4. Follow the recovery runbook: docs/architecture/session-storage-recovery-runbook.zh-CN.md',
|
|
54
|
+
].join('\n')
|
|
55
|
+
|
|
56
|
+
export function withStartupRecoveryGuidance(errorMessage) {
|
|
57
|
+
return `${errorMessage || 'Startup failed'}\n\n${STARTUP_RECOVERY_GUIDANCE}`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Test helper: restore the module-evaluation defaults.
|
|
61
|
+
export function resetStartupState() {
|
|
62
|
+
startupState = STARTUP_STATES.MIGRATING
|
|
63
|
+
startupError = null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Maintenance gate for /api/* routes. Returns null when the request may be
|
|
67
|
+
// dispatched, or the maintenance refusal payload ({status, state}) otherwise.
|
|
68
|
+
export function resolveMaintenanceGate(method, pathname) {
|
|
69
|
+
const state = getStartupState()
|
|
70
|
+
if (state === STARTUP_STATES.READY) return null
|
|
71
|
+
if (!pathname || !pathname.startsWith('/api/')) return null
|
|
72
|
+
if (method === 'GET' && MAINTENANCE_API_WHITELIST.includes(pathname)) return null
|
|
73
|
+
return { status: 503, state }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readDomain(read) {
|
|
77
|
+
try {
|
|
78
|
+
return read()
|
|
79
|
+
} catch (error) {
|
|
80
|
+
// A domain whose storage is not initialized yet (or whose state table
|
|
81
|
+
// cannot be read) must not fail the whole status endpoint; report the
|
|
82
|
+
// error name so the UI can still render the remaining domains.
|
|
83
|
+
return { phase: 'unknown', error: error?.name || 'Error' }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Aggregated migration status across the four storage domains. `storage` is
|
|
88
|
+
// optional (scheduled-runs accepts an injected handle; the other domains read
|
|
89
|
+
// the process-wide SQLite storage internally).
|
|
90
|
+
export function readMigrationStatus(storage) {
|
|
91
|
+
const state = getStartupState()
|
|
92
|
+
const status = {
|
|
93
|
+
ok: true,
|
|
94
|
+
state,
|
|
95
|
+
domains: {
|
|
96
|
+
scheduledRuns: readDomain(() => readScheduledRunsState(storage)),
|
|
97
|
+
// Storage v2: the session domain is SQLite-authoritative by
|
|
98
|
+
// construction; readSessionStorageState reports the constant
|
|
99
|
+
// authoritative phase with a live session count.
|
|
100
|
+
sessionState: readDomain(() => readSessionStorageState()),
|
|
101
|
+
share: readDomain(() => readShareStorageState()),
|
|
102
|
+
lanAccess: readDomain(() => readLanAccessStorageState()),
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
if (state === STARTUP_STATES.FAILED) status.startupError = getStartupError()
|
|
106
|
+
return status
|
|
107
|
+
}
|