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.
- package/LICENSE +199 -0
- package/README.md +29 -59
- package/bin/miravo.js +84 -0
- package/package.json +21 -37
- package/console-dist/assets/activity-DsWeRq4h.js +0 -1
- package/console-dist/assets/api-4AqKvsm9.js +0 -1
- package/console-dist/assets/arrow-right-Ea_UhTNi.js +0 -1
- package/console-dist/assets/arrow-up-down-DFSjzzl2.js +0 -1
- package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +0 -1
- package/console-dist/assets/box-C1WaoOMF.js +0 -1
- package/console-dist/assets/card-BjeP32PN.js +0 -1
- package/console-dist/assets/catalog-BJzyzbLR.js +0 -1
- package/console-dist/assets/catalog-DKjEm-c4.js +0 -1
- package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +0 -1
- package/console-dist/assets/checkbox-DZlJXTE4.js +0 -1
- package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +0 -3
- package/console-dist/assets/combobox-BGnuCYxO.js +0 -1
- package/console-dist/assets/control-CPhvwx9o.js +0 -1
- package/console-dist/assets/control-Cg2up9NA.js +0 -1
- package/console-dist/assets/copy-button-WkbNkMcd.js +0 -1
- package/console-dist/assets/createLucideIcon-Bwxw859K.js +0 -1
- package/console-dist/assets/deploy-tab-JeWDwPoZ.js +0 -1
- package/console-dist/assets/dist-BhbDML0a.js +0 -1
- package/console-dist/assets/dist-Cy8XTNFT.js +0 -2
- package/console-dist/assets/download-DQ6NH16U.js +0 -1
- package/console-dist/assets/engine-store-CrLnz692.js +0 -1
- package/console-dist/assets/events-CXm29Ok4.js +0 -1
- package/console-dist/assets/events-D01IxhnP.js +0 -1
- package/console-dist/assets/fira-code-cyrillic-ext-wght-normal-DhYMMuQd.woff2 +0 -0
- package/console-dist/assets/fira-code-cyrillic-wght-normal-Y3u8pIsh.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-ext-wght-normal-wWus70Ix.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-wght-normal-B2SviObF.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-ext-wght-normal-Dvfvaomy.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
- package/console-dist/assets/fira-code-symbols2-wght-normal-CE6EOz_n.woff2 +0 -0
- package/console-dist/assets/fleet-CfxbKXt9.js +0 -1
- package/console-dist/assets/fleet-lotMGVqe.js +0 -1
- package/console-dist/assets/format-kXTonpDJ.js +0 -1
- package/console-dist/assets/gauge-Ci8zNZlk.js +0 -1
- package/console-dist/assets/get-error-message-3Q5Q-2G5.js +0 -1
- package/console-dist/assets/index-CVPKSDvJ.js +0 -12
- package/console-dist/assets/index-DJkgDeun.css +0 -2
- package/console-dist/assets/input-BWEQFptx.js +0 -1
- package/console-dist/assets/instance-detail-C5IVwuh3.js +0 -1
- package/console-dist/assets/karla-latin-ext-wght-normal-Dq0rrjgs.woff2 +0 -0
- package/console-dist/assets/karla-latin-wght-normal-C3-ma4ov.woff2 +0 -0
- package/console-dist/assets/label-uephX5ZG.js +0 -1
- package/console-dist/assets/layers-CUsLQ1fq.js +0 -1
- package/console-dist/assets/login-C3RJli0m.js +0 -126
- package/console-dist/assets/models-B_UioWQ5.js +0 -1
- package/console-dist/assets/models-_IPlPiZ7.js +0 -1
- package/console-dist/assets/not-found-B8gAm08S.js +0 -1
- package/console-dist/assets/platform-targets-R3T-4eJ7.js +0 -1
- package/console-dist/assets/platforms-BWh0BQ3Y.js +0 -1
- package/console-dist/assets/platforms-DStNbtVc.js +0 -1
- package/console-dist/assets/proxy-CsnYqnPp.js +0 -9
- package/console-dist/assets/query-keys-BvtE7BMs.js +0 -1
- package/console-dist/assets/radio-CoHegOv5.js +0 -1
- package/console-dist/assets/rolldown-runtime-COnpUsM8.js +0 -1
- package/console-dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
- package/console-dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
- package/console-dist/assets/select-CeO4vP0G.js +0 -1
- package/console-dist/assets/select-display-items-CIzY8953.js +0 -1
- package/console-dist/assets/server-D9Kfn8fQ.js +0 -1
- package/console-dist/assets/shallow-6lGjQhtJ.js +0 -1
- package/console-dist/assets/tags-CiXOIHtY.js +0 -4
- package/console-dist/assets/tags-FQCXIxrP.js +0 -1
- package/console-dist/assets/tooltip-HGLF366Q.js +0 -1
- package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +0 -1
- package/console-dist/assets/transition-types-CPlGtM8w.js +0 -1
- package/console-dist/assets/triangle-alert-adNk_sea.js +0 -1
- package/console-dist/assets/use-platform-stream-DGbK1k2L.js +0 -2
- package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +0 -2
- package/console-dist/assets/validation-Blvud9_O.js +0 -1
- package/console-dist/assets/vendor-charts-CAZdaPlq.js +0 -60
- package/console-dist/assets/vendor-table-wd8JuMmT.js +0 -4
- package/console-dist/assets/vendor-ui-ByahnaY0.js +0 -1
- package/console-dist/favicon.svg +0 -37
- package/console-dist/index.html +0 -82
- package/console-dist/miravo-logo.png +0 -0
- package/src/__tests__/integration/executable-methods-runner.ts +0 -262
- package/src/__tests__/integration/executable-methods.integration.ts +0 -2348
- package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +0 -314
- package/src/commands/adapter.ts +0 -249
- package/src/commands/apply.test.ts +0 -62
- package/src/commands/apply.ts +0 -216
- package/src/commands/cleanup.test.ts +0 -34
- package/src/commands/cleanup.ts +0 -201
- package/src/commands/cli-test-setup.ts +0 -91
- package/src/commands/export.test.ts +0 -51
- package/src/commands/export.ts +0 -404
- package/src/commands/fork.ts +0 -219
- package/src/commands/inject.test.ts +0 -57
- package/src/commands/inject.ts +0 -212
- package/src/commands/load.ts +0 -167
- package/src/commands/logs.ts +0 -173
- package/src/commands/models.test.ts +0 -162
- package/src/commands/models.ts +0 -338
- package/src/commands/pause.ts +0 -67
- package/src/commands/platforms.test.ts +0 -186
- package/src/commands/platforms.ts +0 -371
- package/src/commands/ps.ts +0 -116
- package/src/commands/remove.ts +0 -38
- package/src/commands/reset.ts +0 -44
- package/src/commands/rm.ts +0 -97
- package/src/commands/spawn.ts +0 -45
- package/src/commands/speed.ts +0 -40
- package/src/commands/start/adapter-config.ts +0 -157
- package/src/commands/start/catalog-le-dt.test.ts +0 -50
- package/src/commands/start/catalog-le-dt.ts +0 -187
- package/src/commands/start/catalog-model-schema.ts +0 -89
- package/src/commands/start/catalog-template-detail.ts +0 -54
- package/src/commands/start/daemon.ts +0 -255
- package/src/commands/start/display.ts +0 -150
- package/src/commands/start/engine-setup.ts +0 -247
- package/src/commands/start/events.ts +0 -126
- package/src/commands/start/index.ts +0 -897
- package/src/commands/start/protocol-plans.ts +0 -239
- package/src/commands/start/template-loader.ts +0 -197
- package/src/commands/start/template-resolver.ts +0 -185
- package/src/commands/start/types.ts +0 -73
- package/src/commands/start.test.ts +0 -480
- package/src/commands/status.test.ts +0 -20
- package/src/commands/status.ts +0 -470
- package/src/commands/stop.test.ts +0 -84
- package/src/commands/stop.ts +0 -169
- package/src/commands/templates.test.ts +0 -58
- package/src/commands/templates.ts +0 -277
- package/src/index.ts +0 -166
- package/src/lib/admin-auth.ts +0 -157
- package/src/lib/admin-client.test.ts +0 -51
- package/src/lib/admin-client.ts +0 -326
- package/src/lib/admin-platform-routes.ts +0 -1017
- package/src/lib/admin-platform-stream.test.ts +0 -434
- package/src/lib/admin-platform-stream.ts +0 -788
- package/src/lib/admin-responses.ts +0 -211
- package/src/lib/admin-server.test.ts +0 -781
- package/src/lib/admin-server.ts +0 -797
- package/src/lib/auth-store.ts +0 -87
- package/src/lib/daemon-registry.test.ts +0 -429
- package/src/lib/daemon-registry.ts +0 -343
- package/src/lib/le/le-apply.test.ts +0 -591
- package/src/lib/le/le-apply.ts +0 -832
- package/src/lib/le/le-auth.ts +0 -89
- package/src/lib/le/le-cleanup.test.ts +0 -168
- package/src/lib/le/le-cleanup.ts +0 -399
- package/src/lib/le/le-cli-helpers.ts +0 -77
- package/src/lib/le/le-client.ts +0 -184
- package/src/lib/le/le-concurrency.test.ts +0 -157
- package/src/lib/le/le-concurrency.ts +0 -78
- package/src/lib/le/le-connection-test.ts +0 -42
- package/src/lib/le/le-devicehub-properties.test.ts +0 -53
- package/src/lib/le/le-devicehub-properties.ts +0 -33
- package/src/lib/le/le-devicehub.ts +0 -213
- package/src/lib/le/le-digital-twins.ts +0 -285
- package/src/lib/le/le-drivers.ts +0 -23
- package/src/lib/le/le-endpoint.ts +0 -40
- package/src/lib/le/le-environments.test.ts +0 -73
- package/src/lib/le/le-environments.ts +0 -156
- package/src/lib/le/le-markers.ts +0 -87
- package/src/lib/le/le-plan.test.ts +0 -26
- package/src/lib/le/le-plan.ts +0 -109
- package/src/lib/le/le-platform-operations.test.ts +0 -198
- package/src/lib/le/le-platform-operations.ts +0 -340
- package/src/lib/le/le-static-model-schema.ts +0 -78
- package/src/lib/le/le-static-planner.test.ts +0 -255
- package/src/lib/le/le-static-planner.ts +0 -442
- package/src/lib/le/le-template-exporter.test.ts +0 -527
- package/src/lib/le/le-template-exporter.ts +0 -459
- package/src/lib/le/le-value-types.ts +0 -23
- package/src/lib/mqtt-client.ts +0 -115
- package/src/lib/parse.test.ts +0 -123
- package/src/lib/parse.ts +0 -87
- package/src/lib/resolve-simulation.test.ts +0 -118
- package/src/lib/resolve-simulation.ts +0 -181
- package/src/lib/session-store.ts +0 -110
- package/src/lib/shutdown.test.ts +0 -562
- package/src/lib/shutdown.ts +0 -621
- package/src/lib/template-category.test.ts +0 -71
- package/src/lib/template-category.ts +0 -72
- package/src/lib/template-locator.ts +0 -55
- package/src/lib/template-utils.ts +0 -45
- package/src/lib/ws-hub.ts +0 -281
- package/src/logger.test.ts +0 -172
- package/src/logger.ts +0 -402
- package/src/output.test.ts +0 -64
- 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
|
-
});
|