miravo 0.6.0 → 0.6.2

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 (187) hide show
  1. package/LICENSE +199 -0
  2. package/README.md +29 -59
  3. package/bin/miravo.js +84 -0
  4. package/package.json +21 -37
  5. package/console-dist/assets/activity-DsWeRq4h.js +0 -1
  6. package/console-dist/assets/api-4AqKvsm9.js +0 -1
  7. package/console-dist/assets/arrow-right-Ea_UhTNi.js +0 -1
  8. package/console-dist/assets/arrow-up-down-DFSjzzl2.js +0 -1
  9. package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +0 -1
  10. package/console-dist/assets/box-C1WaoOMF.js +0 -1
  11. package/console-dist/assets/card-BjeP32PN.js +0 -1
  12. package/console-dist/assets/catalog-BJzyzbLR.js +0 -1
  13. package/console-dist/assets/catalog-DKjEm-c4.js +0 -1
  14. package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +0 -1
  15. package/console-dist/assets/checkbox-DZlJXTE4.js +0 -1
  16. package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +0 -3
  17. package/console-dist/assets/combobox-BGnuCYxO.js +0 -1
  18. package/console-dist/assets/control-CPhvwx9o.js +0 -1
  19. package/console-dist/assets/control-Cg2up9NA.js +0 -1
  20. package/console-dist/assets/copy-button-WkbNkMcd.js +0 -1
  21. package/console-dist/assets/createLucideIcon-Bwxw859K.js +0 -1
  22. package/console-dist/assets/deploy-tab-JeWDwPoZ.js +0 -1
  23. package/console-dist/assets/dist-BhbDML0a.js +0 -1
  24. package/console-dist/assets/dist-Cy8XTNFT.js +0 -2
  25. package/console-dist/assets/download-DQ6NH16U.js +0 -1
  26. package/console-dist/assets/engine-store-CrLnz692.js +0 -1
  27. package/console-dist/assets/events-CXm29Ok4.js +0 -1
  28. package/console-dist/assets/events-D01IxhnP.js +0 -1
  29. package/console-dist/assets/fira-code-cyrillic-ext-wght-normal-DhYMMuQd.woff2 +0 -0
  30. package/console-dist/assets/fira-code-cyrillic-wght-normal-Y3u8pIsh.woff2 +0 -0
  31. package/console-dist/assets/fira-code-greek-ext-wght-normal-wWus70Ix.woff2 +0 -0
  32. package/console-dist/assets/fira-code-greek-wght-normal-B2SviObF.woff2 +0 -0
  33. package/console-dist/assets/fira-code-latin-ext-wght-normal-Dvfvaomy.woff2 +0 -0
  34. package/console-dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
  35. package/console-dist/assets/fira-code-symbols2-wght-normal-CE6EOz_n.woff2 +0 -0
  36. package/console-dist/assets/fleet-CfxbKXt9.js +0 -1
  37. package/console-dist/assets/fleet-lotMGVqe.js +0 -1
  38. package/console-dist/assets/format-kXTonpDJ.js +0 -1
  39. package/console-dist/assets/gauge-Ci8zNZlk.js +0 -1
  40. package/console-dist/assets/get-error-message-3Q5Q-2G5.js +0 -1
  41. package/console-dist/assets/index-CVPKSDvJ.js +0 -12
  42. package/console-dist/assets/index-DJkgDeun.css +0 -2
  43. package/console-dist/assets/input-BWEQFptx.js +0 -1
  44. package/console-dist/assets/instance-detail-C5IVwuh3.js +0 -1
  45. package/console-dist/assets/karla-latin-ext-wght-normal-Dq0rrjgs.woff2 +0 -0
  46. package/console-dist/assets/karla-latin-wght-normal-C3-ma4ov.woff2 +0 -0
  47. package/console-dist/assets/label-uephX5ZG.js +0 -1
  48. package/console-dist/assets/layers-CUsLQ1fq.js +0 -1
  49. package/console-dist/assets/login-C3RJli0m.js +0 -126
  50. package/console-dist/assets/models-B_UioWQ5.js +0 -1
  51. package/console-dist/assets/models-_IPlPiZ7.js +0 -1
  52. package/console-dist/assets/not-found-B8gAm08S.js +0 -1
  53. package/console-dist/assets/platform-targets-R3T-4eJ7.js +0 -1
  54. package/console-dist/assets/platforms-BWh0BQ3Y.js +0 -1
  55. package/console-dist/assets/platforms-DStNbtVc.js +0 -1
  56. package/console-dist/assets/proxy-CsnYqnPp.js +0 -9
  57. package/console-dist/assets/query-keys-BvtE7BMs.js +0 -1
  58. package/console-dist/assets/radio-CoHegOv5.js +0 -1
  59. package/console-dist/assets/rolldown-runtime-COnpUsM8.js +0 -1
  60. package/console-dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
  61. package/console-dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
  62. package/console-dist/assets/select-CeO4vP0G.js +0 -1
  63. package/console-dist/assets/select-display-items-CIzY8953.js +0 -1
  64. package/console-dist/assets/server-D9Kfn8fQ.js +0 -1
  65. package/console-dist/assets/shallow-6lGjQhtJ.js +0 -1
  66. package/console-dist/assets/tags-CiXOIHtY.js +0 -4
  67. package/console-dist/assets/tags-FQCXIxrP.js +0 -1
  68. package/console-dist/assets/tooltip-HGLF366Q.js +0 -1
  69. package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +0 -1
  70. package/console-dist/assets/transition-types-CPlGtM8w.js +0 -1
  71. package/console-dist/assets/triangle-alert-adNk_sea.js +0 -1
  72. package/console-dist/assets/use-platform-stream-DGbK1k2L.js +0 -2
  73. package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +0 -2
  74. package/console-dist/assets/validation-Blvud9_O.js +0 -1
  75. package/console-dist/assets/vendor-charts-CAZdaPlq.js +0 -60
  76. package/console-dist/assets/vendor-table-wd8JuMmT.js +0 -4
  77. package/console-dist/assets/vendor-ui-ByahnaY0.js +0 -1
  78. package/console-dist/favicon.svg +0 -37
  79. package/console-dist/index.html +0 -82
  80. package/console-dist/miravo-logo.png +0 -0
  81. package/src/__tests__/integration/executable-methods-runner.ts +0 -262
  82. package/src/__tests__/integration/executable-methods.integration.ts +0 -2348
  83. package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +0 -314
  84. package/src/commands/adapter.ts +0 -249
  85. package/src/commands/apply.test.ts +0 -62
  86. package/src/commands/apply.ts +0 -216
  87. package/src/commands/cleanup.test.ts +0 -34
  88. package/src/commands/cleanup.ts +0 -201
  89. package/src/commands/cli-test-setup.ts +0 -91
  90. package/src/commands/export.test.ts +0 -51
  91. package/src/commands/export.ts +0 -404
  92. package/src/commands/fork.ts +0 -219
  93. package/src/commands/inject.test.ts +0 -57
  94. package/src/commands/inject.ts +0 -212
  95. package/src/commands/load.ts +0 -167
  96. package/src/commands/logs.ts +0 -173
  97. package/src/commands/models.test.ts +0 -162
  98. package/src/commands/models.ts +0 -338
  99. package/src/commands/pause.ts +0 -67
  100. package/src/commands/platforms.test.ts +0 -186
  101. package/src/commands/platforms.ts +0 -371
  102. package/src/commands/ps.ts +0 -116
  103. package/src/commands/remove.ts +0 -38
  104. package/src/commands/reset.ts +0 -44
  105. package/src/commands/rm.ts +0 -97
  106. package/src/commands/spawn.ts +0 -45
  107. package/src/commands/speed.ts +0 -40
  108. package/src/commands/start/adapter-config.ts +0 -157
  109. package/src/commands/start/catalog-le-dt.test.ts +0 -50
  110. package/src/commands/start/catalog-le-dt.ts +0 -187
  111. package/src/commands/start/catalog-model-schema.ts +0 -89
  112. package/src/commands/start/catalog-template-detail.ts +0 -54
  113. package/src/commands/start/daemon.ts +0 -255
  114. package/src/commands/start/display.ts +0 -150
  115. package/src/commands/start/engine-setup.ts +0 -247
  116. package/src/commands/start/events.ts +0 -126
  117. package/src/commands/start/index.ts +0 -897
  118. package/src/commands/start/protocol-plans.ts +0 -239
  119. package/src/commands/start/template-loader.ts +0 -197
  120. package/src/commands/start/template-resolver.ts +0 -185
  121. package/src/commands/start/types.ts +0 -73
  122. package/src/commands/start.test.ts +0 -480
  123. package/src/commands/status.test.ts +0 -20
  124. package/src/commands/status.ts +0 -470
  125. package/src/commands/stop.test.ts +0 -84
  126. package/src/commands/stop.ts +0 -169
  127. package/src/commands/templates.test.ts +0 -58
  128. package/src/commands/templates.ts +0 -277
  129. package/src/index.ts +0 -166
  130. package/src/lib/admin-auth.ts +0 -157
  131. package/src/lib/admin-client.test.ts +0 -51
  132. package/src/lib/admin-client.ts +0 -326
  133. package/src/lib/admin-platform-routes.ts +0 -1017
  134. package/src/lib/admin-platform-stream.test.ts +0 -434
  135. package/src/lib/admin-platform-stream.ts +0 -788
  136. package/src/lib/admin-responses.ts +0 -211
  137. package/src/lib/admin-server.test.ts +0 -781
  138. package/src/lib/admin-server.ts +0 -797
  139. package/src/lib/auth-store.ts +0 -87
  140. package/src/lib/daemon-registry.test.ts +0 -429
  141. package/src/lib/daemon-registry.ts +0 -343
  142. package/src/lib/le/le-apply.test.ts +0 -591
  143. package/src/lib/le/le-apply.ts +0 -832
  144. package/src/lib/le/le-auth.ts +0 -89
  145. package/src/lib/le/le-cleanup.test.ts +0 -168
  146. package/src/lib/le/le-cleanup.ts +0 -399
  147. package/src/lib/le/le-cli-helpers.ts +0 -77
  148. package/src/lib/le/le-client.ts +0 -184
  149. package/src/lib/le/le-concurrency.test.ts +0 -157
  150. package/src/lib/le/le-concurrency.ts +0 -78
  151. package/src/lib/le/le-connection-test.ts +0 -42
  152. package/src/lib/le/le-devicehub-properties.test.ts +0 -53
  153. package/src/lib/le/le-devicehub-properties.ts +0 -33
  154. package/src/lib/le/le-devicehub.ts +0 -213
  155. package/src/lib/le/le-digital-twins.ts +0 -285
  156. package/src/lib/le/le-drivers.ts +0 -23
  157. package/src/lib/le/le-endpoint.ts +0 -40
  158. package/src/lib/le/le-environments.test.ts +0 -73
  159. package/src/lib/le/le-environments.ts +0 -156
  160. package/src/lib/le/le-markers.ts +0 -87
  161. package/src/lib/le/le-plan.test.ts +0 -26
  162. package/src/lib/le/le-plan.ts +0 -109
  163. package/src/lib/le/le-platform-operations.test.ts +0 -198
  164. package/src/lib/le/le-platform-operations.ts +0 -340
  165. package/src/lib/le/le-static-model-schema.ts +0 -78
  166. package/src/lib/le/le-static-planner.test.ts +0 -255
  167. package/src/lib/le/le-static-planner.ts +0 -442
  168. package/src/lib/le/le-template-exporter.test.ts +0 -527
  169. package/src/lib/le/le-template-exporter.ts +0 -459
  170. package/src/lib/le/le-value-types.ts +0 -23
  171. package/src/lib/mqtt-client.ts +0 -115
  172. package/src/lib/parse.test.ts +0 -123
  173. package/src/lib/parse.ts +0 -87
  174. package/src/lib/resolve-simulation.test.ts +0 -118
  175. package/src/lib/resolve-simulation.ts +0 -181
  176. package/src/lib/session-store.ts +0 -110
  177. package/src/lib/shutdown.test.ts +0 -562
  178. package/src/lib/shutdown.ts +0 -621
  179. package/src/lib/template-category.test.ts +0 -71
  180. package/src/lib/template-category.ts +0 -72
  181. package/src/lib/template-locator.ts +0 -55
  182. package/src/lib/template-utils.ts +0 -45
  183. package/src/lib/ws-hub.ts +0 -281
  184. package/src/logger.test.ts +0 -172
  185. package/src/logger.ts +0 -402
  186. package/src/output.test.ts +0 -64
  187. package/src/output.ts +0 -160
@@ -1,340 +0,0 @@
1
- /**
2
- * Persisted platform operations audit trail.
3
- *
4
- * Records completed apply/cleanup summaries in:
5
- * ~/.miravo/platforms/operations.jsonl
6
- */
7
-
8
- import { appendFile, chmod } from "node:fs/promises";
9
- import { z } from "zod";
10
- import type { ResourceProgress } from "./le-apply.ts";
11
- import { ensurePlatformsDir, operationsFilePath } from "./le-environments.ts";
12
- import type { ApplyScope } from "./le-plan.ts";
13
-
14
- const MAX_OPERATION_ENTRIES = 10_000;
15
- const OPERATIONS_FILE_MODE = 0o600;
16
-
17
- const PlatformOperationTypeSchema = z.enum(["apply", "cleanup"]);
18
- const PlatformOperationStatusSchema = z.enum(["success", "partial", "blocked", "noop", "failed"]);
19
- const PlatformResourceTypeSchema = z.enum(["device", "model", "instance", "param-sync"]);
20
- const PlatformResourceActionSchema = z.enum(["create", "update", "replace", "delete"]);
21
-
22
- const ActionCountsSchema = z.object({
23
- create: z.number().int().nonnegative(),
24
- update: z.number().int().nonnegative(),
25
- replace: z.number().int().nonnegative().default(0),
26
- delete: z.number().int().nonnegative(),
27
- failed: z.number().int().nonnegative(),
28
- total: z.number().int().nonnegative(),
29
- });
30
-
31
- const ResourceCountsSchema = z.object({
32
- device: ActionCountsSchema,
33
- model: ActionCountsSchema,
34
- instance: ActionCountsSchema,
35
- "param-sync": ActionCountsSchema,
36
- });
37
-
38
- const PlatformOperationRecordSchema = z.object({
39
- schemaVersion: z.literal(1),
40
- operationId: z.string().min(1),
41
- operationType: PlatformOperationTypeSchema,
42
- status: PlatformOperationStatusSchema,
43
- templateId: z.string().min(1),
44
- environmentId: z.string().min(1),
45
- environmentLabel: z.string().min(1),
46
- hostname: z.string().min(1),
47
- scope: z.array(z.string()),
48
- filters: z.object({
49
- area: z.string().optional(),
50
- line: z.string().optional(),
51
- instance: z.string().optional(),
52
- }),
53
- successCount: z.number().int().nonnegative(),
54
- failedCount: z.number().int().nonnegative(),
55
- blockedCount: z.number().int().nonnegative(),
56
- startedAt: z.number().int().nonnegative(),
57
- completedAt: z.number().int().nonnegative(),
58
- durationMs: z.number().int().nonnegative(),
59
- resourceCounts: ResourceCountsSchema,
60
- error: z.string().optional(),
61
- });
62
-
63
- export type PlatformOperationRecord = z.infer<typeof PlatformOperationRecordSchema>;
64
-
65
- type PlatformOperationType = z.infer<typeof PlatformOperationTypeSchema>;
66
- type PlatformResourceType = z.infer<typeof PlatformResourceTypeSchema>;
67
- type PlatformResourceAction = z.infer<typeof PlatformResourceActionSchema>;
68
-
69
- type MutableActionCounts = {
70
- create: number;
71
- update: number;
72
- replace: number;
73
- delete: number;
74
- failed: number;
75
- total: number;
76
- };
77
-
78
- type MutableResourceCounts = Record<PlatformResourceType, MutableActionCounts>;
79
-
80
- interface PlatformOperationsQuery {
81
- readonly type?: PlatformOperationType | undefined;
82
- readonly environmentId?: string | undefined;
83
- readonly limit?: number | undefined;
84
- readonly offset?: number | undefined;
85
- }
86
-
87
- interface PlatformOperationsPage {
88
- readonly operations: readonly PlatformOperationRecord[];
89
- readonly total: number;
90
- readonly hasMore: boolean;
91
- }
92
-
93
- interface PlatformOperationTrackerInput {
94
- readonly operationId: string;
95
- readonly operationType: PlatformOperationType;
96
- readonly templateId: string;
97
- readonly environmentId: string;
98
- readonly environmentLabel: string;
99
- readonly hostname: string;
100
- readonly scope: readonly ApplyScope[];
101
- readonly filters: {
102
- readonly area?: string | undefined;
103
- readonly line?: string | undefined;
104
- readonly instance?: string | undefined;
105
- };
106
- readonly startedAt: number;
107
- }
108
-
109
- interface PlatformOperationFinalizeInput {
110
- readonly successCount: number;
111
- readonly failedCount: number;
112
- readonly blockedCount?: number | undefined;
113
- readonly completedAt?: number | undefined;
114
- readonly error?: string | undefined;
115
- }
116
-
117
- interface PlatformOperationTracker {
118
- onResourceComplete(progress: ResourceProgress): void;
119
- finalize(input: PlatformOperationFinalizeInput): PlatformOperationRecord;
120
- }
121
-
122
- let writeQueue: Promise<void> = Promise.resolve();
123
-
124
- function nextWrite(task: () => Promise<void>): Promise<void> {
125
- writeQueue = writeQueue.then(task, task);
126
- return writeQueue;
127
- }
128
-
129
- function createEmptyActionCounts(): MutableActionCounts {
130
- return { create: 0, update: 0, replace: 0, delete: 0, failed: 0, total: 0 };
131
- }
132
-
133
- function createEmptyResourceCounts(): MutableResourceCounts {
134
- return {
135
- device: createEmptyActionCounts(),
136
- model: createEmptyActionCounts(),
137
- instance: createEmptyActionCounts(),
138
- "param-sync": createEmptyActionCounts(),
139
- };
140
- }
141
-
142
- function normalizeResourceType(raw: string): PlatformResourceType | null {
143
- const normalized = raw.trim().toLowerCase();
144
- if (
145
- normalized === "device" ||
146
- normalized === "model" ||
147
- normalized === "instance" ||
148
- normalized === "param-sync"
149
- ) {
150
- return normalized;
151
- }
152
- return null;
153
- }
154
-
155
- function normalizeResourceAction(raw: string): PlatformResourceAction | null {
156
- const normalized = raw.trim().toLowerCase();
157
- if (
158
- normalized === "create" ||
159
- normalized === "update" ||
160
- normalized === "replace" ||
161
- normalized === "delete"
162
- ) {
163
- return normalized;
164
- }
165
- return null;
166
- }
167
-
168
- function toReadonlyCounts(
169
- source: MutableResourceCounts,
170
- ): PlatformOperationRecord["resourceCounts"] {
171
- return {
172
- device: { ...source.device },
173
- model: { ...source.model },
174
- instance: { ...source.instance },
175
- "param-sync": { ...source["param-sync"] },
176
- };
177
- }
178
-
179
- function resolveStatus(input: {
180
- readonly successCount: number;
181
- readonly failedCount: number;
182
- readonly blockedCount: number;
183
- readonly error?: string | undefined;
184
- }): PlatformOperationRecord["status"] {
185
- if (input.error && input.successCount === 0 && input.failedCount > 0) return "failed";
186
- if (input.blockedCount > 0) return "blocked";
187
- if (input.successCount === 0 && input.failedCount === 0) return "noop";
188
- if (input.failedCount === 0) return "success";
189
- return "partial";
190
- }
191
-
192
- export function createPlatformOperationTracker(
193
- input: PlatformOperationTrackerInput,
194
- ): PlatformOperationTracker {
195
- const resourceCounts = createEmptyResourceCounts();
196
-
197
- return {
198
- onResourceComplete(progress: ResourceProgress) {
199
- const resourceType = normalizeResourceType(progress.resourceType);
200
- if (!resourceType) return;
201
-
202
- const count = progress.count ?? 1;
203
- const bucket = resourceCounts[resourceType];
204
- bucket.total += count;
205
-
206
- const action = normalizeResourceAction(progress.action);
207
- if (action) bucket[action] += count;
208
- if (progress.status === "failed") bucket.failed += count;
209
- },
210
-
211
- finalize(finalizeInput: PlatformOperationFinalizeInput): PlatformOperationRecord {
212
- const completedAt = finalizeInput.completedAt ?? Date.now();
213
- const blockedCount = finalizeInput.blockedCount ?? 0;
214
- const status = resolveStatus({
215
- successCount: finalizeInput.successCount,
216
- failedCount: finalizeInput.failedCount,
217
- blockedCount,
218
- error: finalizeInput.error,
219
- });
220
-
221
- const record: PlatformOperationRecord = {
222
- schemaVersion: 1,
223
- operationId: input.operationId,
224
- operationType: input.operationType,
225
- status,
226
- templateId: input.templateId,
227
- environmentId: input.environmentId,
228
- environmentLabel: input.environmentLabel,
229
- hostname: input.hostname,
230
- scope: [...input.scope],
231
- filters: {
232
- ...(input.filters.area ? { area: input.filters.area } : {}),
233
- ...(input.filters.line ? { line: input.filters.line } : {}),
234
- ...(input.filters.instance ? { instance: input.filters.instance } : {}),
235
- },
236
- successCount: finalizeInput.successCount,
237
- failedCount: finalizeInput.failedCount,
238
- blockedCount,
239
- startedAt: input.startedAt,
240
- completedAt,
241
- durationMs: Math.max(0, completedAt - input.startedAt),
242
- resourceCounts: toReadonlyCounts(resourceCounts),
243
- ...(finalizeInput.error ? { error: finalizeInput.error } : {}),
244
- };
245
-
246
- return PlatformOperationRecordSchema.parse(record);
247
- },
248
- };
249
- }
250
-
251
- async function trimOperationsFile(maxEntries: number): Promise<void> {
252
- const path = operationsFilePath();
253
- const text = await Bun.file(path).text();
254
- const lines = text
255
- .split("\n")
256
- .map((line) => line.trim())
257
- .filter((line) => line.length > 0);
258
-
259
- if (lines.length <= maxEntries) return;
260
-
261
- const trimmed = `${lines.slice(lines.length - maxEntries).join("\n")}\n`;
262
- await Bun.write(path, trimmed, { mode: OPERATIONS_FILE_MODE });
263
- try {
264
- await chmod(path, OPERATIONS_FILE_MODE);
265
- } catch {
266
- // Best effort hardening; platform may reject chmod.
267
- }
268
- }
269
-
270
- export async function appendPlatformOperation(record: PlatformOperationRecord): Promise<void> {
271
- const parsed = PlatformOperationRecordSchema.parse(record);
272
- const line = `${JSON.stringify(parsed)}\n`;
273
-
274
- await nextWrite(async () => {
275
- await ensurePlatformsDir();
276
- const path = operationsFilePath();
277
- await appendFile(path, line, { encoding: "utf8", mode: OPERATIONS_FILE_MODE });
278
- try {
279
- await chmod(path, OPERATIONS_FILE_MODE);
280
- } catch {
281
- // Best effort hardening; platform may reject chmod.
282
- }
283
- await trimOperationsFile(MAX_OPERATION_ENTRIES);
284
- });
285
- }
286
-
287
- function parseOperationsFile(text: string): PlatformOperationRecord[] {
288
- const lines = text
289
- .split("\n")
290
- .map((line) => line.trim())
291
- .filter((line) => line.length > 0);
292
-
293
- const records: PlatformOperationRecord[] = [];
294
- for (const line of lines) {
295
- let parsed: unknown;
296
- try {
297
- parsed = JSON.parse(line) as unknown;
298
- } catch {
299
- continue;
300
- }
301
- const result = PlatformOperationRecordSchema.safeParse(parsed);
302
- if (result.success) records.push(result.data);
303
- }
304
- return records;
305
- }
306
-
307
- function sortNewestFirst(a: PlatformOperationRecord, b: PlatformOperationRecord): number {
308
- if (a.completedAt !== b.completedAt) return b.completedAt - a.completedAt;
309
- if (a.startedAt !== b.startedAt) return b.startedAt - a.startedAt;
310
- return b.operationId.localeCompare(a.operationId);
311
- }
312
-
313
- export async function listPlatformOperations(
314
- query: PlatformOperationsQuery = {},
315
- ): Promise<PlatformOperationsPage> {
316
- const limit = Math.min(Math.max(query.limit ?? 50, 1), 500);
317
- const offsetRaw = query.offset ?? 0;
318
- const offset = Number.isFinite(offsetRaw)
319
- ? Math.min(Math.max(Math.trunc(offsetRaw), 0), MAX_OPERATION_ENTRIES)
320
- : 0;
321
-
322
- let text = "";
323
- try {
324
- text = await Bun.file(operationsFilePath()).text();
325
- } catch {
326
- return { operations: [], total: 0, hasMore: false };
327
- }
328
-
329
- const allRecords = parseOperationsFile(text).sort(sortNewestFirst);
330
- const filtered = allRecords.filter((record) => {
331
- if (query.type && record.operationType !== query.type) return false;
332
- if (query.environmentId && record.environmentId !== query.environmentId) return false;
333
- return true;
334
- });
335
-
336
- const total = filtered.length;
337
- const operations = filtered.slice(offset, offset + limit);
338
- const hasMore = offset + operations.length < total;
339
- return { operations, total, hasMore };
340
- }
@@ -1,78 +0,0 @@
1
- import type { ModelSchema, TwinModel } from "@miravo/shared";
2
-
3
- export function toModelSchema(model: TwinModel): ModelSchema {
4
- const parameters: Record<
5
- string,
6
- { type: string; default: number | string | boolean; unit?: string }
7
- > = {};
8
-
9
- if (model.parameters) {
10
- for (const [name, def] of Object.entries(model.parameters)) {
11
- parameters[name] = {
12
- type: def.type,
13
- default: def.default,
14
- ...(def.unit ? { unit: def.unit } : {}),
15
- };
16
- }
17
- }
18
-
19
- const members: Record<string, ModelSchema["members"][string]> = {};
20
- for (const [name, def] of Object.entries(model.members)) {
21
- const rawDefault = def.default;
22
-
23
- members[name] = {
24
- kind: def.kind,
25
- dataType: def.data_type,
26
- access: def.access,
27
- unit: def.unit ?? null,
28
- euRange: "eu_range" in def ? (def.eu_range ?? null) : null,
29
- default:
30
- typeof rawDefault === "number" ||
31
- typeof rawDefault === "string" ||
32
- typeof rawDefault === "boolean"
33
- ? rawDefault
34
- : null,
35
- };
36
- }
37
-
38
- const methods: Record<string, ModelSchema["methods"][string]> = {};
39
- if (model.methods) {
40
- for (const [name, def] of Object.entries(model.methods)) {
41
- methods[name] = {
42
- description: def.description ?? null,
43
- inputArguments: def.input_arguments
44
- ? Object.entries(def.input_arguments).map(([argName, arg]) => ({
45
- name: argName,
46
- dataType: arg.data_type,
47
- description: arg.description ?? null,
48
- }))
49
- : [],
50
- outputArguments: def.output_arguments
51
- ? Object.entries(def.output_arguments).map(([argName, arg]) => ({
52
- name: argName,
53
- dataType: arg.data_type,
54
- description: arg.description ?? null,
55
- }))
56
- : [],
57
- };
58
- }
59
- }
60
-
61
- return {
62
- name: model.model.name,
63
- description: model.model.description ?? null,
64
- parameters,
65
- members,
66
- methods,
67
- faults: model.faults?.map((fault) => fault.name) ?? [],
68
- lifecycle: model.lifecycle
69
- ? {
70
- stages: model.lifecycle.stages.map((stage) => stage.name),
71
- totalHours: model.lifecycle.stages.reduce(
72
- (sum, stage) => sum + stage.duration_hours[1],
73
- 0,
74
- ),
75
- }
76
- : null,
77
- };
78
- }
@@ -1,255 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { resolve } from "node:path";
3
- import { ContentCatalog } from "@miravo/core";
4
- import { createTmpFileUtils } from "@miravo/core/test-tmp-utils";
5
- import { MiravoError } from "@miravo/shared";
6
- import { buildDesiredStateFromTemplate } from "./le-static-planner.ts";
7
-
8
- const { tmpDir, writeTmpFile, cleanupTmpDir } = createTmpFileUtils(
9
- resolve(import.meta.dir, ".tmp-test-le-static-planner"),
10
- );
11
-
12
- afterEach(async () => {
13
- await cleanupTmpDir();
14
- });
15
-
16
- function createCatalog(): ContentCatalog {
17
- return new ContentCatalog({
18
- cwd: tmpDir,
19
- miravoHome: resolve(tmpDir, ".miravo-home"),
20
- builtinModelsDir: resolve(tmpDir, ".builtin-models"),
21
- builtinTemplatesDir: resolve(tmpDir, ".builtin-templates"),
22
- });
23
- }
24
-
25
- const PUMP_MODEL = `schema_version: "2"
26
- model:
27
- name: pump
28
- members:
29
- rpm:
30
- kind: variable
31
- data_type: Float
32
- access: read
33
- running:
34
- kind: variable
35
- data_type: Boolean
36
- access: read
37
- label:
38
- kind: property
39
- data_type: String
40
- access: read
41
- default: "pump-a"
42
- `;
43
-
44
- const TANK_MODEL = `schema_version: "2"
45
- model:
46
- name: tank
47
- members:
48
- level:
49
- kind: variable
50
- data_type: Float
51
- access: read
52
- active:
53
- kind: variable
54
- data_type: Boolean
55
- access: read
56
- pulses:
57
- kind: variable
58
- data_type: UInt16
59
- access: read
60
- note:
61
- kind: property
62
- data_type: String
63
- access: read
64
- default: "tank-a"
65
- `;
66
-
67
- describe("buildDesiredStateFromTemplate", () => {
68
- test("builds desired devices/tags/models/instances with per-instance protocol scope", async () => {
69
- const templatePath = await writeTmpFile(
70
- "scope-template.miravo.yaml",
71
- `kind: MiravoTemplate
72
- version: "1"
73
- name: scope-template
74
- protocols:
75
- mqtt: {}
76
- opcua: {}
77
- modbus:
78
- port: 1502
79
- areas:
80
- - name: area_a
81
- protocol_scope: [mqtt]
82
- lines:
83
- - name: line_a
84
- twins:
85
- - model: pump
86
- count: 2
87
- - name: area_b
88
- lines:
89
- - name: line_b
90
- protocol_scope: [modbus]
91
- twins:
92
- - model: tank
93
- count: 1
94
- - model: pump
95
- count: 1
96
- protocol_scope: [opcua]
97
- `,
98
- );
99
-
100
- await writeTmpFile("pump.twin.yaml", PUMP_MODEL);
101
- await writeTmpFile("tank.twin.yaml", TANK_MODEL);
102
-
103
- const result = await buildDesiredStateFromTemplate({
104
- templatePath,
105
- catalog: createCatalog(),
106
- sourceHostname: "source.example.local",
107
- });
108
-
109
- expect(result.templateId).toBe("scope-template");
110
- expect(result.desired.devices).toHaveLength(4);
111
- expect(result.desired.instances).toHaveLength(4);
112
- expect(result.desired.models.map((model) => model.name).sort()).toEqual(["PUMP", "TANK"]);
113
-
114
- const protocolByInstance = new Map(
115
- result.desired.devices.map((device) => [device.instanceId, device.protocol]),
116
- );
117
- expect(protocolByInstance.get("pump-001")).toBe("mqtt");
118
- expect(protocolByInstance.get("pump-002")).toBe("mqtt");
119
- expect(protocolByInstance.get("tank-001")).toBe("modbus");
120
- expect(protocolByInstance.get("pump-003")).toBe("opcua");
121
-
122
- const mqttDevice = result.desired.devices.find((device) => device.instanceId === "pump-001");
123
- expect(mqttDevice?.endpoint).toEqual({ host: "source.example.local", port: 1883 });
124
- expect(mqttDevice?.browsePath).toBe("area_a/line_a/pump-001");
125
- expect(mqttDevice?.tags[0]?.name).toBe("area_a/line_a/pump-001/rpm");
126
- expect(mqttDevice?.tags[0]?.unsPath).toBe("area_a/line_a/pump-001/rpm");
127
- expect(mqttDevice?.tags[0]?.access).toBe("read");
128
- expect(mqttDevice?.properties).toEqual([
129
- { Key: "BrokerAddress", Value: "source.example.local" },
130
- { Key: "Port", Value: "1883" },
131
- ]);
132
-
133
- const modbusDevice = result.desired.devices.find((device) => device.instanceId === "tank-001");
134
- expect(modbusDevice?.endpoint).toEqual({ host: "source.example.local", port: 1502 });
135
- expect(modbusDevice?.browsePath).toBe("area_b/line_b/tank-001");
136
- expect(modbusDevice?.tags.map((tag) => tag.tagName)).toEqual(["level", "active", "pulses"]);
137
- expect(modbusDevice?.tags[0]?.unsPath).toBe("area_b/line_b/tank-001/level");
138
- expect(modbusDevice?.tags[0]?.access).toBe("read");
139
- expect(
140
- modbusDevice?.tags.map(
141
- (tag) => tag.properties.find((property) => property.Key === "address")?.Value,
142
- ),
143
- ).toEqual(["0", "2", "3"]);
144
- expect(modbusDevice?.properties).toEqual([
145
- { Key: "networkAddress", Value: "source.example.local" },
146
- { Key: "networkPort", Value: "1502" },
147
- { Key: "stationId", Value: "1" },
148
- ]);
149
-
150
- const opcuaDevice = result.desired.devices.find((device) => device.instanceId === "pump-003");
151
- expect(opcuaDevice?.endpoint).toEqual({ host: "source.example.local", port: 4840 });
152
- expect(opcuaDevice?.browsePath).toBe("area_b/line_b/pump-003");
153
- expect(opcuaDevice?.tags[0]?.name).toBe("ns=1;s=area_b/line_b/pump-003/rpm");
154
- expect(opcuaDevice?.tags[0]?.unsPath).toBe("area_b/line_b/pump-003/rpm");
155
- expect(opcuaDevice?.tags[0]?.access).toBe("read");
156
- expect(opcuaDevice?.properties).toEqual([
157
- { Key: "ServerURL", Value: "opc.tcp://source.example.local:4840" },
158
- { Key: "SecurityPolicy", Value: "0" },
159
- { Key: "SecurityMode", Value: "0" },
160
- ]);
161
-
162
- const mqttInstance = result.desired.instances.find((i) => i.instanceId === "pump-001");
163
- expect(mqttInstance?.browsePath).toBe("area_a/line_a/pump-001");
164
- const opcuaInstance = result.desired.instances.find((i) => i.instanceId === "pump-003");
165
- expect(opcuaInstance?.browsePath).toBe("area_b/line_b/pump-003");
166
-
167
- const tankModel = result.desired.models.find((model) => model.name === "TANK");
168
- expect(Array.isArray(tankModel?.leDtJson)).toBe(true);
169
- });
170
-
171
- test("fails when an instance resolves to multiple protocols", async () => {
172
- const templatePath = await writeTmpFile(
173
- "ambiguous-scope.miravo.yaml",
174
- `kind: MiravoTemplate
175
- version: "1"
176
- name: ambiguous-scope
177
- protocols:
178
- mqtt: {}
179
- opcua: {}
180
- areas:
181
- - name: area_a
182
- lines:
183
- - name: line_a
184
- twins:
185
- - model: pump
186
- count: 1
187
- `,
188
- );
189
-
190
- await writeTmpFile("pump.twin.yaml", PUMP_MODEL);
191
-
192
- await expect(
193
- buildDesiredStateFromTemplate({
194
- templatePath,
195
- catalog: createCatalog(),
196
- sourceHostname: "source.example.local",
197
- }),
198
- ).rejects.toThrow("resolved to multiple protocols");
199
- });
200
-
201
- test("rejects empty sourceHostname before touching the filesystem", async () => {
202
- // nonexistent path proves the guard fires before any FS access
203
- const templatePath = `${tmpDir}/nonexistent.miravo.yaml`;
204
-
205
- await expect(
206
- buildDesiredStateFromTemplate({
207
- templatePath,
208
- catalog: createCatalog(),
209
- sourceHostname: " ",
210
- }),
211
- ).rejects.toThrow("sourceHostname must not be empty");
212
-
213
- await expect(
214
- buildDesiredStateFromTemplate({
215
- templatePath,
216
- catalog: createCatalog(),
217
- sourceHostname: "",
218
- }),
219
- ).rejects.toThrow("sourceHostname must not be empty");
220
- });
221
-
222
- test("fails with actionable error when a referenced model is missing", async () => {
223
- const templatePath = await writeTmpFile(
224
- "missing-model.miravo.yaml",
225
- `kind: MiravoTemplate
226
- version: "1"
227
- name: missing-model
228
- protocols:
229
- mqtt: {}
230
- areas:
231
- - name: area_a
232
- lines:
233
- - name: line_a
234
- twins:
235
- - model: missing-pump
236
- count: 1
237
- `,
238
- );
239
-
240
- try {
241
- await buildDesiredStateFromTemplate({
242
- templatePath,
243
- catalog: createCatalog(),
244
- sourceHostname: "source.example.local",
245
- });
246
- throw new Error("expected planner to throw for missing model");
247
- } catch (error) {
248
- expect(error).toBeInstanceOf(MiravoError);
249
- if (error instanceof MiravoError) {
250
- expect(error.message).toContain('Model "missing-pump" not found');
251
- expect(error.fix).toContain("missing-pump.twin.yaml");
252
- }
253
- }
254
- });
255
- });