@gaia-ai/addon-gaia-ui 0.11.2 → 0.11.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/dist/src/Component/top-tabs/index.d.ts +18 -1
- package/dist/src/Component/top-tabs/index.js +15 -2
- package/dist/src/Entity/conductor/Data/conductors.d.ts +174 -15
- package/dist/src/Entity/conductor/Data/conductors.js +74 -16
- package/dist/src/Entity/conductor/Data/own-conductor.js +17 -3
- package/dist/src/Entity/conductor/Data/process.d.ts +29 -2
- package/dist/src/Entity/conductor/Data/process.js +40 -4
- package/dist/src/Entity/conductor/Screen/conductors.d.ts +43 -0
- package/dist/src/Entity/conductor/Screen/conductors.js +113 -32
- package/dist/src/Entity/conductor/Screen/project-conductors.d.ts +87 -0
- package/dist/src/Entity/conductor/Screen/project-conductors.js +245 -0
- package/dist/src/Entity/project/Data/projects.d.ts +7 -5
- package/dist/src/Entity/project/Data/projects.js +8 -6
- package/dist/src/Kernel/splash.d.ts +8 -0
- package/dist/src/Kernel/splash.js +11 -1
- package/dist/src/Routing/routes.js +2 -0
- package/package.json +2 -2
|
@@ -17,7 +17,18 @@ export declare const ROOT_TABS: readonly [{
|
|
|
17
17
|
readonly id: "teams";
|
|
18
18
|
readonly label: "Teams";
|
|
19
19
|
}];
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* The project level: the tabs of an opened project.
|
|
22
|
+
*
|
|
23
|
+
* GAIA-450 added `conductors` (`ProjectConductorsScreen`, `project.conductors`,
|
|
24
|
+
* order 50) — LAST, by the same "which project it serves is a COLUMN of that
|
|
25
|
+
* screen" argument GAIA-232 made for the root level, applied the other way:
|
|
26
|
+
* once a project IS chosen, seeing who serves it is a natural final stop
|
|
27
|
+
* beside Tickets, Ticket-Agents and Runs. Missing here — as it was for one
|
|
28
|
+
* `coding` pass, review B11 — makes the screen and its route entirely
|
|
29
|
+
* unreachable by Tab or click: `tabsOfLocation` reads this list, not the
|
|
30
|
+
* route registry's `tab` metadata, for what a level cycles through.
|
|
31
|
+
*/
|
|
21
32
|
export declare const PROJECT_TABS: readonly [{
|
|
22
33
|
readonly id: "dashboard";
|
|
23
34
|
readonly label: "Dashboard";
|
|
@@ -30,6 +41,9 @@ export declare const PROJECT_TABS: readonly [{
|
|
|
30
41
|
}, {
|
|
31
42
|
readonly id: "runs";
|
|
32
43
|
readonly label: "Runs";
|
|
44
|
+
}, {
|
|
45
|
+
readonly id: "conductors";
|
|
46
|
+
readonly label: "Conductors";
|
|
33
47
|
}];
|
|
34
48
|
/**
|
|
35
49
|
* The ticket level: the sections of ONE OPEN TICKET — GAIA-346.
|
|
@@ -139,6 +153,9 @@ export declare const TAB_LEVELS: {
|
|
|
139
153
|
}, {
|
|
140
154
|
readonly id: "runs";
|
|
141
155
|
readonly label: "Runs";
|
|
156
|
+
}, {
|
|
157
|
+
readonly id: "conductors";
|
|
158
|
+
readonly label: "Conductors";
|
|
142
159
|
}];
|
|
143
160
|
readonly ticket: readonly [{
|
|
144
161
|
readonly id: "overview";
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// root dashboard, projects, project-picker → Dashboard · Projects ·
|
|
6
6
|
// Conductors · Teams
|
|
7
7
|
// project dashboard, project, ticket-agents, → Dashboard · Tickets ·
|
|
8
|
-
// project-runs
|
|
8
|
+
// project-runs, project-conductors Ticket-Agents · Runs ·
|
|
9
|
+
// Conductors
|
|
9
10
|
// ticket overview, comments, related, runs → the sections of one open
|
|
10
11
|
// ticket, in the bar row every
|
|
11
12
|
// other level uses (GAIA-346)
|
|
@@ -82,12 +83,24 @@ export const ROOT_TABS = [
|
|
|
82
83
|
// rows still open one person directly.
|
|
83
84
|
{ id: 'teams', label: 'Teams' },
|
|
84
85
|
];
|
|
85
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* The project level: the tabs of an opened project.
|
|
88
|
+
*
|
|
89
|
+
* GAIA-450 added `conductors` (`ProjectConductorsScreen`, `project.conductors`,
|
|
90
|
+
* order 50) — LAST, by the same "which project it serves is a COLUMN of that
|
|
91
|
+
* screen" argument GAIA-232 made for the root level, applied the other way:
|
|
92
|
+
* once a project IS chosen, seeing who serves it is a natural final stop
|
|
93
|
+
* beside Tickets, Ticket-Agents and Runs. Missing here — as it was for one
|
|
94
|
+
* `coding` pass, review B11 — makes the screen and its route entirely
|
|
95
|
+
* unreachable by Tab or click: `tabsOfLocation` reads this list, not the
|
|
96
|
+
* route registry's `tab` metadata, for what a level cycles through.
|
|
97
|
+
*/
|
|
86
98
|
export const PROJECT_TABS = [
|
|
87
99
|
{ id: 'dashboard', label: 'Dashboard' },
|
|
88
100
|
{ id: 'tickets', label: 'Tickets' },
|
|
89
101
|
{ id: 'ticket-agents', label: 'Ticket-Agents' },
|
|
90
102
|
{ id: 'runs', label: 'Runs' },
|
|
103
|
+
{ id: 'conductors', label: 'Conductors' },
|
|
91
104
|
];
|
|
92
105
|
/**
|
|
93
106
|
* The ticket level: the sections of ONE OPEN TICKET — GAIA-346.
|
|
@@ -41,18 +41,39 @@ export interface ConductorRow extends Conductor {
|
|
|
41
41
|
lastPid: number | undefined;
|
|
42
42
|
/** The config stem it was started from, as `--conductor <name>` takes it. */
|
|
43
43
|
conductorName: string | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* The MachineContext host stem this record last heartbeat with (GAIA-450
|
|
46
|
+
* AC-4/B7) — what a stop compares against this machine's own before
|
|
47
|
+
* signalling. Resolved from the related `gaia_machine` (`host_key` is that
|
|
48
|
+
* entity's field, never `gaia_conductor`'s), never from the conductor row
|
|
49
|
+
* itself.
|
|
50
|
+
*/
|
|
51
|
+
hostKey: string | undefined;
|
|
44
52
|
/** Liveness, derived — never the raw `status`. */
|
|
45
53
|
derivedStatus: ConductorStatus;
|
|
46
54
|
/**
|
|
47
55
|
* Whether this conductor's workspace exists on THIS machine.
|
|
48
56
|
*
|
|
49
57
|
* 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
|
|
51
|
-
* — filtering by machine_id would mean substring-matching a composed
|
|
52
|
-
*
|
|
53
|
-
*
|
|
58
|
+
* machines' conductors appear in this list. They are shown rather than
|
|
59
|
+
* hidden — filtering by machine_id would mean substring-matching a composed
|
|
60
|
+
* string. This is a FILESYSTEM fact only (used to decide whether the log
|
|
61
|
+
* tail can read a local file); it is not an identity gate — see {@link mine}
|
|
62
|
+
* for that.
|
|
54
63
|
*/
|
|
55
64
|
isLocal: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Whether this row's `hostKey` matches THIS machine's own (GAIA-450 B10) —
|
|
67
|
+
* the identity gate for starting/stopping and for the view-only marking.
|
|
68
|
+
*
|
|
69
|
+
* Deliberately distinct from {@link isLocal}: two `gaia_conductor` records
|
|
70
|
+
* can name the SAME `workspace_root` under different machines (GAIA-281
|
|
71
|
+
* Finding 2), which makes `isLocal` true for both — a path is not proof of
|
|
72
|
+
* identity, which is the whole thesis this ticket is built on. `false` when
|
|
73
|
+
* `hostKey` is absent (a record from before GAIA-450, or an unreadable
|
|
74
|
+
* machine): an unproven identity is never treated as this machine's own.
|
|
75
|
+
*/
|
|
76
|
+
mine: boolean;
|
|
56
77
|
}
|
|
57
78
|
/** What a conductor ROW cannot know on its own, handed to the columns per frame. */
|
|
58
79
|
export interface ConductorListContext {
|
|
@@ -65,12 +86,12 @@ export interface ConductorListContext {
|
|
|
65
86
|
readonly projectNames: ReadonlyMap<string, string>;
|
|
66
87
|
}
|
|
67
88
|
/**
|
|
68
|
-
* The conductors list, declared once (GAIA-345 D2/D10) — its
|
|
89
|
+
* The conductors list, declared once (GAIA-345 D2/D10) — its nine columns, the
|
|
69
90
|
* noun every one of its node ids is prefixed with, and what it says when it is
|
|
70
91
|
* empty.
|
|
71
92
|
*
|
|
72
93
|
* It GAINED A HEADER with this ticket (D6). This is the list a header earns its
|
|
73
|
-
* keep in most:
|
|
94
|
+
* keep in most: nine columns of ids, paths and numbers are not self-describing
|
|
74
95
|
* the way three named ones were. The consequence is stated because it is a
|
|
75
96
|
* behaviour change — the list grows one row at the top and its selection
|
|
76
97
|
* highlight moves down one physical row. Nothing computes that offset:
|
|
@@ -94,7 +115,7 @@ export interface ConductorListContext {
|
|
|
94
115
|
* than as a bound, so a narrow terminal leaves it exactly the columns it had
|
|
95
116
|
* before this ticket and a wide one hands it a share.
|
|
96
117
|
*
|
|
97
|
-
* The other
|
|
118
|
+
* The other seven do not grow, and each for its own reason. `MACHINE` and
|
|
98
119
|
* `WORKSPACE` are free text too, and are the ONE exception to the rule: they
|
|
99
120
|
* are elided from the LEFT (see {@link elideLeft}), which needs a declared
|
|
100
121
|
* width to cut against and keeps the distinguishing tail — a grower would hand
|
|
@@ -102,7 +123,8 @@ export interface ConductorListContext {
|
|
|
102
123
|
* elision preserves. `STATUS`, `AGE` and `PID` are closed vocabularies that
|
|
103
124
|
* cannot use a surplus at all. `MARKS` is a closed vocabulary too and keeps a
|
|
104
125
|
* real budget for a measured reason: `width: 0, grow: 0` collapses it to zero
|
|
105
|
-
* columns and the mark vanishes.
|
|
126
|
+
* columns and the mark vanishes. `LOAD` (GAIA-450 AC-7) is a fixed `<current>/
|
|
127
|
+
* <max>` ratio, not free text, so it takes a budget rather than a share too.
|
|
106
128
|
*/
|
|
107
129
|
export declare const CONDUCTOR_TABLE: {
|
|
108
130
|
readonly noun: "conductor";
|
|
@@ -162,7 +184,7 @@ export declare const CONDUCTOR_TABLE: {
|
|
|
162
184
|
readonly get: (row: ConductorRow, ctx: ConductorListContext) => string;
|
|
163
185
|
}, {
|
|
164
186
|
/** A CLOSED VOCABULARY. `ConductorStatus`'s reachable members here are
|
|
165
|
-
* `
|
|
187
|
+
* `online | stale | offline` — this screen can never produce
|
|
166
188
|
* `registry-only`, because every row IS a control-plane record — whose
|
|
167
189
|
* longest is 7, so 9 shows each of them whole. */
|
|
168
190
|
readonly key: "status";
|
|
@@ -181,7 +203,7 @@ export declare const CONDUCTOR_TABLE: {
|
|
|
181
203
|
}, {
|
|
182
204
|
readonly key: "workspace";
|
|
183
205
|
readonly label: "WORKSPACE";
|
|
184
|
-
readonly width:
|
|
206
|
+
readonly width: 18;
|
|
185
207
|
readonly fields: readonly ["workspace_root"];
|
|
186
208
|
readonly get: (row: ConductorRow) => string;
|
|
187
209
|
}, {
|
|
@@ -199,26 +221,159 @@ export declare const CONDUCTOR_TABLE: {
|
|
|
199
221
|
* trailing slot it used to hold: everything it can say is `…`,
|
|
200
222
|
* `elsewhere` or both, and 11 is `… elsewhere`.
|
|
201
223
|
*
|
|
202
|
-
* A
|
|
203
|
-
* is user-scoped, not machine-scoped, so a user's other machines'
|
|
224
|
+
* A conductor that is not MINE is MARKED rather than hidden. The control
|
|
225
|
+
* plane is user-scoped, not machine-scoped, so a user's other machines'
|
|
204
226
|
* conductors are legitimately in this list — and silently dropping them
|
|
205
227
|
* would make the cockpit disagree with `gaia conductor ls` for no visible
|
|
206
|
-
* reason. The mark says why their actions are inert here:
|
|
207
|
-
*
|
|
228
|
+
* reason. The mark says why their actions are inert here: an identity
|
|
229
|
+
* that is not this machine's own (GAIA-450 B10 — `mine`, not `isLocal`'s
|
|
230
|
+
* path check, since two records can share one `workspace_root` under
|
|
231
|
+
* different machines).
|
|
208
232
|
*/
|
|
209
233
|
readonly key: "marks";
|
|
210
234
|
readonly label: "MARKS";
|
|
211
235
|
readonly width: 11;
|
|
212
236
|
readonly fields: readonly ["workspace_root"];
|
|
213
237
|
readonly get: (row: ConductorRow, ctx: ConductorListContext) => string;
|
|
238
|
+
}, {
|
|
239
|
+
/**
|
|
240
|
+
* `<current>/<max>` (GAIA-450 AC-7) — the utilization `capacityOf()`
|
|
241
|
+
* already computes from the sideloaded machine, reaching the row but no
|
|
242
|
+
* column until this one. 5 fits every realistic pair (`max_parallel` is
|
|
243
|
+
* a small hand-set integer); a triple-digit load would overflow it, and
|
|
244
|
+
* that is a capacity-planning problem this list only reports, not one
|
|
245
|
+
* this column need budget for.
|
|
246
|
+
*/
|
|
247
|
+
readonly key: "load";
|
|
248
|
+
readonly label: "LOAD";
|
|
249
|
+
readonly width: 5;
|
|
250
|
+
readonly fields: readonly ["current_load", "max_parallel"];
|
|
251
|
+
readonly get: (row: ConductorRow) => string;
|
|
214
252
|
}];
|
|
215
253
|
};
|
|
254
|
+
/**
|
|
255
|
+
* The project-scoped Conductors tab's table (GAIA-450 D13) — the SAME column
|
|
256
|
+
* definitions as {@link CONDUCTOR_TABLE}, minus `project`: every row is
|
|
257
|
+
* already this one project's, so the column would repeat one value down the
|
|
258
|
+
* whole list. No new component or column is declared; this reuses the exact
|
|
259
|
+
* objects, so the two lists cannot drift out of step with each other.
|
|
260
|
+
*/
|
|
261
|
+
export declare const PROJECT_CONDUCTOR_TABLE: {
|
|
262
|
+
readonly columns: ({
|
|
263
|
+
/**
|
|
264
|
+
* A GROWER — free text, right-truncated, and the column an operator reads
|
|
265
|
+
* first. Declared FIRST and growing all the same: `flexGrow` is a
|
|
266
|
+
* property of the cell, not of its position in the row (measured, spec
|
|
267
|
+
* R2). Its `grow: 2` against `PROJECT`'s 1 is the identity column's larger
|
|
268
|
+
* claim on the surplus, argued above.
|
|
269
|
+
*
|
|
270
|
+
* `min: 20` is the ONE floor on this list, and it is preservation rather
|
|
271
|
+
* than invention: 20 is the budget this column has carried since
|
|
272
|
+
* GAIA-232, and without it a narrow terminal squeezes the grower to
|
|
273
|
+
* nothing while seven fixed budgets keep theirs — measured at 80 columns,
|
|
274
|
+
* NAME came out ONE column wide and the label read `c…`. The other seven
|
|
275
|
+
* columns need 106 columns between them, so below ~126 the row runs past
|
|
276
|
+
* the right edge and the renderer clips it there. That is exactly what
|
|
277
|
+
* this list did before this ticket, at every width; the floor keeps the
|
|
278
|
+
* narrow case as it was and lets the wide case improve.
|
|
279
|
+
*/
|
|
280
|
+
readonly key: "name";
|
|
281
|
+
readonly label: "NAME";
|
|
282
|
+
readonly width: 0;
|
|
283
|
+
readonly min: 20;
|
|
284
|
+
readonly grow: 2;
|
|
285
|
+
readonly fields: readonly ["label", "machine_id"];
|
|
286
|
+
readonly get: (row: ConductorRow) => string;
|
|
287
|
+
} | {
|
|
288
|
+
readonly key: "machine";
|
|
289
|
+
readonly label: "MACHINE";
|
|
290
|
+
readonly width: 20;
|
|
291
|
+
readonly fields: readonly ["machine_id"];
|
|
292
|
+
readonly get: (row: ConductorRow) => string;
|
|
293
|
+
} | {
|
|
294
|
+
/** A CLOSED VOCABULARY. `ConductorStatus`'s reachable members here are
|
|
295
|
+
* `online | stale | offline` — this screen can never produce
|
|
296
|
+
* `registry-only`, because every row IS a control-plane record — whose
|
|
297
|
+
* longest is 7, so 9 shows each of them whole. */
|
|
298
|
+
readonly key: "status";
|
|
299
|
+
readonly label: "STATUS";
|
|
300
|
+
readonly width: 9;
|
|
301
|
+
readonly fields: readonly ["status", "lease_expires_at", "last_seen"];
|
|
302
|
+
readonly get: (row: ConductorRow) => ConductorStatus;
|
|
303
|
+
} | {
|
|
304
|
+
/** `humanizeAge`'s own vocabulary, exactly as on the projects list: `—`,
|
|
305
|
+
* `just now`, `<n>m|h|d ago`. 8 is the longest of them. */
|
|
306
|
+
readonly key: "age";
|
|
307
|
+
readonly label: "AGE";
|
|
308
|
+
readonly width: 8;
|
|
309
|
+
readonly fields: readonly ["last_seen"];
|
|
310
|
+
readonly get: (row: ConductorRow, ctx: ConductorListContext) => string;
|
|
311
|
+
} | {
|
|
312
|
+
readonly key: "workspace";
|
|
313
|
+
readonly label: "WORKSPACE";
|
|
314
|
+
readonly width: 18;
|
|
315
|
+
readonly fields: readonly ["workspace_root"];
|
|
316
|
+
readonly get: (row: ConductorRow) => string;
|
|
317
|
+
} | {
|
|
318
|
+
/** A pid is at most 7 digits — Linux's `pid_max` ceiling is 4 194 304 —
|
|
319
|
+
* and a conductor that never reported one shows `-` rather than a
|
|
320
|
+
* fabricated number. */
|
|
321
|
+
readonly key: "pid";
|
|
322
|
+
readonly label: "PID";
|
|
323
|
+
readonly width: 7;
|
|
324
|
+
readonly fields: readonly ["last_pid"];
|
|
325
|
+
readonly get: (row: ConductorRow) => string;
|
|
326
|
+
} | {
|
|
327
|
+
/**
|
|
328
|
+
* A closed vocabulary too, which is why it takes a budget instead of the
|
|
329
|
+
* trailing slot it used to hold: everything it can say is `…`,
|
|
330
|
+
* `elsewhere` or both, and 11 is `… elsewhere`.
|
|
331
|
+
*
|
|
332
|
+
* A conductor that is not MINE is MARKED rather than hidden. The control
|
|
333
|
+
* plane is user-scoped, not machine-scoped, so a user's other machines'
|
|
334
|
+
* conductors are legitimately in this list — and silently dropping them
|
|
335
|
+
* would make the cockpit disagree with `gaia conductor ls` for no visible
|
|
336
|
+
* reason. The mark says why their actions are inert here: an identity
|
|
337
|
+
* that is not this machine's own (GAIA-450 B10 — `mine`, not `isLocal`'s
|
|
338
|
+
* path check, since two records can share one `workspace_root` under
|
|
339
|
+
* different machines).
|
|
340
|
+
*/
|
|
341
|
+
readonly key: "marks";
|
|
342
|
+
readonly label: "MARKS";
|
|
343
|
+
readonly width: 11;
|
|
344
|
+
readonly fields: readonly ["workspace_root"];
|
|
345
|
+
readonly get: (row: ConductorRow, ctx: ConductorListContext) => string;
|
|
346
|
+
} | {
|
|
347
|
+
/**
|
|
348
|
+
* `<current>/<max>` (GAIA-450 AC-7) — the utilization `capacityOf()`
|
|
349
|
+
* already computes from the sideloaded machine, reaching the row but no
|
|
350
|
+
* column until this one. 5 fits every realistic pair (`max_parallel` is
|
|
351
|
+
* a small hand-set integer); a triple-digit load would overflow it, and
|
|
352
|
+
* that is a capacity-planning problem this list only reports, not one
|
|
353
|
+
* this column need budget for.
|
|
354
|
+
*/
|
|
355
|
+
readonly key: "load";
|
|
356
|
+
readonly label: "LOAD";
|
|
357
|
+
readonly width: 5;
|
|
358
|
+
readonly fields: readonly ["current_load", "max_parallel"];
|
|
359
|
+
readonly get: (row: ConductorRow) => string;
|
|
360
|
+
})[];
|
|
361
|
+
readonly noun: "conductor";
|
|
362
|
+
readonly emptyMessage: "No conductors on this control plane.";
|
|
363
|
+
};
|
|
216
364
|
/** What the row projection needs from the outside world, injected for tests. */
|
|
217
365
|
export interface ConductorRowContext {
|
|
218
366
|
/** Unix seconds; the liveness derivation's clock. */
|
|
219
367
|
now: number;
|
|
220
368
|
/** Whether a path exists on this machine. */
|
|
221
369
|
exists: (path: string) => boolean;
|
|
370
|
+
/**
|
|
371
|
+
* THIS machine's own `host_key` (GAIA-450 B10), resolved once by the caller
|
|
372
|
+
* before the read — the same MachineContext value `signalVerifiedProcess`
|
|
373
|
+
* and the CLI's `hostKeyOf(config)` compare against. Compared against each
|
|
374
|
+
* row's own `hostKey` to derive {@link ConductorRow.mine}.
|
|
375
|
+
*/
|
|
376
|
+
thisHostKey: string;
|
|
222
377
|
}
|
|
223
378
|
/**
|
|
224
379
|
* The conductor collection as cockpit rows, in server order.
|
|
@@ -227,8 +382,12 @@ export interface ConductorRowContext {
|
|
|
227
382
|
* not removed conductor fields. Swallows a read failure into an empty list on
|
|
228
383
|
* purpose: the Conductors screen re-reads every 5 s, so one failed poll must
|
|
229
384
|
* leave the screen navigable rather than replace it with an error.
|
|
385
|
+
*
|
|
386
|
+
* GAIA-450 D13: an optional `projectId` narrows the read to the project's own
|
|
387
|
+
* conductors — the project-scoped Conductors tab's whole filter, applied at
|
|
388
|
+
* the query rather than client-side so the read stays one round trip.
|
|
230
389
|
*/
|
|
231
|
-
export declare const listConductorRows: (client: DropshClient, ctx: ConductorRowContext) => Promise<ConductorRow[]>;
|
|
390
|
+
export declare const listConductorRows: (client: DropshClient, ctx: ConductorRowContext, projectId?: string) => Promise<ConductorRow[]>;
|
|
232
391
|
/**
|
|
233
392
|
* Project uuid → name, for the Conductors screen's project column.
|
|
234
393
|
*
|
|
@@ -65,31 +65,37 @@ const MACHINE_ID_WIDTH = 20;
|
|
|
65
65
|
* STATUS 9, AGE 8, WORKSPACE 30, PID 7, MARKS 11, plus seven separators) while
|
|
66
66
|
* `gaia ui` opens at 120, so the terminal cut the LAST column: measured on a
|
|
67
67
|
* real pty, the header ended `PID MAR` and a non-local conductor's
|
|
68
|
-
* `elsewhere` came out `elsew`.
|
|
68
|
+
* `elsewhere` came out `elsew`. GAIA-345 brought it to 24, fitting exactly 120.
|
|
69
|
+
*
|
|
70
|
+
* GAIA-450 AC-7 spent six of those back: adding `LOAD` (its own 5-wide budget
|
|
71
|
+
* plus the separator every column past the first pays) reopened the same
|
|
72
|
+
* 126-column overrun this constant exists to close, so `WORKSPACE` pays the
|
|
73
|
+
* same six columns again — 18, not 24 — to keep the row at exactly 120.
|
|
69
74
|
*
|
|
70
75
|
* WHY THIS COLUMN GIVES THE SIX. `WORKSPACE` is the only column in the row whose
|
|
71
76
|
* cut is free: it is elided from the LEFT, so it loses six columns of the HEAD —
|
|
72
77
|
* the `/home/<user>/projects/…` prefix every conductor workspace on a machine
|
|
73
78
|
* shares — and keeps the distinguishing tail either way. Every other column
|
|
74
79
|
* would pay in information: `MACHINE` elides left too but is already the
|
|
75
|
-
* narrower of the pair, the three closed vocabularies
|
|
76
|
-
* longest member, and `NAME`/`PROJECT` are the
|
|
80
|
+
* narrower of the pair, the three closed vocabularies (and `LOAD`'s fixed
|
|
81
|
+
* ratio) are sized to their own longest member, and `NAME`/`PROJECT` are the
|
|
82
|
+
* growers this ticket widened.
|
|
77
83
|
*
|
|
78
|
-
* A DECLARED BUDGET, never a measurement (spec D4).
|
|
84
|
+
* A DECLARED BUDGET, never a measurement (spec D4). 18 is not the width of
|
|
79
85
|
* anyone's path — no page's data is consulted to arrive at it, so the columns do
|
|
80
86
|
* not move when the list does. It is shared by the column's `width` and by the
|
|
81
87
|
* {@link elideLeft} its accessor applies for exactly that reason: two numbers
|
|
82
88
|
* could drift, and a marker cut at one width inside a cell laid out at another
|
|
83
89
|
* is the defect the single constant makes unspellable.
|
|
84
90
|
*/
|
|
85
|
-
const PATH_WIDTH =
|
|
91
|
+
const PATH_WIDTH = 18;
|
|
86
92
|
/**
|
|
87
|
-
* The conductors list, declared once (GAIA-345 D2/D10) — its
|
|
93
|
+
* The conductors list, declared once (GAIA-345 D2/D10) — its nine columns, the
|
|
88
94
|
* noun every one of its node ids is prefixed with, and what it says when it is
|
|
89
95
|
* empty.
|
|
90
96
|
*
|
|
91
97
|
* It GAINED A HEADER with this ticket (D6). This is the list a header earns its
|
|
92
|
-
* keep in most:
|
|
98
|
+
* keep in most: nine columns of ids, paths and numbers are not self-describing
|
|
93
99
|
* the way three named ones were. The consequence is stated because it is a
|
|
94
100
|
* behaviour change — the list grows one row at the top and its selection
|
|
95
101
|
* highlight moves down one physical row. Nothing computes that offset:
|
|
@@ -113,7 +119,7 @@ const PATH_WIDTH = 24;
|
|
|
113
119
|
* than as a bound, so a narrow terminal leaves it exactly the columns it had
|
|
114
120
|
* before this ticket and a wide one hands it a share.
|
|
115
121
|
*
|
|
116
|
-
* The other
|
|
122
|
+
* The other seven do not grow, and each for its own reason. `MACHINE` and
|
|
117
123
|
* `WORKSPACE` are free text too, and are the ONE exception to the rule: they
|
|
118
124
|
* are elided from the LEFT (see {@link elideLeft}), which needs a declared
|
|
119
125
|
* width to cut against and keeps the distinguishing tail — a grower would hand
|
|
@@ -121,7 +127,8 @@ const PATH_WIDTH = 24;
|
|
|
121
127
|
* elision preserves. `STATUS`, `AGE` and `PID` are closed vocabularies that
|
|
122
128
|
* cannot use a surplus at all. `MARKS` is a closed vocabulary too and keeps a
|
|
123
129
|
* real budget for a measured reason: `width: 0, grow: 0` collapses it to zero
|
|
124
|
-
* columns and the mark vanishes.
|
|
130
|
+
* columns and the mark vanishes. `LOAD` (GAIA-450 AC-7) is a fixed `<current>/
|
|
131
|
+
* <max>` ratio, not free text, so it takes a budget rather than a share too.
|
|
125
132
|
*/
|
|
126
133
|
export const CONDUCTOR_TABLE = {
|
|
127
134
|
noun: 'conductor',
|
|
@@ -188,7 +195,7 @@ export const CONDUCTOR_TABLE = {
|
|
|
188
195
|
},
|
|
189
196
|
{
|
|
190
197
|
/** A CLOSED VOCABULARY. `ConductorStatus`'s reachable members here are
|
|
191
|
-
* `
|
|
198
|
+
* `online | stale | offline` — this screen can never produce
|
|
192
199
|
* `registry-only`, because every row IS a control-plane record — whose
|
|
193
200
|
* longest is 7, so 9 shows each of them whole. */
|
|
194
201
|
key: 'status',
|
|
@@ -229,23 +236,51 @@ export const CONDUCTOR_TABLE = {
|
|
|
229
236
|
* trailing slot it used to hold: everything it can say is `…`,
|
|
230
237
|
* `elsewhere` or both, and 11 is `… elsewhere`.
|
|
231
238
|
*
|
|
232
|
-
* A
|
|
233
|
-
* is user-scoped, not machine-scoped, so a user's other machines'
|
|
239
|
+
* A conductor that is not MINE is MARKED rather than hidden. The control
|
|
240
|
+
* plane is user-scoped, not machine-scoped, so a user's other machines'
|
|
234
241
|
* conductors are legitimately in this list — and silently dropping them
|
|
235
242
|
* would make the cockpit disagree with `gaia conductor ls` for no visible
|
|
236
|
-
* reason. The mark says why their actions are inert here:
|
|
237
|
-
*
|
|
243
|
+
* reason. The mark says why their actions are inert here: an identity
|
|
244
|
+
* that is not this machine's own (GAIA-450 B10 — `mine`, not `isLocal`'s
|
|
245
|
+
* path check, since two records can share one `workspace_root` under
|
|
246
|
+
* different machines).
|
|
238
247
|
*/
|
|
239
248
|
key: 'marks',
|
|
240
249
|
label: 'MARKS',
|
|
241
250
|
width: 11,
|
|
242
251
|
fields: ['workspace_root'],
|
|
243
|
-
get: (row, ctx) => [ctx.busyIds.has(row.id) ? '…' : '', row.
|
|
252
|
+
get: (row, ctx) => [ctx.busyIds.has(row.id) ? '…' : '', row.mine ? '' : 'elsewhere']
|
|
244
253
|
.filter((mark) => mark !== '')
|
|
245
254
|
.join(' '),
|
|
246
255
|
},
|
|
256
|
+
{
|
|
257
|
+
/**
|
|
258
|
+
* `<current>/<max>` (GAIA-450 AC-7) — the utilization `capacityOf()`
|
|
259
|
+
* already computes from the sideloaded machine, reaching the row but no
|
|
260
|
+
* column until this one. 5 fits every realistic pair (`max_parallel` is
|
|
261
|
+
* a small hand-set integer); a triple-digit load would overflow it, and
|
|
262
|
+
* that is a capacity-planning problem this list only reports, not one
|
|
263
|
+
* this column need budget for.
|
|
264
|
+
*/
|
|
265
|
+
key: 'load',
|
|
266
|
+
label: 'LOAD',
|
|
267
|
+
width: 5,
|
|
268
|
+
fields: ['current_load', 'max_parallel'],
|
|
269
|
+
get: (row) => `${row.currentLoad}/${row.maxParallel}`,
|
|
270
|
+
},
|
|
247
271
|
],
|
|
248
272
|
};
|
|
273
|
+
/**
|
|
274
|
+
* The project-scoped Conductors tab's table (GAIA-450 D13) — the SAME column
|
|
275
|
+
* definitions as {@link CONDUCTOR_TABLE}, minus `project`: every row is
|
|
276
|
+
* already this one project's, so the column would repeat one value down the
|
|
277
|
+
* whole list. No new component or column is declared; this reuses the exact
|
|
278
|
+
* objects, so the two lists cannot drift out of step with each other.
|
|
279
|
+
*/
|
|
280
|
+
export const PROJECT_CONDUCTOR_TABLE = {
|
|
281
|
+
...CONDUCTOR_TABLE,
|
|
282
|
+
columns: CONDUCTOR_TABLE.columns.filter((column) => column.key !== 'project'),
|
|
283
|
+
};
|
|
249
284
|
const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
250
285
|
/** Id of a single-valued relationship linkage (`data` object), or null. */
|
|
251
286
|
function singleRelId(rel) {
|
|
@@ -294,6 +329,18 @@ function capacityOf(raw, machinesById) {
|
|
|
294
329
|
maxParallel: num(machine?.attributes?.max_parallel) ?? num(attrs.max_parallel) ?? 0,
|
|
295
330
|
};
|
|
296
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* `host_key` from the sideloaded machine (GAIA-450 B7) — `gaia_conductor`
|
|
334
|
+
* carries no such field at all; `filter[host_key]` against it is a
|
|
335
|
+
* server-side 400. Reading `raw.attributes.host_key` here would repeat the
|
|
336
|
+
* exact defect the CLI's `listConductors()` had: always `undefined` in
|
|
337
|
+
* reality, present only in a fixture that hand-writes it on the wrong entity.
|
|
338
|
+
*/
|
|
339
|
+
function hostKeyOf(raw, machinesById) {
|
|
340
|
+
const machineId = singleRelId(raw.relationships?.machine);
|
|
341
|
+
const machine = machineId ? machinesById.get(machineId) : undefined;
|
|
342
|
+
return str(machine?.attributes?.host_key);
|
|
343
|
+
}
|
|
297
344
|
const toConductorRow = (raw, ctx, machinesById) => {
|
|
298
345
|
const r = asResource(raw);
|
|
299
346
|
const workspaceRoot = str(r.attr('workspace_root')) ?? '';
|
|
@@ -302,6 +349,7 @@ const toConductorRow = (raw, ctx, machinesById) => {
|
|
|
302
349
|
const lastSeen = toUnixSeconds(r.attr('last_seen')) ?? 0;
|
|
303
350
|
const leaseExpiresAt = toUnixSeconds(r.attr('lease_expires_at')) ?? 0;
|
|
304
351
|
const { currentLoad, maxParallel } = capacityOf(raw, machinesById);
|
|
352
|
+
const hostKey = hostKeyOf(raw, machinesById);
|
|
305
353
|
return {
|
|
306
354
|
...toConductor(r),
|
|
307
355
|
workspaceRoot,
|
|
@@ -311,6 +359,7 @@ const toConductorRow = (raw, ctx, machinesById) => {
|
|
|
311
359
|
maxParallel,
|
|
312
360
|
lastPid: num(r.attr('last_pid')),
|
|
313
361
|
conductorName: str(r.attr('conductor_name')),
|
|
362
|
+
hostKey,
|
|
314
363
|
// `present: true` because the row IS a control-plane record — which is why
|
|
315
364
|
// `registry-only` can never come out of this screen.
|
|
316
365
|
derivedStatus: deriveConductorStatus({
|
|
@@ -322,6 +371,9 @@ const toConductorRow = (raw, ctx, machinesById) => {
|
|
|
322
371
|
// A conductor that never reported a workspace cannot be local, whatever the
|
|
323
372
|
// filesystem would say about an empty path.
|
|
324
373
|
isLocal: workspaceRoot !== '' && ctx.exists(workspaceRoot),
|
|
374
|
+
// GAIA-450 B10: an absent hostKey is never waved through as "mine" — an
|
|
375
|
+
// unproven identity is refused, the same rule signalVerifiedProcess uses.
|
|
376
|
+
mine: hostKey !== undefined && hostKey === ctx.thisHostKey,
|
|
325
377
|
};
|
|
326
378
|
};
|
|
327
379
|
/**
|
|
@@ -331,9 +383,15 @@ const toConductorRow = (raw, ctx, machinesById) => {
|
|
|
331
383
|
* not removed conductor fields. Swallows a read failure into an empty list on
|
|
332
384
|
* purpose: the Conductors screen re-reads every 5 s, so one failed poll must
|
|
333
385
|
* leave the screen navigable rather than replace it with an error.
|
|
386
|
+
*
|
|
387
|
+
* GAIA-450 D13: an optional `projectId` narrows the read to the project's own
|
|
388
|
+
* conductors — the project-scoped Conductors tab's whole filter, applied at
|
|
389
|
+
* the query rather than client-side so the read stays one round trip.
|
|
334
390
|
*/
|
|
335
|
-
export const listConductorRows = async (client, ctx) => {
|
|
391
|
+
export const listConductorRows = async (client, ctx, projectId) => {
|
|
336
392
|
const params = new DrupalJsonApiParams().addInclude(['machine']);
|
|
393
|
+
if (projectId)
|
|
394
|
+
params.addFilter('project_id.id', projectId);
|
|
337
395
|
try {
|
|
338
396
|
const doc = (await client.get('gaia_conductor/gaia_conductor', params));
|
|
339
397
|
const machinesById = new Map();
|
|
@@ -14,7 +14,14 @@
|
|
|
14
14
|
// path comparison, never a longest-prefix walk. So a worktree — which has its
|
|
15
15
|
// own `.gaia` — resolves to no conductor rather than quietly inheriting its
|
|
16
16
|
// parent checkout's project, exactly as before.
|
|
17
|
-
|
|
17
|
+
//
|
|
18
|
+
// GAIA-450 D3/D15: `workspace_root` now holds the checkout root, not this
|
|
19
|
+
// `.gaia` config directory, because every heartbeat rewrites the field going
|
|
20
|
+
// forward. A record written before that change still carries the legacy
|
|
21
|
+
// `.gaia` value until its next heartbeat, so the match accepts EITHER form —
|
|
22
|
+
// the derived checkout root (`dirname(gaiaDir)`) or the legacy `gaiaDir`
|
|
23
|
+
// itself — so a cockpit opened against an un-healed record still resolves.
|
|
24
|
+
import { dirname, resolve } from 'node:path';
|
|
18
25
|
import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
|
|
19
26
|
const NOTHING = { machineId: undefined, project: undefined };
|
|
20
27
|
/** Id of a single-valued relationship linkage (`data` object), or null. */
|
|
@@ -35,12 +42,19 @@ const str = (v) => typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
|
35
42
|
* which the writer sees and can change.
|
|
36
43
|
*/
|
|
37
44
|
export function selectOwnConductor(doc, gaiaDir) {
|
|
38
|
-
|
|
45
|
+
// The current form (GAIA-450 D3): workspace_root is the checkout root.
|
|
46
|
+
const checkoutRoot = resolve(dirname(gaiaDir));
|
|
47
|
+
// The legacy form: a record whose heartbeat has not yet rewritten the
|
|
48
|
+
// field still carries the `.gaia` config directory itself.
|
|
49
|
+
const legacyTarget = resolve(gaiaDir);
|
|
39
50
|
const own = (doc.data ?? []).find((c) => {
|
|
40
51
|
const root = str(c.attributes?.workspace_root);
|
|
41
52
|
// A conductor that never reported a workspace matches no directory,
|
|
42
53
|
// whatever `resolve('')` would make of an empty path.
|
|
43
|
-
|
|
54
|
+
if (root === undefined)
|
|
55
|
+
return false;
|
|
56
|
+
const resolved = resolve(root);
|
|
57
|
+
return resolved === checkoutRoot || resolved === legacyTarget;
|
|
44
58
|
});
|
|
45
59
|
if (own === undefined)
|
|
46
60
|
return NOTHING;
|
|
@@ -24,20 +24,47 @@ export interface ConductorProcessDeps {
|
|
|
24
24
|
spawn?: (request: SpawnRequest) => void | Promise<void>;
|
|
25
25
|
signal?: (input: {
|
|
26
26
|
pid: number | undefined;
|
|
27
|
-
|
|
27
|
+
hostKey: string | undefined;
|
|
28
|
+
thisHostKey: string;
|
|
28
29
|
signal: NodeJS.Signals;
|
|
29
30
|
}) => SignalOutcome;
|
|
31
|
+
/**
|
|
32
|
+
* This machine's own host_key (MachineContext.machine_id), read once per
|
|
33
|
+
* stop. Injected for tests; defaults to the real `~/.gaia/machine.config.js`
|
|
34
|
+
* (GAIA-450 D1 — `signalVerifiedProcess` refuses a `host_key` mismatch, so a
|
|
35
|
+
* stop needs this machine's own value to compare against).
|
|
36
|
+
*/
|
|
37
|
+
thisHostKey?: () => Promise<string>;
|
|
30
38
|
}
|
|
31
39
|
export interface ConductorProcessHost {
|
|
32
40
|
start(input: {
|
|
33
41
|
workspaceRoot: string;
|
|
34
42
|
conductorName: string | undefined;
|
|
43
|
+
/** The row's `host_key` (GAIA-450 review B5) — verified against this
|
|
44
|
+
* machine's own before anything is spawned. */
|
|
45
|
+
hostKey: string | undefined;
|
|
35
46
|
}): Promise<ConductorActionResult>;
|
|
36
47
|
stop(input: {
|
|
37
48
|
pid: number | undefined;
|
|
38
|
-
|
|
49
|
+
hostKey: string | undefined;
|
|
39
50
|
}): Promise<ConductorActionResult>;
|
|
51
|
+
/**
|
|
52
|
+
* THIS machine's own `host_key` (GAIA-450 B10) — the same value `start`/
|
|
53
|
+
* `stop` already verify against, exposed so the Conductors screens can key
|
|
54
|
+
* their `mine` row projection and their `actions()` gate on the identical
|
|
55
|
+
* seam rather than resolving it a second, divergent way.
|
|
56
|
+
*/
|
|
57
|
+
thisHostKey(): Promise<string>;
|
|
40
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* This machine's host_key, from the same MachineContext the CLI reads.
|
|
61
|
+
*
|
|
62
|
+
* Exported so the Conductors screens (GAIA-450 B10) can key their view-only
|
|
63
|
+
* marking and their `actions()` gate on IDENTITY — this row's `host_key`
|
|
64
|
+
* against this machine's own — rather than on `isLocal`'s filesystem check,
|
|
65
|
+
* which two machines whose checkouts share a path can both satisfy.
|
|
66
|
+
*/
|
|
67
|
+
export declare function defaultThisHostKey(): Promise<string>;
|
|
41
68
|
/**
|
|
42
69
|
* Start and stop a conductor from the cockpit.
|
|
43
70
|
*
|