@camstack/addon-terminal 0.1.31 → 0.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon.js +657 -397
- package/dist/addon.mjs +657 -397
- package/package.json +1 -1
package/dist/addon.mjs
CHANGED
|
@@ -5810,6 +5810,13 @@ var BaseAddon = class {
|
|
|
5810
5810
|
_readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
|
|
5811
5811
|
/** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
|
|
5812
5812
|
_registeredCapNames = [];
|
|
5813
|
+
/**
|
|
5814
|
+
* True only after `readAddonStore` actually answered. Constructor
|
|
5815
|
+
* defaults look like stored config when the store is down — a forked
|
|
5816
|
+
* addon that auto-starts from those defaults (cloudflare-tunnel quick
|
|
5817
|
+
* mode, 2026-08-25) is not "the operator chose this".
|
|
5818
|
+
*/
|
|
5819
|
+
settingsStoreReady = false;
|
|
5813
5820
|
/** Default config values. Provided via constructor. */
|
|
5814
5821
|
defaults;
|
|
5815
5822
|
constructor(defaults) {
|
|
@@ -6210,7 +6217,9 @@ var BaseAddon = class {
|
|
|
6210
6217
|
];
|
|
6211
6218
|
let lastErr;
|
|
6212
6219
|
for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
|
|
6213
|
-
|
|
6220
|
+
const stored = await settings.readAddonStore() ?? {};
|
|
6221
|
+
this.settingsStoreReady = true;
|
|
6222
|
+
return stored;
|
|
6214
6223
|
} catch (err) {
|
|
6215
6224
|
lastErr = err;
|
|
6216
6225
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -6218,6 +6227,7 @@ var BaseAddon = class {
|
|
|
6218
6227
|
if (attempt === delaysMs.length) break;
|
|
6219
6228
|
await new Promise((r) => setTimeout(r, delaysMs[attempt]));
|
|
6220
6229
|
}
|
|
6230
|
+
this.settingsStoreReady = false;
|
|
6221
6231
|
this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
|
|
6222
6232
|
return {};
|
|
6223
6233
|
}
|
|
@@ -8096,6 +8106,15 @@ var LabelDefinitionSchema = object({
|
|
|
8096
8106
|
description: string().optional(),
|
|
8097
8107
|
icon: string().optional()
|
|
8098
8108
|
});
|
|
8109
|
+
var ClassMapDefinitionSchema = object({
|
|
8110
|
+
mapping: record(string(), _enum([
|
|
8111
|
+
"person",
|
|
8112
|
+
"vehicle",
|
|
8113
|
+
"animal",
|
|
8114
|
+
"package"
|
|
8115
|
+
])),
|
|
8116
|
+
preserveOriginal: boolean()
|
|
8117
|
+
});
|
|
8099
8118
|
var MODEL_FORMATS = [
|
|
8100
8119
|
"onnx",
|
|
8101
8120
|
"coreml",
|
|
@@ -8179,6 +8198,12 @@ var ModelVariantGroupSchema = object({
|
|
|
8179
8198
|
*/
|
|
8180
8199
|
resolution: number().int().positive().optional()
|
|
8181
8200
|
});
|
|
8201
|
+
var ModelProviderIdSchema = _enum([
|
|
8202
|
+
"camstack",
|
|
8203
|
+
"frigate",
|
|
8204
|
+
"scrypted",
|
|
8205
|
+
"custom"
|
|
8206
|
+
]);
|
|
8182
8207
|
var ModelCatalogEntrySchema = object({
|
|
8183
8208
|
id: string(),
|
|
8184
8209
|
name: string(),
|
|
@@ -8274,7 +8299,19 @@ var ModelCatalogEntrySchema = object({
|
|
|
8274
8299
|
* `id` stays the source of truth for resolution/download/persistence; grouping
|
|
8275
8300
|
* is a presentation overlay resolved back to an `id`.
|
|
8276
8301
|
*/
|
|
8277
|
-
group: ModelVariantGroupSchema.optional()
|
|
8302
|
+
group: ModelVariantGroupSchema.optional(),
|
|
8303
|
+
/**
|
|
8304
|
+
* Catalog source for the pipeline stepper's provider-first picker. Absent on
|
|
8305
|
+
* built-in CamStack entries (treated as `camstack`) and on registry rows
|
|
8306
|
+
* persisted before this field existed (`inferModelProvider` fills those).
|
|
8307
|
+
*/
|
|
8308
|
+
provider: ModelProviderIdSchema.optional(),
|
|
8309
|
+
/**
|
|
8310
|
+
* Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
|
|
8311
|
+
* applies (Frigate / COCO public catalog). Set on a custom model whose raw
|
|
8312
|
+
* labels already ARE the CamStack macros (Scrypted identity map).
|
|
8313
|
+
*/
|
|
8314
|
+
classMap: ClassMapDefinitionSchema.optional()
|
|
8278
8315
|
});
|
|
8279
8316
|
var ConvertTargetSchema = discriminatedUnion("format", [object({
|
|
8280
8317
|
format: literal("openvino"),
|
|
@@ -8303,7 +8340,8 @@ var ModelConvertMetadataSchema = object({
|
|
|
8303
8340
|
"ocr",
|
|
8304
8341
|
"segmentation"
|
|
8305
8342
|
]),
|
|
8306
|
-
faceAlignment: boolean().optional()
|
|
8343
|
+
faceAlignment: boolean().optional(),
|
|
8344
|
+
classMap: ClassMapDefinitionSchema.optional()
|
|
8307
8345
|
});
|
|
8308
8346
|
var ConvertResultSchema = object({
|
|
8309
8347
|
entry: ModelCatalogEntrySchema,
|
|
@@ -12005,6 +12043,27 @@ var LinkedDeviceSchema = object({
|
|
|
12005
12043
|
features: array(string()),
|
|
12006
12044
|
producesTrackedEvents: boolean().optional()
|
|
12007
12045
|
});
|
|
12046
|
+
/** One camera's resolved linked set, tagged with the camera it belongs to.
|
|
12047
|
+
* The batch answer needs the tag; the single-device answer already has it
|
|
12048
|
+
* from the input, which is why `getLinkedDevices` keeps the untagged shape. */
|
|
12049
|
+
var LinkedDevicesForDeviceSchema = object({
|
|
12050
|
+
deviceId: number(),
|
|
12051
|
+
mode: LinkedDevicesModeSchema,
|
|
12052
|
+
devices: array(LinkedDeviceSchema)
|
|
12053
|
+
});
|
|
12054
|
+
/** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
|
|
12055
|
+
* `getAllBindings` all answer in. Declared once: three copies of the same
|
|
12056
|
+
* object literal is exactly how the three drift apart. */
|
|
12057
|
+
var DeviceBindingsForDeviceSchema = object({
|
|
12058
|
+
deviceId: number(),
|
|
12059
|
+
entries: array(object({
|
|
12060
|
+
capName: string(),
|
|
12061
|
+
kind: _enum(["native", "wrapped"]),
|
|
12062
|
+
providerAddonId: string(),
|
|
12063
|
+
providerNodeId: string(),
|
|
12064
|
+
nativeAddonId: string()
|
|
12065
|
+
}))
|
|
12066
|
+
});
|
|
12008
12067
|
var SavedDeviceRowSchema = object({
|
|
12009
12068
|
/** Numeric id reserved at allocateDeviceId time. */
|
|
12010
12069
|
id: number(),
|
|
@@ -12230,11 +12289,25 @@ method(object({
|
|
|
12230
12289
|
projection: _enum(["full", "slim"]).optional(),
|
|
12231
12290
|
/** Return only camera devices. Filtering server-side instead of
|
|
12232
12291
|
* shipping 293 rows to find 12. */
|
|
12233
|
-
isCamera: boolean().optional()
|
|
12292
|
+
isCamera: boolean().optional(),
|
|
12293
|
+
/**
|
|
12294
|
+
* Return only these device ids. For the caller that already KNOWS the
|
|
12295
|
+
* handful it wants and needs a field the id-bearing answer does not
|
|
12296
|
+
* carry — the viewer's linked-devices panel joins `type` and `online`
|
|
12297
|
+
* onto ~8 linked ids and, unfiltered, dragged the fleet across to do
|
|
12298
|
+
* it: 433 KB slim / 958 KB full for 967 devices, on a query that
|
|
12299
|
+
* refetches on the reconcile interval, on a phone.
|
|
12300
|
+
*
|
|
12301
|
+
* Safe to send at a hub that predates it: Zod STRIPS unknown input
|
|
12302
|
+
* keys rather than rejecting them (verified against the live hub
|
|
12303
|
+
* 2026-08-25 — 967 rows came back), so an old hub answers exactly what
|
|
12304
|
+
* it answers today and the caller filters as it already does.
|
|
12305
|
+
*/
|
|
12306
|
+
deviceIds: array(number()).optional()
|
|
12234
12307
|
}), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
|
|
12235
12308
|
mode: LinkedDevicesModeSchema,
|
|
12236
12309
|
devices: array(LinkedDeviceSchema)
|
|
12237
|
-
})), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
|
|
12310
|
+
})), method(object({ deviceIds: array(number()) }), array(LinkedDevicesForDeviceSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
|
|
12238
12311
|
deviceId: number(),
|
|
12239
12312
|
values: record(string(), unknown())
|
|
12240
12313
|
}), object({ success: literal(true) }), {
|
|
@@ -12261,25 +12334,7 @@ method(object({
|
|
|
12261
12334
|
}), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
|
|
12262
12335
|
kind: "mutation",
|
|
12263
12336
|
auth: "admin"
|
|
12264
|
-
}), method(object({ deviceId: number() }), object({
|
|
12265
|
-
deviceId: number(),
|
|
12266
|
-
entries: array(object({
|
|
12267
|
-
capName: string(),
|
|
12268
|
-
kind: _enum(["native", "wrapped"]),
|
|
12269
|
-
providerAddonId: string(),
|
|
12270
|
-
providerNodeId: string(),
|
|
12271
|
-
nativeAddonId: string()
|
|
12272
|
-
}))
|
|
12273
|
-
})), method(object({}), array(object({
|
|
12274
|
-
deviceId: number(),
|
|
12275
|
-
entries: array(object({
|
|
12276
|
-
capName: string(),
|
|
12277
|
-
kind: _enum(["native", "wrapped"]),
|
|
12278
|
-
providerAddonId: string(),
|
|
12279
|
-
providerNodeId: string(),
|
|
12280
|
-
nativeAddonId: string()
|
|
12281
|
-
}))
|
|
12282
|
-
}))), method(object({
|
|
12337
|
+
}), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
|
|
12283
12338
|
deviceId: number(),
|
|
12284
12339
|
capName: string(),
|
|
12285
12340
|
wrapperAddonId: string(),
|
|
@@ -14689,12 +14744,15 @@ var NcOccupancyConditionSchema = object({
|
|
|
14689
14744
|
* there is no second switch that can disagree with the first and every rule
|
|
14690
14745
|
* authored before the decision migrates for free (`audioModeOf`):
|
|
14691
14746
|
*
|
|
14692
|
-
* - **LABEL mode — `labels` present.** The rule fires
|
|
14693
|
-
*
|
|
14694
|
-
* `hitPercent` and `samplingSeconds` are ignored
|
|
14695
|
-
*
|
|
14696
|
-
*
|
|
14697
|
-
*
|
|
14747
|
+
* - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
|
|
14748
|
+
* labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
|
|
14749
|
+
* `hitPercent` and `samplingSeconds` are still ignored — a percentage of
|
|
14750
|
+
* frames is the wrong question for a classifier that labels 1–3 frames
|
|
14751
|
+
* per episode. The count window is the brake that drops a single-frame
|
|
14752
|
+
* false positive; the rule's own `throttle` cooldown is the other. The
|
|
14753
|
+
* per-label confidence floor is the analyzer's (`classificationMinScore`,
|
|
14754
|
+
* per device) — a label only reaches this condition if the classifier was
|
|
14755
|
+
* already confident enough. `confirmHits: 1` restores first-frame fire.
|
|
14698
14756
|
* - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
|
|
14699
14757
|
* the condition: at least `hitPercent`% of the samples over
|
|
14700
14758
|
* `samplingSeconds` must be at or above `dbThreshold` dBFS (see
|
|
@@ -14721,14 +14779,22 @@ var NcOccupancyConditionSchema = object({
|
|
|
14721
14779
|
* an operator who typed `dog` mean the same thing.
|
|
14722
14780
|
*/
|
|
14723
14781
|
var NcAudioConditionSchema = object({
|
|
14724
|
-
/** LABEL MODE: audio macro labels. Present ⇒
|
|
14782
|
+
/** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
|
|
14725
14783
|
labels: array(string().min(1)).min(1).optional(),
|
|
14726
14784
|
/** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
|
|
14727
14785
|
dbThreshold: number().min(-96).max(0).optional(),
|
|
14728
14786
|
/** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
|
|
14729
14787
|
hitPercent: number().int().min(1).max(100).default(60),
|
|
14730
14788
|
/** LEVEL MODE ONLY: length of the sampling window in seconds. */
|
|
14731
|
-
samplingSeconds: number().int().min(1).max(300).default(10)
|
|
14789
|
+
samplingSeconds: number().int().min(1).max(300).default(10),
|
|
14790
|
+
/**
|
|
14791
|
+
* LABEL MODE: how many labelled frames must land inside
|
|
14792
|
+
* {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
|
|
14793
|
+
* Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
|
|
14794
|
+
*/
|
|
14795
|
+
confirmHits: number().int().min(1).max(20).optional(),
|
|
14796
|
+
/** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
|
|
14797
|
+
confirmWindowSec: number().int().min(1).max(60).optional()
|
|
14732
14798
|
});
|
|
14733
14799
|
/**
|
|
14734
14800
|
* Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
|
|
@@ -17102,6 +17168,46 @@ var RecentTracksPageSchema = object({
|
|
|
17102
17168
|
/** Cursor for the next page, or null when this page is the last. */
|
|
17103
17169
|
nextCursor: string().nullable()
|
|
17104
17170
|
});
|
|
17171
|
+
var LIST_GROUPS_DEFAULT_LIMIT = 40;
|
|
17172
|
+
var LIST_GROUPS_MAX_LIMIT = 100;
|
|
17173
|
+
var AnalyticsGroupRecordSchema = object({
|
|
17174
|
+
id: string(),
|
|
17175
|
+
deviceId: number().int(),
|
|
17176
|
+
openedAt: number().int(),
|
|
17177
|
+
closedAt: number().int(),
|
|
17178
|
+
timestamp: number().int(),
|
|
17179
|
+
memberCount: number().int(),
|
|
17180
|
+
memberTrackIds: array(string()).readonly(),
|
|
17181
|
+
className: string(),
|
|
17182
|
+
classes: array(string()).readonly(),
|
|
17183
|
+
/** Relative event-media path, or null when the group has no picture yet. */
|
|
17184
|
+
mediaUrl: string().nullable(),
|
|
17185
|
+
singleton: boolean()
|
|
17186
|
+
});
|
|
17187
|
+
var AnalyticsGroupMemberSchema = object({
|
|
17188
|
+
trackId: string(),
|
|
17189
|
+
deviceId: number().int(),
|
|
17190
|
+
className: string(),
|
|
17191
|
+
firstSeen: number().int(),
|
|
17192
|
+
lastSeen: number().int(),
|
|
17193
|
+
mediaUrl: string().nullable()
|
|
17194
|
+
});
|
|
17195
|
+
var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
|
|
17196
|
+
var ListGroupsQueryInput = object({
|
|
17197
|
+
/** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
|
|
17198
|
+
deviceIds: array(number()),
|
|
17199
|
+
/** Window lower bound on `closedAt` (inclusive). */
|
|
17200
|
+
since: number().optional(),
|
|
17201
|
+
/** Window upper bound on `openedAt` (inclusive). */
|
|
17202
|
+
until: number().optional(),
|
|
17203
|
+
limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
|
|
17204
|
+
/** Opaque continuation cursor from a previous page's `nextCursor`. */
|
|
17205
|
+
cursor: string().optional()
|
|
17206
|
+
});
|
|
17207
|
+
var ListGroupsPageSchema = object({
|
|
17208
|
+
groups: array(AnalyticsGroupRecordSchema).readonly(),
|
|
17209
|
+
nextCursor: string().nullable()
|
|
17210
|
+
});
|
|
17105
17211
|
var KeyEventQueryInput = object({
|
|
17106
17212
|
deviceId: number(),
|
|
17107
17213
|
/** Window lower bound (track firstSeen ≥ since). */
|
|
@@ -17177,7 +17283,9 @@ var TrackCascadeCountsSchema = object({
|
|
|
17177
17283
|
/** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
|
|
17178
17284
|
plates: number().int(),
|
|
17179
17285
|
/** Per-track CLIP search vectors removed (best-effort). */
|
|
17180
|
-
embeddings: number().int()
|
|
17286
|
+
embeddings: number().int(),
|
|
17287
|
+
/** Group membership + group rows removed with their last member (best-effort). */
|
|
17288
|
+
groups: number().int()
|
|
17181
17289
|
});
|
|
17182
17290
|
/** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
|
|
17183
17291
|
var DiskReconcileCountsSchema = object({
|
|
@@ -17323,7 +17431,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
17323
17431
|
* stationary registry). Default false: the timeline lists passages,
|
|
17324
17432
|
* not parking records (operator decision, 2026-08-15). */
|
|
17325
17433
|
includeStationary: boolean().optional()
|
|
17326
|
-
}), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(
|
|
17434
|
+
}), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
|
|
17435
|
+
deviceId: number(),
|
|
17436
|
+
groupId: string().min(1)
|
|
17437
|
+
}), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
|
|
17327
17438
|
kind: "mutation",
|
|
17328
17439
|
auth: "admin"
|
|
17329
17440
|
}), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
|
|
@@ -17541,6 +17652,33 @@ var NativeCropRefSchema = object({
|
|
|
17541
17652
|
h: number()
|
|
17542
17653
|
})
|
|
17543
17654
|
});
|
|
17655
|
+
object({
|
|
17656
|
+
crop: object({
|
|
17657
|
+
left: number(),
|
|
17658
|
+
top: number(),
|
|
17659
|
+
width: number().positive(),
|
|
17660
|
+
height: number().positive()
|
|
17661
|
+
}).optional(),
|
|
17662
|
+
content: object({
|
|
17663
|
+
width: number().int().positive(),
|
|
17664
|
+
height: number().int().positive()
|
|
17665
|
+
}),
|
|
17666
|
+
fit: _enum(["stretch", "contain"]),
|
|
17667
|
+
format: _enum([
|
|
17668
|
+
"rgb",
|
|
17669
|
+
"gray",
|
|
17670
|
+
"jpeg"
|
|
17671
|
+
])
|
|
17672
|
+
});
|
|
17673
|
+
var FrameRefSchema = object({
|
|
17674
|
+
registryId: string().min(1),
|
|
17675
|
+
id: string().min(1),
|
|
17676
|
+
width: number().int().positive(),
|
|
17677
|
+
height: number().int().positive(),
|
|
17678
|
+
format: _enum(["rgb", "gray"]),
|
|
17679
|
+
timestamp: number(),
|
|
17680
|
+
capturedAt: number().optional()
|
|
17681
|
+
});
|
|
17544
17682
|
var ModelFormatSchema$1 = _enum([
|
|
17545
17683
|
"onnx",
|
|
17546
17684
|
"coreml",
|
|
@@ -17606,7 +17744,8 @@ var PipelineModelOptionSchema = object({
|
|
|
17606
17744
|
sizeMB: number()
|
|
17607
17745
|
})),
|
|
17608
17746
|
group: ModelVariantGroupSchema.optional(),
|
|
17609
|
-
legacy: boolean().optional()
|
|
17747
|
+
legacy: boolean().optional(),
|
|
17748
|
+
provider: ModelProviderIdSchema.optional()
|
|
17610
17749
|
});
|
|
17611
17750
|
var ConfigFieldBridge = custom();
|
|
17612
17751
|
var PipelineAddonSchemaSchema = object({
|
|
@@ -17785,6 +17924,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
|
|
|
17785
17924
|
steps: array(PipelineStepInputSchema).min(1),
|
|
17786
17925
|
frame: FrameInputSchema.optional(),
|
|
17787
17926
|
/**
|
|
17927
|
+
* Process-local lazy frame. Valid only when caller and provider resolve
|
|
17928
|
+
* in the same execution-group process; split/cross-node callers use
|
|
17929
|
+
* `frame`/`image` inline compatibility instead.
|
|
17930
|
+
*/
|
|
17931
|
+
frameRef: FrameRefSchema.optional(),
|
|
17932
|
+
/**
|
|
17788
17933
|
* CB5 shm passthrough — a `FrameHandle` naming the same ring slot
|
|
17789
17934
|
* the decoded pixels live in. One more member of the one-of
|
|
17790
17935
|
* frame/frameHandle/image/imageBase64/referenceImage group.
|
|
@@ -18080,7 +18225,10 @@ var NativeCropResultSchema = object({
|
|
|
18080
18225
|
* Which source served this crop, so a quality-sensitive consumer (the native
|
|
18081
18226
|
* `keyFrame`) can reject a degraded fallback:
|
|
18082
18227
|
* - `native` — cut from the decode worker's retained NATIVE surface (the
|
|
18083
|
-
* quality path).
|
|
18228
|
+
* quality path). A subject-tile serve is also native-resolution and stays
|
|
18229
|
+
* `native` here: the public enum cannot name `tile` without a breaking cap
|
|
18230
|
+
* change. Runner telemetry distinguishes lease vs tile via `source` on the
|
|
18231
|
+
* internal crop result (`nativeHits` vs `tileHits`).
|
|
18084
18232
|
* - `ram-fullframe` — the native surface MISSED but the request was full-frame,
|
|
18085
18233
|
* so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
|
|
18086
18234
|
* the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
|
|
@@ -18571,12 +18719,41 @@ var RunnerLocalLoadSchema = object({
|
|
|
18571
18719
|
* legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
|
|
18572
18720
|
* working unchanged when they switch to reading from the runner cap.
|
|
18573
18721
|
*/
|
|
18722
|
+
var FrameLazyCountersSchema = object({
|
|
18723
|
+
framesDecoded: number(),
|
|
18724
|
+
framesAdmitted: number(),
|
|
18725
|
+
framesDroppedPixelFree: number(),
|
|
18726
|
+
viewsMaterialized: number(),
|
|
18727
|
+
viewsSkipped: number(),
|
|
18728
|
+
workerToRunnerBytes: number(),
|
|
18729
|
+
runnerToPoolRawBytes: number(),
|
|
18730
|
+
runnerToPoolJpegBytes: number(),
|
|
18731
|
+
onDemandFullFrameRequests: number(),
|
|
18732
|
+
onDemandCropRequests: number(),
|
|
18733
|
+
nativeHits: number(),
|
|
18734
|
+
nativeMisses: number(),
|
|
18735
|
+
tileHits: number(),
|
|
18736
|
+
tileMisses: number(),
|
|
18737
|
+
fallbackHits: number(),
|
|
18738
|
+
fallbackMisses: number(),
|
|
18739
|
+
retainedWritesAvoided: number(),
|
|
18740
|
+
residentRefs: number(),
|
|
18741
|
+
residentBytes: number(),
|
|
18742
|
+
releases: number(),
|
|
18743
|
+
evictions: number(),
|
|
18744
|
+
staleMisses: number()
|
|
18745
|
+
});
|
|
18746
|
+
var FrameLazyMetricsSchema = object({
|
|
18747
|
+
node: FrameLazyCountersSchema,
|
|
18748
|
+
cameras: array(FrameLazyCountersSchema.extend({ deviceId: number() }))
|
|
18749
|
+
});
|
|
18574
18750
|
var RunnerLocalMetricsSchema = object({
|
|
18575
18751
|
nodeId: string(),
|
|
18576
18752
|
activeCameras: number(),
|
|
18577
18753
|
throttledCameras: number(),
|
|
18578
18754
|
avgInferenceTimeMs: number(),
|
|
18579
|
-
queueDepth: number()
|
|
18755
|
+
queueDepth: number(),
|
|
18756
|
+
frameLazy: FrameLazyMetricsSchema.optional()
|
|
18580
18757
|
});
|
|
18581
18758
|
method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mutation" }), method(object({ deviceId: number() }), object({ success: literal(true) }), { kind: "mutation" }), method(ReportMotionInputSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), RunnerLocalLoadSchema), method(_void(), RunnerLocalMetricsSchema), method(object({ deviceId: number() }), CameraMetricsSchema.nullable()), method(_void(), array(CameraMetricsWithDeviceIdSchema).readonly()), method(_void(), array(number()).readonly()), method(object({
|
|
18582
18759
|
handle: FrameHandleSchema,
|
|
@@ -24186,6 +24363,17 @@ var NotificationEndpointSchema = object({
|
|
|
24186
24363
|
/** What the ranking currently resolves to (null when nothing is reachable). */
|
|
24187
24364
|
resolved: string().nullable()
|
|
24188
24365
|
});
|
|
24366
|
+
/**
|
|
24367
|
+
* The URLs the SDK / viewer should race for API access. `baseUrls` empty =
|
|
24368
|
+
* AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
|
|
24369
|
+
* currently expands to, so the UI can show the effective set either way.
|
|
24370
|
+
*/
|
|
24371
|
+
var ViewerEndpointsSchema = object({
|
|
24372
|
+
/** The operator's explicit race set, or empty for AUTO. */
|
|
24373
|
+
baseUrls: array(string()).readonly(),
|
|
24374
|
+
/** What the ranking currently resolves to (may be empty if nothing is up). */
|
|
24375
|
+
resolved: array(string()).readonly()
|
|
24376
|
+
});
|
|
24189
24377
|
var AllowedAddressesSchema = object({
|
|
24190
24378
|
/**
|
|
24191
24379
|
* Allowlist of interface addresses operators have explicitly opted
|
|
@@ -24217,17 +24405,18 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
|
|
|
24217
24405
|
*/
|
|
24218
24406
|
port: number().int().min(1).max(65535).optional(),
|
|
24219
24407
|
/** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
|
|
24220
|
-
* candidate. Default `
|
|
24408
|
+
* candidate. Default `false` — loopback is not a client route. */
|
|
24221
24409
|
includeLoopback: boolean().optional(),
|
|
24222
|
-
/** Skip IPv6 entries.
|
|
24223
|
-
*
|
|
24410
|
+
/** Skip IPv6 entries. Default `false` — the palette includes stable
|
|
24411
|
+
* LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
|
|
24412
|
+
* hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
|
|
24224
24413
|
ipv4Only: boolean().optional(),
|
|
24225
24414
|
/** Scheme to emit for LAN/loopback URLs. Default `'http'`.
|
|
24226
24415
|
* Pass `'https'` when the caller is itself loaded over HTTPS
|
|
24227
24416
|
* to avoid mixed-content blocks in the browser. The public
|
|
24228
24417
|
* tunnel always emits `https://` regardless. */
|
|
24229
24418
|
scheme: _enum(["http", "https"]).optional()
|
|
24230
|
-
}), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" }), method(_void(), TlsStatusSchema), method(object({ reason: string().optional() }), TlsStatusSchema, {
|
|
24419
|
+
}), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), ViewerEndpointsSchema), method(object({ baseUrls: array(string()).readonly() }), ViewerEndpointsSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" }), method(_void(), TlsStatusSchema), method(object({ reason: string().optional() }), TlsStatusSchema, {
|
|
24231
24420
|
kind: "mutation",
|
|
24232
24421
|
auth: "admin"
|
|
24233
24422
|
}), method(object({
|
|
@@ -25801,7 +25990,12 @@ var PlateInfoSchema = object({
|
|
|
25801
25990
|
plateBbox: BoundingBoxSchema.optional(),
|
|
25802
25991
|
/** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
|
|
25803
25992
|
keyFrameMediaKey: string().optional(),
|
|
25804
|
-
base64: string().optional()
|
|
25993
|
+
base64: string().optional(),
|
|
25994
|
+
/**
|
|
25995
|
+
* Same crop as a data-plane URL. `getPlateByTrack` returns this and
|
|
25996
|
+
* never inlines JPEG; `listPlates` still inlines for the admin-ui.
|
|
25997
|
+
*/
|
|
25998
|
+
cropUrl: string().optional()
|
|
25805
25999
|
});
|
|
25806
26000
|
var MediaFileLiteSchema = object({
|
|
25807
26001
|
key: string(),
|
|
@@ -31323,6 +31517,12 @@ Object.freeze({
|
|
|
31323
31517
|
addonId: null,
|
|
31324
31518
|
access: "view"
|
|
31325
31519
|
},
|
|
31520
|
+
"deviceManager.getBindingsBatch": {
|
|
31521
|
+
capName: "device-manager",
|
|
31522
|
+
capScope: "system",
|
|
31523
|
+
addonId: null,
|
|
31524
|
+
access: "view"
|
|
31525
|
+
},
|
|
31326
31526
|
"deviceManager.getChildren": {
|
|
31327
31527
|
capName: "device-manager",
|
|
31328
31528
|
capScope: "system",
|
|
@@ -31383,6 +31583,12 @@ Object.freeze({
|
|
|
31383
31583
|
addonId: null,
|
|
31384
31584
|
access: "view"
|
|
31385
31585
|
},
|
|
31586
|
+
"deviceManager.getLinkedDevicesBatch": {
|
|
31587
|
+
capName: "device-manager",
|
|
31588
|
+
capScope: "system",
|
|
31589
|
+
addonId: null,
|
|
31590
|
+
access: "view"
|
|
31591
|
+
},
|
|
31386
31592
|
"deviceManager.getRoleDisplayDefaults": {
|
|
31387
31593
|
capName: "device-manager",
|
|
31388
31594
|
capScope: "system",
|
|
@@ -32301,6 +32507,12 @@ Object.freeze({
|
|
|
32301
32507
|
addonId: null,
|
|
32302
32508
|
access: "view"
|
|
32303
32509
|
},
|
|
32510
|
+
"localNetwork.getViewerEndpoints": {
|
|
32511
|
+
capName: "local-network",
|
|
32512
|
+
capScope: "system",
|
|
32513
|
+
addonId: null,
|
|
32514
|
+
access: "view"
|
|
32515
|
+
},
|
|
32304
32516
|
"localNetwork.list": {
|
|
32305
32517
|
capName: "local-network",
|
|
32306
32518
|
capScope: "system",
|
|
@@ -32337,6 +32549,12 @@ Object.freeze({
|
|
|
32337
32549
|
addonId: null,
|
|
32338
32550
|
access: "create"
|
|
32339
32551
|
},
|
|
32552
|
+
"localNetwork.setViewerEndpoints": {
|
|
32553
|
+
capName: "local-network",
|
|
32554
|
+
capScope: "system",
|
|
32555
|
+
addonId: null,
|
|
32556
|
+
access: "create"
|
|
32557
|
+
},
|
|
32340
32558
|
"localNetwork.uploadCertificate": {
|
|
32341
32559
|
capName: "local-network",
|
|
32342
32560
|
capScope: "system",
|
|
@@ -33141,6 +33359,12 @@ Object.freeze({
|
|
|
33141
33359
|
addonId: null,
|
|
33142
33360
|
access: "view"
|
|
33143
33361
|
},
|
|
33362
|
+
"pipelineAnalytics.getGroup": {
|
|
33363
|
+
capName: "pipeline-analytics",
|
|
33364
|
+
capScope: "device",
|
|
33365
|
+
addonId: null,
|
|
33366
|
+
access: "view"
|
|
33367
|
+
},
|
|
33144
33368
|
"pipelineAnalytics.getKeyEvents": {
|
|
33145
33369
|
capName: "pipeline-analytics",
|
|
33146
33370
|
capScope: "device",
|
|
@@ -33225,6 +33449,12 @@ Object.freeze({
|
|
|
33225
33449
|
addonId: null,
|
|
33226
33450
|
access: "view"
|
|
33227
33451
|
},
|
|
33452
|
+
"pipelineAnalytics.listGroups": {
|
|
33453
|
+
capName: "pipeline-analytics",
|
|
33454
|
+
capScope: "device",
|
|
33455
|
+
addonId: null,
|
|
33456
|
+
access: "view"
|
|
33457
|
+
},
|
|
33228
33458
|
"pipelineAnalytics.listOpsLog": {
|
|
33229
33459
|
capName: "pipeline-analytics",
|
|
33230
33460
|
capScope: "device",
|
|
@@ -36006,6 +36236,11 @@ Object.freeze({
|
|
|
36006
36236
|
form: "single",
|
|
36007
36237
|
optional: false
|
|
36008
36238
|
}],
|
|
36239
|
+
"deviceManager.getBindingsBatch": [{
|
|
36240
|
+
name: "deviceIds",
|
|
36241
|
+
form: "array",
|
|
36242
|
+
optional: false
|
|
36243
|
+
}],
|
|
36009
36244
|
"deviceManager.getChildren": [{
|
|
36010
36245
|
name: "parentDeviceId",
|
|
36011
36246
|
form: "single",
|
|
@@ -36051,6 +36286,11 @@ Object.freeze({
|
|
|
36051
36286
|
form: "single",
|
|
36052
36287
|
optional: false
|
|
36053
36288
|
}],
|
|
36289
|
+
"deviceManager.getLinkedDevicesBatch": [{
|
|
36290
|
+
name: "deviceIds",
|
|
36291
|
+
form: "array",
|
|
36292
|
+
optional: false
|
|
36293
|
+
}],
|
|
36054
36294
|
"deviceManager.getSettingsSchema": [{
|
|
36055
36295
|
name: "deviceId",
|
|
36056
36296
|
form: "single",
|
|
@@ -36071,6 +36311,11 @@ Object.freeze({
|
|
|
36071
36311
|
form: "single",
|
|
36072
36312
|
optional: false
|
|
36073
36313
|
}],
|
|
36314
|
+
"deviceManager.listAll": [{
|
|
36315
|
+
name: "deviceIds",
|
|
36316
|
+
form: "array",
|
|
36317
|
+
optional: true
|
|
36318
|
+
}],
|
|
36074
36319
|
"deviceManager.loadConfig": [{
|
|
36075
36320
|
name: "deviceId",
|
|
36076
36321
|
form: "single",
|
|
@@ -36644,6 +36889,11 @@ Object.freeze({
|
|
|
36644
36889
|
form: "single",
|
|
36645
36890
|
optional: false
|
|
36646
36891
|
}],
|
|
36892
|
+
"pipelineAnalytics.getGroup": [{
|
|
36893
|
+
name: "deviceId",
|
|
36894
|
+
form: "single",
|
|
36895
|
+
optional: false
|
|
36896
|
+
}],
|
|
36647
36897
|
"pipelineAnalytics.getKeyEvents": [{
|
|
36648
36898
|
name: "deviceId",
|
|
36649
36899
|
form: "single",
|
|
@@ -36699,6 +36949,11 @@ Object.freeze({
|
|
|
36699
36949
|
form: "array",
|
|
36700
36950
|
optional: false
|
|
36701
36951
|
}],
|
|
36952
|
+
"pipelineAnalytics.listGroups": [{
|
|
36953
|
+
name: "deviceIds",
|
|
36954
|
+
form: "array",
|
|
36955
|
+
optional: false
|
|
36956
|
+
}],
|
|
36702
36957
|
"pipelineAnalytics.listOpsLog": [{
|
|
36703
36958
|
name: "deviceId",
|
|
36704
36959
|
form: "single",
|
|
@@ -38116,356 +38371,6 @@ async function silenceAnalysisFor(deps, deviceId) {
|
|
|
38116
38371
|
if (failures.length > 0) throw new Error(`terminal camera ${deviceId}: could not switch off ${failures.length} analyzer(s) — it will run at full detection cost (${failures.join("; ")})`);
|
|
38117
38372
|
}
|
|
38118
38373
|
//#endregion
|
|
38119
|
-
//#region src/terminal-camera-declarations.ts
|
|
38120
|
-
/**
|
|
38121
|
-
* Feed DeclaredDevices every live declaration plus one deterministic orphan
|
|
38122
|
-
* batch. The generic sweep intentionally refuses an over-limit set; selecting
|
|
38123
|
-
* a batch here drains large historical Terminal orphan sets across convergence
|
|
38124
|
-
* passes without weakening that global safety guard.
|
|
38125
|
-
*/
|
|
38126
|
-
function terminalCameraReconciliationIndex(rows, integrationId, declarations, managedStableIds = /* @__PURE__ */ new Set()) {
|
|
38127
|
-
if (!integrationId) return [];
|
|
38128
|
-
const declared = new Set(declarations.map((camera) => camera.stableId));
|
|
38129
|
-
const owned = rows.filter((row) => row.integrationId === integrationId && (declared.has(row.stableId) || managedStableIds.has(row.stableId) || row.stableId.startsWith("terminal-camera-instance-")));
|
|
38130
|
-
return [...owned.filter((row) => declared.has(row.stableId)), ...owned.filter((row) => !declared.has(row.stableId)).sort((left, right) => left.id - right.id).slice(0, 32)];
|
|
38131
|
-
}
|
|
38132
|
-
/** Explicit persisted instances, never the node × profile template matrix. */
|
|
38133
|
-
function buildTerminalInstanceCameraDeclarations(instances) {
|
|
38134
|
-
return instances.filter((instance) => instance.enabled).map((instance) => ({
|
|
38135
|
-
stableId: instance.cameraStableId,
|
|
38136
|
-
name: instance.name,
|
|
38137
|
-
config: {
|
|
38138
|
-
instanceId: instance.id,
|
|
38139
|
-
nodeId: instance.nodeId,
|
|
38140
|
-
profileId: instance.profileId,
|
|
38141
|
-
profileLabel: instance.profileLabel
|
|
38142
|
-
}
|
|
38143
|
-
}));
|
|
38144
|
-
}
|
|
38145
|
-
/**
|
|
38146
|
-
* `DeviceConfig` materializes schema defaults in memory, so comparing
|
|
38147
|
-
* `config.get()` cannot detect a legacy `{ nodeId }` row. Reconciliation must
|
|
38148
|
-
* inspect the raw persisted blob to make the profile migration durable.
|
|
38149
|
-
*/
|
|
38150
|
-
function needsTerminalCameraConfigMigration(persistedConfig, declaration) {
|
|
38151
|
-
return persistedConfig.instanceId !== declaration.config.instanceId || persistedConfig.nodeId !== declaration.config.nodeId || persistedConfig.profileId !== declaration.config.profileId || persistedConfig.profileLabel !== declaration.config.profileLabel;
|
|
38152
|
-
}
|
|
38153
|
-
//#endregion
|
|
38154
|
-
//#region src/terminal-cell-runs.ts
|
|
38155
|
-
var TERMINAL_DEFAULT_FG = "#d7dce2";
|
|
38156
|
-
var TERMINAL_DEFAULT_BG = "#0b0d10";
|
|
38157
|
-
/**
|
|
38158
|
-
* The 16 ANSI colours, in a dark-theme variant chosen to sit with the existing
|
|
38159
|
-
* frame (`#0b0d10` ground, `#d7dce2` text) rather than the raw xterm defaults,
|
|
38160
|
-
* whose pure `#0000ff` blue and `#008000` green are close to unreadable on a
|
|
38161
|
-
* near-black background at 13px. Index 7 IS the default foreground, so plain
|
|
38162
|
-
* `CSI 37m` text renders identically to unstyled text.
|
|
38163
|
-
*/
|
|
38164
|
-
var TERMINAL_ANSI_PALETTE = [
|
|
38165
|
-
"#282c34",
|
|
38166
|
-
"#e06c75",
|
|
38167
|
-
"#98c379",
|
|
38168
|
-
"#e5c07b",
|
|
38169
|
-
"#61afef",
|
|
38170
|
-
"#c678dd",
|
|
38171
|
-
"#56b6c2",
|
|
38172
|
-
TERMINAL_DEFAULT_FG,
|
|
38173
|
-
"#5c6370",
|
|
38174
|
-
"#ef596f",
|
|
38175
|
-
"#89ca78",
|
|
38176
|
-
"#f0c674",
|
|
38177
|
-
"#6cb6ff",
|
|
38178
|
-
"#d55fde",
|
|
38179
|
-
"#2bbac5",
|
|
38180
|
-
"#ffffff"
|
|
38181
|
-
];
|
|
38182
|
-
/** The six levels of the xterm 6×6×6 colour cube (indices 16-231). */
|
|
38183
|
-
var TERMINAL_CUBE_LEVELS = [
|
|
38184
|
-
0,
|
|
38185
|
-
95,
|
|
38186
|
-
135,
|
|
38187
|
-
175,
|
|
38188
|
-
215,
|
|
38189
|
-
255
|
|
38190
|
-
];
|
|
38191
|
-
var TERMINAL_CUBE_FIRST = 16;
|
|
38192
|
-
var TERMINAL_GRAYSCALE_FIRST = 232;
|
|
38193
|
-
var TERMINAL_GRAYSCALE_BASE = 8;
|
|
38194
|
-
var TERMINAL_GRAYSCALE_STEP = 10;
|
|
38195
|
-
/** SGR 2 keeps the foreground legible; it must not become the background. */
|
|
38196
|
-
var TERMINAL_DIM_WEIGHT = .6;
|
|
38197
|
-
function channel(value) {
|
|
38198
|
-
return Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, "0");
|
|
38199
|
-
}
|
|
38200
|
-
function hex(red, green, blue) {
|
|
38201
|
-
return `#${channel(red)}${channel(green)}${channel(blue)}`;
|
|
38202
|
-
}
|
|
38203
|
-
function parseHex(color) {
|
|
38204
|
-
return [
|
|
38205
|
-
Number.parseInt(color.slice(1, 3), 16),
|
|
38206
|
-
Number.parseInt(color.slice(3, 5), 16),
|
|
38207
|
-
Number.parseInt(color.slice(5, 7), 16)
|
|
38208
|
-
];
|
|
38209
|
-
}
|
|
38210
|
-
/** Resolve an xterm palette index (0-255) to a hex colour. */
|
|
38211
|
-
function terminalPaletteColor(index) {
|
|
38212
|
-
const ansi = TERMINAL_ANSI_PALETTE[index];
|
|
38213
|
-
if (ansi !== void 0) return ansi;
|
|
38214
|
-
if (index >= TERMINAL_GRAYSCALE_FIRST) {
|
|
38215
|
-
const level = TERMINAL_GRAYSCALE_BASE + (index - TERMINAL_GRAYSCALE_FIRST) * TERMINAL_GRAYSCALE_STEP;
|
|
38216
|
-
return hex(level, level, level);
|
|
38217
|
-
}
|
|
38218
|
-
if (index >= TERMINAL_CUBE_FIRST) {
|
|
38219
|
-
const offset = index - TERMINAL_CUBE_FIRST;
|
|
38220
|
-
return hex(TERMINAL_CUBE_LEVELS[Math.floor(offset / 36) % 6] ?? 0, TERMINAL_CUBE_LEVELS[Math.floor(offset / 6) % 6] ?? 0, TERMINAL_CUBE_LEVELS[offset % 6] ?? 0);
|
|
38221
|
-
}
|
|
38222
|
-
return TERMINAL_DEFAULT_FG;
|
|
38223
|
-
}
|
|
38224
|
-
/** Resolve a 0xRRGGBB truecolor value to a hex colour. */
|
|
38225
|
-
function terminalRgbColor(value) {
|
|
38226
|
-
return hex(value >> 16 & 255, value >> 8 & 255, value & 255);
|
|
38227
|
-
}
|
|
38228
|
-
function blend(color, toward, weight) {
|
|
38229
|
-
const [red, green, blue] = parseHex(color);
|
|
38230
|
-
const [targetRed, targetGreen, targetBlue] = parseHex(toward);
|
|
38231
|
-
return hex(red * weight + targetRed * (1 - weight), green * weight + targetGreen * (1 - weight), blue * weight + targetBlue * (1 - weight));
|
|
38232
|
-
}
|
|
38233
|
-
function resolveForeground(cell) {
|
|
38234
|
-
if (cell.isFgRGB()) return terminalRgbColor(cell.getFgColor());
|
|
38235
|
-
if (cell.isFgPalette()) return terminalPaletteColor(cell.getFgColor());
|
|
38236
|
-
return TERMINAL_DEFAULT_FG;
|
|
38237
|
-
}
|
|
38238
|
-
function resolveBackground(cell) {
|
|
38239
|
-
if (cell.isBgRGB()) return terminalRgbColor(cell.getBgColor());
|
|
38240
|
-
if (cell.isBgPalette()) return terminalPaletteColor(cell.getBgColor());
|
|
38241
|
-
return TERMINAL_DEFAULT_BG;
|
|
38242
|
-
}
|
|
38243
|
-
/**
|
|
38244
|
-
* Resolve one cell's attributes into concrete colours.
|
|
38245
|
-
*
|
|
38246
|
-
* Inverse is applied by SWAPPING the already-resolved pair, so inverse over two
|
|
38247
|
-
* defaults is still a visible swap rather than a no-op — that is how a selected
|
|
38248
|
-
* or highlighted row in Glances reads. Invisible is then conceal-by-equality
|
|
38249
|
-
* (foreground painted in its own background): the cell keeps its columns, which
|
|
38250
|
-
* a dropped cell would not, and dropping it would shift the whole rest of the
|
|
38251
|
-
* row left.
|
|
38252
|
-
*/
|
|
38253
|
-
function resolveCellStyle(cell) {
|
|
38254
|
-
const inverse = cell.isInverse() !== 0;
|
|
38255
|
-
const plainFg = resolveForeground(cell);
|
|
38256
|
-
const plainBg = resolveBackground(cell);
|
|
38257
|
-
const background = inverse ? plainFg : plainBg;
|
|
38258
|
-
let foreground = inverse ? plainBg : plainFg;
|
|
38259
|
-
if (cell.isDim() !== 0) foreground = blend(foreground, background, TERMINAL_DIM_WEIGHT);
|
|
38260
|
-
if (cell.isInvisible() !== 0) foreground = background;
|
|
38261
|
-
return {
|
|
38262
|
-
fg: foreground === "#d7dce2" ? null : foreground,
|
|
38263
|
-
bg: background === "#0b0d10" ? null : background,
|
|
38264
|
-
bold: cell.isBold() !== 0
|
|
38265
|
-
};
|
|
38266
|
-
}
|
|
38267
|
-
function sameStyle(left, right) {
|
|
38268
|
-
return left.fg === right.fg && left.bg === right.bg && left.bold === right.bold;
|
|
38269
|
-
}
|
|
38270
|
-
/**
|
|
38271
|
-
* Merge adjacent same-style cells into runs, then drop the trailing run of
|
|
38272
|
-
* default-styled whitespace so a row costs what it draws — the same trim
|
|
38273
|
-
* `translateToString(true)` performs. Whitespace carrying a BACKGROUND is kept:
|
|
38274
|
-
* a green bar of spaces out to the right margin is a pixel Glances drew.
|
|
38275
|
-
*/
|
|
38276
|
-
function buildCellRuns(cells) {
|
|
38277
|
-
const runs = [];
|
|
38278
|
-
let text = "";
|
|
38279
|
-
let style = null;
|
|
38280
|
-
for (const cell of cells) {
|
|
38281
|
-
if (style !== null && sameStyle(style, cell.style)) {
|
|
38282
|
-
text += cell.text;
|
|
38283
|
-
continue;
|
|
38284
|
-
}
|
|
38285
|
-
if (style !== null) runs.push({
|
|
38286
|
-
text,
|
|
38287
|
-
...style
|
|
38288
|
-
});
|
|
38289
|
-
text = cell.text;
|
|
38290
|
-
style = cell.style;
|
|
38291
|
-
}
|
|
38292
|
-
if (style !== null) runs.push({
|
|
38293
|
-
text,
|
|
38294
|
-
...style
|
|
38295
|
-
});
|
|
38296
|
-
while (runs.length > 0) {
|
|
38297
|
-
const last = runs[runs.length - 1];
|
|
38298
|
-
if (last === void 0 || last.bg !== null) break;
|
|
38299
|
-
const trimmed = last.text.replace(/\s+$/u, "");
|
|
38300
|
-
if (trimmed === last.text) break;
|
|
38301
|
-
if (trimmed === "") {
|
|
38302
|
-
runs.pop();
|
|
38303
|
-
continue;
|
|
38304
|
-
}
|
|
38305
|
-
runs[runs.length - 1] = {
|
|
38306
|
-
...last,
|
|
38307
|
-
text: trimmed
|
|
38308
|
-
};
|
|
38309
|
-
break;
|
|
38310
|
-
}
|
|
38311
|
-
return runs;
|
|
38312
|
-
}
|
|
38313
|
-
/**
|
|
38314
|
-
* Monospace families to try, in order — NOT one family and a generic.
|
|
38315
|
-
*
|
|
38316
|
-
* A terminal screen is mostly box-drawing and block characters, and a font
|
|
38317
|
-
* without them renders the frame as noise rather than as missing detail.
|
|
38318
|
-
* `DejaVu Sans Mono` covers them and ships in the hub image; macOS has neither
|
|
38319
|
-
* DejaVu nor fontconfig, so the Mac agent fell through to a generic whose
|
|
38320
|
-
* coverage is not, and its Glances camera came out unreadable while the hub's
|
|
38321
|
-
* was pixel-perfect (measured 2026-08-11, same Glances, two nodes).
|
|
38322
|
-
*
|
|
38323
|
-
* `Menlo` is the fix rather than a guess: it is macOS's default terminal face,
|
|
38324
|
-
* present on every install, and derived from DejaVu Sans Mono — the same glyph
|
|
38325
|
-
* coverage by ancestry. Monaco and Courier New follow as older fallbacks; the
|
|
38326
|
-
* generic stays last so a host with none of them still draws something.
|
|
38327
|
-
*/
|
|
38328
|
-
var TERMINAL_FONT_STACK = "DejaVu Sans Mono,Menlo,Monaco,Courier New,monospace";
|
|
38329
|
-
var TERMINAL_FONT_SIZE = 13;
|
|
38330
|
-
var TERMINAL_TEXT_MARGIN_X = 8;
|
|
38331
|
-
var TERMINAL_ROW_HEIGHT = 15;
|
|
38332
|
-
var TERMINAL_BASELINE_Y = 18;
|
|
38333
|
-
/**
|
|
38334
|
-
* Distance from a row's baseline up to the top of its cell box. Chosen so
|
|
38335
|
-
* consecutive rows tile exactly: row N's box runs from `baseline - this` for
|
|
38336
|
-
* `TERMINAL_ROW_HEIGHT`, and row N+1's box starts where it ends. A background
|
|
38337
|
-
* bar that stopped short would draw as stripes across a `CSI 42m` panel.
|
|
38338
|
-
*/
|
|
38339
|
-
var TERMINAL_CELL_ASCENT = 11.5;
|
|
38340
|
-
var TERMINAL_CELL_WIDTH = TERMINAL_FONT_SIZE * (1233 / 2048);
|
|
38341
|
-
/** Two decimals is under a tenth of a pixel and keeps the SVG small. */
|
|
38342
|
-
function coordinate(value) {
|
|
38343
|
-
return String(Number(value.toFixed(2)));
|
|
38344
|
-
}
|
|
38345
|
-
function escapeXml(value) {
|
|
38346
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
38347
|
-
}
|
|
38348
|
-
/**
|
|
38349
|
-
* Render already-interpreted terminal rows into a compact MJPEG frame.
|
|
38350
|
-
*
|
|
38351
|
-
* `xml:space="preserve"` is load-bearing, not tidiness. SVG `<text>` collapses
|
|
38352
|
-
* runs of whitespace by default, and a terminal's entire column alignment IS
|
|
38353
|
-
* runs of whitespace — Glances pads every field with spaces. Without it the
|
|
38354
|
-
* frame drew each line at roughly half its true width, crammed into the
|
|
38355
|
-
* top-left of a mostly-black image, while the SAME session over `attach`
|
|
38356
|
-
* looked perfect — which is exactly how the operator reported it. Measured in
|
|
38357
|
-
* the hub's own rasterizer 2026-08-11: `AAA<10 spaces>BBB<3>CCC` drew 92 px
|
|
38358
|
-
* collapsed against 178 px preserved.
|
|
38359
|
-
*
|
|
38360
|
-
* Every run is anchored at its OWN column (`x = margin + startCol * cellW`),
|
|
38361
|
-
* never appended to the one before it, so the background rects and the glyphs
|
|
38362
|
-
* are placed off the same grid and cannot drift apart. `textLength` is emitted
|
|
38363
|
-
* with it because it is the correct declaration and renderers that honour it
|
|
38364
|
-
* get an exact grid — but it is not what makes this work: librsvg, which sharp
|
|
38365
|
-
* uses, ignores it outright (measured 2026-08-12, a 120-glyph run pinned to
|
|
38366
|
-
* 600 px still drew its natural 937 px). The anchoring is the guarantee.
|
|
38367
|
-
*/
|
|
38368
|
-
function renderTerminalSvg(rows) {
|
|
38369
|
-
const backgrounds = [];
|
|
38370
|
-
const texts = [];
|
|
38371
|
-
rows.slice(0, 40).forEach((row, index) => {
|
|
38372
|
-
const baseline = TERMINAL_BASELINE_Y + index * TERMINAL_ROW_HEIGHT;
|
|
38373
|
-
const top = baseline - TERMINAL_CELL_ASCENT;
|
|
38374
|
-
let column = 0;
|
|
38375
|
-
for (const run of row) {
|
|
38376
|
-
if (column >= 120) break;
|
|
38377
|
-
const clipped = clipRun(run, 120 - column);
|
|
38378
|
-
const columns = [...clipped].length;
|
|
38379
|
-
if (columns === 0) continue;
|
|
38380
|
-
const x = TERMINAL_TEXT_MARGIN_X + column * TERMINAL_CELL_WIDTH;
|
|
38381
|
-
const width = columns * TERMINAL_CELL_WIDTH;
|
|
38382
|
-
if (run.bg !== null) backgrounds.push(`<rect x="${coordinate(x)}" y="${coordinate(top)}" width="${coordinate(width)}" height="${String(TERMINAL_ROW_HEIGHT)}" fill="${run.bg}"/>`);
|
|
38383
|
-
if (clipped.trim() !== "") {
|
|
38384
|
-
const fill = run.fg === null ? "" : ` fill="${run.fg}"`;
|
|
38385
|
-
const weight = run.bold ? " font-weight=\"bold\"" : "";
|
|
38386
|
-
texts.push(`<text xml:space="preserve" x="${coordinate(x)}" y="${coordinate(baseline)}" textLength="${coordinate(width)}" lengthAdjust="spacingAndGlyphs"${fill}${weight}>${escapeXml(clipped)}</text>`);
|
|
38387
|
-
}
|
|
38388
|
-
column += columns;
|
|
38389
|
-
}
|
|
38390
|
-
});
|
|
38391
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${String(960)}" height="${String(640)}"><rect width="100%" height="100%" fill="${TERMINAL_DEFAULT_BG}"/>${backgrounds.join("")}<g fill="${TERMINAL_DEFAULT_FG}" font-family="${TERMINAL_FONT_STACK}" font-size="${String(TERMINAL_FONT_SIZE)}">${texts.join("")}</g></svg>`;
|
|
38392
|
-
}
|
|
38393
|
-
/** Cut a run to the columns still left in the row, by code point not unit. */
|
|
38394
|
-
function clipRun(run, remaining) {
|
|
38395
|
-
const points = [...run.text];
|
|
38396
|
-
return points.length <= remaining ? run.text : points.slice(0, remaining).join("");
|
|
38397
|
-
}
|
|
38398
|
-
async function renderTerminalJpeg(rows) {
|
|
38399
|
-
return sharp(Buffer.from(renderTerminalSvg(rows))).jpeg({
|
|
38400
|
-
quality: 82,
|
|
38401
|
-
chromaSubsampling: "4:2:0"
|
|
38402
|
-
}).toBuffer();
|
|
38403
|
-
}
|
|
38404
|
-
//#endregion
|
|
38405
|
-
//#region src/terminal-camera-device.ts
|
|
38406
|
-
var terminalCameraSchema = object({
|
|
38407
|
-
instanceId: string().min(1).optional(),
|
|
38408
|
-
nodeId: string().min(1),
|
|
38409
|
-
profileId: string().min(1).default("monitor"),
|
|
38410
|
-
profileLabel: string().min(1).default("BTM")
|
|
38411
|
-
});
|
|
38412
|
-
var relay = null;
|
|
38413
|
-
function installTerminalCameraRelay(next) {
|
|
38414
|
-
relay = next;
|
|
38415
|
-
}
|
|
38416
|
-
var TerminalCameraDevice = class extends BaseDevice {
|
|
38417
|
-
features = [DeviceFeature.NativeSnapshot];
|
|
38418
|
-
constructor(ctx) {
|
|
38419
|
-
super(ctx, terminalCameraSchema, { type: ctx.deviceMeta.type });
|
|
38420
|
-
this.ctx.registerNativeCap(streamCatalogCapability, { getCatalog: async ({ deviceId }) => {
|
|
38421
|
-
if (deviceId !== this.id) return [];
|
|
38422
|
-
return this.catalog();
|
|
38423
|
-
} });
|
|
38424
|
-
this.ctx.registerNativeCap(snapshotCapability, {
|
|
38425
|
-
getSnapshot: async ({ deviceId }) => {
|
|
38426
|
-
if (deviceId !== this.id) throw new Error(`TerminalCameraDevice: deviceId mismatch, expected ${String(this.id)}, got ${String(deviceId)}`);
|
|
38427
|
-
const activeRelay = relay;
|
|
38428
|
-
if (!activeRelay) throw new Error("terminal camera relay is unavailable");
|
|
38429
|
-
return {
|
|
38430
|
-
base64: (await activeRelay.snapshot(this.relayInstanceId(), this.config.get("nodeId"), this.config.get("profileId"))).toString("base64"),
|
|
38431
|
-
contentType: "image/jpeg"
|
|
38432
|
-
};
|
|
38433
|
-
},
|
|
38434
|
-
invalidateCache: async () => {}
|
|
38435
|
-
});
|
|
38436
|
-
this.markOnline(true);
|
|
38437
|
-
}
|
|
38438
|
-
async catalog() {
|
|
38439
|
-
const activeRelay = relay;
|
|
38440
|
-
if (!activeRelay) throw new Error("terminal camera relay is unavailable");
|
|
38441
|
-
const nodeId = this.config.get("nodeId");
|
|
38442
|
-
const profileId = this.config.get("profileId");
|
|
38443
|
-
const instanceId = this.relayInstanceId();
|
|
38444
|
-
return [{
|
|
38445
|
-
camStreamId: profileId,
|
|
38446
|
-
kind: "pull-http",
|
|
38447
|
-
url: activeRelay.streamUrl(instanceId, nodeId, profileId),
|
|
38448
|
-
codec: "h264",
|
|
38449
|
-
resolution: {
|
|
38450
|
-
width: 960,
|
|
38451
|
-
height: 640
|
|
38452
|
-
},
|
|
38453
|
-
fps: 2,
|
|
38454
|
-
label: this.config.get("profileLabel")
|
|
38455
|
-
}];
|
|
38456
|
-
}
|
|
38457
|
-
setNodeOnline(online) {
|
|
38458
|
-
this.markOnline(online);
|
|
38459
|
-
if (!online) relay?.closeInstance(this.relayInstanceId());
|
|
38460
|
-
}
|
|
38461
|
-
async removeDevice() {
|
|
38462
|
-
await relay?.closeInstance(this.relayInstanceId());
|
|
38463
|
-
}
|
|
38464
|
-
relayInstanceId() {
|
|
38465
|
-
return this.config.get("instanceId") ?? `legacy:${this.stableId}`;
|
|
38466
|
-
}
|
|
38467
|
-
};
|
|
38468
|
-
//#endregion
|
|
38469
38374
|
//#region ../../node_modules/@xterm/addon-serialize/lib/addon-serialize.js
|
|
38470
38375
|
var require_addon_serialize = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
38471
38376
|
(function(e, t) {
|
|
@@ -43273,11 +43178,176 @@ var require_xterm_headless = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
43273
43178
|
})();
|
|
43274
43179
|
}));
|
|
43275
43180
|
//#endregion
|
|
43276
|
-
//#region src/
|
|
43181
|
+
//#region src/terminal-cell-runs.ts
|
|
43277
43182
|
var import_addon_serialize = require_addon_serialize();
|
|
43278
43183
|
var import_xterm_headless = require_xterm_headless();
|
|
43184
|
+
var TERMINAL_DEFAULT_FG = "#d7dce2";
|
|
43185
|
+
var TERMINAL_DEFAULT_BG = "#0b0d10";
|
|
43186
|
+
/**
|
|
43187
|
+
* The 16 ANSI colours, in a dark-theme variant chosen to sit with the existing
|
|
43188
|
+
* frame (`#0b0d10` ground, `#d7dce2` text) rather than the raw xterm defaults,
|
|
43189
|
+
* whose pure `#0000ff` blue and `#008000` green are close to unreadable on a
|
|
43190
|
+
* near-black background at 13px. Index 7 IS the default foreground, so plain
|
|
43191
|
+
* `CSI 37m` text renders identically to unstyled text.
|
|
43192
|
+
*/
|
|
43193
|
+
var TERMINAL_ANSI_PALETTE = [
|
|
43194
|
+
"#282c34",
|
|
43195
|
+
"#e06c75",
|
|
43196
|
+
"#98c379",
|
|
43197
|
+
"#e5c07b",
|
|
43198
|
+
"#61afef",
|
|
43199
|
+
"#c678dd",
|
|
43200
|
+
"#56b6c2",
|
|
43201
|
+
TERMINAL_DEFAULT_FG,
|
|
43202
|
+
"#5c6370",
|
|
43203
|
+
"#ef596f",
|
|
43204
|
+
"#89ca78",
|
|
43205
|
+
"#f0c674",
|
|
43206
|
+
"#6cb6ff",
|
|
43207
|
+
"#d55fde",
|
|
43208
|
+
"#2bbac5",
|
|
43209
|
+
"#ffffff"
|
|
43210
|
+
];
|
|
43211
|
+
/** The six levels of the xterm 6×6×6 colour cube (indices 16-231). */
|
|
43212
|
+
var TERMINAL_CUBE_LEVELS = [
|
|
43213
|
+
0,
|
|
43214
|
+
95,
|
|
43215
|
+
135,
|
|
43216
|
+
175,
|
|
43217
|
+
215,
|
|
43218
|
+
255
|
|
43219
|
+
];
|
|
43220
|
+
var TERMINAL_CUBE_FIRST = 16;
|
|
43221
|
+
var TERMINAL_GRAYSCALE_FIRST = 232;
|
|
43222
|
+
var TERMINAL_GRAYSCALE_BASE = 8;
|
|
43223
|
+
var TERMINAL_GRAYSCALE_STEP = 10;
|
|
43224
|
+
/** SGR 2 keeps the foreground legible; it must not become the background. */
|
|
43225
|
+
var TERMINAL_DIM_WEIGHT = .6;
|
|
43226
|
+
function channel(value) {
|
|
43227
|
+
return Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, "0");
|
|
43228
|
+
}
|
|
43229
|
+
function hex(red, green, blue) {
|
|
43230
|
+
return `#${channel(red)}${channel(green)}${channel(blue)}`;
|
|
43231
|
+
}
|
|
43232
|
+
function parseHex(color) {
|
|
43233
|
+
return [
|
|
43234
|
+
Number.parseInt(color.slice(1, 3), 16),
|
|
43235
|
+
Number.parseInt(color.slice(3, 5), 16),
|
|
43236
|
+
Number.parseInt(color.slice(5, 7), 16)
|
|
43237
|
+
];
|
|
43238
|
+
}
|
|
43239
|
+
/** Resolve an xterm palette index (0-255) to a hex colour. */
|
|
43240
|
+
function terminalPaletteColor(index) {
|
|
43241
|
+
const ansi = TERMINAL_ANSI_PALETTE[index];
|
|
43242
|
+
if (ansi !== void 0) return ansi;
|
|
43243
|
+
if (index >= TERMINAL_GRAYSCALE_FIRST) {
|
|
43244
|
+
const level = TERMINAL_GRAYSCALE_BASE + (index - TERMINAL_GRAYSCALE_FIRST) * TERMINAL_GRAYSCALE_STEP;
|
|
43245
|
+
return hex(level, level, level);
|
|
43246
|
+
}
|
|
43247
|
+
if (index >= TERMINAL_CUBE_FIRST) {
|
|
43248
|
+
const offset = index - TERMINAL_CUBE_FIRST;
|
|
43249
|
+
return hex(TERMINAL_CUBE_LEVELS[Math.floor(offset / 36) % 6] ?? 0, TERMINAL_CUBE_LEVELS[Math.floor(offset / 6) % 6] ?? 0, TERMINAL_CUBE_LEVELS[offset % 6] ?? 0);
|
|
43250
|
+
}
|
|
43251
|
+
return TERMINAL_DEFAULT_FG;
|
|
43252
|
+
}
|
|
43253
|
+
/** Resolve a 0xRRGGBB truecolor value to a hex colour. */
|
|
43254
|
+
function terminalRgbColor(value) {
|
|
43255
|
+
return hex(value >> 16 & 255, value >> 8 & 255, value & 255);
|
|
43256
|
+
}
|
|
43257
|
+
function blend(color, toward, weight) {
|
|
43258
|
+
const [red, green, blue] = parseHex(color);
|
|
43259
|
+
const [targetRed, targetGreen, targetBlue] = parseHex(toward);
|
|
43260
|
+
return hex(red * weight + targetRed * (1 - weight), green * weight + targetGreen * (1 - weight), blue * weight + targetBlue * (1 - weight));
|
|
43261
|
+
}
|
|
43262
|
+
function resolveForeground(cell) {
|
|
43263
|
+
if (cell.isFgRGB()) return terminalRgbColor(cell.getFgColor());
|
|
43264
|
+
if (cell.isFgPalette()) return terminalPaletteColor(cell.getFgColor());
|
|
43265
|
+
return TERMINAL_DEFAULT_FG;
|
|
43266
|
+
}
|
|
43267
|
+
function resolveBackground(cell) {
|
|
43268
|
+
if (cell.isBgRGB()) return terminalRgbColor(cell.getBgColor());
|
|
43269
|
+
if (cell.isBgPalette()) return terminalPaletteColor(cell.getBgColor());
|
|
43270
|
+
return TERMINAL_DEFAULT_BG;
|
|
43271
|
+
}
|
|
43272
|
+
/**
|
|
43273
|
+
* Resolve one cell's attributes into concrete colours.
|
|
43274
|
+
*
|
|
43275
|
+
* Inverse is applied by SWAPPING the already-resolved pair, so inverse over two
|
|
43276
|
+
* defaults is still a visible swap rather than a no-op — that is how a selected
|
|
43277
|
+
* or highlighted row in Glances reads. Invisible is then conceal-by-equality
|
|
43278
|
+
* (foreground painted in its own background): the cell keeps its columns, which
|
|
43279
|
+
* a dropped cell would not, and dropping it would shift the whole rest of the
|
|
43280
|
+
* row left.
|
|
43281
|
+
*/
|
|
43282
|
+
function resolveCellStyle(cell) {
|
|
43283
|
+
const inverse = cell.isInverse() !== 0;
|
|
43284
|
+
const plainFg = resolveForeground(cell);
|
|
43285
|
+
const plainBg = resolveBackground(cell);
|
|
43286
|
+
const background = inverse ? plainFg : plainBg;
|
|
43287
|
+
let foreground = inverse ? plainBg : plainFg;
|
|
43288
|
+
if (cell.isDim() !== 0) foreground = blend(foreground, background, TERMINAL_DIM_WEIGHT);
|
|
43289
|
+
if (cell.isInvisible() !== 0) foreground = background;
|
|
43290
|
+
return {
|
|
43291
|
+
fg: foreground === "#d7dce2" ? null : foreground,
|
|
43292
|
+
bg: background === "#0b0d10" ? null : background,
|
|
43293
|
+
bold: cell.isBold() !== 0
|
|
43294
|
+
};
|
|
43295
|
+
}
|
|
43296
|
+
function sameStyle(left, right) {
|
|
43297
|
+
return left.fg === right.fg && left.bg === right.bg && left.bold === right.bold;
|
|
43298
|
+
}
|
|
43299
|
+
/**
|
|
43300
|
+
* Merge adjacent same-style cells into runs, then drop the trailing run of
|
|
43301
|
+
* default-styled whitespace so a row costs what it draws — the same trim
|
|
43302
|
+
* `translateToString(true)` performs. Whitespace carrying a BACKGROUND is kept:
|
|
43303
|
+
* a green bar of spaces out to the right margin is a pixel Glances drew.
|
|
43304
|
+
*/
|
|
43305
|
+
function buildCellRuns(cells) {
|
|
43306
|
+
const runs = [];
|
|
43307
|
+
let text = "";
|
|
43308
|
+
let style = null;
|
|
43309
|
+
for (const cell of cells) {
|
|
43310
|
+
if (style !== null && sameStyle(style, cell.style)) {
|
|
43311
|
+
text += cell.text;
|
|
43312
|
+
continue;
|
|
43313
|
+
}
|
|
43314
|
+
if (style !== null) runs.push({
|
|
43315
|
+
text,
|
|
43316
|
+
...style
|
|
43317
|
+
});
|
|
43318
|
+
text = cell.text;
|
|
43319
|
+
style = cell.style;
|
|
43320
|
+
}
|
|
43321
|
+
if (style !== null) runs.push({
|
|
43322
|
+
text,
|
|
43323
|
+
...style
|
|
43324
|
+
});
|
|
43325
|
+
while (runs.length > 0) {
|
|
43326
|
+
const last = runs[runs.length - 1];
|
|
43327
|
+
if (last === void 0 || last.bg !== null) break;
|
|
43328
|
+
const trimmed = last.text.replace(/\s+$/u, "");
|
|
43329
|
+
if (trimmed === last.text) break;
|
|
43330
|
+
if (trimmed === "") {
|
|
43331
|
+
runs.pop();
|
|
43332
|
+
continue;
|
|
43333
|
+
}
|
|
43334
|
+
runs[runs.length - 1] = {
|
|
43335
|
+
...last,
|
|
43336
|
+
text: trimmed
|
|
43337
|
+
};
|
|
43338
|
+
break;
|
|
43339
|
+
}
|
|
43340
|
+
return runs;
|
|
43341
|
+
}
|
|
43342
|
+
//#endregion
|
|
43343
|
+
//#region src/xterm-screen.ts
|
|
43344
|
+
/**
|
|
43345
|
+
* Real {@link ScreenBuffer} backed by `@xterm/headless` — the DOM-less xterm
|
|
43346
|
+
* build — plus the serialize addon, which turns the current buffer into a
|
|
43347
|
+
* self-contained repaint escape sequence for reconnecting clients.
|
|
43348
|
+
*/
|
|
43279
43349
|
var SCROLLBACK_LINES = 2e3;
|
|
43280
|
-
function createXtermScreen
|
|
43350
|
+
function createXtermScreen(cols, rows) {
|
|
43281
43351
|
const term = new import_xterm_headless.Terminal({
|
|
43282
43352
|
cols,
|
|
43283
43353
|
rows,
|
|
@@ -43342,6 +43412,196 @@ function createXtermScreen$1(cols, rows) {
|
|
|
43342
43412
|
};
|
|
43343
43413
|
}
|
|
43344
43414
|
//#endregion
|
|
43415
|
+
//#region src/terminal-camera-declarations.ts
|
|
43416
|
+
/**
|
|
43417
|
+
* Feed DeclaredDevices every live declaration plus one deterministic orphan
|
|
43418
|
+
* batch. The generic sweep intentionally refuses an over-limit set; selecting
|
|
43419
|
+
* a batch here drains large historical Terminal orphan sets across convergence
|
|
43420
|
+
* passes without weakening that global safety guard.
|
|
43421
|
+
*/
|
|
43422
|
+
function terminalCameraReconciliationIndex(rows, integrationId, declarations, managedStableIds = /* @__PURE__ */ new Set()) {
|
|
43423
|
+
if (!integrationId) return [];
|
|
43424
|
+
const declared = new Set(declarations.map((camera) => camera.stableId));
|
|
43425
|
+
const owned = rows.filter((row) => row.integrationId === integrationId && (declared.has(row.stableId) || managedStableIds.has(row.stableId) || row.stableId.startsWith("terminal-camera-instance-")));
|
|
43426
|
+
return [...owned.filter((row) => declared.has(row.stableId)), ...owned.filter((row) => !declared.has(row.stableId)).sort((left, right) => left.id - right.id).slice(0, 32)];
|
|
43427
|
+
}
|
|
43428
|
+
/** Explicit persisted instances, never the node × profile template matrix. */
|
|
43429
|
+
function buildTerminalInstanceCameraDeclarations(instances) {
|
|
43430
|
+
return instances.filter((instance) => instance.enabled).map((instance) => ({
|
|
43431
|
+
stableId: instance.cameraStableId,
|
|
43432
|
+
name: instance.name,
|
|
43433
|
+
config: {
|
|
43434
|
+
instanceId: instance.id,
|
|
43435
|
+
nodeId: instance.nodeId,
|
|
43436
|
+
profileId: instance.profileId,
|
|
43437
|
+
profileLabel: instance.profileLabel
|
|
43438
|
+
}
|
|
43439
|
+
}));
|
|
43440
|
+
}
|
|
43441
|
+
/**
|
|
43442
|
+
* `DeviceConfig` materializes schema defaults in memory, so comparing
|
|
43443
|
+
* `config.get()` cannot detect a legacy `{ nodeId }` row. Reconciliation must
|
|
43444
|
+
* inspect the raw persisted blob to make the profile migration durable.
|
|
43445
|
+
*/
|
|
43446
|
+
function needsTerminalCameraConfigMigration(persistedConfig, declaration) {
|
|
43447
|
+
return persistedConfig.instanceId !== declaration.config.instanceId || persistedConfig.nodeId !== declaration.config.nodeId || persistedConfig.profileId !== declaration.config.profileId || persistedConfig.profileLabel !== declaration.config.profileLabel;
|
|
43448
|
+
}
|
|
43449
|
+
/**
|
|
43450
|
+
* Monospace families to try, in order — NOT one family and a generic.
|
|
43451
|
+
*
|
|
43452
|
+
* A terminal screen is mostly box-drawing and block characters, and a font
|
|
43453
|
+
* without them renders the frame as noise rather than as missing detail.
|
|
43454
|
+
* `DejaVu Sans Mono` covers them and ships in the hub image; macOS has neither
|
|
43455
|
+
* DejaVu nor fontconfig, so the Mac agent fell through to a generic whose
|
|
43456
|
+
* coverage is not, and its Glances camera came out unreadable while the hub's
|
|
43457
|
+
* was pixel-perfect (measured 2026-08-11, same Glances, two nodes).
|
|
43458
|
+
*
|
|
43459
|
+
* `Menlo` is the fix rather than a guess: it is macOS's default terminal face,
|
|
43460
|
+
* present on every install, and derived from DejaVu Sans Mono — the same glyph
|
|
43461
|
+
* coverage by ancestry. Monaco and Courier New follow as older fallbacks; the
|
|
43462
|
+
* generic stays last so a host with none of them still draws something.
|
|
43463
|
+
*/
|
|
43464
|
+
var TERMINAL_FONT_STACK = "DejaVu Sans Mono,Menlo,Monaco,Courier New,monospace";
|
|
43465
|
+
var TERMINAL_FONT_SIZE = 13;
|
|
43466
|
+
var TERMINAL_TEXT_MARGIN_X = 8;
|
|
43467
|
+
var TERMINAL_ROW_HEIGHT = 15;
|
|
43468
|
+
var TERMINAL_BASELINE_Y = 18;
|
|
43469
|
+
/**
|
|
43470
|
+
* Distance from a row's baseline up to the top of its cell box. Chosen so
|
|
43471
|
+
* consecutive rows tile exactly: row N's box runs from `baseline - this` for
|
|
43472
|
+
* `TERMINAL_ROW_HEIGHT`, and row N+1's box starts where it ends. A background
|
|
43473
|
+
* bar that stopped short would draw as stripes across a `CSI 42m` panel.
|
|
43474
|
+
*/
|
|
43475
|
+
var TERMINAL_CELL_ASCENT = 11.5;
|
|
43476
|
+
var TERMINAL_CELL_WIDTH = TERMINAL_FONT_SIZE * (1233 / 2048);
|
|
43477
|
+
/** Two decimals is under a tenth of a pixel and keeps the SVG small. */
|
|
43478
|
+
function coordinate(value) {
|
|
43479
|
+
return String(Number(value.toFixed(2)));
|
|
43480
|
+
}
|
|
43481
|
+
function escapeXml(value) {
|
|
43482
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
43483
|
+
}
|
|
43484
|
+
/**
|
|
43485
|
+
* Render already-interpreted terminal rows into a compact MJPEG frame.
|
|
43486
|
+
*
|
|
43487
|
+
* `xml:space="preserve"` is load-bearing, not tidiness. SVG `<text>` collapses
|
|
43488
|
+
* runs of whitespace by default, and a terminal's entire column alignment IS
|
|
43489
|
+
* runs of whitespace — Glances pads every field with spaces. Without it the
|
|
43490
|
+
* frame drew each line at roughly half its true width, crammed into the
|
|
43491
|
+
* top-left of a mostly-black image, while the SAME session over `attach`
|
|
43492
|
+
* looked perfect — which is exactly how the operator reported it. Measured in
|
|
43493
|
+
* the hub's own rasterizer 2026-08-11: `AAA<10 spaces>BBB<3>CCC` drew 92 px
|
|
43494
|
+
* collapsed against 178 px preserved.
|
|
43495
|
+
*
|
|
43496
|
+
* Every run is anchored at its OWN column (`x = margin + startCol * cellW`),
|
|
43497
|
+
* never appended to the one before it, so the background rects and the glyphs
|
|
43498
|
+
* are placed off the same grid and cannot drift apart. `textLength` is emitted
|
|
43499
|
+
* with it because it is the correct declaration and renderers that honour it
|
|
43500
|
+
* get an exact grid — but it is not what makes this work: librsvg, which sharp
|
|
43501
|
+
* uses, ignores it outright (measured 2026-08-12, a 120-glyph run pinned to
|
|
43502
|
+
* 600 px still drew its natural 937 px). The anchoring is the guarantee.
|
|
43503
|
+
*/
|
|
43504
|
+
function renderTerminalSvg(rows) {
|
|
43505
|
+
const backgrounds = [];
|
|
43506
|
+
const texts = [];
|
|
43507
|
+
rows.slice(0, 40).forEach((row, index) => {
|
|
43508
|
+
const baseline = TERMINAL_BASELINE_Y + index * TERMINAL_ROW_HEIGHT;
|
|
43509
|
+
const top = baseline - TERMINAL_CELL_ASCENT;
|
|
43510
|
+
let column = 0;
|
|
43511
|
+
for (const run of row) {
|
|
43512
|
+
if (column >= 120) break;
|
|
43513
|
+
const clipped = clipRun(run, 120 - column);
|
|
43514
|
+
const columns = [...clipped].length;
|
|
43515
|
+
if (columns === 0) continue;
|
|
43516
|
+
const x = TERMINAL_TEXT_MARGIN_X + column * TERMINAL_CELL_WIDTH;
|
|
43517
|
+
const width = columns * TERMINAL_CELL_WIDTH;
|
|
43518
|
+
if (run.bg !== null) backgrounds.push(`<rect x="${coordinate(x)}" y="${coordinate(top)}" width="${coordinate(width)}" height="${String(TERMINAL_ROW_HEIGHT)}" fill="${run.bg}"/>`);
|
|
43519
|
+
if (clipped.trim() !== "") {
|
|
43520
|
+
const fill = run.fg === null ? "" : ` fill="${run.fg}"`;
|
|
43521
|
+
const weight = run.bold ? " font-weight=\"bold\"" : "";
|
|
43522
|
+
texts.push(`<text xml:space="preserve" x="${coordinate(x)}" y="${coordinate(baseline)}" textLength="${coordinate(width)}" lengthAdjust="spacingAndGlyphs"${fill}${weight}>${escapeXml(clipped)}</text>`);
|
|
43523
|
+
}
|
|
43524
|
+
column += columns;
|
|
43525
|
+
}
|
|
43526
|
+
});
|
|
43527
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${String(960)}" height="${String(640)}"><rect width="100%" height="100%" fill="${TERMINAL_DEFAULT_BG}"/>${backgrounds.join("")}<g fill="${TERMINAL_DEFAULT_FG}" font-family="${TERMINAL_FONT_STACK}" font-size="${String(TERMINAL_FONT_SIZE)}">${texts.join("")}</g></svg>`;
|
|
43528
|
+
}
|
|
43529
|
+
/** Cut a run to the columns still left in the row, by code point not unit. */
|
|
43530
|
+
function clipRun(run, remaining) {
|
|
43531
|
+
const points = [...run.text];
|
|
43532
|
+
return points.length <= remaining ? run.text : points.slice(0, remaining).join("");
|
|
43533
|
+
}
|
|
43534
|
+
async function renderTerminalJpeg(rows) {
|
|
43535
|
+
return sharp(Buffer.from(renderTerminalSvg(rows))).jpeg({
|
|
43536
|
+
quality: 82,
|
|
43537
|
+
chromaSubsampling: "4:2:0"
|
|
43538
|
+
}).toBuffer();
|
|
43539
|
+
}
|
|
43540
|
+
//#endregion
|
|
43541
|
+
//#region src/terminal-camera-device.ts
|
|
43542
|
+
var terminalCameraSchema = object({
|
|
43543
|
+
instanceId: string().min(1).optional(),
|
|
43544
|
+
nodeId: string().min(1),
|
|
43545
|
+
profileId: string().min(1).default("monitor"),
|
|
43546
|
+
profileLabel: string().min(1).default("BTM")
|
|
43547
|
+
});
|
|
43548
|
+
var relay = null;
|
|
43549
|
+
function installTerminalCameraRelay(next) {
|
|
43550
|
+
relay = next;
|
|
43551
|
+
}
|
|
43552
|
+
var TerminalCameraDevice = class extends BaseDevice {
|
|
43553
|
+
features = [DeviceFeature.NativeSnapshot];
|
|
43554
|
+
constructor(ctx) {
|
|
43555
|
+
super(ctx, terminalCameraSchema, { type: ctx.deviceMeta.type });
|
|
43556
|
+
this.ctx.registerNativeCap(streamCatalogCapability, { getCatalog: async ({ deviceId }) => {
|
|
43557
|
+
if (deviceId !== this.id) return [];
|
|
43558
|
+
return this.catalog();
|
|
43559
|
+
} });
|
|
43560
|
+
this.ctx.registerNativeCap(snapshotCapability, {
|
|
43561
|
+
getSnapshot: async ({ deviceId }) => {
|
|
43562
|
+
if (deviceId !== this.id) throw new Error(`TerminalCameraDevice: deviceId mismatch, expected ${String(this.id)}, got ${String(deviceId)}`);
|
|
43563
|
+
const activeRelay = relay;
|
|
43564
|
+
if (!activeRelay) throw new Error("terminal camera relay is unavailable");
|
|
43565
|
+
return {
|
|
43566
|
+
base64: (await activeRelay.snapshot(this.relayInstanceId(), this.config.get("nodeId"), this.config.get("profileId"))).toString("base64"),
|
|
43567
|
+
contentType: "image/jpeg"
|
|
43568
|
+
};
|
|
43569
|
+
},
|
|
43570
|
+
invalidateCache: async () => {}
|
|
43571
|
+
});
|
|
43572
|
+
this.markOnline(true);
|
|
43573
|
+
}
|
|
43574
|
+
async catalog() {
|
|
43575
|
+
const activeRelay = relay;
|
|
43576
|
+
if (!activeRelay) throw new Error("terminal camera relay is unavailable");
|
|
43577
|
+
const nodeId = this.config.get("nodeId");
|
|
43578
|
+
const profileId = this.config.get("profileId");
|
|
43579
|
+
const instanceId = this.relayInstanceId();
|
|
43580
|
+
return [{
|
|
43581
|
+
camStreamId: profileId,
|
|
43582
|
+
kind: "pull-http",
|
|
43583
|
+
url: activeRelay.streamUrl(instanceId, nodeId, profileId),
|
|
43584
|
+
codec: "h264",
|
|
43585
|
+
resolution: {
|
|
43586
|
+
width: 960,
|
|
43587
|
+
height: 640
|
|
43588
|
+
},
|
|
43589
|
+
fps: 2,
|
|
43590
|
+
label: this.config.get("profileLabel")
|
|
43591
|
+
}];
|
|
43592
|
+
}
|
|
43593
|
+
setNodeOnline(online) {
|
|
43594
|
+
this.markOnline(online);
|
|
43595
|
+
if (!online) relay?.closeInstance(this.relayInstanceId());
|
|
43596
|
+
}
|
|
43597
|
+
async removeDevice() {
|
|
43598
|
+
await relay?.closeInstance(this.relayInstanceId());
|
|
43599
|
+
}
|
|
43600
|
+
relayInstanceId() {
|
|
43601
|
+
return this.config.get("instanceId") ?? `legacy:${this.stableId}`;
|
|
43602
|
+
}
|
|
43603
|
+
};
|
|
43604
|
+
//#endregion
|
|
43345
43605
|
//#region src/terminal-camera-relay.ts
|
|
43346
43606
|
var MJPEG_BOUNDARY = "camstack-terminal-frame";
|
|
43347
43607
|
var SESSION_IDLE_MS = 3e4;
|
|
@@ -43421,7 +43681,7 @@ var TerminalCameraRelay = class {
|
|
|
43421
43681
|
instanceId,
|
|
43422
43682
|
nodeId,
|
|
43423
43683
|
profileId,
|
|
43424
|
-
screen: createXtermScreen
|
|
43684
|
+
screen: createXtermScreen(120, 40),
|
|
43425
43685
|
sessionId: null,
|
|
43426
43686
|
cursor: 0,
|
|
43427
43687
|
clients: 0,
|
|
@@ -43478,7 +43738,7 @@ var TerminalCameraRelay = class {
|
|
|
43478
43738
|
applyBatch(state, batch) {
|
|
43479
43739
|
if (batch.reset) {
|
|
43480
43740
|
state.screen.dispose();
|
|
43481
|
-
state.screen = createXtermScreen
|
|
43741
|
+
state.screen = createXtermScreen(120, 40);
|
|
43482
43742
|
if (batch.snapshot) state.screen.write(batch.snapshot);
|
|
43483
43743
|
}
|
|
43484
43744
|
let exited = false;
|
|
@@ -45236,4 +45496,4 @@ var TerminalAddon = class extends BaseAddon {
|
|
|
45236
45496
|
}
|
|
45237
45497
|
};
|
|
45238
45498
|
//#endregion
|
|
45239
|
-
export { TerminalAddon, createXtermScreen
|
|
45499
|
+
export { TerminalAddon, createXtermScreen as a, buildCellRuns as c, terminalRgbColor as d, createNodePtySpawner as f, createTerminalDataPlaneHandler as i, resolveCellStyle as l, buildProfiles as n, TERMINAL_DEFAULT_BG as o, warmNodePty as p, findProfile as r, TERMINAL_DEFAULT_FG as s, TerminalSessionManager as t, terminalPaletteColor as u };
|