@remit/web-client 0.0.116 → 0.0.118
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/package.json +1 -1
- package/src/components/layout/MailTopBar.tsx +1 -2
- package/src/components/mail/MessageToolbar.tsx +1 -1
- package/src/components/mail/ThreadListInteraction.tsx +1 -2
- package/src/components/self-update/SelfUpdateOverlay.tsx +2 -2
- package/src/components/ui/KeyboardShortcutsModal.tsx +2 -3
- package/src/hooks/use-system-update.test.ts +179 -92
- package/src/hooks/use-system-update.ts +16 -20
- package/src/hooks/useTriageLayer.ts +1 -4
- package/src/lib/self-update-state.test.ts +24 -65
- package/src/lib/self-update-state.ts +66 -165
- package/src/hooks/useListCursor.test.ts +0 -273
- package/src/hooks/useListCursor.ts +0 -259
- package/src/hooks/useTriageKeyboard.ts +0 -108
- package/src/lib/keymap-dispatch.test.ts +0 -255
- package/src/lib/keymap-dispatch.ts +0 -244
- package/src/lib/keymap.test.ts +0 -68
- package/src/lib/keymap.ts +0 -210
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import {
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
3
|
import type {
|
|
4
4
|
RemitImapSystemUpdateResponse,
|
|
5
5
|
RemitImapSystemUpdateRun,
|
|
@@ -8,17 +8,13 @@ import { ApiError } from "./api";
|
|
|
8
8
|
import {
|
|
9
9
|
APPLY_BUDGET_SECONDS,
|
|
10
10
|
appliesSchemaMigration,
|
|
11
|
-
clearStoredRun,
|
|
12
11
|
type DeriveInput,
|
|
13
12
|
deriveUpdateSurface,
|
|
14
13
|
type HeldRun,
|
|
15
14
|
isSurfaceAbsent,
|
|
16
|
-
loadHeldRun,
|
|
17
15
|
mapUpdatePhase,
|
|
18
16
|
NEVER_CAME_BACK_MARGIN_SECONDS,
|
|
19
17
|
releaseFromCheck,
|
|
20
|
-
SELF_UPDATE_RUN_KEY,
|
|
21
|
-
saveHeldRun,
|
|
22
18
|
} from "./self-update-state";
|
|
23
19
|
|
|
24
20
|
const NOW = Date.parse("2026-07-20T12:00:00.000Z");
|
|
@@ -60,6 +56,7 @@ function held(overrides: Partial<HeldRun> = {}): HeldRun {
|
|
|
60
56
|
runId: "upd_1",
|
|
61
57
|
attemptedVersion: "0.9.4",
|
|
62
58
|
previousVersion: "0.9.3",
|
|
59
|
+
phase: "preparing",
|
|
63
60
|
startedAt: NOW - 10_000,
|
|
64
61
|
...overrides,
|
|
65
62
|
};
|
|
@@ -279,7 +276,7 @@ describe("deriveUpdateSurface — check and run stay independent", () => {
|
|
|
279
276
|
});
|
|
280
277
|
|
|
281
278
|
describe("deriveUpdateSurface — terminal outcomes", () => {
|
|
282
|
-
test("a rolledBack run renders message and command verbatim
|
|
279
|
+
test("a rolledBack run renders message and command verbatim", () => {
|
|
283
280
|
const result = deriveUpdateSurface(
|
|
284
281
|
input({
|
|
285
282
|
data: response({
|
|
@@ -291,7 +288,6 @@ describe("deriveUpdateSurface — terminal outcomes", () => {
|
|
|
291
288
|
}),
|
|
292
289
|
}),
|
|
293
290
|
);
|
|
294
|
-
assert.equal(result.clearStoredRun, true);
|
|
295
291
|
assert.equal(result.surface.status, "ready");
|
|
296
292
|
if (result.surface.status !== "ready") return;
|
|
297
293
|
const section = result.surface.section;
|
|
@@ -374,7 +370,11 @@ describe("deriveUpdateSurface — a held run across a restart", () => {
|
|
|
374
370
|
if (result.surface.status !== "ready") return;
|
|
375
371
|
assert.equal(result.surface.section.status, "applying");
|
|
376
372
|
assert.equal(result.surface.overlay.kind, "applying");
|
|
377
|
-
assert.equal(
|
|
373
|
+
assert.equal(
|
|
374
|
+
result.surface.overlay.kind === "applying" &&
|
|
375
|
+
result.surface.overlay.phase,
|
|
376
|
+
"reconnecting",
|
|
377
|
+
);
|
|
378
378
|
});
|
|
379
379
|
|
|
380
380
|
test("a failed request without a held run is a check-level failure", () => {
|
|
@@ -402,7 +402,6 @@ describe("deriveUpdateSurface — a held run across a restart", () => {
|
|
|
402
402
|
assert.equal(result.surface.status, "ready");
|
|
403
403
|
if (result.surface.status !== "ready") return;
|
|
404
404
|
assert.equal(result.surface.overlay.kind, "neverCameBack");
|
|
405
|
-
assert.equal(result.clearStoredRun, true);
|
|
406
405
|
});
|
|
407
406
|
|
|
408
407
|
test("never-came-back never claims the rollback ran", () => {
|
|
@@ -449,7 +448,6 @@ describe("deriveUpdateSurface — a held run across a restart", () => {
|
|
|
449
448
|
}),
|
|
450
449
|
);
|
|
451
450
|
assert.equal(result.releaseHeld, true);
|
|
452
|
-
assert.equal(result.clearStoredRun, true);
|
|
453
451
|
assert.equal(
|
|
454
452
|
result.surface.status === "ready" && result.surface.section.status,
|
|
455
453
|
"succeeded",
|
|
@@ -470,72 +468,33 @@ describe("deriveUpdateSurface — a held run across a restart", () => {
|
|
|
470
468
|
assert.equal(result.releaseHeld, true);
|
|
471
469
|
});
|
|
472
470
|
|
|
473
|
-
test("a
|
|
471
|
+
test("a poll with no answer yet claims no phase at all", () => {
|
|
474
472
|
const result = deriveUpdateSurface(
|
|
475
473
|
input({ held: held({ startedAt: NOW - 20_000 }) }),
|
|
476
474
|
);
|
|
477
|
-
|
|
478
|
-
result.surface.status !== "ready" ||
|
|
479
|
-
result.surface.overlay.kind !== "applying"
|
|
480
|
-
) {
|
|
481
|
-
assert.fail("expected the applying overlay");
|
|
482
|
-
}
|
|
483
|
-
assert.equal(result.surface.overlay.phase, "preparing");
|
|
484
|
-
assert.equal(result.clearStoredRun, false);
|
|
475
|
+
assert.equal(result.surface.status, "loading");
|
|
485
476
|
});
|
|
486
477
|
|
|
487
|
-
test("a poll that never answers
|
|
478
|
+
test("a poll that never answers still claims no phase past the budget", () => {
|
|
488
479
|
const result = deriveUpdateSurface(
|
|
489
480
|
input({ held: held({ startedAt: NOW - BUDGET_MS - 60_000 }) }),
|
|
490
481
|
);
|
|
491
|
-
assert.equal(result.surface.status, "
|
|
492
|
-
if (result.surface.status !== "ready") return;
|
|
493
|
-
assert.equal(result.surface.overlay.kind, "neverCameBack");
|
|
494
|
-
assert.equal(result.clearStoredRun, true);
|
|
482
|
+
assert.equal(result.surface.status, "loading");
|
|
495
483
|
});
|
|
496
484
|
|
|
497
|
-
test("
|
|
485
|
+
test("the server's own account of the run it accepted carries the phase", () => {
|
|
498
486
|
const result = deriveUpdateSurface(
|
|
499
|
-
input({
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
"applying",
|
|
504
|
-
);
|
|
505
|
-
});
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
function installMemoryStorage(): void {
|
|
509
|
-
const store = new Map<string, string>();
|
|
510
|
-
globalThis.localStorage = {
|
|
511
|
-
getItem: (k: string) => store.get(k) ?? null,
|
|
512
|
-
setItem: (k: string, v: string) => void store.set(k, v),
|
|
513
|
-
removeItem: (k: string) => void store.delete(k),
|
|
514
|
-
clear: () => store.clear(),
|
|
515
|
-
key: () => null,
|
|
516
|
-
length: 0,
|
|
517
|
-
} as Storage;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
describe("held-run persistence", () => {
|
|
521
|
-
beforeEach(installMemoryStorage);
|
|
522
|
-
|
|
523
|
-
test("saves, loads and clears under the documented key", () => {
|
|
524
|
-
const record = held();
|
|
525
|
-
saveHeldRun(record);
|
|
526
|
-
assert.equal(
|
|
527
|
-
localStorage.getItem(SELF_UPDATE_RUN_KEY),
|
|
528
|
-
JSON.stringify(record),
|
|
487
|
+
input({
|
|
488
|
+
data: response({ run: null }),
|
|
489
|
+
held: held({ phase: "restarting" }),
|
|
490
|
+
}),
|
|
529
491
|
);
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
assert.equal(loadHeldRun(), null);
|
|
538
|
-
localStorage.setItem(SELF_UPDATE_RUN_KEY, JSON.stringify({ runId: 1 }));
|
|
539
|
-
assert.equal(loadHeldRun(), null);
|
|
492
|
+
if (
|
|
493
|
+
result.surface.status !== "ready" ||
|
|
494
|
+
result.surface.overlay.kind !== "applying"
|
|
495
|
+
) {
|
|
496
|
+
assert.fail("expected the applying overlay");
|
|
497
|
+
}
|
|
498
|
+
assert.equal(result.surface.overlay.phase, "restarting");
|
|
540
499
|
});
|
|
541
500
|
});
|
|
@@ -3,16 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `GET /system/update` returns three independent things: the running version,
|
|
5
5
|
* the outcome of the last manifest check, and the state of the current or last
|
|
6
|
-
* run. This module folds that response — plus
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* run. This module folds that response — plus the run this client started and
|
|
7
|
+
* holds in memory, plus the clock — into the single `SelfUpdateState` the
|
|
8
|
+
* `@remit/ui` components render, and into the full-window blocking overlay.
|
|
9
9
|
*
|
|
10
10
|
* The design constraints (RFC 037 Interface, issue #135) live here:
|
|
11
|
-
* -
|
|
11
|
+
* - The server decides whether an update is running and how far along it is.
|
|
12
|
+
* Every phase shown is one the server reported for this run; an answer the
|
|
13
|
+
* client is still waiting for renders no phase at all.
|
|
14
|
+
* - A held run turns a failed request into `applying`, never `unreachable`.
|
|
12
15
|
* - Once the apply budget plus a margin has passed with no answer — a failed
|
|
13
|
-
* request or
|
|
14
|
-
* came back", a state that never claims
|
|
15
|
-
* dead connection the client cannot know.
|
|
16
|
+
* request, or a server answering without accounting for the run — that
|
|
17
|
+
* silence becomes "the server never came back", a state that never claims
|
|
18
|
+
* the rollback ran, because from a dead connection the client cannot know.
|
|
16
19
|
* - The check block and the run block are independent: a check that cannot
|
|
17
20
|
* reach the update source is a failed check, never a failed update.
|
|
18
21
|
*/
|
|
@@ -25,9 +28,6 @@ import type {
|
|
|
25
28
|
import type { ReleaseInfo, SelfUpdateState, UpdatePhase } from "@remit/ui";
|
|
26
29
|
import { getErrorStatus } from "./error-classifier";
|
|
27
30
|
|
|
28
|
-
/** localStorage key holding the run the client is resuming across a restart. */
|
|
29
|
-
export const SELF_UPDATE_RUN_KEY = "remit.self-update.run";
|
|
30
|
-
|
|
31
31
|
/**
|
|
32
32
|
* The longest an apply can plausibly take — pull, snapshot, stop, start, gate —
|
|
33
33
|
* before a silent server is treated as gone rather than still working. A five
|
|
@@ -42,15 +42,17 @@ export const FALLBACK_LOGS_COMMAND = "remit logs";
|
|
|
42
42
|
const RELEASE_TAG_BASE = "https://github.com/remit-mail/reader/releases/tag/";
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
* The
|
|
46
|
-
*
|
|
47
|
-
* it
|
|
48
|
-
* server
|
|
45
|
+
* The run this client asked for, kept in memory for as long as the page that
|
|
46
|
+
* asked lives. Nothing is written to storage: a run lasts about a minute and
|
|
47
|
+
* only the server knows how it ends, so a page that was not there for the
|
|
48
|
+
* request starts from the server's answer instead of from a record of its own.
|
|
49
49
|
*/
|
|
50
50
|
export interface HeldRun {
|
|
51
51
|
runId: string;
|
|
52
52
|
attemptedVersion: string;
|
|
53
53
|
previousVersion: string;
|
|
54
|
+
/** The phase the server reported when it accepted the run. */
|
|
55
|
+
phase: UpdatePhase;
|
|
54
56
|
/** Epoch millis when the client began holding this run. */
|
|
55
57
|
startedAt: number;
|
|
56
58
|
}
|
|
@@ -89,9 +91,7 @@ export interface DeriveInput {
|
|
|
89
91
|
|
|
90
92
|
export interface DeriveResult {
|
|
91
93
|
surface: UpdateSurface;
|
|
92
|
-
/** The
|
|
93
|
-
clearStoredRun: boolean;
|
|
94
|
-
/** The in-memory held run should be dropped — the run is fully resolved. */
|
|
94
|
+
/** The held run should be dropped — the server has accounted for it. */
|
|
95
95
|
releaseHeld: boolean;
|
|
96
96
|
}
|
|
97
97
|
|
|
@@ -159,46 +159,6 @@ export function appliesSchemaMigration(
|
|
|
159
159
|
return target > current;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
export function loadHeldRun(): HeldRun | null {
|
|
163
|
-
try {
|
|
164
|
-
const raw = localStorage.getItem(SELF_UPDATE_RUN_KEY);
|
|
165
|
-
if (!raw) return null;
|
|
166
|
-
const parsed: unknown = JSON.parse(raw);
|
|
167
|
-
if (!isHeldRun(parsed)) return null;
|
|
168
|
-
return parsed;
|
|
169
|
-
} catch {
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export function saveHeldRun(run: HeldRun): void {
|
|
175
|
-
try {
|
|
176
|
-
localStorage.setItem(SELF_UPDATE_RUN_KEY, JSON.stringify(run));
|
|
177
|
-
} catch {
|
|
178
|
-
// Best effort: private mode or quota. A lost token degrades resume, not
|
|
179
|
-
// correctness — the server remains the authority on the run.
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export function clearStoredRun(): void {
|
|
184
|
-
try {
|
|
185
|
-
localStorage.removeItem(SELF_UPDATE_RUN_KEY);
|
|
186
|
-
} catch {
|
|
187
|
-
// Best effort, as above.
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function isHeldRun(value: unknown): value is HeldRun {
|
|
192
|
-
if (!value || typeof value !== "object") return false;
|
|
193
|
-
const candidate = value as Record<string, unknown>;
|
|
194
|
-
return (
|
|
195
|
-
typeof candidate.runId === "string" &&
|
|
196
|
-
typeof candidate.attemptedVersion === "string" &&
|
|
197
|
-
typeof candidate.previousVersion === "string" &&
|
|
198
|
-
typeof candidate.startedAt === "number"
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
162
|
function budgetLimitSeconds(): number {
|
|
203
163
|
return APPLY_BUDGET_SECONDS + NEVER_CAME_BACK_MARGIN_SECONDS;
|
|
204
164
|
}
|
|
@@ -316,20 +276,14 @@ function checkSection(
|
|
|
316
276
|
function ready(
|
|
317
277
|
section: SelfUpdateState,
|
|
318
278
|
overlay: UpdateOverlay,
|
|
319
|
-
clearStoredRun: boolean,
|
|
320
279
|
releaseHeld: boolean,
|
|
321
280
|
): DeriveResult {
|
|
322
|
-
return {
|
|
323
|
-
surface: { status: "ready", section, overlay },
|
|
324
|
-
clearStoredRun,
|
|
325
|
-
releaseHeld,
|
|
326
|
-
};
|
|
281
|
+
return { surface: { status: "ready", section, overlay }, releaseHeld };
|
|
327
282
|
}
|
|
328
283
|
|
|
329
284
|
/**
|
|
330
|
-
* The client gave up waiting. The
|
|
331
|
-
*
|
|
332
|
-
* still, and its retry has to keep polling, until the server answers for itself.
|
|
285
|
+
* The client gave up waiting. The hold stays: the screen has to sit still, and
|
|
286
|
+
* its retry has to keep polling, until the server answers for itself.
|
|
333
287
|
*/
|
|
334
288
|
function neverCameBack(held: HeldRun, elapsedSeconds: number): DeriveResult {
|
|
335
289
|
return {
|
|
@@ -350,15 +304,15 @@ function neverCameBack(held: HeldRun, elapsedSeconds: number): DeriveResult {
|
|
|
350
304
|
logsCommand: FALLBACK_LOGS_COMMAND,
|
|
351
305
|
},
|
|
352
306
|
},
|
|
353
|
-
clearStoredRun: true,
|
|
354
307
|
releaseHeld: false,
|
|
355
308
|
};
|
|
356
309
|
}
|
|
357
310
|
|
|
358
311
|
/**
|
|
359
312
|
* A held run resolves to one of: still applying, gave up ("never came back"),
|
|
360
|
-
*
|
|
361
|
-
* the server, in which case the caller renders the
|
|
313
|
+
* unaccounted for, still waiting on a first answer, or — returning `null` —
|
|
314
|
+
* resolved terminally on the server, in which case the caller renders the
|
|
315
|
+
* outcome from the response.
|
|
362
316
|
*/
|
|
363
317
|
function deriveHeld(
|
|
364
318
|
held: HeldRun,
|
|
@@ -385,7 +339,6 @@ function deriveHeld(
|
|
|
385
339
|
),
|
|
386
340
|
{ kind: "applying", target: run.targetVersion, phase, elapsedSeconds },
|
|
387
341
|
false,
|
|
388
|
-
false,
|
|
389
342
|
);
|
|
390
343
|
}
|
|
391
344
|
|
|
@@ -408,35 +361,13 @@ function deriveHeld(
|
|
|
408
361
|
elapsedSeconds,
|
|
409
362
|
},
|
|
410
363
|
false,
|
|
411
|
-
false,
|
|
412
364
|
);
|
|
413
365
|
}
|
|
414
366
|
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
// this wait too: within it the client stays applying, past it it stops
|
|
418
|
-
// claiming an install is running and says what it cannot account for.
|
|
367
|
+
// Nothing has come back yet. Silence is not a phase, so the surface waits
|
|
368
|
+
// rather than describing an install it has heard nothing about.
|
|
419
369
|
if (data === undefined) {
|
|
420
|
-
|
|
421
|
-
return neverCameBack(held, elapsedSeconds);
|
|
422
|
-
}
|
|
423
|
-
return ready(
|
|
424
|
-
applyingSection(
|
|
425
|
-
held.runId,
|
|
426
|
-
currentVersion,
|
|
427
|
-
held.attemptedVersion,
|
|
428
|
-
"preparing",
|
|
429
|
-
elapsedSeconds,
|
|
430
|
-
),
|
|
431
|
-
{
|
|
432
|
-
kind: "applying",
|
|
433
|
-
target: held.attemptedVersion,
|
|
434
|
-
phase: "preparing",
|
|
435
|
-
elapsedSeconds,
|
|
436
|
-
},
|
|
437
|
-
false,
|
|
438
|
-
false,
|
|
439
|
-
);
|
|
370
|
+
return { surface: { status: "loading" }, releaseHeld: false };
|
|
440
371
|
}
|
|
441
372
|
|
|
442
373
|
// The server answered, but not with our run. If we have been gone longer than
|
|
@@ -455,55 +386,48 @@ function deriveHeld(
|
|
|
455
386
|
},
|
|
456
387
|
overlay: { kind: "none" },
|
|
457
388
|
},
|
|
458
|
-
clearStoredRun: true,
|
|
459
389
|
releaseHeld: true,
|
|
460
390
|
};
|
|
461
391
|
}
|
|
462
392
|
|
|
463
|
-
//
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
? mapUpdatePhase(run.phase)
|
|
467
|
-
: "preparing";
|
|
393
|
+
// The updater picks the request up off a control file, so the seam keeps
|
|
394
|
+
// reporting the previous run for a moment. The phase stays the one the server
|
|
395
|
+
// gave when it accepted this run, until the server reports a newer one.
|
|
468
396
|
return ready(
|
|
469
397
|
applyingSection(
|
|
470
398
|
held.runId,
|
|
471
399
|
currentVersion,
|
|
472
400
|
held.attemptedVersion,
|
|
473
|
-
phase,
|
|
401
|
+
held.phase,
|
|
474
402
|
elapsedSeconds,
|
|
475
403
|
),
|
|
476
|
-
{
|
|
477
|
-
|
|
404
|
+
{
|
|
405
|
+
kind: "applying",
|
|
406
|
+
target: held.attemptedVersion,
|
|
407
|
+
phase: held.phase,
|
|
408
|
+
elapsedSeconds,
|
|
409
|
+
},
|
|
478
410
|
false,
|
|
479
411
|
);
|
|
480
412
|
}
|
|
481
413
|
|
|
482
|
-
function displayFromData(input: DeriveInput): {
|
|
483
|
-
surface: UpdateSurface;
|
|
484
|
-
clearStoredRun: boolean;
|
|
485
|
-
} {
|
|
414
|
+
function displayFromData(input: DeriveInput): UpdateSurface {
|
|
486
415
|
const { data, isError, isFetching, dismissedRunId, checkRequested, now } =
|
|
487
416
|
input;
|
|
488
417
|
|
|
489
418
|
if (isError) {
|
|
490
419
|
return {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
reason: "Remit could not reach the update service.",
|
|
497
|
-
},
|
|
498
|
-
overlay: { kind: "none" },
|
|
420
|
+
status: "ready",
|
|
421
|
+
section: {
|
|
422
|
+
status: "checkFailed",
|
|
423
|
+
version: data?.currentVersion ?? "the current version",
|
|
424
|
+
reason: "Remit could not reach the update service.",
|
|
499
425
|
},
|
|
500
|
-
|
|
426
|
+
overlay: { kind: "none" },
|
|
501
427
|
};
|
|
502
428
|
}
|
|
503
429
|
|
|
504
|
-
if (!data) {
|
|
505
|
-
return { surface: { status: "loading" }, clearStoredRun: false };
|
|
506
|
-
}
|
|
430
|
+
if (!data) return { status: "loading" };
|
|
507
431
|
|
|
508
432
|
const run = data.run;
|
|
509
433
|
const dismissed =
|
|
@@ -511,12 +435,9 @@ function displayFromData(input: DeriveInput): {
|
|
|
511
435
|
|
|
512
436
|
if (run !== null && !dismissed && run.outcome !== null) {
|
|
513
437
|
return {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
overlay: { kind: "none" },
|
|
518
|
-
},
|
|
519
|
-
clearStoredRun: true,
|
|
438
|
+
status: "ready",
|
|
439
|
+
section: terminalSection(data, run, run.outcome),
|
|
440
|
+
overlay: { kind: "none" },
|
|
520
441
|
};
|
|
521
442
|
}
|
|
522
443
|
|
|
@@ -524,33 +445,27 @@ function displayFromData(input: DeriveInput): {
|
|
|
524
445
|
const elapsedSeconds = elapsedSince(parseIso(run.startedAt) ?? now, now);
|
|
525
446
|
const phase = mapUpdatePhase(run.phase);
|
|
526
447
|
return {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
elapsedSeconds,
|
|
541
|
-
},
|
|
448
|
+
status: "ready",
|
|
449
|
+
section: applyingSection(
|
|
450
|
+
run.runId,
|
|
451
|
+
run.fromVersion,
|
|
452
|
+
run.targetVersion,
|
|
453
|
+
phase,
|
|
454
|
+
elapsedSeconds,
|
|
455
|
+
),
|
|
456
|
+
overlay: {
|
|
457
|
+
kind: "applying",
|
|
458
|
+
target: run.targetVersion,
|
|
459
|
+
phase,
|
|
460
|
+
elapsedSeconds,
|
|
542
461
|
},
|
|
543
|
-
clearStoredRun: false,
|
|
544
462
|
};
|
|
545
463
|
}
|
|
546
464
|
|
|
547
465
|
return {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
overlay: { kind: "none" },
|
|
552
|
-
},
|
|
553
|
-
clearStoredRun: false,
|
|
466
|
+
status: "ready",
|
|
467
|
+
section: checkSection(data, checkRequested && isFetching, now),
|
|
468
|
+
overlay: { kind: "none" },
|
|
554
469
|
};
|
|
555
470
|
}
|
|
556
471
|
|
|
@@ -559,29 +474,15 @@ export function deriveUpdateSurface(input: DeriveInput): DeriveResult {
|
|
|
559
474
|
const run = data?.run ?? null;
|
|
560
475
|
|
|
561
476
|
if (isSurfaceAbsent(error) && !held) {
|
|
562
|
-
return {
|
|
563
|
-
surface: { status: "absent" },
|
|
564
|
-
clearStoredRun: true,
|
|
565
|
-
releaseHeld: true,
|
|
566
|
-
};
|
|
477
|
+
return { surface: { status: "absent" }, releaseHeld: true };
|
|
567
478
|
}
|
|
568
479
|
|
|
569
480
|
if (held) {
|
|
570
481
|
const heldResult = deriveHeld(held, data, run, isError, now);
|
|
571
482
|
if (heldResult) return heldResult;
|
|
572
483
|
// Our run resolved terminally — render it from the response and let go.
|
|
573
|
-
|
|
574
|
-
return {
|
|
575
|
-
surface: resolved.surface,
|
|
576
|
-
clearStoredRun: true,
|
|
577
|
-
releaseHeld: true,
|
|
578
|
-
};
|
|
484
|
+
return { surface: displayFromData(input), releaseHeld: true };
|
|
579
485
|
}
|
|
580
486
|
|
|
581
|
-
|
|
582
|
-
return {
|
|
583
|
-
surface: display.surface,
|
|
584
|
-
clearStoredRun: display.clearStoredRun,
|
|
585
|
-
releaseHeld: false,
|
|
586
|
-
};
|
|
487
|
+
return { surface: displayFromData(input), releaseHeld: false };
|
|
587
488
|
}
|