@bbigbang/core 0.1.0

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 (175) hide show
  1. package/dist/config.js +380 -0
  2. package/dist/execution/executionDispatcher.js +3810 -0
  3. package/dist/main.js +90 -0
  4. package/dist/nodeEventHistory.js +206 -0
  5. package/dist/scheduler/dreamLogic.js +50 -0
  6. package/dist/scheduler/dreamScheduler.js +65 -0
  7. package/dist/services/agentFileAccessService.js +1913 -0
  8. package/dist/services/agentRuntimeCleanupBroker.js +62 -0
  9. package/dist/services/agentSkillsBroker.js +118 -0
  10. package/dist/services/agentSkillsService.js +83 -0
  11. package/dist/services/agentWorkspaceBroker.js +937 -0
  12. package/dist/services/agentWorkspaceService.js +70 -0
  13. package/dist/services/appVersion.js +14 -0
  14. package/dist/services/auth.js +586 -0
  15. package/dist/services/claudeControlBroker.js +154 -0
  16. package/dist/services/claudeTranscriptBroker.js +100 -0
  17. package/dist/services/claudeTranscriptService.js +359 -0
  18. package/dist/services/codexAppServerBroker.js +155 -0
  19. package/dist/services/codexTranscriptBroker.js +98 -0
  20. package/dist/services/codexTranscriptService.js +961 -0
  21. package/dist/services/droidMissionBroker.js +124 -0
  22. package/dist/services/droidMissionImporter.js +630 -0
  23. package/dist/services/droidModelOptions.js +165 -0
  24. package/dist/services/hubServerRegistrationService.js +268 -0
  25. package/dist/services/libraryManifest.js +43 -0
  26. package/dist/services/libraryScaffold.js +26 -0
  27. package/dist/services/libraryService.js +2263 -0
  28. package/dist/services/memoryService.js +386 -0
  29. package/dist/services/missionEvidence.js +377 -0
  30. package/dist/services/missionService.js +2361 -0
  31. package/dist/services/missionTrace.js +158 -0
  32. package/dist/services/nativeMissionBriefParser.js +120 -0
  33. package/dist/services/nativeMissionOrchestrator.js +2045 -0
  34. package/dist/services/nativeMissionReportGenerator.js +227 -0
  35. package/dist/services/nativeMissionValidationRunner.js +452 -0
  36. package/dist/services/nativeMissionWorkerBroker.js +190 -0
  37. package/dist/services/nodeRegistry.js +34 -0
  38. package/dist/services/nodeStateReconciler.js +97 -0
  39. package/dist/services/panelMediaScanner.js +119 -0
  40. package/dist/services/persistentRuntimeJsonlClient.js +153 -0
  41. package/dist/services/platformAgentPolicy.js +180 -0
  42. package/dist/services/platformAgentService.js +2041 -0
  43. package/dist/services/projectAccessResolver.js +93 -0
  44. package/dist/services/projectService.js +392 -0
  45. package/dist/services/resourceSpaceService.js +140 -0
  46. package/dist/services/scenarioRuntimeService.js +1130 -0
  47. package/dist/services/suggestedPlannerService.js +868 -0
  48. package/dist/services/workbenchGitBroker.js +161 -0
  49. package/dist/services/workbenchGitService.js +69 -0
  50. package/dist/services/workbenchInspectBroker.js +65 -0
  51. package/dist/services/workbenchNodePathService.js +79 -0
  52. package/dist/services/workbenchRegistryService.js +240 -0
  53. package/dist/services/workbenchRootService.js +181 -0
  54. package/dist/services/workbenchTerminalBroker.js +378 -0
  55. package/dist/services/workspaceRunOwnership.js +60 -0
  56. package/dist/services/workspaceScaffold.js +105 -0
  57. package/dist/services/workspaceSessionRuntimeService.js +576 -0
  58. package/dist/services/workspaceSessionService.js +245 -0
  59. package/dist/services/workspaceToolActionRunner.js +1582 -0
  60. package/dist/services/workspaceToolErrors.js +10 -0
  61. package/dist/services/workspaceToolExecutionUtils.js +895 -0
  62. package/dist/services/workspaceToolLatestStateProjector.js +91 -0
  63. package/dist/services/workspaceToolManifest.js +572 -0
  64. package/dist/services/workspaceToolMutationQueue.js +43 -0
  65. package/dist/services/workspaceToolPanelProjection.js +460 -0
  66. package/dist/services/workspaceToolPromotion.js +255 -0
  67. package/dist/services/workspaceToolPromotionState.js +224 -0
  68. package/dist/services/workspaceToolPublishDiagnostics.js +189 -0
  69. package/dist/services/workspaceToolPublishIdentityResolver.js +146 -0
  70. package/dist/services/workspaceToolReadModel.js +378 -0
  71. package/dist/services/workspaceToolRunLedger.js +239 -0
  72. package/dist/services/workspaceToolService.js +3067 -0
  73. package/dist/services/workspaceToolSnapshotPanelSync.js +293 -0
  74. package/dist/services/workspaceToolTerminalLifecycle.js +283 -0
  75. package/dist/services/workspaceToolTypes.js +1 -0
  76. package/dist/services/workspaceToolUploadMaterializer.js +228 -0
  77. package/dist/web/actionCardRoutes.js +129 -0
  78. package/dist/web/actionCards.js +469 -0
  79. package/dist/web/activationContext.js +684 -0
  80. package/dist/web/agentChannelGuards.js +48 -0
  81. package/dist/web/agentMentionCooldowns.js +32 -0
  82. package/dist/web/agentReminders.js +1668 -0
  83. package/dist/web/agentRuntimePresence.js +197 -0
  84. package/dist/web/agentSelfState.js +494 -0
  85. package/dist/web/agentTaskLinks.js +26 -0
  86. package/dist/web/agentVisibility.js +79 -0
  87. package/dist/web/assets.js +95 -0
  88. package/dist/web/channelActivationPrompt.js +395 -0
  89. package/dist/web/channelMemoryNotes.js +127 -0
  90. package/dist/web/channelMentions.js +10 -0
  91. package/dist/web/channelMessageSequences.js +19 -0
  92. package/dist/web/channelSubscriptions.js +26 -0
  93. package/dist/web/clearedTaskRoots.js +10 -0
  94. package/dist/web/collaborationPromptGuidance.js +36 -0
  95. package/dist/web/collaborationSurfaceState.js +140 -0
  96. package/dist/web/contextBundleRanking.js +154 -0
  97. package/dist/web/contextBundleResolver.js +488 -0
  98. package/dist/web/conversationBuiltinSkillRoots.js +50 -0
  99. package/dist/web/conversationControls.js +232 -0
  100. package/dist/web/conversationHandoffs.js +612 -0
  101. package/dist/web/conversationManager.js +2511 -0
  102. package/dist/web/conversationSummaries.js +876 -0
  103. package/dist/web/conversationSurfaceKinds.js +17 -0
  104. package/dist/web/conversationTargets.js +173 -0
  105. package/dist/web/directActivationPrompt.js +122 -0
  106. package/dist/web/directReplyTargets.js +69 -0
  107. package/dist/web/directThreadResolver.js +129 -0
  108. package/dist/web/dmTaskHandoffPrompt.js +120 -0
  109. package/dist/web/dmTaskThreadStatusProjection.js +229 -0
  110. package/dist/web/ftsQuery.js +33 -0
  111. package/dist/web/internalAgentRouter.js +11341 -0
  112. package/dist/web/libraryCuratorScheduler.js +58 -0
  113. package/dist/web/libraryDocumentPromptGuidance.js +8 -0
  114. package/dist/web/messageCheckpoints.js +19 -0
  115. package/dist/web/nodeWsHandler.js +2495 -0
  116. package/dist/web/notificationRounds.js +1061 -0
  117. package/dist/web/panelActionMessages.js +108 -0
  118. package/dist/web/panelActivationPrompt.js +18 -0
  119. package/dist/web/panelAudit.js +273 -0
  120. package/dist/web/panelLifecycle.js +222 -0
  121. package/dist/web/panelMediaPolicy.js +43 -0
  122. package/dist/web/panelPathPolicy.js +63 -0
  123. package/dist/web/panelPreviews.js +175 -0
  124. package/dist/web/panelQueryHandles.js +2749 -0
  125. package/dist/web/panelRoutes.js +2147 -0
  126. package/dist/web/panels.js +904 -0
  127. package/dist/web/peerInboxAggregates.js +1247 -0
  128. package/dist/web/planApprovalState.js +92 -0
  129. package/dist/web/platformAgentScheduler.js +66 -0
  130. package/dist/web/proactiveOpportunities.js +452 -0
  131. package/dist/web/promptContextSections.js +242 -0
  132. package/dist/web/promptHistorySanitizer.js +26 -0
  133. package/dist/web/promptSlashCommands.js +158 -0
  134. package/dist/web/rollingConversationSummary.js +453 -0
  135. package/dist/web/routeHelpers.js +11 -0
  136. package/dist/web/routes/handoff.js +288 -0
  137. package/dist/web/routes/history.js +345 -0
  138. package/dist/web/routes/memory.js +258 -0
  139. package/dist/web/routes/selfState.js +171 -0
  140. package/dist/web/routes/workspace.js +154 -0
  141. package/dist/web/runSurfaceWatermarks.js +431 -0
  142. package/dist/web/runtimeCapabilities.js +48 -0
  143. package/dist/web/sameAgentHandoffs.js +494 -0
  144. package/dist/web/server.js +15567 -0
  145. package/dist/web/sharedCollaborationCapsules.js +163 -0
  146. package/dist/web/soloSessionRelay.js +42 -0
  147. package/dist/web/soloWsHandler.js +138 -0
  148. package/dist/web/suggestedPlannerScheduler.js +56 -0
  149. package/dist/web/surfaceActivationPolicy.js +108 -0
  150. package/dist/web/surfaceCollaborators.js +61 -0
  151. package/dist/web/surfaceSystemStatus.js +263 -0
  152. package/dist/web/targetParticipants.js +77 -0
  153. package/dist/web/taskEvents.js +49 -0
  154. package/dist/web/taskLifecycleMessages.js +165 -0
  155. package/dist/web/taskLoops.js +732 -0
  156. package/dist/web/taskMemoryNotes.js +224 -0
  157. package/dist/web/taskNumbers.js +16 -0
  158. package/dist/web/taskOwnerGuards.js +49 -0
  159. package/dist/web/taskParticipantResolver.js +42 -0
  160. package/dist/web/taskParticipants.js +97 -0
  161. package/dist/web/taskSourceDetails.js +20 -0
  162. package/dist/web/taskStateViews.js +210 -0
  163. package/dist/web/taskStatusTransitions.js +9 -0
  164. package/dist/web/taskThreadFollowups.js +599 -0
  165. package/dist/web/taskThreadRuntimeClosure.js +685 -0
  166. package/dist/web/taskUpdateDelivery.js +104 -0
  167. package/dist/web/threadReplyContentHeuristics.js +30 -0
  168. package/dist/web/threadRoots.js +61 -0
  169. package/dist/web/threadTaskBindings.js +365 -0
  170. package/dist/web/uiPanelPromptGuidance.js +27 -0
  171. package/dist/web/workspaceMemoryHints.js +143 -0
  172. package/dist/web/workspaceToolPromptGuidance.js +30 -0
  173. package/dist/web/wsHandler.js +397 -0
  174. package/dist/web/wsSink.js +116 -0
  175. package/package.json +54 -0
@@ -0,0 +1,904 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ export function insertPanel(db, params) {
3
+ const id = params.id ?? randomUUID();
4
+ const now = params.createdAt ?? Date.now();
5
+ db.prepare(`INSERT INTO panels(
6
+ id, agent_id, conversation_id, owner_user_id, handle, scope_type, scope_id,
7
+ component, props_json, actions_json, dataset_source_json, row_count, status, progress_json, result_json, version,
8
+ created_by_run_id, created_at, updated_at
9
+ )
10
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, params.agentId, params.conversationId, params.ownerUserId ?? null, normalizePanelHandle(params.handle), params.scopeType, params.scopeId ?? null, params.component, JSON.stringify(params.props ?? {}), JSON.stringify(params.actions ?? []), params.datasetSource ? JSON.stringify(params.datasetSource) : null, params.rowCount ?? 0, params.status ?? 'idle', params.progress == null ? null : JSON.stringify(params.progress), params.result == null ? null : JSON.stringify(params.result), params.version ?? 1, params.createdByRunId ?? null, now, params.updatedAt ?? now);
11
+ return id;
12
+ }
13
+ function normalizePanelHandle(handle) {
14
+ const normalized = handle?.trim() ?? '';
15
+ return normalized || null;
16
+ }
17
+ export function insertPanelRows(db, rows) {
18
+ const insert = db.prepare(`INSERT INTO panel_rows(
19
+ panel_id, row_index, row_id, fields_json, media_json,
20
+ node_id, cached_asset_ids_json
21
+ )
22
+ VALUES(?, ?, ?, ?, ?, ?, ?)`);
23
+ for (const row of rows) {
24
+ insert.run(row.panelId, row.rowIndex, row.rowId ?? null, JSON.stringify(row.fields), JSON.stringify(row.media), row.nodeId ?? null, JSON.stringify(row.cachedAssetIds ?? {}));
25
+ }
26
+ }
27
+ export function getPanelById(db, id) {
28
+ const row = db.prepare(`SELECT
29
+ id, agent_id as agentId, conversation_id as conversationId, owner_user_id as ownerUserId,
30
+ handle,
31
+ scope_type as scopeType, scope_id as scopeId,
32
+ component, props_json as propsJson, actions_json as actionsJson,
33
+ dataset_source_json as datasetSourceJson,
34
+ row_count as rowCount, status, progress_json as progressJson,
35
+ result_json as resultJson, version, created_by_run_id as createdByRunId,
36
+ created_at as createdAt, updated_at as updatedAt, archived_at as archivedAt
37
+ FROM panels
38
+ WHERE id = ?
39
+ LIMIT 1`).get(id);
40
+ if (!row)
41
+ return null;
42
+ const datasetSource = parseNullableJsonObject(row.datasetSourceJson);
43
+ return {
44
+ id: row.id,
45
+ agentId: row.agentId,
46
+ conversationId: row.conversationId,
47
+ ownerUserId: row.ownerUserId ?? undefined,
48
+ handle: row.handle,
49
+ scopeType: row.scopeType,
50
+ scopeId: row.scopeId ?? undefined,
51
+ toolId: row.scopeType === 'tool' ? (row.scopeId ?? undefined) : undefined,
52
+ component: row.component,
53
+ props: parseJsonObject(row.propsJson),
54
+ actions: parseJsonArray(row.actionsJson),
55
+ rowCount: row.rowCount,
56
+ rowCountKnown: row.rowCount >= 0,
57
+ datasetSource: datasetSource ?? undefined,
58
+ status: normalizePanelStatus(row.status),
59
+ progress: parseNullableJsonObject(row.progressJson),
60
+ result: parseNullableJsonObject(row.resultJson),
61
+ version: row.version,
62
+ createdByRunId: row.createdByRunId ?? undefined,
63
+ createdAt: new Date(row.createdAt).toISOString(),
64
+ updatedAt: new Date(row.updatedAt).toISOString(),
65
+ archivedAt: row.archivedAt == null ? null : new Date(row.archivedAt).toISOString(),
66
+ };
67
+ }
68
+ export function getPanelByStableHandle(db, params) {
69
+ const ownerUserId = params.ownerUserId?.trim() ?? '';
70
+ const handle = normalizePanelHandle(params.handle);
71
+ if (!ownerUserId || !handle)
72
+ return null;
73
+ const row = db.prepare(`SELECT id
74
+ FROM panels
75
+ WHERE owner_user_id = ?
76
+ AND agent_id = ?
77
+ AND conversation_id = ?
78
+ AND handle = ?
79
+ AND archived_at IS NULL
80
+ LIMIT 1`).get(ownerUserId, params.agentId, params.conversationId, handle);
81
+ return row ? getPanelById(db, row.id) : null;
82
+ }
83
+ export function deletePanel(db, id) {
84
+ const result = db.prepare('DELETE FROM panels WHERE id = ?').run(id);
85
+ return result.changes > 0;
86
+ }
87
+ export function getPanelState(db, panelId, userId) {
88
+ const row = db.prepare(`SELECT panel_id as panelId, user_id as userId,
89
+ state_json as stateJson, updated_at as updatedAt
90
+ FROM panel_state
91
+ WHERE panel_id = ? AND user_id = ?
92
+ LIMIT 1`).get(panelId, userId);
93
+ if (!row)
94
+ return null;
95
+ return {
96
+ panelId: row.panelId,
97
+ userId: row.userId,
98
+ state: parseJsonObject(row.stateJson),
99
+ updatedAt: new Date(row.updatedAt).toISOString(),
100
+ };
101
+ }
102
+ export function upsertPanelState(db, panelId, userId, state) {
103
+ const now = Date.now();
104
+ db.prepare(`INSERT INTO panel_state(panel_id, user_id, state_json, updated_at)
105
+ VALUES(?, ?, ?, ?)
106
+ ON CONFLICT(panel_id, user_id)
107
+ DO UPDATE SET state_json = excluded.state_json, updated_at = excluded.updated_at`).run(panelId, userId, JSON.stringify(state), now);
108
+ }
109
+ export function listPanelsForConversation(db, conversationId) {
110
+ const rows = db.prepare(`SELECT
111
+ id, agent_id as agentId, conversation_id as conversationId, owner_user_id as ownerUserId,
112
+ handle,
113
+ scope_type as scopeType, scope_id as scopeId,
114
+ component, props_json as propsJson, actions_json as actionsJson,
115
+ dataset_source_json as datasetSourceJson,
116
+ row_count as rowCount, status, progress_json as progressJson,
117
+ result_json as resultJson, version, created_by_run_id as createdByRunId,
118
+ created_at as createdAt, updated_at as updatedAt, archived_at as archivedAt
119
+ FROM panels
120
+ WHERE conversation_id = ?
121
+ ORDER BY created_at DESC`).all(conversationId);
122
+ return rows.map((row) => ({
123
+ id: row.id,
124
+ agentId: row.agentId,
125
+ conversationId: row.conversationId,
126
+ ownerUserId: row.ownerUserId ?? undefined,
127
+ handle: row.handle,
128
+ scopeType: row.scopeType,
129
+ scopeId: row.scopeId ?? undefined,
130
+ toolId: row.scopeType === 'tool' ? (row.scopeId ?? undefined) : undefined,
131
+ component: row.component,
132
+ props: parseJsonObject(row.propsJson),
133
+ actions: parseJsonArray(row.actionsJson),
134
+ rowCount: row.rowCount,
135
+ rowCountKnown: row.rowCount >= 0,
136
+ datasetSource: parseNullableJsonObject(row.datasetSourceJson) ?? undefined,
137
+ status: normalizePanelStatus(row.status),
138
+ progress: parseNullableJsonObject(row.progressJson),
139
+ result: parseNullableJsonObject(row.resultJson),
140
+ version: row.version,
141
+ createdByRunId: row.createdByRunId ?? undefined,
142
+ createdAt: new Date(row.createdAt).toISOString(),
143
+ updatedAt: new Date(row.updatedAt).toISOString(),
144
+ archivedAt: row.archivedAt == null ? null : new Date(row.archivedAt).toISOString(),
145
+ }));
146
+ }
147
+ export function countPanelRows(db, panelId) {
148
+ const row = db.prepare(`SELECT COUNT(*) as count FROM panel_rows WHERE panel_id = ?`).get(panelId);
149
+ return row?.count ?? 0;
150
+ }
151
+ function parseJsonObject(value) {
152
+ try {
153
+ return JSON.parse(value);
154
+ }
155
+ catch {
156
+ return {};
157
+ }
158
+ }
159
+ function parseNullableJsonObject(value) {
160
+ if (!value)
161
+ return null;
162
+ try {
163
+ const parsed = JSON.parse(value);
164
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
165
+ ? parsed
166
+ : null;
167
+ }
168
+ catch {
169
+ return null;
170
+ }
171
+ }
172
+ function normalizePanelStatus(value) {
173
+ if (value === 'running' || value === 'completed' || value === 'failed' || value === 'idle') {
174
+ return value;
175
+ }
176
+ if (value === 'ready')
177
+ return 'idle';
178
+ return 'idle';
179
+ }
180
+ const PANEL_FIELD_PATH_PATTERN = /^[A-Za-z_][A-Za-z0-9_:-]{0,63}(?:\.[A-Za-z_][A-Za-z0-9_:-]{0,63}){0,15}$/;
181
+ export function getPanelFieldValue(fields, fieldPath) {
182
+ if (Object.prototype.hasOwnProperty.call(fields, fieldPath)) {
183
+ return fields[fieldPath];
184
+ }
185
+ if (!fieldPath.includes('.')) {
186
+ return fields[fieldPath];
187
+ }
188
+ let current = fields;
189
+ for (const segment of fieldPath.split('.')) {
190
+ if (!current || typeof current !== 'object' || Array.isArray(current)) {
191
+ return undefined;
192
+ }
193
+ const record = current;
194
+ if (!Object.prototype.hasOwnProperty.call(record, segment)) {
195
+ return undefined;
196
+ }
197
+ current = record[segment];
198
+ }
199
+ return current;
200
+ }
201
+ function quotedJsonPathForField(fieldPath, mode) {
202
+ if (!PANEL_FIELD_PATH_PATTERN.test(fieldPath)) {
203
+ throw new Error(`Unsafe panel field path: ${fieldPath}`);
204
+ }
205
+ const segments = mode === 'direct' ? [fieldPath] : fieldPath.split('.');
206
+ return `$${segments.map((segment) => `."${segment}"`).join('')}`;
207
+ }
208
+ function panelFieldJsonExtractExpression(fieldPath) {
209
+ const directPath = quotedJsonPathForField(fieldPath, 'direct');
210
+ const nestedPath = quotedJsonPathForField(fieldPath, 'nested');
211
+ const directExtract = `json_extract(fields_json, '${directPath}')`;
212
+ if (directPath === nestedPath)
213
+ return directExtract;
214
+ return `COALESCE(${directExtract}, json_extract(fields_json, '${nestedPath}'))`;
215
+ }
216
+ function toSqlJsonValue(value) {
217
+ if (typeof value === 'boolean')
218
+ return value ? 1 : 0;
219
+ if (typeof value === 'number' && Number.isFinite(value))
220
+ return value;
221
+ if (typeof value === 'string')
222
+ return value;
223
+ return null;
224
+ }
225
+ export function listPanelRows(db, panelId, options) {
226
+ const limit = Math.min(Math.max(1, options.limit), 200);
227
+ let cursorData = null;
228
+ if (options.cursor) {
229
+ try {
230
+ cursorData = JSON.parse(Buffer.from(options.cursor, 'base64').toString('utf8'));
231
+ }
232
+ catch {
233
+ cursorData = null;
234
+ }
235
+ }
236
+ const conditions = ['panel_id = ?'];
237
+ const params = [panelId];
238
+ if (options.filterField && options.filterValue !== undefined) {
239
+ conditions.push(`${panelFieldJsonExtractExpression(options.filterField)} = ?`);
240
+ params.push(toSqlJsonValue(options.filterValue));
241
+ }
242
+ if (options.sortField && options.sortDirection) {
243
+ const sortField = options.sortField;
244
+ const direction = options.sortDirection;
245
+ const isNumericSort = options.sortFieldType === 'number';
246
+ const fieldExtract = panelFieldJsonExtractExpression(sortField);
247
+ const extract = isNumericSort
248
+ ? `COALESCE(CAST(${fieldExtract} AS REAL), 0)`
249
+ : fieldExtract;
250
+ if (cursorData && cursorData.ri !== undefined) {
251
+ if (direction === 'asc') {
252
+ conditions.push(`(${extract} > ? OR (${extract} = ? AND row_index > ?))`);
253
+ const sortValue = toSqlJsonValue(cursorData.sv);
254
+ params.push(sortValue, sortValue, cursorData.ri);
255
+ }
256
+ else {
257
+ conditions.push(`(${extract} < ? OR (${extract} = ? AND row_index < ?))`);
258
+ const sortValue = toSqlJsonValue(cursorData.sv);
259
+ params.push(sortValue, sortValue, cursorData.ri);
260
+ }
261
+ }
262
+ const orderBy = direction === 'asc' ? 'ASC' : 'DESC';
263
+ const tieBreaker = direction === 'asc' ? 'ASC' : 'DESC';
264
+ const sql = `SELECT row_index as rowIndex, row_id as rowId, fields_json as fieldsJson, media_json as mediaJson
265
+ FROM panel_rows
266
+ WHERE ${conditions.join(' AND ')}
267
+ ORDER BY ${extract} ${orderBy}, row_index ${tieBreaker}
268
+ LIMIT ?`;
269
+ params.push(limit + 1);
270
+ const rows = db.prepare(sql).all(...params);
271
+ const hasMore = rows.length > limit;
272
+ const resultRows = hasMore ? rows.slice(0, limit) : rows;
273
+ const lastRow = resultRows[resultRows.length - 1];
274
+ const nextCursor = hasMore && lastRow
275
+ ? Buffer.from(JSON.stringify({
276
+ ri: lastRow.rowIndex,
277
+ sv: getPanelFieldValue(parseJsonObject(lastRow.fieldsJson), sortField),
278
+ }), 'utf8').toString('base64')
279
+ : null;
280
+ return {
281
+ rows: resultRows.map((r) => ({
282
+ rowIndex: r.rowIndex,
283
+ rowId: r.rowId,
284
+ fields: parseJsonObject(r.fieldsJson),
285
+ media: parseJsonObject(r.mediaJson),
286
+ })),
287
+ nextCursor,
288
+ };
289
+ }
290
+ // Default sort by row_index ASC
291
+ if (cursorData && cursorData.ri !== undefined) {
292
+ conditions.push('row_index > ?');
293
+ params.push(cursorData.ri);
294
+ }
295
+ const sql = `SELECT row_index as rowIndex, row_id as rowId, fields_json as fieldsJson, media_json as mediaJson
296
+ FROM panel_rows
297
+ WHERE ${conditions.join(' AND ')}
298
+ ORDER BY row_index ASC
299
+ LIMIT ?`;
300
+ params.push(limit + 1);
301
+ const rows = db.prepare(sql).all(...params);
302
+ const hasMore = rows.length > limit;
303
+ const resultRows = hasMore ? rows.slice(0, limit) : rows;
304
+ const lastRow = resultRows[resultRows.length - 1];
305
+ const nextCursor = hasMore && lastRow
306
+ ? Buffer.from(JSON.stringify({ ri: lastRow.rowIndex }), 'utf8').toString('base64')
307
+ : null;
308
+ return {
309
+ rows: resultRows.map((r) => ({
310
+ rowIndex: r.rowIndex,
311
+ rowId: r.rowId,
312
+ fields: parseJsonObject(r.fieldsJson),
313
+ media: parseJsonObject(r.mediaJson),
314
+ })),
315
+ nextCursor,
316
+ };
317
+ }
318
+ export function aggregatePanelRows(db, panelId, options) {
319
+ const conditions = ['panel_id = ?'];
320
+ const params = [panelId];
321
+ if (options.filterField && options.filterValue !== undefined) {
322
+ conditions.push(`${panelFieldJsonExtractExpression(options.filterField)} = ?`);
323
+ params.push(toSqlJsonValue(options.filterValue));
324
+ }
325
+ const rows = db.prepare(`SELECT fields_json as fieldsJson
326
+ FROM panel_rows
327
+ WHERE ${conditions.join(' AND ')}
328
+ ORDER BY row_index ASC`).all(...params);
329
+ return aggregatePanelFieldRecords(rows.map((row) => parseJsonObject(row.fieldsJson)), {
330
+ op: options.op,
331
+ field: options.field,
332
+ });
333
+ }
334
+ export function aggregatePanelFieldRecords(rows, options) {
335
+ const totalCount = rows.length;
336
+ if (options.op === 'count' && !options.field) {
337
+ return {
338
+ op: options.op,
339
+ field: null,
340
+ value: totalCount,
341
+ count: totalCount,
342
+ totalCount,
343
+ hasValue: true,
344
+ };
345
+ }
346
+ const field = options.field ?? null;
347
+ if (!field) {
348
+ return {
349
+ op: options.op,
350
+ field,
351
+ value: null,
352
+ count: 0,
353
+ totalCount,
354
+ hasValue: false,
355
+ };
356
+ }
357
+ if (options.op === 'count') {
358
+ const count = rows.filter((fields) => hasNonEmptyPanelValue(getPanelFieldValue(fields, field))).length;
359
+ return {
360
+ op: options.op,
361
+ field,
362
+ value: count,
363
+ count,
364
+ totalCount,
365
+ hasValue: true,
366
+ };
367
+ }
368
+ const values = rows
369
+ .map((fields) => toFinitePanelNumber(getPanelFieldValue(fields, field)))
370
+ .filter((value) => value !== null);
371
+ if (values.length === 0) {
372
+ return {
373
+ op: options.op,
374
+ field,
375
+ value: null,
376
+ count: 0,
377
+ totalCount,
378
+ hasValue: false,
379
+ };
380
+ }
381
+ let value;
382
+ switch (options.op) {
383
+ case 'sum':
384
+ value = values.reduce((total, item) => total + item, 0);
385
+ break;
386
+ case 'avg':
387
+ value = values.reduce((total, item) => total + item, 0) / values.length;
388
+ break;
389
+ case 'min':
390
+ value = Math.min(...values);
391
+ break;
392
+ case 'max':
393
+ value = Math.max(...values);
394
+ break;
395
+ default:
396
+ value = Number.NaN;
397
+ break;
398
+ }
399
+ return {
400
+ op: options.op,
401
+ field,
402
+ value: Number.isFinite(value) ? value : null,
403
+ count: values.length,
404
+ totalCount,
405
+ hasValue: Number.isFinite(value),
406
+ };
407
+ }
408
+ function hasNonEmptyPanelValue(value) {
409
+ if (value === null || value === undefined)
410
+ return false;
411
+ if (typeof value === 'string')
412
+ return value.trim().length > 0;
413
+ if (Array.isArray(value))
414
+ return value.length > 0;
415
+ if (typeof value === 'object')
416
+ return Object.keys(value).length > 0;
417
+ return true;
418
+ }
419
+ function toFinitePanelNumber(value) {
420
+ if (typeof value === 'number' && Number.isFinite(value))
421
+ return value;
422
+ if (typeof value === 'string' && value.trim() !== '') {
423
+ const parsed = Number(value);
424
+ if (Number.isFinite(parsed))
425
+ return parsed;
426
+ }
427
+ return null;
428
+ }
429
+ export function validateStateBlob(stateSchema, blob) {
430
+ const errors = [];
431
+ if (typeof blob !== 'object' || blob === null || Array.isArray(blob)) {
432
+ errors.push('State must be an object');
433
+ return errors;
434
+ }
435
+ const schemaObj = stateSchema;
436
+ if (schemaObj?.type === 'object' && schemaObj.properties) {
437
+ for (const [key, propSchema] of Object.entries(schemaObj.properties)) {
438
+ const value = blob[key];
439
+ if (value === undefined)
440
+ continue;
441
+ const expectedType = propSchema.type;
442
+ if (!expectedType)
443
+ continue;
444
+ const actualType = Array.isArray(value) ? 'array' : typeof value;
445
+ if (actualType !== expectedType) {
446
+ errors.push(`Property "${key}" must be of type ${expectedType}`);
447
+ }
448
+ }
449
+ }
450
+ return errors;
451
+ }
452
+ function parseJsonArray(value) {
453
+ try {
454
+ const parsed = JSON.parse(value);
455
+ return Array.isArray(parsed) ? parsed : [];
456
+ }
457
+ catch {
458
+ return [];
459
+ }
460
+ }
461
+ export function getPanelRowByIndex(db, panelId, rowIndex) {
462
+ const row = db.prepare(`SELECT row_index as rowIndex, row_id as rowId, fields_json as fieldsJson, media_json as mediaJson,
463
+ node_id as nodeId, cached_asset_ids_json as cachedAssetIdsJson
464
+ FROM panel_rows
465
+ WHERE panel_id = ? AND row_index = ?
466
+ LIMIT 1`).get(panelId, rowIndex);
467
+ if (!row)
468
+ return null;
469
+ return {
470
+ rowIndex: row.rowIndex,
471
+ rowId: row.rowId,
472
+ fields: parseJsonObject(row.fieldsJson),
473
+ media: parseJsonObject(row.mediaJson),
474
+ nodeId: row.nodeId,
475
+ cachedAssetIds: parseJsonObject(row.cachedAssetIdsJson),
476
+ };
477
+ }
478
+ export function generatePanelRowId(db, panelId, preferredIndex) {
479
+ const existingRows = db.prepare(`SELECT row_id as rowId FROM panel_rows WHERE panel_id = ? AND row_id IS NOT NULL`).all(panelId);
480
+ const existing = new Set(existingRows.map((row) => row.rowId));
481
+ let candidate = `row-${preferredIndex}`;
482
+ let suffix = 1;
483
+ while (existing.has(candidate)) {
484
+ candidate = `row-${preferredIndex}-${suffix}`;
485
+ suffix += 1;
486
+ }
487
+ return candidate;
488
+ }
489
+ export function patchPanel(db, params) {
490
+ const current = getPanelById(db, params.panelId);
491
+ if (!current) {
492
+ return { ok: false, failureClass: 'panel_not_found', details: { panelId: params.panelId } };
493
+ }
494
+ if (current.archivedAt) {
495
+ return { ok: false, failureClass: 'panel_archived', details: { panelId: params.panelId } };
496
+ }
497
+ if (params.expectedVersion !== undefined && params.expectedVersion !== current.version) {
498
+ return {
499
+ ok: false,
500
+ failureClass: 'version_conflict',
501
+ details: {
502
+ panelId: params.panelId,
503
+ expectedVersion: params.expectedVersion,
504
+ currentVersion: current.version,
505
+ recommendedAction: 'Read the latest panel state/events/rows, merge user changes, then retry with the current version. Do not blindly overwrite saved user state or annotations.',
506
+ },
507
+ };
508
+ }
509
+ const changed = new Set();
510
+ const nextProps = params.props !== undefined
511
+ ? params.props
512
+ : params.propsPatch
513
+ ? { ...(current.props ?? {}), ...params.propsPatch }
514
+ : current.props ?? {};
515
+ if (params.props !== undefined || params.propsPatch)
516
+ changed.add('props');
517
+ const nextActions = params.actions ?? current.actions ?? [];
518
+ if (params.actions)
519
+ changed.add('actions');
520
+ const nextStatus = params.status ?? current.status;
521
+ if (params.status !== undefined)
522
+ changed.add('status');
523
+ const nextProgress = params.progress === undefined ? current.progress ?? null : params.progress;
524
+ if (params.progress !== undefined)
525
+ changed.add('progress');
526
+ const nextResult = params.result === undefined ? current.result ?? null : params.result;
527
+ if (params.result !== undefined)
528
+ changed.add('result');
529
+ const currentRows = listAllPanelRows(db, params.panelId);
530
+ let nextRows = currentRows;
531
+ if (params.rows) {
532
+ const rowPatch = params.rows.replace
533
+ ? {
534
+ ok: true,
535
+ rows: params.rows.replace.map((row, index) => toPanelRowMedia(params.panelId, index, normalizePatchRowId(row.rowId) ?? generatePanelRowId(db, params.panelId, index), row, params.defaultNodeId ?? null)),
536
+ }
537
+ : applyRowPatch(db, params.panelId, nextRows, params.rows, params.defaultNodeId ?? null);
538
+ if (!rowPatch.ok)
539
+ return rowPatch;
540
+ nextRows = rowPatch.rows;
541
+ changed.add('rows');
542
+ }
543
+ if (changed.size === 0) {
544
+ return {
545
+ ok: false,
546
+ failureClass: 'empty_patch',
547
+ details: { panelId: params.panelId, issue: 'At least one patch field is required.' },
548
+ };
549
+ }
550
+ const validation = params.validate?.({
551
+ panel: current,
552
+ props: nextProps,
553
+ actions: nextActions,
554
+ rows: nextRows,
555
+ status: nextStatus,
556
+ progress: nextProgress,
557
+ result: nextResult,
558
+ });
559
+ if (validation && !validation.ok) {
560
+ return validation;
561
+ }
562
+ const now = Date.now();
563
+ const nextVersion = current.version + 1;
564
+ let updateChanges = 0;
565
+ db.transaction(() => {
566
+ const updateResult = db.prepare(`UPDATE panels
567
+ SET props_json = ?,
568
+ actions_json = ?,
569
+ row_count = ?,
570
+ status = ?,
571
+ progress_json = ?,
572
+ result_json = ?,
573
+ dataset_source_json = ?,
574
+ version = ?,
575
+ updated_at = ?
576
+ WHERE id = ? AND version = ? AND archived_at IS NULL`).run(JSON.stringify(nextProps), JSON.stringify(nextActions), Number.isInteger(params.rowCount) ? params.rowCount : nextRows.length, nextStatus, nextProgress == null ? null : JSON.stringify(nextProgress), nextResult == null ? null : JSON.stringify(nextResult), params.datasetSource === undefined
577
+ ? current.datasetSource ? JSON.stringify(current.datasetSource) : null
578
+ : params.datasetSource ? JSON.stringify(params.datasetSource) : null, nextVersion, now, params.panelId, current.version);
579
+ updateChanges = updateResult.changes;
580
+ if (updateChanges === 0)
581
+ return;
582
+ if (params.rows) {
583
+ replacePanelRows(db, params.panelId, nextRows);
584
+ remapPanelSelectionState(db, params.panelId, currentRows, nextRows, now);
585
+ }
586
+ })();
587
+ if (updateChanges === 0) {
588
+ const latest = getPanelById(db, params.panelId);
589
+ if (latest?.archivedAt) {
590
+ return { ok: false, failureClass: 'panel_archived', details: { panelId: params.panelId } };
591
+ }
592
+ return {
593
+ ok: false,
594
+ failureClass: 'version_conflict',
595
+ details: {
596
+ panelId: params.panelId,
597
+ expectedVersion: current.version,
598
+ currentVersion: getPanelById(db, params.panelId)?.version ?? current.version,
599
+ recommendedAction: 'Read the latest panel state/events/rows, merge user changes, then retry with the current version. Do not blindly overwrite saved user state or annotations.',
600
+ },
601
+ };
602
+ }
603
+ const panel = getPanelById(db, params.panelId);
604
+ if (!panel) {
605
+ return { ok: false, failureClass: 'patch_failed', details: { panelId: params.panelId } };
606
+ }
607
+ return { ok: true, panel, changed: Array.from(changed) };
608
+ }
609
+ export function listAllPanelRows(db, panelId) {
610
+ const rows = db.prepare(`SELECT row_index as rowIndex, row_id as rowId, fields_json as fieldsJson, media_json as mediaJson,
611
+ node_id as nodeId, cached_asset_ids_json as cachedAssetIdsJson
612
+ FROM panel_rows
613
+ WHERE panel_id = ?
614
+ ORDER BY row_index ASC`).all(panelId);
615
+ return rows.map((row) => ({
616
+ rowIndex: row.rowIndex,
617
+ rowId: row.rowId,
618
+ fields: parseJsonObject(row.fieldsJson),
619
+ media: parseJsonObject(row.mediaJson),
620
+ nodeId: row.nodeId,
621
+ cachedAssetIds: parseJsonObject(row.cachedAssetIdsJson),
622
+ }));
623
+ }
624
+ function applyRowPatch(db, panelId, currentRows, patch, defaultNodeId) {
625
+ let rows = currentRows.map((row) => ({ ...row, fields: { ...row.fields }, media: { ...row.media } }));
626
+ const byRowId = new Map();
627
+ for (const row of rows) {
628
+ if (row.rowId)
629
+ byRowId.set(row.rowId, row.rowIndex);
630
+ }
631
+ const removeIds = Array.from(new Set((patch.removeRowIds ?? []).map((id) => id.trim()).filter(Boolean)));
632
+ if (removeIds.length) {
633
+ const missing = removeIds.filter((rowId) => !byRowId.has(rowId));
634
+ if (missing.length) {
635
+ return {
636
+ ok: false,
637
+ failureClass: 'row_not_found',
638
+ details: { panelId, rowIds: missing },
639
+ };
640
+ }
641
+ const removeSet = new Set(removeIds);
642
+ rows = rows.filter((row) => !row.rowId || !removeSet.has(row.rowId));
643
+ byRowId.clear();
644
+ rows.forEach((row, index) => {
645
+ if (row.rowId)
646
+ byRowId.set(row.rowId, index);
647
+ });
648
+ }
649
+ const appendRows = patch.append ?? [];
650
+ for (const input of appendRows) {
651
+ const rowId = normalizePatchRowId(input.rowId)
652
+ ?? generatePanelRowId(db, panelId, rows.length);
653
+ if (byRowId.has(rowId)) {
654
+ return {
655
+ ok: false,
656
+ failureClass: 'duplicate_row_id',
657
+ details: { panelId, rowId, issue: 'append rowId already exists' },
658
+ };
659
+ }
660
+ rows.push(toPanelRowMedia(panelId, rows.length, rowId, input, defaultNodeId));
661
+ byRowId.set(rowId, rows.length - 1);
662
+ }
663
+ const upsertRows = patch.upsert ?? [];
664
+ for (const input of upsertRows) {
665
+ const rowId = normalizePatchRowId(input.rowId);
666
+ if (!rowId) {
667
+ return {
668
+ ok: false,
669
+ failureClass: 'missing_row_id',
670
+ details: { panelId, issue: 'rows.upsert entries require rowId.' },
671
+ };
672
+ }
673
+ const existingIndex = byRowId.get(rowId);
674
+ if (existingIndex === undefined) {
675
+ rows.push(toPanelRowMedia(panelId, rows.length, rowId, input, defaultNodeId));
676
+ byRowId.set(rowId, rows.length - 1);
677
+ continue;
678
+ }
679
+ const existing = rows[existingIndex];
680
+ const nextMedia = { ...existing.media, ...(input.media ?? {}) };
681
+ const nextCachedAssetIds = { ...existing.cachedAssetIds };
682
+ for (const [slot, media] of Object.entries(input.media ?? {})) {
683
+ const previous = existing.media[slot];
684
+ if (!previous || previous.kind !== media.kind || previous.value !== media.value) {
685
+ delete nextCachedAssetIds[slot];
686
+ }
687
+ }
688
+ rows[existingIndex] = {
689
+ ...existing,
690
+ fields: { ...existing.fields, ...input.fields },
691
+ media: nextMedia,
692
+ cachedAssetIds: nextCachedAssetIds,
693
+ };
694
+ }
695
+ return {
696
+ ok: true,
697
+ rows: rows.map((row, index) => ({ ...row, rowIndex: index })),
698
+ };
699
+ }
700
+ function normalizePatchRowId(rowId) {
701
+ if (typeof rowId !== 'string')
702
+ return null;
703
+ const trimmed = rowId.trim();
704
+ return trimmed || null;
705
+ }
706
+ function toPanelRowMedia(panelId, rowIndex, rowId, row, defaultNodeId) {
707
+ return {
708
+ rowIndex,
709
+ rowId,
710
+ fields: { ...row.fields },
711
+ media: { ...(row.media ?? {}) },
712
+ nodeId: defaultNodeId,
713
+ cachedAssetIds: {},
714
+ };
715
+ }
716
+ function replacePanelRows(db, panelId, rows) {
717
+ db.prepare('DELETE FROM panel_rows WHERE panel_id = ?').run(panelId);
718
+ insertPanelRows(db, rows.map((row, index) => ({
719
+ panelId,
720
+ rowIndex: index,
721
+ rowId: row.rowId,
722
+ fields: row.fields,
723
+ media: row.media,
724
+ nodeId: row.nodeId,
725
+ cachedAssetIds: row.cachedAssetIds,
726
+ })));
727
+ }
728
+ function remapPanelSelectionState(db, panelId, oldRows, newRows, now) {
729
+ const oldIndexToRowId = new Map();
730
+ for (const row of oldRows) {
731
+ if (row.rowId)
732
+ oldIndexToRowId.set(row.rowIndex, row.rowId);
733
+ }
734
+ const rowIdToNewIndex = new Map();
735
+ for (const row of newRows) {
736
+ if (row.rowId)
737
+ rowIdToNewIndex.set(row.rowId, row.rowIndex);
738
+ }
739
+ const remapSelection = (selection) => {
740
+ if (!Array.isArray(selection))
741
+ return [];
742
+ const out = new Set();
743
+ for (const value of selection) {
744
+ if (!Number.isInteger(value) || value < 0)
745
+ continue;
746
+ const rowId = oldIndexToRowId.get(value);
747
+ const nextIndex = rowId ? rowIdToNewIndex.get(rowId) : undefined;
748
+ if (nextIndex !== undefined)
749
+ out.add(nextIndex);
750
+ }
751
+ return Array.from(out).sort((a, b) => a - b);
752
+ };
753
+ const panelStateRows = db.prepare(`SELECT user_id as userId, state_json as stateJson FROM panel_state WHERE panel_id = ?`).all(panelId);
754
+ const updatePanelState = db.prepare(`UPDATE panel_state SET state_json = ?, updated_at = ? WHERE panel_id = ? AND user_id = ?`);
755
+ for (const row of panelStateRows) {
756
+ const state = parseJsonObject(row.stateJson);
757
+ const nextSelection = remapSelection(state.selection);
758
+ if (JSON.stringify(state.selection ?? []) === JSON.stringify(nextSelection))
759
+ continue;
760
+ updatePanelState.run(JSON.stringify({ ...state, selection: nextSelection }), now, panelId, row.userId);
761
+ }
762
+ const shared = db.prepare(`SELECT state_json as stateJson, version FROM panel_shared_state WHERE panel_id = ? LIMIT 1`).get(panelId);
763
+ if (!shared)
764
+ return;
765
+ const sharedState = parseJsonObject(shared.stateJson);
766
+ const nextSharedSelection = remapSelection(sharedState.selection);
767
+ if (JSON.stringify(sharedState.selection ?? []) === JSON.stringify(nextSharedSelection))
768
+ return;
769
+ db.prepare(`UPDATE panel_shared_state
770
+ SET state_json = ?, version = ?, updated_at = ?
771
+ WHERE panel_id = ?`).run(JSON.stringify({ ...sharedState, selection: nextSharedSelection }), shared.version + 1, now, panelId);
772
+ }
773
+ export function updatePanelRowCachedAssetId(db, panelId, rowIndex, slot, assetId) {
774
+ const row = db.prepare(`SELECT cached_asset_ids_json FROM panel_rows WHERE panel_id = ? AND row_index = ?`).get(panelId, rowIndex);
775
+ const current = parseJsonObject(row?.cached_asset_ids_json ?? '{}');
776
+ current[slot] = assetId;
777
+ db.prepare(`UPDATE panel_rows SET cached_asset_ids_json = ? WHERE panel_id = ? AND row_index = ?`).run(JSON.stringify(current), panelId, rowIndex);
778
+ }
779
+ export function updatePanelRowCachedAssetIdIfCurrent(db, params) {
780
+ const row = db.prepare(`SELECT pr.row_id as rowId,
781
+ pr.node_id as nodeId,
782
+ pr.media_json as mediaJson,
783
+ pr.cached_asset_ids_json as cachedAssetIdsJson,
784
+ p.version as panelVersion
785
+ FROM panel_rows pr
786
+ JOIN panels p ON p.id = pr.panel_id
787
+ WHERE pr.panel_id = ? AND pr.row_index = ?
788
+ LIMIT 1`).get(params.panelId, params.rowIndex);
789
+ if (!row)
790
+ return false;
791
+ if (row.panelVersion !== params.panelVersion)
792
+ return false;
793
+ if (row.rowId !== params.rowId)
794
+ return false;
795
+ if (row.nodeId !== params.nodeId)
796
+ return false;
797
+ const media = parseJsonObject(row.mediaJson);
798
+ const currentMediaSlot = media[params.slot];
799
+ if (!currentMediaSlot
800
+ || currentMediaSlot.kind !== params.mediaSlot.kind
801
+ || currentMediaSlot.value !== params.mediaSlot.value) {
802
+ return false;
803
+ }
804
+ const current = parseJsonObject(row.cachedAssetIdsJson);
805
+ if ((current[params.slot] ?? null) !== (params.expectedCachedAssetId ?? null))
806
+ return false;
807
+ current[params.slot] = params.assetId;
808
+ const result = db.prepare(`UPDATE panel_rows SET cached_asset_ids_json = ? WHERE panel_id = ? AND row_index = ?`).run(JSON.stringify(current), params.panelId, params.rowIndex);
809
+ return result.changes > 0;
810
+ }
811
+ export function normalizePanelSelectionForRowCount(rowCount, selection) {
812
+ if (!Array.isArray(selection))
813
+ return [];
814
+ const selected = new Set();
815
+ for (const value of selection) {
816
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0)
817
+ continue;
818
+ if (Number.isInteger(rowCount) && rowCount != null && rowCount >= 0 && value >= rowCount)
819
+ continue;
820
+ selected.add(value);
821
+ }
822
+ return Array.from(selected).sort((a, b) => a - b);
823
+ }
824
+ export function normalizePanelStateSelectionForRowCount(rowCount, state) {
825
+ if (!Object.prototype.hasOwnProperty.call(state, 'selection'))
826
+ return state;
827
+ if (!Array.isArray(state.selection))
828
+ return { ...state, selection: [] };
829
+ const originalSelection = state.selection;
830
+ const selection = normalizePanelSelectionForRowCount(rowCount, state.selection);
831
+ if (selection.length === originalSelection.length
832
+ && selection.every((rowIndex, index) => rowIndex === originalSelection[index])) {
833
+ return state;
834
+ }
835
+ return { ...state, selection };
836
+ }
837
+ export function applySharedSelectionPatch(db, panelId, patch, updatedByUserId) {
838
+ const now = Date.now();
839
+ const current = getSharedPanelState(db, panelId);
840
+ const currentSelection = patch.baseSelection ?? (current?.state?.selection ?? []);
841
+ const currentSet = new Set(currentSelection);
842
+ const toggled = patch.selection ?? [];
843
+ for (const rowIndex of toggled) {
844
+ if (currentSet.has(rowIndex)) {
845
+ currentSet.delete(rowIndex);
846
+ }
847
+ else {
848
+ currentSet.add(rowIndex);
849
+ }
850
+ }
851
+ const newSelection = Array.from(currentSet).sort((a, b) => a - b);
852
+ const newState = { selection: newSelection };
853
+ const newVersion = (current?.version ?? 0) + 1;
854
+ db.prepare(`INSERT INTO panel_shared_state(panel_id, state_json, version, updated_at, updated_by_user_id)
855
+ VALUES(?, ?, ?, ?, ?)
856
+ ON CONFLICT(panel_id)
857
+ DO UPDATE SET
858
+ state_json = excluded.state_json,
859
+ version = excluded.version,
860
+ updated_at = excluded.updated_at,
861
+ updated_by_user_id = excluded.updated_by_user_id`).run(panelId, JSON.stringify(newState), newVersion, now, updatedByUserId ?? null);
862
+ return {
863
+ panelId,
864
+ state: newState,
865
+ version: newVersion,
866
+ updatedAt: new Date(now).toISOString(),
867
+ ...(updatedByUserId ? { updatedByUserId } : {}),
868
+ };
869
+ }
870
+ export function getSharedPanelState(db, panelId) {
871
+ const row = db.prepare(`SELECT panel_id as panelId, state_json as stateJson, version,
872
+ updated_at as updatedAt, updated_by_user_id as updatedByUserId
873
+ FROM panel_shared_state
874
+ WHERE panel_id = ?
875
+ LIMIT 1`).get(panelId);
876
+ if (!row)
877
+ return null;
878
+ return {
879
+ panelId: row.panelId,
880
+ state: parseJsonObject(row.stateJson),
881
+ version: row.version,
882
+ updatedAt: new Date(row.updatedAt).toISOString(),
883
+ ...(row.updatedByUserId ? { updatedByUserId: row.updatedByUserId } : {}),
884
+ };
885
+ }
886
+ export function upsertSharedPanelState(db, panelId, state, updatedByUserId) {
887
+ const now = Date.now();
888
+ db.prepare(`INSERT INTO panel_shared_state(panel_id, state_json, version, updated_at, updated_by_user_id)
889
+ VALUES(?, ?, ?, ?, ?)
890
+ ON CONFLICT(panel_id)
891
+ DO UPDATE SET
892
+ state_json = excluded.state_json,
893
+ version = excluded.version,
894
+ updated_at = excluded.updated_at,
895
+ updated_by_user_id = excluded.updated_by_user_id`).run(panelId, JSON.stringify(state), 1, now, updatedByUserId ?? null);
896
+ }
897
+ export function loadAttachmentRow(db, id) {
898
+ const row = db.prepare(`SELECT id, filename, original_filename as originalFilename, mime_type as mimeType,
899
+ size_bytes as sizeBytes, storage_path as storagePath, kind
900
+ FROM attachments
901
+ WHERE id = ?
902
+ LIMIT 1`).get(id);
903
+ return row ?? null;
904
+ }