@gaia-ai/addon-gaia-ui 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/Agent/launch.d.ts +4 -4
- package/dist/src/Agent/launch.js +14 -7
- package/dist/src/Component/stats-panel/index.d.ts +13 -2
- package/dist/src/Component/stats-panel/index.js +28 -18
- package/dist/src/Component/top-tabs/index.d.ts +6 -0
- package/dist/src/Component/top-tabs/index.js +15 -6
- package/dist/src/Entity/collection.d.ts +98 -0
- package/dist/src/Entity/collection.js +46 -0
- package/dist/src/Entity/conductor/Component/conductor-list/index.d.ts +25 -0
- package/dist/src/Entity/conductor/Component/conductor-list/index.js +60 -0
- package/dist/src/Entity/conductor/Data/conductors.d.ts +57 -0
- package/dist/src/Entity/conductor/Data/conductors.js +67 -0
- package/dist/src/Entity/conductor/Data/liveness.d.ts +2 -1
- package/dist/src/Entity/conductor/Data/liveness.js +12 -16
- package/dist/src/Entity/conductor/Data/log-tail.d.ts +25 -0
- package/dist/src/Entity/conductor/Data/log-tail.js +133 -0
- package/dist/src/Entity/conductor/Data/own-conductor.d.ts +30 -0
- package/dist/src/Entity/conductor/Data/own-conductor.js +73 -0
- package/dist/src/Entity/conductor/Data/process.d.ts +78 -0
- package/dist/src/Entity/conductor/Data/process.js +101 -0
- package/dist/src/Entity/conductor/Screen/conductors.d.ts +42 -0
- package/dist/src/Entity/conductor/Screen/conductors.js +200 -0
- package/dist/src/Entity/cycle.d.ts +18 -0
- package/dist/src/Entity/cycle.js +30 -0
- package/dist/src/Entity/project/Data/project.d.ts +8 -0
- package/dist/src/Entity/project/Data/project.js +12 -0
- package/dist/src/Entity/project/Data/projects.d.ts +3 -0
- package/dist/src/Entity/project/Data/projects.js +3 -0
- package/dist/src/Entity/project/Data/with-tickets.d.ts +5 -0
- package/dist/src/Entity/project/Data/with-tickets.js +15 -13
- package/dist/src/Entity/project/Screen/project-picker.d.ts +36 -0
- package/dist/src/Entity/project/Screen/project-picker.js +90 -0
- package/dist/src/Entity/project/Screen/projects-screen.d.ts +40 -0
- package/dist/src/Entity/project/Screen/projects-screen.js +103 -0
- package/dist/src/Entity/run/Component/run-table/index.d.ts +5 -7
- package/dist/src/Entity/run/Component/run-table/index.js +9 -18
- package/dist/src/Entity/run/Data/query.d.ts +22 -0
- package/dist/src/Entity/run/Data/query.js +41 -0
- package/dist/src/Entity/run/Data/runs.d.ts +85 -4
- package/dist/src/Entity/run/Data/runs.js +143 -27
- package/dist/src/Entity/run/Screen/run-detail.d.ts +25 -0
- package/dist/src/Entity/run/Screen/run-detail.js +51 -0
- package/dist/src/Entity/run/Screen/runs.d.ts +82 -0
- package/dist/src/Entity/run/Screen/runs.js +169 -0
- package/dist/src/Entity/statistics/Screen/dashboard.d.ts +42 -0
- package/dist/src/Entity/statistics/Screen/dashboard.js +177 -0
- package/dist/src/Entity/ticket/Component/create-form/index.d.ts +10 -0
- package/dist/src/Entity/ticket/Component/create-form/index.js +4 -0
- package/dist/src/Entity/ticket/Component/edit-form/index.d.ts +3 -2
- package/dist/src/Entity/ticket/Component/edit-form/index.js +30 -8
- package/dist/src/Entity/ticket/Component/tab-bar/index.js +6 -0
- package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +1 -1
- package/dist/src/Entity/ticket/Component/ticket-detail/index.js +33 -2
- package/dist/src/Entity/ticket/Component/ticket-table/index.d.ts +4 -2
- package/dist/src/Entity/ticket/Component/ticket-table/index.js +8 -12
- package/dist/src/Entity/ticket/Data/parents.d.ts +13 -1
- package/dist/src/Entity/ticket/Data/parents.js +64 -12
- package/dist/src/Entity/ticket/Data/query.d.ts +44 -7
- package/dist/src/Entity/ticket/Data/query.js +60 -26
- package/dist/src/Entity/ticket/Data/tickets.d.ts +105 -4
- package/dist/src/Entity/ticket/Data/tickets.js +164 -12
- package/dist/src/Entity/ticket/Data/workflow.d.ts +1 -5
- package/dist/src/Entity/ticket/Data/workflow.js +5 -13
- package/dist/src/Entity/ticket/Data/write.d.ts +32 -0
- package/dist/src/Entity/ticket/Data/write.js +80 -0
- package/dist/src/Entity/ticket/Form/create-data.d.ts +1 -11
- package/dist/src/Entity/ticket/Form/create-data.js +7 -36
- package/dist/src/Entity/ticket/Form/create.d.ts +45 -5
- package/dist/src/Entity/ticket/Form/create.js +45 -8
- package/dist/src/Entity/ticket/Form/edit-data.d.ts +1 -1
- package/dist/src/Entity/ticket/Form/edit-data.js +4 -0
- package/dist/src/Entity/ticket/Form/edit.d.ts +79 -12
- package/dist/src/Entity/ticket/Form/edit.js +165 -18
- package/dist/src/Entity/ticket/Form/keys.d.ts +7 -0
- package/dist/src/Entity/ticket/Form/keys.js +7 -0
- package/dist/src/Entity/ticket/Form/parent-filter.d.ts +36 -0
- package/dist/src/Entity/ticket/Form/parent-filter.js +37 -0
- package/dist/src/Entity/ticket/Form/parent-source.d.ts +27 -0
- package/dist/src/Entity/ticket/Form/parent-source.js +56 -0
- package/dist/src/Entity/ticket/Form/who-filter.d.ts +33 -0
- package/dist/src/Entity/ticket/Form/who-filter.js +33 -0
- package/dist/src/Entity/ticket/Form/who-source.d.ts +18 -0
- package/dist/src/Entity/ticket/Form/who-source.js +38 -0
- package/dist/src/Entity/ticket/Screen/create-ticket-form.d.ts +68 -0
- package/dist/src/Entity/ticket/Screen/create-ticket-form.js +336 -0
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +81 -0
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +200 -0
- package/dist/src/Entity/ticket/Screen/parent-filter-form.d.ts +27 -0
- package/dist/src/Entity/ticket/Screen/parent-filter-form.js +177 -0
- package/dist/src/Entity/ticket/Screen/ticket-agents.d.ts +45 -0
- package/dist/src/Entity/ticket/Screen/ticket-agents.js +135 -0
- package/dist/src/Entity/ticket/Screen/ticket-detail.d.ts +61 -0
- package/dist/src/Entity/ticket/Screen/ticket-detail.js +300 -0
- package/dist/src/Entity/ticket/Screen/tickets.d.ts +129 -0
- package/dist/src/Entity/ticket/Screen/tickets.js +507 -0
- package/dist/src/Entity/ticket/Screen/who-filter-form.d.ts +27 -0
- package/dist/src/Entity/ticket/Screen/who-filter-form.js +154 -0
- package/dist/src/Entity/user/Data/users.d.ts +17 -0
- package/dist/src/Entity/user/Data/users.js +66 -6
- package/dist/src/Form/field.d.ts +3 -1
- package/dist/src/Form/field.js +1 -0
- package/dist/src/Form/form.d.ts +1 -1
- package/dist/src/Form/form.js +61 -9
- package/dist/src/Form/keys.d.ts +5 -1
- package/dist/src/Form/keys.js +38 -5
- package/dist/src/Kernel/tui-kernel.d.ts +0 -10
- package/dist/src/Kernel/tui-kernel.js +250 -2384
- package/dist/src/Routing/context.d.ts +31 -0
- package/dist/src/Routing/context.js +52 -0
- package/dist/src/Routing/registry.d.ts +19 -0
- package/dist/src/Routing/registry.js +97 -0
- package/dist/src/Routing/route.d.ts +39 -0
- package/dist/src/Routing/route.js +1 -0
- package/dist/src/Routing/router.d.ts +33 -0
- package/dist/src/Routing/router.js +102 -0
- package/dist/src/Routing/routes.d.ts +17 -0
- package/dist/src/Routing/routes.js +57 -0
- package/dist/src/Screen/async-screen.d.ts +50 -0
- package/dist/src/Screen/async-screen.js +164 -0
- package/dist/src/Screen/list-screen.d.ts +158 -0
- package/dist/src/Screen/list-screen.js +237 -0
- package/dist/src/Screen/screen.d.ts +106 -0
- package/dist/src/Screen/screen.js +18 -0
- package/dist/src/Tui/controller.d.ts +13 -0
- package/dist/src/Tui/controller.js +1794 -0
- package/dist/src/Widget/text.js +49 -4
- package/dist/src/Widget/types.d.ts +2 -1
- package/dist/src/launcher.d.ts +9 -2
- package/dist/src/launcher.js +2 -1
- package/dist/src/lib/ansi.js +13 -5
- package/dist/src/lib/cursor.d.ts +31 -0
- package/dist/src/lib/cursor.js +45 -0
- package/dist/src/lib/format.d.ts +21 -0
- package/dist/src/lib/format.js +59 -0
- package/dist/src/lib/table.d.ts +37 -0
- package/dist/src/lib/table.js +20 -0
- package/dist/src/lib/width.d.ts +33 -1
- package/dist/src/lib/width.js +107 -12
- package/dist/src/plugin.js +19 -0
- package/dist/src/types.d.ts +26 -0
- package/dist/src/types.js +0 -4
- package/package.json +2 -2
|
@@ -17,9 +17,9 @@ export declare function buildCreateLaunch(opts: {
|
|
|
17
17
|
label?: string | undefined;
|
|
18
18
|
}): CreateLaunch;
|
|
19
19
|
/**
|
|
20
|
-
* A short human label for the agent list: the
|
|
21
|
-
* incl. the ellipsis), else
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* A short human label for the agent list: the ticket's title, else the
|
|
21
|
+
* description's first line (both capped to 40 incl. the ellipsis), else
|
|
22
|
+
* "New <workflow>". GAIA-236 AC-8: it builds the `label` `buildCreateLaunch`
|
|
23
|
+
* takes, so it belongs to the module that builds the launch intent.
|
|
24
24
|
*/
|
|
25
25
|
export declare function shortName(payload: CreateFormPayload): string;
|
package/dist/src/Agent/launch.js
CHANGED
|
@@ -32,18 +32,25 @@ export function buildCreateLaunch(opts) {
|
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
|
-
* A short human label for the agent list: the
|
|
36
|
-
* incl. the ellipsis), else
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
* A short human label for the agent list: the ticket's title, else the
|
|
36
|
+
* description's first line (both capped to 40 incl. the ellipsis), else
|
|
37
|
+
* "New <workflow>". GAIA-236 AC-8: it builds the `label` `buildCreateLaunch`
|
|
38
|
+
* takes, so it belongs to the module that builds the launch intent.
|
|
39
39
|
*/
|
|
40
40
|
export function shortName(payload) {
|
|
41
|
+
// GAIA-309 D9: the title is what the operator NAMED the ticket, so it is the
|
|
42
|
+
// better tab label; the description's first line stays the fallback for a
|
|
43
|
+
// payload that has no title yet. This touches the launch intent's `label`
|
|
44
|
+
// ONLY — never `prompt`, never `env`, which is what AC-6 pins.
|
|
45
|
+
const cap = (line) => line.length > 40 ? `${line.slice(0, 39)}…` : line;
|
|
46
|
+
const title = payload.title.trim();
|
|
47
|
+
if (title)
|
|
48
|
+
return cap(title);
|
|
41
49
|
const firstLine = payload.description
|
|
42
50
|
.split('\n')
|
|
43
51
|
.map((l) => l.trim())
|
|
44
52
|
.find((l) => l.length > 0);
|
|
45
|
-
if (firstLine)
|
|
46
|
-
return firstLine
|
|
47
|
-
}
|
|
53
|
+
if (firstLine)
|
|
54
|
+
return cap(firstLine);
|
|
48
55
|
return `New ${payload.workflow?.label ?? 'ticket'}`;
|
|
49
56
|
}
|
|
@@ -36,14 +36,25 @@ export interface StatsPanelOptions {
|
|
|
36
36
|
* components keep their boundary.
|
|
37
37
|
*/
|
|
38
38
|
columns?: readonly string[] | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* The group's name, printed in the header row's already-empty label field
|
|
41
|
+
* (GAIA-324 D4). A caller that renders several groups under one heading uses
|
|
42
|
+
* it to say which group each block of bars is.
|
|
43
|
+
*
|
|
44
|
+
* The component stays ENTITY-NEUTRAL: this is a string, and nothing here knows
|
|
45
|
+
* a ticket from a run. It is padded exactly like a data label, so the title,
|
|
46
|
+
* the labels beneath it and the column titles keep one geometry.
|
|
47
|
+
*/
|
|
48
|
+
title?: string | undefined;
|
|
39
49
|
/** What to render instead of rows when there are none (spec R7). */
|
|
40
50
|
emptyMessage?: string | undefined;
|
|
41
51
|
}
|
|
42
52
|
/**
|
|
43
|
-
* One block's body rows: an optional
|
|
53
|
+
* One block's body rows: an optional header row (the group's title, the column
|
|
54
|
+
* titles, or both), then one row per entry.
|
|
44
55
|
*
|
|
45
56
|
* Every column is measured with `displayWidth` (through `pad`/`padNumber`), so a
|
|
46
57
|
* wide-character label cannot shift the numbers out of alignment — the failure
|
|
47
58
|
* that makes a table of counts unreadable exactly when the data is interesting.
|
|
48
59
|
*/
|
|
49
|
-
export declare function statsPanelRows(rows: readonly StatsPanelRow[], { columns, emptyMessage }?: StatsPanelOptions): string[];
|
|
60
|
+
export declare function statsPanelRows(rows: readonly StatsPanelRow[], { columns, title, emptyMessage }?: StatsPanelOptions): string[];
|
|
@@ -31,12 +31,16 @@ export const BAR_WIDTH = 20;
|
|
|
31
31
|
/**
|
|
32
32
|
* The label column, in display columns: 26 for the text plus {@link LABEL_GAP}.
|
|
33
33
|
*
|
|
34
|
-
* The text budget is
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
34
|
+
* The text budget is what the field has to hold, and after GAIA-324 that is two
|
|
35
|
+
* different kinds of text sharing one column: a breakdown row's bare entity name
|
|
36
|
+
* (a real project name reaches 26 columns on its own — `gaia-e2e-project` plus a
|
|
37
|
+
* suffix used to; a long one still does), and the group TITLE printed in the
|
|
38
|
+
* header row's label field above it. Both are padded to the same field, which is
|
|
39
|
+
* what keeps a title, the labels it captions and the column titles on one
|
|
40
|
+
* geometry. Kept at 26 rather than narrowed with the `· tickets` suffix GAIA-324
|
|
41
|
+
* deleted: the budget was never sized for the suffix alone, and a project name
|
|
42
|
+
* cut to `a-very-long-project-…` is the same unreadable row the width exists to
|
|
43
|
+
* prevent.
|
|
40
44
|
*/
|
|
41
45
|
const LABEL_WIDTH = 27;
|
|
42
46
|
/**
|
|
@@ -44,16 +48,16 @@ const LABEL_WIDTH = 27;
|
|
|
44
48
|
*
|
|
45
49
|
* Load-bearing, not decoration. `pad` fills a label to exactly `LABEL_WIDTH`, so
|
|
46
50
|
* a label of precisely that width left NO separator and the row rendered
|
|
47
|
-
* `gaia-e2e-project
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
51
|
+
* `gaia-e2e-project-name██████` — a real project name, at exactly 26 columns. A
|
|
52
|
+
* truncated label collided the same way, ending in `…` against the bar. Every
|
|
53
|
+
* fixture in the tests was shorter, which is why the suite was green while the
|
|
54
|
+
* screen was wrong; the gap is part of the geometry now so the label length
|
|
55
|
+
* cannot decide whether it exists.
|
|
52
56
|
*
|
|
53
57
|
* It is ADDED to the field rather than taken out of it: the 26 columns above are
|
|
54
58
|
* a content budget with a stated reason, and spending one of them on the gap
|
|
55
|
-
* would
|
|
56
|
-
*
|
|
59
|
+
* would truncate one more column off the longest names — the labels that are
|
|
60
|
+
* already hardest to tell apart.
|
|
57
61
|
*/
|
|
58
62
|
const LABEL_GAP = 1;
|
|
59
63
|
/** One number column, in display columns. */
|
|
@@ -81,24 +85,30 @@ function bar(value, max) {
|
|
|
81
85
|
return BAR_FULL.repeat(filled) + BAR_EMPTY.repeat(BAR_WIDTH - filled);
|
|
82
86
|
}
|
|
83
87
|
/**
|
|
84
|
-
* One block's body rows: an optional
|
|
88
|
+
* One block's body rows: an optional header row (the group's title, the column
|
|
89
|
+
* titles, or both), then one row per entry.
|
|
85
90
|
*
|
|
86
91
|
* Every column is measured with `displayWidth` (through `pad`/`padNumber`), so a
|
|
87
92
|
* wide-character label cannot shift the numbers out of alignment — the failure
|
|
88
93
|
* that makes a table of counts unreadable exactly when the data is interesting.
|
|
89
94
|
*/
|
|
90
|
-
export function statsPanelRows(rows, { columns, emptyMessage } = {}) {
|
|
95
|
+
export function statsPanelRows(rows, { columns, title, emptyMessage } = {}) {
|
|
91
96
|
if (rows.length === 0) {
|
|
92
97
|
return emptyMessage === undefined ? [] : [`${INDENT}${emptyMessage}`];
|
|
93
98
|
}
|
|
94
99
|
const barValue = (row) => row.numbers.at(-1) ?? 0;
|
|
95
100
|
const max = Math.max(...rows.map(barValue));
|
|
96
101
|
const out = [];
|
|
97
|
-
if (columns && columns.length > 0) {
|
|
102
|
+
if ((columns && columns.length > 0) || title !== undefined) {
|
|
98
103
|
out.push(INDENT +
|
|
99
|
-
|
|
104
|
+
// The SAME label geometry as a data row below (padded to the field width
|
|
105
|
+
// minus the gap, then the gap), so a group title sits exactly over the
|
|
106
|
+
// labels it captions. With no title this is the blank label field the
|
|
107
|
+
// header row always had, byte for byte.
|
|
108
|
+
pad(title ?? '', LABEL_WIDTH - LABEL_GAP) +
|
|
109
|
+
' '.repeat(LABEL_GAP) +
|
|
100
110
|
' '.repeat(BAR_WIDTH) +
|
|
101
|
-
columns.map((c) => padNumber(c, NUMBER_WIDTH)).join(''));
|
|
111
|
+
(columns ?? []).map((c) => padNumber(c, NUMBER_WIDTH)).join(''));
|
|
102
112
|
}
|
|
103
113
|
for (const row of rows) {
|
|
104
114
|
out.push(INDENT +
|
|
@@ -10,6 +10,9 @@ export declare const ROOT_TABS: readonly [{
|
|
|
10
10
|
}, {
|
|
11
11
|
readonly id: "projects";
|
|
12
12
|
readonly label: "Projects";
|
|
13
|
+
}, {
|
|
14
|
+
readonly id: "conductors";
|
|
15
|
+
readonly label: "Conductors";
|
|
13
16
|
}];
|
|
14
17
|
/** The project level: the tabs of an opened project. */
|
|
15
18
|
export declare const PROJECT_TABS: readonly [{
|
|
@@ -33,6 +36,9 @@ export declare const TAB_LEVELS: {
|
|
|
33
36
|
}, {
|
|
34
37
|
readonly id: "projects";
|
|
35
38
|
readonly label: "Projects";
|
|
39
|
+
}, {
|
|
40
|
+
readonly id: "conductors";
|
|
41
|
+
readonly label: "Conductors";
|
|
36
42
|
}];
|
|
37
43
|
readonly project: readonly [{
|
|
38
44
|
readonly id: "dashboard";
|
|
@@ -26,15 +26,23 @@ import { displayWidth } from '../../lib/width.js';
|
|
|
26
26
|
// `topTabSpans(level, id)`, `tabBarFor(location)`, `TabIdOf<L>`). It was a
|
|
27
27
|
// property of ONE function, the level-blind `locateTab`, which walked the levels
|
|
28
28
|
// in key order and would now resolve the project level's `dashboard` to the ROOT
|
|
29
|
-
// location.
|
|
29
|
+
// location. Top-tab navigation was its only caller, and it only moves within one
|
|
30
30
|
// level — the bar you Tab through or click IS the current level's bar — so it
|
|
31
31
|
// pairs the target with `here.level` directly and `locateTab` is deleted.
|
|
32
32
|
//
|
|
33
|
-
// Only tabs that are actually navigable are listed. A planned screen (`Users`
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
33
|
+
// Only tabs that are actually navigable are listed. A planned screen (`Users`)
|
|
34
|
+
// arrives with its own ticket and adds its entry here then — a tab drawn before
|
|
35
|
+
// its screen exists would advertise navigation the operator cannot take, which
|
|
36
|
+
// is the very defect GAIA-239 removes.
|
|
37
|
+
//
|
|
38
|
+
// GAIA-232 added `conductors`, and put it on the ROOT level although the note
|
|
39
|
+
// above once reserved it for the project level. A conductor is an operator
|
|
40
|
+
// concern: "is my conductor alive?" has to be answerable without first choosing
|
|
41
|
+
// a project, and one conductor commonly serves work across a checkout rather
|
|
42
|
+
// than sitting inside one project's screen. Which project it serves is a COLUMN
|
|
43
|
+
// of that screen, not a navigation level above it. Being an ordinary entry here
|
|
44
|
+
// is also what keeps it inside the derived id union and gives it GAIA-241's
|
|
45
|
+
// click spans for free.
|
|
38
46
|
//
|
|
39
47
|
// `as const satisfies` is load-bearing on both halves: `satisfies` still
|
|
40
48
|
// shape-checks every entry against `TopTab`, while `as const` keeps the literal
|
|
@@ -45,6 +53,7 @@ import { displayWidth } from '../../lib/width.js';
|
|
|
45
53
|
export const ROOT_TABS = [
|
|
46
54
|
{ id: 'dashboard', label: 'Dashboard' },
|
|
47
55
|
{ id: 'projects', label: 'Projects' },
|
|
56
|
+
{ id: 'conductors', label: 'Conductors' },
|
|
48
57
|
];
|
|
49
58
|
/** The project level: the tabs of an opened project. */
|
|
50
59
|
export const PROJECT_TABS = [
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { DropshClient, JsonApiResource } from '../types.js';
|
|
2
|
+
/** A JSON:API collection document (the raw shape `client.get` returns). */
|
|
3
|
+
export interface JsonApiCollectionDoc {
|
|
4
|
+
data?: JsonApiResource[];
|
|
5
|
+
included?: JsonApiResource[];
|
|
6
|
+
/**
|
|
7
|
+
* The document-level `meta`. GAIA-305 makes every `gaia_*` collection report its
|
|
8
|
+
* total here (`GaiaResourceType::includeCount()` -> TRUE, applied by
|
|
9
|
+
* `GaiaResourceTypeRepository::countsCollections()`); see {@link Page.total}.
|
|
10
|
+
*
|
|
11
|
+
* Optional and loosely typed on purpose: a control plane that predates GAIA-305
|
|
12
|
+
* answers either with no `meta` at all or with a `meta` carrying only `omitted`
|
|
13
|
+
* — both measured this session — and neither is an error.
|
|
14
|
+
*/
|
|
15
|
+
meta?: {
|
|
16
|
+
count?: unknown;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* JSON:API's own paging links. `next` is the ONLY valid end-of-list signal, and
|
|
20
|
+
* it stays so after GAIA-305: a page can simply BE the last one, and `next` is
|
|
21
|
+
* what says whether it is. `rows.length === limit` cannot answer that — a full
|
|
22
|
+
* page is exactly as consistent with "more follows" as with "that was all", and
|
|
23
|
+
* the run collection's last page is short for the ordinary reason that
|
|
24
|
+
* collections do not divide evenly.
|
|
25
|
+
*
|
|
26
|
+
* (GAIA-305 does retire the older reason given here — that access filtering
|
|
27
|
+
* emptied pages after `page[limit]` had bounded the query, so a SHORT page said
|
|
28
|
+
* nothing at all. Its query-level access filtering makes pages full again. The
|
|
29
|
+
* conclusion is unchanged; only the argument for it is.)
|
|
30
|
+
*
|
|
31
|
+
* Optional, because the three non-paged readers (the parent picker, the projects
|
|
32
|
+
* screen, the own-conductor lookup) read one bounded bag and never page.
|
|
33
|
+
*/
|
|
34
|
+
links?: {
|
|
35
|
+
next?: unknown;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The window a paged read ASKED the server for: the request's own `page[offset]`
|
|
40
|
+
* and `page[limit]` (GAIA-312 D3).
|
|
41
|
+
*
|
|
42
|
+
* A fact about the REQUEST, never an inference about the pages before it. Both
|
|
43
|
+
* list queries send `page[limit]=size` and `page[offset]=index * size`
|
|
44
|
+
* (`Entity/*/Data/query.ts`), and since GAIA-305 the server skips and counts the
|
|
45
|
+
* same access-filtered collection — which is what lets a screen turn an offset
|
|
46
|
+
* into a position at all.
|
|
47
|
+
*/
|
|
48
|
+
export interface PageWindow {
|
|
49
|
+
/** Rows of the collection the request told the server to skip. */
|
|
50
|
+
offset: number;
|
|
51
|
+
/** Rows the request told the server to return at most. */
|
|
52
|
+
limit: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One page of rows plus everything the screen states about it.
|
|
56
|
+
*
|
|
57
|
+
* Carried together so a `hasNext`, a page number or a total cannot go stale beside
|
|
58
|
+
* fresh rows (GAIA-292 D4/D4a).
|
|
59
|
+
*/
|
|
60
|
+
export interface Page<Row> {
|
|
61
|
+
rows: Row[];
|
|
62
|
+
hasNext: boolean;
|
|
63
|
+
/** The page these rows CAME FROM, not the one most recently requested. */
|
|
64
|
+
page: number;
|
|
65
|
+
/**
|
|
66
|
+
* The size of the filtered collection as the SERVER reported it, or null when it
|
|
67
|
+
* reported none (GAIA-292 D6e).
|
|
68
|
+
*
|
|
69
|
+
* It rides the page response's `meta.count`, so it costs no read of its own and
|
|
70
|
+
* cannot describe a different collection than the rows beside it: the server
|
|
71
|
+
* applied one query and answered both from it. That is why the earlier background
|
|
72
|
+
* walk — 28 sequential reads for the open-run collection — is gone rather than
|
|
73
|
+
* kept as a fallback.
|
|
74
|
+
*
|
|
75
|
+
* A number renders as `20 of 107 ticket(s)`; `null` renders as `20 of ?
|
|
76
|
+
* ticket(s)` (D6d), which is a visible statement that this control plane reports
|
|
77
|
+
* no total — never a number nobody established.
|
|
78
|
+
*/
|
|
79
|
+
total: number | null;
|
|
80
|
+
/**
|
|
81
|
+
* The window these rows were read with, or `null` when they no longer fill a
|
|
82
|
+
* window that positions them (GAIA-312 D3/D4).
|
|
83
|
+
*
|
|
84
|
+
* Required and explicitly nullable, exactly like {@link Page.total}: "not
|
|
85
|
+
* established" occupies the field's own place instead of being spelled by an
|
|
86
|
+
* absent key. `null` means the screen states a row COUNT; a window means it may
|
|
87
|
+
* state a RANGE, under the test in `ListScreen.countLabel`.
|
|
88
|
+
*/
|
|
89
|
+
window: PageWindow | null;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Read one page of a collection and map it to rows.
|
|
93
|
+
*
|
|
94
|
+
* `mapRows` receives `data` AND `included` so each slice keeps its own sideload
|
|
95
|
+
* indexing — the ticket list indexes every include by id, the run list builds a
|
|
96
|
+
* ticket-only map — without this function knowing either.
|
|
97
|
+
*/
|
|
98
|
+
export declare function loadPage<Row>(client: DropshClient, path: string, params: unknown, mapRows: (data: JsonApiResource[], included: JsonApiResource[]) => Row[], page: number, window: PageWindow): Promise<Page<Row>>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// The ONE JSON:API collection-document shape and the ONE page read (GAIA-294).
|
|
2
|
+
//
|
|
3
|
+
// Slice-less on purpose, directly under `Entity/` beside `transport.ts`,
|
|
4
|
+
// `count.ts` and `cycle.ts`. That is the only placement every slice's `Data/` may
|
|
5
|
+
// import: `tests/boundary.test.ts` G2 forbids a data module from reaching another
|
|
6
|
+
// slice, and its `sliceOf` returns undefined for a file sitting here, so these
|
|
7
|
+
// edges are not cross-slice ones.
|
|
8
|
+
//
|
|
9
|
+
// Before this file the document was declared FIVE times — in ticket/Data/tickets,
|
|
10
|
+
// run/Data/runs, ticket/Data/parents, project/Data/projects and
|
|
11
|
+
// conductor/Data/own-conductor — and only the first two carried `links.next`. Same
|
|
12
|
+
// name, different shapes: the drift GAIA-292's "mirroring Task 3 exactly so the
|
|
13
|
+
// two lists cannot drift" was meant to prevent had already happened.
|
|
14
|
+
/**
|
|
15
|
+
* The collection total a document reports, or null when it reports none.
|
|
16
|
+
*
|
|
17
|
+
* Defensive by contract rather than by suspicion: this value is rendered to the
|
|
18
|
+
* operator as a total, so anything that is not a usable count must become the
|
|
19
|
+
* explicit unknown instead of a guess (AC 8.5). A count that is absent, not a
|
|
20
|
+
* number, not finite, or negative is no count.
|
|
21
|
+
*/
|
|
22
|
+
function readTotal(doc) {
|
|
23
|
+
const count = doc?.meta?.count;
|
|
24
|
+
if (typeof count !== 'number')
|
|
25
|
+
return null;
|
|
26
|
+
if (!Number.isFinite(count) || count < 0)
|
|
27
|
+
return null;
|
|
28
|
+
return count;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Read one page of a collection and map it to rows.
|
|
32
|
+
*
|
|
33
|
+
* `mapRows` receives `data` AND `included` so each slice keeps its own sideload
|
|
34
|
+
* indexing — the ticket list indexes every include by id, the run list builds a
|
|
35
|
+
* ticket-only map — without this function knowing either.
|
|
36
|
+
*/
|
|
37
|
+
export async function loadPage(client, path, params, mapRows, page, window) {
|
|
38
|
+
const doc = (await client.get(path, params));
|
|
39
|
+
return {
|
|
40
|
+
rows: mapRows(doc?.data ?? [], doc?.included ?? []),
|
|
41
|
+
hasNext: Boolean(doc?.links?.next),
|
|
42
|
+
page,
|
|
43
|
+
total: readTotal(doc),
|
|
44
|
+
window,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** The minimum shape a conductor row needs to render. */
|
|
2
|
+
export interface ConductorRowInput {
|
|
3
|
+
label: string;
|
|
4
|
+
machineId: string | undefined;
|
|
5
|
+
project: string;
|
|
6
|
+
derivedStatus: string;
|
|
7
|
+
ageLabel: string;
|
|
8
|
+
workspaceRoot: string;
|
|
9
|
+
lastPid: number | undefined;
|
|
10
|
+
/** Whether this conductor's workspace exists on THIS machine. */
|
|
11
|
+
isLocal: boolean;
|
|
12
|
+
/** Whether an action on this row is in flight. */
|
|
13
|
+
busy: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* One line per conductor: who, where, alive, since when, and its process.
|
|
17
|
+
*
|
|
18
|
+
* A non-local row is marked rather than hidden. The control plane is
|
|
19
|
+
* user-scoped, not machine-scoped, so a user's other machines' conductors are
|
|
20
|
+
* legitimately in this list — and silently dropping them would make the cockpit
|
|
21
|
+
* disagree with `gaia conductor ls` for no visible reason. Marking them says
|
|
22
|
+
* why their actions are inert here: a pid from another machine means nothing on
|
|
23
|
+
* this one.
|
|
24
|
+
*/
|
|
25
|
+
export declare function conductorRows(conductors: ConductorRowInput[]): string[];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { pad } from '../../../../lib/format.js';
|
|
2
|
+
/** How much width the workspace column gets before it is elided. */
|
|
3
|
+
const PATH_WIDTH = 30;
|
|
4
|
+
/**
|
|
5
|
+
* How much width the machine_id column gets before it is elided.
|
|
6
|
+
*
|
|
7
|
+
* A `machine_id` is COMPOSED (`${user_id}-${machine_id}-${project}`), so on a
|
|
8
|
+
* user-scoped control plane every row repeats the same uuid at the head. It is
|
|
9
|
+
* therefore elided from the left like the path: the tail is the only part that
|
|
10
|
+
* tells one conductor from another.
|
|
11
|
+
*/
|
|
12
|
+
const MACHINE_ID_WIDTH = 20;
|
|
13
|
+
/**
|
|
14
|
+
* One line per conductor: who, where, alive, since when, and its process.
|
|
15
|
+
*
|
|
16
|
+
* A non-local row is marked rather than hidden. The control plane is
|
|
17
|
+
* user-scoped, not machine-scoped, so a user's other machines' conductors are
|
|
18
|
+
* legitimately in this list — and silently dropping them would make the cockpit
|
|
19
|
+
* disagree with `gaia conductor ls` for no visible reason. Marking them says
|
|
20
|
+
* why their actions are inert here: a pid from another machine means nothing on
|
|
21
|
+
* this one.
|
|
22
|
+
*/
|
|
23
|
+
export function conductorRows(conductors) {
|
|
24
|
+
if (conductors.length === 0)
|
|
25
|
+
return ['No conductors on this control plane.'];
|
|
26
|
+
return conductors.map((c) => {
|
|
27
|
+
const name = c.label !== '' ? c.label : (c.machineId ?? '');
|
|
28
|
+
const pid = c.lastPid === undefined ? '-' : String(c.lastPid);
|
|
29
|
+
const marks = [c.busy ? '…' : '', c.isLocal ? '' : 'elsewhere']
|
|
30
|
+
.filter((m) => m !== '')
|
|
31
|
+
.join(' ');
|
|
32
|
+
return [
|
|
33
|
+
pad(name, 20),
|
|
34
|
+
// Its OWN column, not merely the label's fallback (AC-1, spec D2): two
|
|
35
|
+
// conductors of one repo share a project and a workspace_root, and the
|
|
36
|
+
// machine_id is the only thing that separates them.
|
|
37
|
+
pad(elideLeft(c.machineId ?? '', MACHINE_ID_WIDTH), MACHINE_ID_WIDTH),
|
|
38
|
+
pad(c.project, 14),
|
|
39
|
+
pad(c.derivedStatus, 9),
|
|
40
|
+
pad(c.ageLabel, 8),
|
|
41
|
+
pad(elideLeft(c.workspaceRoot, PATH_WIDTH), PATH_WIDTH),
|
|
42
|
+
pad(pid, 7),
|
|
43
|
+
marks,
|
|
44
|
+
]
|
|
45
|
+
.join(' ')
|
|
46
|
+
.trimEnd();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Shorten a path from the LEFT, keeping its end.
|
|
51
|
+
*
|
|
52
|
+
* Conductor workspaces share a long common head (`/home/<user>/projects/…`) and
|
|
53
|
+
* differ at the tail, which is also the part that identifies the checkout.
|
|
54
|
+
* Eliding the right would leave every row looking identical.
|
|
55
|
+
*/
|
|
56
|
+
function elideLeft(path, width) {
|
|
57
|
+
if (path.length <= width)
|
|
58
|
+
return path;
|
|
59
|
+
return `…${path.slice(path.length - (width - 1))}`;
|
|
60
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DropshClient } from '../../../types.js';
|
|
2
2
|
import { type ListOutcome, type OptionClients } from '../../transport.js';
|
|
3
|
+
import { type ConductorStatus } from './liveness.js';
|
|
3
4
|
/** One conductor of the control plane, as this renderer needs it. */
|
|
4
5
|
export interface Conductor {
|
|
5
6
|
id: string;
|
|
@@ -26,3 +27,59 @@ export declare const listConductors: (client: DropshClient) => Promise<ListOutco
|
|
|
26
27
|
* and `pm` sees the FULL conductor set where `session` sees a subset.
|
|
27
28
|
*/
|
|
28
29
|
export declare const listConductorsPreferringWrite: (clients: OptionClients) => Promise<Conductor[]>;
|
|
30
|
+
/** A conductor as the Conductors screen renders it. */
|
|
31
|
+
export interface ConductorRow extends Conductor {
|
|
32
|
+
/** The `.gaia` dir it last registered from; '' when it never reported one. */
|
|
33
|
+
workspaceRoot: string;
|
|
34
|
+
/** Unix seconds of the last heartbeat; 0 when it never reported one. */
|
|
35
|
+
lastSeen: number;
|
|
36
|
+
/** Unix seconds the lease runs out; 0 when unset. */
|
|
37
|
+
leaseExpiresAt: number;
|
|
38
|
+
currentLoad: number;
|
|
39
|
+
maxParallel: number;
|
|
40
|
+
/** The pid its serve loop last reported, if any. */
|
|
41
|
+
lastPid: number | undefined;
|
|
42
|
+
/** The config stem it was started from, as `--conductor <name>` takes it. */
|
|
43
|
+
conductorName: string | undefined;
|
|
44
|
+
/** Liveness, derived — never the raw `status`. */
|
|
45
|
+
derivedStatus: ConductorStatus;
|
|
46
|
+
/**
|
|
47
|
+
* Whether this conductor's workspace exists on THIS machine.
|
|
48
|
+
*
|
|
49
|
+
* The control plane is user-scoped, not machine-scoped, so a user's other
|
|
50
|
+
* machines' conductors appear in this list. They are shown rather than hidden
|
|
51
|
+
* — filtering by machine_id would mean substring-matching a composed string —
|
|
52
|
+
* but only a local row can be started or stopped from here, because a pid
|
|
53
|
+
* from another machine means nothing on this one.
|
|
54
|
+
*/
|
|
55
|
+
isLocal: boolean;
|
|
56
|
+
}
|
|
57
|
+
/** What the row projection needs from the outside world, injected for tests. */
|
|
58
|
+
export interface ConductorRowContext {
|
|
59
|
+
/** Unix seconds; the liveness derivation's clock. */
|
|
60
|
+
now: number;
|
|
61
|
+
/** Whether a path exists on this machine. */
|
|
62
|
+
exists: (path: string) => boolean;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The conductor collection as cockpit rows, in server order.
|
|
66
|
+
*
|
|
67
|
+
* Swallows a read failure into an empty list on purpose: the Conductors screen
|
|
68
|
+
* re-reads every 5 s, so one failed poll must leave the screen navigable rather
|
|
69
|
+
* than replace it with an error.
|
|
70
|
+
*/
|
|
71
|
+
export declare const listConductorRows: (client: DropshClient, ctx: ConductorRowContext) => Promise<ConductorRow[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Project uuid → name, for the Conductors screen's project column.
|
|
74
|
+
*
|
|
75
|
+
* A conductor references its project by uuid; the screen shows the name. This
|
|
76
|
+
* is a separate small read rather than a widened `ProjectRow`, because
|
|
77
|
+
* `loadProjectRows` builds the PROJECTS screen (one row per project, derived
|
|
78
|
+
* from conductors) and adding an id to it would change a shape four screens
|
|
79
|
+
* depend on to serve one column.
|
|
80
|
+
*
|
|
81
|
+
* A failed read yields an empty map: an unnamed project renders blank, which is
|
|
82
|
+
* better than a screen that refuses to list conductors because their projects
|
|
83
|
+
* could not be resolved.
|
|
84
|
+
*/
|
|
85
|
+
export declare const listProjectNames: (client: DropshClient) => Promise<Map<string, string>>;
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
// GAIA-236 — the CONDUCTOR data service. It returns CONDUCTORS and knows nothing
|
|
2
|
+
// about forms: no `Option`, no project filter, no `(status)` in a label. Those are
|
|
3
|
+
// one popup's rules and live with that popup's adapter (`edit-form-data.ts`); a
|
|
4
|
+
// service that applied them would be unusable for any other purpose. The two read
|
|
5
|
+
// strategies are both just reading, so both live here.
|
|
6
|
+
import { toUnixSeconds } from '@gaia-ai/core';
|
|
1
7
|
import { listOutcome, listPreferringWrite, } from '../../transport.js';
|
|
8
|
+
import { deriveConductorStatus } from './liveness.js';
|
|
2
9
|
const str = (v) => typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
3
10
|
const toConductor = (r) => ({
|
|
4
11
|
id: r.id,
|
|
@@ -21,3 +28,63 @@ export const listConductors = (client) => listOutcome(client, 'gaia_conductor',
|
|
|
21
28
|
* and `pm` sees the FULL conductor set where `session` sees a subset.
|
|
22
29
|
*/
|
|
23
30
|
export const listConductorsPreferringWrite = (clients) => listPreferringWrite(clients, 'gaia_conductor', toConductor);
|
|
31
|
+
const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
32
|
+
const toConductorRow = (r, ctx) => {
|
|
33
|
+
const workspaceRoot = str(r.attr('workspace_root')) ?? '';
|
|
34
|
+
// ISO-8601 over the wire, not the unix integer the field name implies — a
|
|
35
|
+
// `typeof === 'number'` guard here is what made a live conductor read `stale`.
|
|
36
|
+
const lastSeen = toUnixSeconds(r.attr('last_seen')) ?? 0;
|
|
37
|
+
const leaseExpiresAt = toUnixSeconds(r.attr('lease_expires_at')) ?? 0;
|
|
38
|
+
return {
|
|
39
|
+
...toConductor(r),
|
|
40
|
+
workspaceRoot,
|
|
41
|
+
lastSeen,
|
|
42
|
+
leaseExpiresAt,
|
|
43
|
+
currentLoad: num(r.attr('current_load')) ?? 0,
|
|
44
|
+
maxParallel: num(r.attr('max_parallel')) ?? 0,
|
|
45
|
+
lastPid: num(r.attr('last_pid')),
|
|
46
|
+
conductorName: str(r.attr('conductor_name')),
|
|
47
|
+
// `present: true` because the row IS a control-plane record — which is why
|
|
48
|
+
// `registry-only` can never come out of this screen.
|
|
49
|
+
derivedStatus: deriveConductorStatus({
|
|
50
|
+
present: true,
|
|
51
|
+
status: str(r.attr('status')) ?? null,
|
|
52
|
+
lastSeen: lastSeen === 0 ? null : lastSeen,
|
|
53
|
+
leaseExpiresAt: leaseExpiresAt === 0 ? null : leaseExpiresAt,
|
|
54
|
+
}, ctx.now),
|
|
55
|
+
// A conductor that never reported a workspace cannot be local, whatever the
|
|
56
|
+
// filesystem would say about an empty path.
|
|
57
|
+
isLocal: workspaceRoot !== '' && ctx.exists(workspaceRoot),
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The conductor collection as cockpit rows, in server order.
|
|
62
|
+
*
|
|
63
|
+
* Swallows a read failure into an empty list on purpose: the Conductors screen
|
|
64
|
+
* re-reads every 5 s, so one failed poll must leave the screen navigable rather
|
|
65
|
+
* than replace it with an error.
|
|
66
|
+
*/
|
|
67
|
+
export const listConductorRows = async (client, ctx) => {
|
|
68
|
+
const outcome = await listOutcome(client, 'gaia_conductor', (r) => toConductorRow(r, ctx));
|
|
69
|
+
return outcome.rows;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Project uuid → name, for the Conductors screen's project column.
|
|
73
|
+
*
|
|
74
|
+
* A conductor references its project by uuid; the screen shows the name. This
|
|
75
|
+
* is a separate small read rather than a widened `ProjectRow`, because
|
|
76
|
+
* `loadProjectRows` builds the PROJECTS screen (one row per project, derived
|
|
77
|
+
* from conductors) and adding an id to it would change a shape four screens
|
|
78
|
+
* depend on to serve one column.
|
|
79
|
+
*
|
|
80
|
+
* A failed read yields an empty map: an unnamed project renders blank, which is
|
|
81
|
+
* better than a screen that refuses to list conductors because their projects
|
|
82
|
+
* could not be resolved.
|
|
83
|
+
*/
|
|
84
|
+
export const listProjectNames = async (client) => {
|
|
85
|
+
const outcome = await listOutcome(client, 'gaia_project', (r) => ({
|
|
86
|
+
id: r.id,
|
|
87
|
+
name: str(r.attr('name')) ?? str(r.attr('label')) ?? '',
|
|
88
|
+
}));
|
|
89
|
+
return new Map(outcome.rows.map((p) => [p.id, p.name]));
|
|
90
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type ConductorLiveness } from '@gaia-ai/core';
|
|
1
2
|
/** The derived liveness state of a conductor. */
|
|
2
|
-
export type ConductorStatus =
|
|
3
|
+
export type ConductorStatus = ConductorLiveness | 'registry-only';
|
|
3
4
|
/** The raw liveness inputs derived from a `gaia_conductor` record. */
|
|
4
5
|
export interface ConductorInput {
|
|
5
6
|
present?: boolean;
|
|
@@ -1,23 +1,19 @@
|
|
|
1
|
-
// Conductor liveness derivation.
|
|
1
|
+
// Conductor liveness derivation.
|
|
2
|
+
//
|
|
3
|
+
// The rule itself lives in `@gaia-ai/core` (`deriveConductorLiveness`) so the
|
|
4
|
+
// CLI listing, this cockpit and the server-side ConductorDeriver
|
|
2
5
|
// (web/modules/custom/gaia_core/modules/gaia_dashboard/src/Dashboard/ConductorDeriver.php)
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
const STALE_WINDOW_SECONDS = 600;
|
|
6
|
+
// cannot drift into describing one conductor three ways. What stays here is the
|
|
7
|
+
// one thing core cannot answer: whether a gaia_conductor record exists at all.
|
|
8
|
+
import { deriveConductorLiveness } from '@gaia-ai/core';
|
|
7
9
|
export function deriveConductorStatus(conductor, nowSeconds) {
|
|
10
|
+
// `registry-only` means "something names this conductor but the control plane
|
|
11
|
+
// has no record of it". The Conductors screen reads the control plane itself,
|
|
12
|
+
// so every row it builds is present by construction and this branch cannot
|
|
13
|
+
// fire there; it remains for callers that join a record against another list.
|
|
8
14
|
if (!conductor?.present)
|
|
9
15
|
return 'registry-only';
|
|
10
|
-
|
|
11
|
-
return 'offline';
|
|
12
|
-
if (conductor.lastSeen == null)
|
|
13
|
-
return 'stale';
|
|
14
|
-
if (nowSeconds - conductor.lastSeen > STALE_WINDOW_SECONDS)
|
|
15
|
-
return 'stale';
|
|
16
|
-
if (conductor.leaseExpiresAt != null &&
|
|
17
|
-
conductor.leaseExpiresAt < nowSeconds) {
|
|
18
|
-
return 'stale';
|
|
19
|
-
}
|
|
20
|
-
return 'running';
|
|
16
|
+
return deriveConductorLiveness(conductor, nowSeconds);
|
|
21
17
|
}
|
|
22
18
|
// `humanizeAge` moved to `lib/format.ts` (GAIA-236, entity-slice Phase 0): it is a
|
|
23
19
|
// generic relative-time formatter with no conductor vocabulary, and keeping it here
|