@formant/formant-cli 0.4.2 → 0.4.3
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/README.md +50 -27
- package/dist/commands/device/list.d.ts +7 -1
- package/dist/commands/device/list.js +64 -10
- package/dist/commands/device/list.js.map +1 -1
- package/dist/commands/device/streams.d.ts +1 -0
- package/dist/commands/device/streams.js +78 -23
- package/dist/commands/device/streams.js.map +1 -1
- package/dist/lib/presence.d.ts +46 -0
- package/dist/lib/presence.js +95 -0
- package/dist/lib/presence.js.map +1 -0
- package/oclif.manifest.json +978 -1013
- package/package.json +1 -1
- package/dist/commands/device/last-seen.d.ts +0 -10
- package/dist/commands/device/last-seen.js +0 -25
- package/dist/commands/device/last-seen.js.map +0 -1
package/README.md
CHANGED
|
@@ -33,7 +33,9 @@ This CLI provides programmatic access to all Formant capabilities:
|
|
|
33
33
|
- List, create, rename, and delete robots/sensors
|
|
34
34
|
- Add tags for organization (environment, location, customer)
|
|
35
35
|
- View real-time device status and configuration
|
|
36
|
-
- List available telemetry streams
|
|
36
|
+
- List available telemetry streams with per-stream data presence (datapoint counts, last seen, freshness)
|
|
37
|
+
- Data-based last seen timestamps and datapoint counts on device listings
|
|
38
|
+
- Filter to devices that have ingested data (`--with-data`)
|
|
37
39
|
|
|
38
40
|
### Telemetry Queries
|
|
39
41
|
- Query historical sensor data (battery, temperature, position, custom metrics)
|
|
@@ -171,8 +173,8 @@ The CLI will automatically load credentials from your `.env` file.
|
|
|
171
173
|
# View your organization
|
|
172
174
|
fcli org get
|
|
173
175
|
|
|
174
|
-
# List
|
|
175
|
-
fcli device list --dev
|
|
176
|
+
# List devices with data
|
|
177
|
+
fcli device list --with-data --dev
|
|
176
178
|
|
|
177
179
|
# Get device details
|
|
178
180
|
fcli device get <device-id> --dev
|
|
@@ -214,15 +216,20 @@ Create, list, and manage robots/sensors in your fleet.
|
|
|
214
216
|
|
|
215
217
|
```bash
|
|
216
218
|
# Listing and filtering
|
|
217
|
-
fcli device list #
|
|
218
|
-
fcli device list --
|
|
219
|
+
fcli device list # Online devices (default)
|
|
220
|
+
fcli device list --include-offline # Include offline devices
|
|
221
|
+
fcli device list --with-data # Only devices with ingested data
|
|
222
|
+
fcli device list --with-data --days 90 # Widen the search window (default: 30d)
|
|
219
223
|
fcli device list --tag location=warehouse # Filter by tag
|
|
220
224
|
|
|
221
225
|
# Device details
|
|
222
226
|
fcli device get <device-id> # Get full device details
|
|
223
227
|
fcli device config <device-id> # Get device configuration
|
|
224
|
-
|
|
225
|
-
|
|
228
|
+
|
|
229
|
+
# Stream discovery with data presence
|
|
230
|
+
fcli device streams <device-id> # List streams with datapoint counts,
|
|
231
|
+
# last seen (ISO), and freshness
|
|
232
|
+
fcli device streams <device-id> --days 30 # Adjust presence lookback (default: 7d)
|
|
226
233
|
|
|
227
234
|
# Device management
|
|
228
235
|
fcli device create --name "robot-001" # Create a new device
|
|
@@ -454,14 +461,29 @@ These flags work with any command:
|
|
|
454
461
|
Human-readable tables optimized for terminal viewing:
|
|
455
462
|
|
|
456
463
|
```bash
|
|
457
|
-
$ fcli device list --dev
|
|
464
|
+
$ fcli device list --with-data --dev
|
|
458
465
|
|
|
459
|
-
Devices (dev):
|
|
466
|
+
Devices — with data (dev):
|
|
460
467
|
|
|
461
|
-
NAME
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
468
|
+
NAME ID ONLINE TYPE LAST SEEN (30D) DATAPOINTS (30D)
|
|
469
|
+
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
470
|
+
carter.1 a8840212-a0a4-4d0b-8a47-098563894748 false default 2026-02-19T19:05:37.536Z 720156
|
|
471
|
+
DJI Matrice 4E 10f78fd5-c00d-42d9-94c4-b310646f59be false default 2026-02-18T01:17:41.167Z 917051
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
$ fcli device streams <device-id> --dev
|
|
476
|
+
|
|
477
|
+
Device Streams (dev):
|
|
478
|
+
|
|
479
|
+
Device: Walt (020e59a1-...)
|
|
480
|
+
Configured: 10 | Discovered from data: 34
|
|
481
|
+
Presence (last 7d): 0 active, 32 recent, 0 stale, 0 dormant, 12 no data
|
|
482
|
+
|
|
483
|
+
NAME TYPE SOURCE DATAPOINTS LAST SEEN FRESHNESS
|
|
484
|
+
spot.robot_state.battery numeric data 20195 2026-02-19T23:37:01.281Z recent
|
|
485
|
+
spot.localization text data 16962 2026-02-19T23:37:01.271Z recent
|
|
486
|
+
spot.hand.image custom config — — —
|
|
465
487
|
```
|
|
466
488
|
|
|
467
489
|
### JSON
|
|
@@ -469,17 +491,15 @@ robot-002 e5f6g7h8-... offline env=dev
|
|
|
469
491
|
Machine-readable JSON for scripting and automation:
|
|
470
492
|
|
|
471
493
|
```bash
|
|
472
|
-
$ fcli device list --dev --json
|
|
494
|
+
$ fcli device list --with-data --dev --json
|
|
473
495
|
{
|
|
474
496
|
"items": [
|
|
475
497
|
{
|
|
476
|
-
"id": "
|
|
477
|
-
"name": "
|
|
478
|
-
"online":
|
|
479
|
-
"
|
|
480
|
-
|
|
481
|
-
"location": "warehouse"
|
|
482
|
-
}
|
|
498
|
+
"id": "a8840212-...",
|
|
499
|
+
"name": "carter.1",
|
|
500
|
+
"online": false,
|
|
501
|
+
"last_seen": "2026-02-19T19:05:37.536Z",
|
|
502
|
+
"datapoints": 720156
|
|
483
503
|
}
|
|
484
504
|
]
|
|
485
505
|
}
|
|
@@ -496,17 +516,20 @@ fcli device list --json | jq '.items[] | select(.online==true) | .name'
|
|
|
496
516
|
### Monitor fleet health
|
|
497
517
|
|
|
498
518
|
```bash
|
|
519
|
+
# Find devices that have data
|
|
520
|
+
fcli device list --with-data
|
|
521
|
+
|
|
522
|
+
# Find devices with data, wider search window
|
|
523
|
+
fcli device list --with-data --days 90
|
|
524
|
+
|
|
499
525
|
# Check which devices are offline
|
|
500
|
-
fcli device list --json | jq '.items[] | select(.online==false) | .name'
|
|
526
|
+
fcli device list --include-offline --json | jq '.items[] | select(.online==false) | .name'
|
|
501
527
|
|
|
502
528
|
# View recent critical events across the fleet
|
|
503
529
|
fcli event list --severity critical --limit 50
|
|
504
530
|
|
|
505
|
-
#
|
|
506
|
-
|
|
507
|
-
echo "Device: $device"
|
|
508
|
-
fcli query latest-values --device $device --stream battery_level
|
|
509
|
-
done
|
|
531
|
+
# Discover what streams a device has and their data freshness
|
|
532
|
+
fcli device streams <device-id>
|
|
510
533
|
```
|
|
511
534
|
|
|
512
535
|
### Delegate a task to an AI persona
|
|
@@ -3,15 +3,21 @@ export default class DevicesList extends BaseCommand<typeof DevicesList> {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
-
|
|
6
|
+
'include-offline': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
days: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
8
|
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
9
|
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'with-data': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
11
|
tag: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
12
|
};
|
|
11
13
|
static summary: string;
|
|
12
14
|
run(): Promise<{
|
|
13
15
|
items: unknown[];
|
|
14
16
|
}>;
|
|
17
|
+
/**
|
|
18
|
+
* Query analytics SQL for per-device last seen and datapoint counts.
|
|
19
|
+
*/
|
|
20
|
+
private getLastSeenMap;
|
|
15
21
|
/**
|
|
16
22
|
* Try to get online device IDs from the presence service.
|
|
17
23
|
* Returns null if the endpoint fails or is unavailable.
|
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import { BaseCommand } from '../../base-command.js';
|
|
3
3
|
import { formatTable } from '../../lib/formatters.js';
|
|
4
|
+
import { buildLastSeenSQL, toIsoDateTime } from '../../lib/presence.js';
|
|
4
5
|
export default class DevicesList extends BaseCommand {
|
|
5
6
|
static description = `List robots and sensors in your fleet. Shows only online devices by default.
|
|
6
7
|
|
|
7
|
-
Use --
|
|
8
|
+
Use --include-offline to include offline devices. Use --with-data to show only
|
|
9
|
+
devices that have ingested data. Each device is enriched with data-based last
|
|
10
|
+
seen timestamp and datapoint count from analytics.`;
|
|
8
11
|
static examples = [
|
|
9
12
|
'<%= config.bin %> device list',
|
|
10
|
-
'<%= config.bin %> device list --
|
|
11
|
-
'<%= config.bin %> device list --
|
|
13
|
+
'<%= config.bin %> device list --include-offline',
|
|
14
|
+
'<%= config.bin %> device list --with-data',
|
|
15
|
+
'<%= config.bin %> device list --with-data --days 90',
|
|
16
|
+
'<%= config.bin %> device list --include-offline --limit 100',
|
|
12
17
|
'<%= config.bin %> device list --tag location=warehouse',
|
|
13
18
|
'<%= config.bin %> device list --name robot --dev --json',
|
|
14
19
|
];
|
|
15
20
|
static flags = {
|
|
16
|
-
|
|
21
|
+
'include-offline': Flags.boolean({
|
|
17
22
|
char: 'a',
|
|
18
|
-
description: '
|
|
23
|
+
description: 'Include offline devices',
|
|
24
|
+
}),
|
|
25
|
+
days: Flags.integer({
|
|
26
|
+
default: 30,
|
|
27
|
+
description: 'How many days back to search for last seen data',
|
|
19
28
|
}),
|
|
20
29
|
limit: Flags.integer({
|
|
21
30
|
char: 'l',
|
|
@@ -26,6 +35,10 @@ Use --all to include offline devices.`;
|
|
|
26
35
|
char: 'n',
|
|
27
36
|
description: 'Filter devices by name (search)',
|
|
28
37
|
}),
|
|
38
|
+
'with-data': Flags.boolean({
|
|
39
|
+
char: 'w',
|
|
40
|
+
description: 'Only show devices that have data in the last --days window',
|
|
41
|
+
}),
|
|
29
42
|
tag: Flags.string({
|
|
30
43
|
char: 't',
|
|
31
44
|
description: 'Filter by tag (key=value), can be specified multiple times',
|
|
@@ -55,7 +68,7 @@ Use --all to include offline devices.`;
|
|
|
55
68
|
}
|
|
56
69
|
const limit = this.flags.limit;
|
|
57
70
|
let filtered;
|
|
58
|
-
if (this.flags.
|
|
71
|
+
if (this.flags['include-offline'] || this.flags['with-data']) {
|
|
59
72
|
// --all: simple single fetch, enrich with online status
|
|
60
73
|
const result = await this.api('admin', 'devices/query', { body: { ...baseBody, count: limit } });
|
|
61
74
|
const onlineIds = await this.getOnlineDeviceIds();
|
|
@@ -96,14 +109,29 @@ Use --all to include offline devices.`;
|
|
|
96
109
|
}
|
|
97
110
|
}
|
|
98
111
|
}
|
|
112
|
+
// ── Enrich with analytics-based last seen + datapoint counts ─────────────
|
|
113
|
+
const deviceIds = filtered.map((d) => d.id).filter(Boolean);
|
|
114
|
+
const lastSeenMap = await this.getLastSeenMap(deviceIds);
|
|
115
|
+
for (const device of filtered) {
|
|
116
|
+
const info = lastSeenMap.get(device.id);
|
|
117
|
+
device.last_seen = info ? toIsoDateTime(info.last_seen) : null;
|
|
118
|
+
device.datapoints = info?.total_points ?? null;
|
|
119
|
+
}
|
|
120
|
+
// ── Filter to devices with data if requested ──────────────────────────────
|
|
121
|
+
if (this.flags['with-data']) {
|
|
122
|
+
filtered = filtered.filter((d) => d.last_seen !== null);
|
|
123
|
+
}
|
|
99
124
|
const output = { items: filtered };
|
|
100
125
|
if (!this.jsonEnabled()) {
|
|
101
|
-
const mode = this.flags.
|
|
126
|
+
const mode = this.flags['with-data'] ? 'with data' : this.flags['include-offline'] ? 'all' : 'online';
|
|
127
|
+
const days = this.flags.days;
|
|
102
128
|
const columns = [
|
|
103
|
-
{ key: 'name', label: 'NAME', width:
|
|
129
|
+
{ key: 'name', label: 'NAME', width: 24 },
|
|
104
130
|
{ key: 'id', label: 'ID', width: 40 },
|
|
105
|
-
{ key: 'online', label: 'ONLINE', width:
|
|
106
|
-
{ key: 'type', label: 'TYPE', width:
|
|
131
|
+
{ key: 'online', label: 'ONLINE', width: 8 },
|
|
132
|
+
{ key: 'type', label: 'TYPE', width: 10 },
|
|
133
|
+
{ key: 'last_seen', label: `LAST SEEN (${days}D)`, width: 28 },
|
|
134
|
+
{ key: 'datapoints', label: `DATAPOINTS (${days}D)`, width: 16 },
|
|
107
135
|
];
|
|
108
136
|
this.log(`\nDevices — ${mode} (${this.env}):\n`);
|
|
109
137
|
this.log(formatTable(filtered, columns));
|
|
@@ -111,6 +139,32 @@ Use --all to include offline devices.`;
|
|
|
111
139
|
}
|
|
112
140
|
return output;
|
|
113
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Query analytics SQL for per-device last seen and datapoint counts.
|
|
144
|
+
*/
|
|
145
|
+
async getLastSeenMap(deviceIds) {
|
|
146
|
+
const map = new Map();
|
|
147
|
+
if (deviceIds.length === 0)
|
|
148
|
+
return map;
|
|
149
|
+
try {
|
|
150
|
+
const sql = buildLastSeenSQL({
|
|
151
|
+
days: this.flags.days,
|
|
152
|
+
deviceIds,
|
|
153
|
+
});
|
|
154
|
+
const result = await this.api('query', 'analytics/rows', {
|
|
155
|
+
body: { sqlQuery: sql, type: 'advanced' },
|
|
156
|
+
});
|
|
157
|
+
if (!result.error && result.rows) {
|
|
158
|
+
for (const row of result.rows) {
|
|
159
|
+
map.set(row.device_id, row);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// non-fatal
|
|
165
|
+
}
|
|
166
|
+
return map;
|
|
167
|
+
}
|
|
114
168
|
/**
|
|
115
169
|
* Try to get online device IDs from the presence service.
|
|
116
170
|
* Returns null if the endpoint fails or is unavailable.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list.js","sourceRoot":"","sources":["../../../src/commands/device/list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,KAAK,EAAC,MAAM,aAAa,CAAA;AAEjC,OAAO,EAAC,WAAW,EAAC,MAAM,uBAAuB,CAAA;AACjD,OAAO,EAAc,WAAW,EAAC,MAAM,yBAAyB,CAAA;
|
|
1
|
+
{"version":3,"file":"list.js","sourceRoot":"","sources":["../../../src/commands/device/list.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,6 +7,7 @@ export default class DevicesStreams extends BaseCommand<typeof DevicesStreams> {
|
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
9
|
days: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'with-data': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
11
|
};
|
|
11
12
|
static summary: string;
|
|
12
13
|
run(): Promise<{
|
|
@@ -1,27 +1,36 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import { BaseCommand } from '../../base-command.js';
|
|
3
3
|
import { formatTable } from '../../lib/formatters.js';
|
|
4
|
+
import { buildPresenceSQL, getFreshness, toIsoDateTime, } from '../../lib/presence.js';
|
|
4
5
|
export default class DevicesStreams extends BaseCommand {
|
|
5
6
|
static args = {
|
|
6
7
|
id: Args.string({ description: 'Device ID (UUID)', required: true }),
|
|
7
8
|
};
|
|
8
|
-
static description = `List telemetry streams for a device.
|
|
9
|
+
static description = `List telemetry streams for a device with data presence.
|
|
9
10
|
|
|
10
|
-
Shows streams
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
Shows streams from the device configuration and any streams discovered from
|
|
12
|
+
ingested data. Each stream is enriched with presence info from the analytics
|
|
13
|
+
backend: data point count, last seen time, and a freshness indicator
|
|
14
|
+
(active/recent/stale/dormant).
|
|
15
|
+
|
|
16
|
+
Unconfigured streams (discovered from data only) are marked with SOURCE "data".`;
|
|
13
17
|
static examples = [
|
|
14
18
|
'<%= config.bin %> device streams <device-id>',
|
|
19
|
+
'<%= config.bin %> device streams <device-id> --with-data',
|
|
20
|
+
'<%= config.bin %> device streams <device-id> --with-data --days 30',
|
|
15
21
|
'<%= config.bin %> device streams <device-id> --json',
|
|
16
|
-
'<%= config.bin %> device streams <device-id> --days 30',
|
|
17
22
|
];
|
|
18
23
|
static flags = {
|
|
19
24
|
days: Flags.integer({
|
|
20
|
-
default:
|
|
21
|
-
description: 'How many days back to look for
|
|
25
|
+
default: 7,
|
|
26
|
+
description: 'How many days back to look for stream data presence',
|
|
27
|
+
}),
|
|
28
|
+
'with-data': Flags.boolean({
|
|
29
|
+
char: 'w',
|
|
30
|
+
description: 'Only show streams that have data in the --days window',
|
|
22
31
|
}),
|
|
23
32
|
};
|
|
24
|
-
static summary = 'List device streams';
|
|
33
|
+
static summary = 'List device streams with data presence';
|
|
25
34
|
async run() {
|
|
26
35
|
const deviceId = this.args.id;
|
|
27
36
|
// ── 1. Fetch config-based streams ────────────────────────────────────────
|
|
@@ -49,48 +58,94 @@ Unconfigured streams are marked with a SOURCE of "data" in the output.`;
|
|
|
49
58
|
start: since.toISOString(),
|
|
50
59
|
},
|
|
51
60
|
});
|
|
52
|
-
|
|
61
|
+
// Deduplicate metadata by stream name (the API may return multiple
|
|
62
|
+
// entries for the same stream with different tags)
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
for (const item of metaResult?.items ?? []) {
|
|
65
|
+
if (!seen.has(item.name)) {
|
|
66
|
+
seen.add(item.name);
|
|
67
|
+
metadataItems.push(item);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
53
70
|
}
|
|
54
71
|
catch {
|
|
55
72
|
// metadata endpoint failure is non-fatal; we still return config streams
|
|
56
73
|
}
|
|
57
|
-
// ── 3.
|
|
58
|
-
|
|
74
|
+
// ── 3. Fetch per-stream data presence via analytics SQL ──────────────────
|
|
75
|
+
let presenceMap = new Map();
|
|
76
|
+
try {
|
|
77
|
+
const sql = buildPresenceSQL({
|
|
78
|
+
days: this.flags.days,
|
|
79
|
+
deviceIds: [deviceId],
|
|
80
|
+
});
|
|
81
|
+
const presenceResult = await this.api('query', 'analytics/rows', {
|
|
82
|
+
body: { sqlQuery: sql, type: 'advanced' },
|
|
83
|
+
});
|
|
84
|
+
if (!presenceResult.error && presenceResult.rows) {
|
|
85
|
+
for (const row of presenceResult.rows) {
|
|
86
|
+
presenceMap.set(row.stream_name, row);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// presence failure is non-fatal
|
|
92
|
+
}
|
|
93
|
+
// ── 4. Build merged stream list ───────────────────────────────────────────
|
|
59
94
|
const rows = configStreams.map((s) => {
|
|
60
95
|
const conf = s.configuration;
|
|
96
|
+
const name = s.name;
|
|
97
|
+
const presence = presenceMap.get(name);
|
|
61
98
|
return {
|
|
62
|
-
|
|
63
|
-
|
|
99
|
+
datapoints: presence?.data_points ?? null,
|
|
100
|
+
first_seen: presence ? toIsoDateTime(presence.first_seen) : null,
|
|
101
|
+
freshness: presence ? getFreshness(presence.last_seen) : null,
|
|
102
|
+
last_seen: presence ? toIsoDateTime(presence.last_seen) : null,
|
|
103
|
+
name,
|
|
64
104
|
source: 'config',
|
|
65
|
-
topic: conf?.topic || '—',
|
|
66
105
|
type: conf?.type || '—',
|
|
67
106
|
};
|
|
68
107
|
});
|
|
69
|
-
// Append streams seen in data but absent from config
|
|
70
108
|
for (const item of metadataItems) {
|
|
71
109
|
if (!configuredStreamNames.has(item.name)) {
|
|
110
|
+
const presence = presenceMap.get(item.name);
|
|
72
111
|
rows.push({
|
|
112
|
+
datapoints: presence?.data_points ?? null,
|
|
113
|
+
first_seen: presence ? toIsoDateTime(presence.first_seen) : null,
|
|
114
|
+
freshness: presence ? getFreshness(presence.last_seen) : null,
|
|
115
|
+
last_seen: presence ? toIsoDateTime(presence.last_seen) : null,
|
|
73
116
|
name: item.name,
|
|
74
|
-
quality: '—',
|
|
75
117
|
source: 'data',
|
|
76
|
-
topic: '—',
|
|
77
118
|
type: item.type || '—',
|
|
78
119
|
});
|
|
79
120
|
}
|
|
80
121
|
}
|
|
122
|
+
// ── 5. Filter to streams with data if requested ────────────────────────
|
|
123
|
+
if (this.flags['with-data']) {
|
|
124
|
+
rows.splice(0, rows.length, ...rows.filter((r) => r.datapoints !== null));
|
|
125
|
+
}
|
|
81
126
|
if (!this.jsonEnabled()) {
|
|
82
127
|
const columns = [
|
|
83
|
-
{ key: 'name', label: 'NAME', width:
|
|
84
|
-
{ key: 'type', label: 'TYPE', width:
|
|
85
|
-
{ key: 'topic', label: 'TOPIC', width: 36 },
|
|
86
|
-
{ key: 'quality', label: 'QUALITY', width: 10 },
|
|
128
|
+
{ key: 'name', label: 'NAME', width: 34 },
|
|
129
|
+
{ key: 'type', label: 'TYPE', width: 14 },
|
|
87
130
|
{ key: 'source', label: 'SOURCE', width: 8 },
|
|
131
|
+
{ key: 'datapoints', label: `DATAPOINTS (${this.flags.days}D)`, width: 18 },
|
|
132
|
+
{ key: 'last_seen', label: `LAST SEEN (${this.flags.days}D)`, width: 28 },
|
|
133
|
+
{ key: 'freshness', label: 'FRESHNESS', width: 10 },
|
|
88
134
|
];
|
|
89
135
|
const configured = rows.filter((r) => r.source === 'config').length;
|
|
90
136
|
const discovered = rows.filter((r) => r.source === 'data').length;
|
|
91
|
-
|
|
137
|
+
const active = rows.filter((r) => r.freshness === 'active').length;
|
|
138
|
+
const recent = rows.filter((r) => r.freshness === 'recent').length;
|
|
139
|
+
const stale = rows.filter((r) => r.freshness === 'stale').length;
|
|
140
|
+
const dormant = rows.filter((r) => r.freshness === 'dormant').length;
|
|
141
|
+
const noData = rows.filter((r) => r.freshness === null).length;
|
|
142
|
+
const mode = this.flags['with-data'] ? ' — with data' : '';
|
|
143
|
+
this.log(`\nDevice Streams${mode} (${this.env}):\n`);
|
|
92
144
|
this.log(` Device: ${device.name} (${deviceId})`);
|
|
93
|
-
this.log(` Configured: ${configured} | Discovered from data
|
|
145
|
+
this.log(` Configured: ${configured} | Discovered from data: ${discovered}`);
|
|
146
|
+
this.log(` Presence (last ${this.flags.days}d): ${active} active, ${recent} recent, ${stale} stale, ${dormant} dormant` +
|
|
147
|
+
(noData > 0 ? `, ${noData} no data` : ''));
|
|
148
|
+
this.log('');
|
|
94
149
|
this.log(formatTable(rows, columns));
|
|
95
150
|
this.log('');
|
|
96
151
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streams.js","sourceRoot":"","sources":["../../../src/commands/device/streams.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,IAAI,EAAE,KAAK,EAAC,MAAM,aAAa,CAAA;AAEvC,OAAO,EAAC,WAAW,EAAC,MAAM,uBAAuB,CAAA;AACjD,OAAO,EAAc,WAAW,EAAC,MAAM,yBAAyB,CAAA;
|
|
1
|
+
{"version":3,"file":"streams.js","sourceRoot":"","sources":["../../../src/commands/device/streams.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for querying per-stream per-device data presence
|
|
3
|
+
* via the analytics SQL backend.
|
|
4
|
+
*
|
|
5
|
+
* Uses a UNION ALL across all telemetry tables to discover what data
|
|
6
|
+
* exists, grouped by (device_id, stream_name, stream_type).
|
|
7
|
+
*/
|
|
8
|
+
export interface StreamPresenceRow {
|
|
9
|
+
data_points: number;
|
|
10
|
+
device_id: string;
|
|
11
|
+
first_seen: string;
|
|
12
|
+
last_seen: string;
|
|
13
|
+
stream_name: string;
|
|
14
|
+
stream_type: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Build a SQL query that returns per-stream per-device data presence.
|
|
18
|
+
*
|
|
19
|
+
* Returns columns: device_id, stream_name, stream_type, data_points, first_seen, last_seen
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildPresenceSQL(options: {
|
|
22
|
+
days?: number;
|
|
23
|
+
deviceIds?: string[];
|
|
24
|
+
limit?: number;
|
|
25
|
+
streamNames?: string[];
|
|
26
|
+
streamTypes?: string[];
|
|
27
|
+
}): string;
|
|
28
|
+
/**
|
|
29
|
+
* Build a SQL query that returns the last time any data was seen per device.
|
|
30
|
+
*
|
|
31
|
+
* Returns columns: device_id, last_seen, total_points, stream_count
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildLastSeenSQL(options: {
|
|
34
|
+
days?: number;
|
|
35
|
+
deviceIds?: string[];
|
|
36
|
+
}): string;
|
|
37
|
+
/**
|
|
38
|
+
* Normalize a timestamp string from analytics SQL (e.g. "2026-02-19 23:37:04.747")
|
|
39
|
+
* to ISO 8601 format ("2026-02-19T23:37:04.747Z").
|
|
40
|
+
*/
|
|
41
|
+
export declare function toIsoDateTime(sqlTimestamp: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Freshness label based on how recently data was seen.
|
|
44
|
+
*/
|
|
45
|
+
export type FreshnessLevel = 'active' | 'dormant' | 'recent' | 'stale';
|
|
46
|
+
export declare function getFreshness(lastSeenIso: string): FreshnessLevel;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for querying per-stream per-device data presence
|
|
3
|
+
* via the analytics SQL backend.
|
|
4
|
+
*
|
|
5
|
+
* Uses a UNION ALL across all telemetry tables to discover what data
|
|
6
|
+
* exists, grouped by (device_id, stream_name, stream_type).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The telemetry tables that hold stream data.
|
|
10
|
+
* Each has: organization_id, device_id, name, tags, time, and type-specific value columns.
|
|
11
|
+
*/
|
|
12
|
+
const TELEMETRY_TABLES = [
|
|
13
|
+
{ streamType: 'numeric', table: 'query_numeric' },
|
|
14
|
+
{ streamType: 'text', table: 'query_text' },
|
|
15
|
+
{ streamType: 'json', table: 'query_json' },
|
|
16
|
+
{ streamType: 'location', table: 'query_location' },
|
|
17
|
+
{ streamType: 'battery', table: 'query_battery' },
|
|
18
|
+
{ streamType: 'health', table: 'query_health' },
|
|
19
|
+
{ streamType: 'bitset', table: 'query_bitset' },
|
|
20
|
+
{ streamType: 'numeric_set', table: 'query_numeric_set' },
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Build a SQL query that returns per-stream per-device data presence.
|
|
24
|
+
*
|
|
25
|
+
* Returns columns: device_id, stream_name, stream_type, data_points, first_seen, last_seen
|
|
26
|
+
*/
|
|
27
|
+
export function buildPresenceSQL(options) {
|
|
28
|
+
const { days = 7, deviceIds, limit = 500, streamNames, streamTypes } = options;
|
|
29
|
+
// Filter to requested stream types or use all
|
|
30
|
+
const tables = streamTypes
|
|
31
|
+
? TELEMETRY_TABLES.filter((t) => streamTypes.includes(t.streamType))
|
|
32
|
+
: TELEMETRY_TABLES;
|
|
33
|
+
if (tables.length === 0) {
|
|
34
|
+
throw new Error(`No matching stream types. Valid types: ${TELEMETRY_TABLES.map((t) => t.streamType).join(', ')}`);
|
|
35
|
+
}
|
|
36
|
+
const deviceFilter = deviceIds?.length
|
|
37
|
+
? `AND device_id IN (${deviceIds.map((id) => `'${id}'`).join(', ')})`
|
|
38
|
+
: '';
|
|
39
|
+
const nameFilter = streamNames?.length
|
|
40
|
+
? `AND name IN (${streamNames.map((n) => `'${n}'`).join(', ')})`
|
|
41
|
+
: '';
|
|
42
|
+
const subqueries = tables.map(({ streamType, table }) => `SELECT device_id, name AS stream_name, '${streamType}' AS stream_type, ` +
|
|
43
|
+
`count(*) AS data_points, min(time) AS first_seen, max(time) AS last_seen ` +
|
|
44
|
+
`FROM ${table} ` +
|
|
45
|
+
`WHERE time > now() - INTERVAL ${days} DAY ${deviceFilter} ${nameFilter} ` +
|
|
46
|
+
`GROUP BY device_id, name`);
|
|
47
|
+
return (`SELECT device_id, stream_name, stream_type, data_points, first_seen, last_seen FROM (\n` +
|
|
48
|
+
subqueries.join('\nUNION ALL\n') +
|
|
49
|
+
`\n) ORDER BY last_seen DESC LIMIT ${limit}`);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Build a SQL query that returns the last time any data was seen per device.
|
|
53
|
+
*
|
|
54
|
+
* Returns columns: device_id, last_seen, total_points, stream_count
|
|
55
|
+
*/
|
|
56
|
+
export function buildLastSeenSQL(options) {
|
|
57
|
+
const { days = 30, deviceIds } = options;
|
|
58
|
+
const deviceFilter = deviceIds?.length
|
|
59
|
+
? `AND device_id IN (${deviceIds.map((id) => `'${id}'`).join(', ')})`
|
|
60
|
+
: '';
|
|
61
|
+
const subqueries = TELEMETRY_TABLES.map(({ table }) => `SELECT device_id, max(time) AS last_seen, count(*) AS data_points, count(DISTINCT name) AS stream_count ` +
|
|
62
|
+
`FROM ${table} ` +
|
|
63
|
+
`WHERE time > now() - INTERVAL ${days} DAY ${deviceFilter} ` +
|
|
64
|
+
`GROUP BY device_id`);
|
|
65
|
+
return (`SELECT device_id, max(last_seen) AS last_seen, sum(data_points) AS total_points, sum(stream_count) AS stream_count FROM (\n` +
|
|
66
|
+
subqueries.join('\nUNION ALL\n') +
|
|
67
|
+
`\n) GROUP BY device_id ORDER BY last_seen DESC`);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Normalize a timestamp string from analytics SQL (e.g. "2026-02-19 23:37:04.747")
|
|
71
|
+
* to ISO 8601 format ("2026-02-19T23:37:04.747Z").
|
|
72
|
+
*/
|
|
73
|
+
export function toIsoDateTime(sqlTimestamp) {
|
|
74
|
+
if (!sqlTimestamp)
|
|
75
|
+
return sqlTimestamp;
|
|
76
|
+
// Already ISO
|
|
77
|
+
if (sqlTimestamp.includes('T'))
|
|
78
|
+
return sqlTimestamp;
|
|
79
|
+
// Replace first space with T, append Z if no timezone
|
|
80
|
+
const iso = sqlTimestamp.replace(' ', 'T');
|
|
81
|
+
return iso.endsWith('Z') ? iso : iso + 'Z';
|
|
82
|
+
}
|
|
83
|
+
export function getFreshness(lastSeenIso) {
|
|
84
|
+
const lastSeen = new Date(lastSeenIso).getTime();
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
const hoursAgo = (now - lastSeen) / (1000 * 60 * 60);
|
|
87
|
+
if (hoursAgo < 1)
|
|
88
|
+
return 'active';
|
|
89
|
+
if (hoursAgo < 24)
|
|
90
|
+
return 'recent';
|
|
91
|
+
if (hoursAgo < 7 * 24)
|
|
92
|
+
return 'stale';
|
|
93
|
+
return 'dormant';
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=presence.js.map
|