gh-glance 0.16.2 → 0.17.1
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/CHANGELOG.md +64 -1
- package/README.md +80 -17
- package/index.mjs +795 -253
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -292,7 +292,7 @@ const ANIMATE = !process.env.GH_GLANCE_NO_ANIMATION;
|
|
|
292
292
|
// legitimate mixed-direction Arabic and Hebrew titles render correctly, and
|
|
293
293
|
// preserving real RTL text is a stated property of this function.
|
|
294
294
|
// eslint-disable-next-line no-control-regex -- matching control characters is the entire purpose
|
|
295
|
-
const CONTROL_CHARS = /[
|
|
295
|
+
const CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]+/g;
|
|
296
296
|
const BIDI_OVERRIDES = /[--]/g;
|
|
297
297
|
|
|
298
298
|
// Titles are rendered at whatever length GitHub returns, and ink memoizes
|
|
@@ -307,7 +307,7 @@ const MAX_FIELD_LENGTH = 300;
|
|
|
307
307
|
function safe(value) {
|
|
308
308
|
// Coerce first, sanitize always. Returning a non-string early skipped both the
|
|
309
309
|
// control-character strip and the length clamp, so an array or an object with a
|
|
310
|
-
// toString() came through untouched -- `safe(["
|
|
310
|
+
// toString() came through untouched -- `safe(["\x1b[2Jx"])` returned the escape
|
|
311
311
|
// verbatim. GitHub returns strings for every field this is called on today, so
|
|
312
312
|
// the guarantee was being provided by the upstream schema rather than by the
|
|
313
313
|
// function that claims to provide it.
|
|
@@ -1005,7 +1005,7 @@ function ghApiArgs(args, etag = null) {
|
|
|
1005
1005
|
];
|
|
1006
1006
|
}
|
|
1007
1007
|
|
|
1008
|
-
async function ghApi(args, { operation, signal, etag = null, run = runGh } = {}) {
|
|
1008
|
+
async function ghApi(args, { operation, signal, etag = null, run = runGh, includeHeaders = false } = {}) {
|
|
1009
1009
|
const argv = ghApiArgs(args, etag);
|
|
1010
1010
|
try {
|
|
1011
1011
|
const parsed = parseGhApiResponse(await run(argv, { operation, signal }));
|
|
@@ -1014,6 +1014,7 @@ async function ghApi(args, { operation, signal, etag = null, run = runGh } = {})
|
|
|
1014
1014
|
etag: parsed.headers.etag ?? null,
|
|
1015
1015
|
rateLimit: pickRateLimit(parsed.headers),
|
|
1016
1016
|
body: parsed.body,
|
|
1017
|
+
...(includeHeaders ? { headers: parsed.headers } : {}),
|
|
1017
1018
|
};
|
|
1018
1019
|
} catch (error) {
|
|
1019
1020
|
const parsed = parseGhApiResponse(error?.stdout);
|
|
@@ -1490,6 +1491,72 @@ function actionsWorkflowsArgs() {
|
|
|
1490
1491
|
];
|
|
1491
1492
|
}
|
|
1492
1493
|
|
|
1494
|
+
const ACTIONS_SETUP_TTL_MS = 5 * 60_000;
|
|
1495
|
+
|
|
1496
|
+
function actionsSetupArgs({ host, repository }) {
|
|
1497
|
+
return [`repos/${repository}/actions/workflows?per_page=100&page=1`, "--hostname", host,
|
|
1498
|
+
"--jq", "{total_count, workflows: [.workflows[] | {id, name, state}]}"];
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function parseActionsSetup(body, headers = {}) {
|
|
1502
|
+
const unusable = () => { throw new Error("Actions setup response is unusable"); };
|
|
1503
|
+
let value;
|
|
1504
|
+
try { value = JSON.parse(body); } catch { return unusable(); }
|
|
1505
|
+
if (!isRecord(value) || !Number.isSafeInteger(value.total_count) || value.total_count < 0 ||
|
|
1506
|
+
!Array.isArray(value.workflows) || value.workflows.length > 100 ||
|
|
1507
|
+
value.total_count < value.workflows.length || (value.total_count > 0 && value.workflows.length === 0)) return unusable();
|
|
1508
|
+
const ids = new Set();
|
|
1509
|
+
const workflows = value.workflows.map((entry) => {
|
|
1510
|
+
if (!isRecord(entry) || !Number.isSafeInteger(entry.id) || entry.id <= 0 || ids.has(entry.id) ||
|
|
1511
|
+
typeof entry.name !== "string" || !safe(entry.name).trim() ||
|
|
1512
|
+
typeof entry.state !== "string" || !entry.state.trim()) return unusable();
|
|
1513
|
+
ids.add(entry.id);
|
|
1514
|
+
return { id: entry.id, name: safe(entry.name), state: entry.state };
|
|
1515
|
+
});
|
|
1516
|
+
const hasNextPage = /rel=["']?next\b/i.test(headers.link ?? "");
|
|
1517
|
+
if (value.total_count === 0 && hasNextPage) return unusable();
|
|
1518
|
+
const complete = value.total_count === workflows.length && !hasNextPage;
|
|
1519
|
+
const disabled = workflows.filter(({ state }) => ["disabled_manually", "disabled_inactivity"].includes(state)).length;
|
|
1520
|
+
const active = workflows.filter(({ state }) => state === "active").length;
|
|
1521
|
+
const title = !complete ? "Workflows found; setup check incomplete"
|
|
1522
|
+
: workflows.length === 0 ? "No workflows found"
|
|
1523
|
+
: disabled === workflows.length ? "All listed workflows are disabled" : "Workflows found";
|
|
1524
|
+
return { total: value.total_count, workflows, complete, disabled, active,
|
|
1525
|
+
other: workflows.length - disabled - active, title };
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function actionsSetupFresh(result, nowMs) {
|
|
1529
|
+
return result != null && nowMs >= result.checkedAt && nowMs - result.checkedAt < ACTIONS_SETUP_TTL_MS;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function actionsSetupRetryAt(nowMs, holdUntil = 0) {
|
|
1533
|
+
return Math.max(nowMs + 30_000, Number.isFinite(holdUntil) ? holdUntil : 0);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function actionsPageUrl(target = dashboardTarget()) {
|
|
1537
|
+
const validated = dashboardTarget(target);
|
|
1538
|
+
return validated.host && validated.repository
|
|
1539
|
+
? `https://${validated.host}/${validated.repository}/actions` : null;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function actionsSetupPresentation(state, nowMs) {
|
|
1543
|
+
const result = state.result;
|
|
1544
|
+
const notes = [];
|
|
1545
|
+
if (result) {
|
|
1546
|
+
notes.push(`${state.saved ? "Saved setup check" : "Setup checked"}: ${new Date(result.checkedAt).toISOString()}`,
|
|
1547
|
+
`${result.workflows.length} of ${result.total} workflows listed: ${result.active} active, ${result.disabled} disabled, ${result.other} other state.`,
|
|
1548
|
+
actionsSetupFresh(result, nowMs)
|
|
1549
|
+
? `Press s to check again after ${new Date(result.checkedAt + ACTIONS_SETUP_TTL_MS).toISOString()}.`
|
|
1550
|
+
: "Setup check expired; press s to check again.");
|
|
1551
|
+
}
|
|
1552
|
+
if (state.error) notes.push(state.error,
|
|
1553
|
+
`Press s to retry${state.retryAt ? ` after ${new Date(state.retryAt).toISOString()}` : ""}.`);
|
|
1554
|
+
if (state.message) notes.push(state.message);
|
|
1555
|
+
return { title: state.busy ? "Checking Actions setup" : state.error ? "Setup check unavailable"
|
|
1556
|
+
: state.message ? state.title ?? "Actions setup" : result?.title,
|
|
1557
|
+
action: state.action ?? (state.error ? "s Retry; g Actions; ? Details" : "s Setup; g Actions; ? Details"), notes };
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1493
1560
|
function parseActionsRuns(runsBody) {
|
|
1494
1561
|
return parseJsonOutput(runsBody).map((run) => ({
|
|
1495
1562
|
databaseId: run.databaseId,
|
|
@@ -2214,6 +2281,7 @@ const OPERATION_COSTS = Object.freeze({
|
|
|
2214
2281
|
// Separately admitted, like a later list page: only fetched when a run has
|
|
2215
2282
|
// no name of its own, and settled against its own reservation.
|
|
2216
2283
|
"catalog:actions-workflows": { core: 1, graphql: 0 },
|
|
2284
|
+
"setup:actions-workflows": { core: 1, graphql: 0 },
|
|
2217
2285
|
"tab:security-endpoint": { core: 1, graphql: 0 },
|
|
2218
2286
|
"failure-context:repository": { core: 0, graphql: GRAPHQL_OBSERVER_POINTS },
|
|
2219
2287
|
// Each page past the first reserves its own envelope, so a denial there is a
|
|
@@ -3207,8 +3275,8 @@ function claimIdentityBootstrap(root, { credentialKey, host, resource = "core",
|
|
|
3207
3275
|
}, { now, kill });
|
|
3208
3276
|
}
|
|
3209
3277
|
|
|
3210
|
-
function readIdentityQuotaState(registry, scope, now) {
|
|
3211
|
-
const loaded = readGovernorState(scope.path, now);
|
|
3278
|
+
function readIdentityQuotaState(registry, scope, now, options = {}) {
|
|
3279
|
+
const loaded = readGovernorState(scope.path, now, options);
|
|
3212
3280
|
const known = Object.values(registry.identities).some((identity) => identity.quotaKey === scope.quotaKey) ||
|
|
3213
3281
|
Object.values(registry.attempts).some((attempt) => attempt.imported && attempt.quotaKey === scope.quotaKey);
|
|
3214
3282
|
return loaded.ok && loaded.missing && known ? { ok: false, reason: "corrupt" } : loaded;
|
|
@@ -3234,9 +3302,9 @@ function importIdentityDebts(root, state, credentialKey, identity, now) {
|
|
|
3234
3302
|
notBefore: attempt.startedAt, status: "started", epochs: { core: null, graphql: null },
|
|
3235
3303
|
startedAt: attempt.startedAt, completedAt: null, outcome: null,
|
|
3236
3304
|
};
|
|
3237
|
-
if (!loaded.value.importMarkers[id]
|
|
3238
|
-
|
|
3239
|
-
|
|
3305
|
+
if (!loaded.value.importMarkers[id]) {
|
|
3306
|
+
const failure = transferReceiptToDebt(loaded.value, legacyControl, now, id);
|
|
3307
|
+
if (failure) return failure;
|
|
3240
3308
|
}
|
|
3241
3309
|
if (Object.keys(loaded.value.importMarkers).length >= IDENTITY_MAX_ATTEMPTS &&
|
|
3242
3310
|
!loaded.value.importMarkers[id]) return { ok: false, reason: "identity-capacity" };
|
|
@@ -3451,7 +3519,8 @@ function startIdentityControl(coordinator, now = Date.now()) {
|
|
|
3451
3519
|
if (Object.keys(registry.attempts).length >= IDENTITY_MAX_ATTEMPTS) return { ok: false, reason: "identity-capacity" };
|
|
3452
3520
|
const priorControl = state.controlReceipts.core;
|
|
3453
3521
|
if (priorControl) {
|
|
3454
|
-
|
|
3522
|
+
const failure = retireControlReceiptToDebt(state, "core", now);
|
|
3523
|
+
if (failure) return failure;
|
|
3455
3524
|
}
|
|
3456
3525
|
const id = governorId();
|
|
3457
3526
|
registry.attempts[id] = { credentialKey: identity.credentialKey, host: identity.host, resource: "core", startedAt: now,
|
|
@@ -3506,8 +3575,9 @@ function settleIdentityControl(coordinator, control, stdout, now = Date.now(),
|
|
|
3506
3575
|
reservation.actualCosts = { core: cost, graphql: 0 };
|
|
3507
3576
|
reservation.accountedCosts = { core: cost, graphql: 0 };
|
|
3508
3577
|
loaded.value.controlReceipts.core = null;
|
|
3509
|
-
} else
|
|
3510
|
-
|
|
3578
|
+
} else {
|
|
3579
|
+
const failure = retireControlReceiptToDebt(loaded.value, "core", now);
|
|
3580
|
+
if (failure) return failure;
|
|
3511
3581
|
}
|
|
3512
3582
|
return writeGovernorQuotaState(scope.path, loaded.value, now);
|
|
3513
3583
|
});
|
|
@@ -3776,6 +3846,7 @@ function identityCoordinationMessage(reason) {
|
|
|
3776
3846
|
if (reason === "throttle-paused") return "Paused after repeated rate limits; refresh to retry";
|
|
3777
3847
|
if (retryableCoordination(reason)) return "Coordinating with your other panes";
|
|
3778
3848
|
if (reason === "identity-capacity") return "Coordination state full; retrying after the current window";
|
|
3849
|
+
if (reason === "debt-journal-full") return "Coordination journal full; retrying after older requests settle";
|
|
3779
3850
|
return "Verified identity unavailable; waiting to retry";
|
|
3780
3851
|
}
|
|
3781
3852
|
|
|
@@ -3821,6 +3892,9 @@ const GOVERNOR_MAX_LEASES = 128;
|
|
|
3821
3892
|
const GOVERNOR_MAX_INTENTS = 512;
|
|
3822
3893
|
const GOVERNOR_MAX_RESERVATIONS = 512;
|
|
3823
3894
|
const GOVERNOR_MAX_DEBT_GROUPS = 128;
|
|
3895
|
+
// Quiescent groups merge once the journal is this full, so retirement never
|
|
3896
|
+
// waits on a probe publication that a full journal would itself block.
|
|
3897
|
+
const DEBT_JOURNAL_COMPACT_AT = GOVERNOR_MAX_DEBT_GROUPS / 2;
|
|
3824
3898
|
const GOVERNOR_MAX_LEDGER_BYTES = 2 * 1024 * 1024;
|
|
3825
3899
|
const GOVERNOR_LOCK_WAIT_MS = 250;
|
|
3826
3900
|
// How old an unreadable lock record may be before it counts as abandoned. A
|
|
@@ -4458,10 +4532,8 @@ function migrateLegacyGovernorReceipts(legacy, nowMs) {
|
|
|
4458
4532
|
if (RATE_RESOURCES.some((resource) => units[resource] > 0)) {
|
|
4459
4533
|
const nonce = governorId();
|
|
4460
4534
|
state.debtGroups[nonce] = {
|
|
4461
|
-
nonce,
|
|
4462
|
-
|
|
4463
|
-
units, counts, at: nowMs, barrierAt: null,
|
|
4464
|
-
barrierRevisions: { core: 0, graphql: 0 },
|
|
4535
|
+
...newDebtGroup({ nonce, bootId: coordinationBootId(), unknownChild: true, at: nowMs }),
|
|
4536
|
+
units, counts,
|
|
4465
4537
|
};
|
|
4466
4538
|
for (const resource of RATE_RESOURCES) {
|
|
4467
4539
|
state.debt[resource].unresolvedUnits = units[resource];
|
|
@@ -4528,25 +4600,57 @@ function debtGroupTarget(state, receipt, groupId = null) {
|
|
|
4528
4600
|
return state.debtGroups[UNKNOWN_DEBT_GROUP_ID] ? governorId() : UNKNOWN_DEBT_GROUP_ID;
|
|
4529
4601
|
}
|
|
4530
4602
|
|
|
4603
|
+
const ACCOUNTING_OVERFLOW = Object.freeze({ ok: false, reason: "accounting-overflow" });
|
|
4604
|
+
|
|
4605
|
+
// The one shape normalizeGovernorState accepts for a group; every site that
|
|
4606
|
+
// creates a group starts from it so a new field cannot be missed in one copy.
|
|
4607
|
+
function newDebtGroup({ nonce = governorId(), bootId, unknownChild, at, quiescent = false, barrierAt = null }) {
|
|
4608
|
+
return {
|
|
4609
|
+
nonce, ownerNonce: null, ownerPid: null, ownerBirth: null, bootId,
|
|
4610
|
+
unknownChild, sealed: true, quiescent,
|
|
4611
|
+
units: { core: 0, graphql: 0 }, counts: { core: 0, graphql: 0 },
|
|
4612
|
+
at, barrierAt, barrierRevisions: { core: 0, graphql: 0 },
|
|
4613
|
+
};
|
|
4614
|
+
}
|
|
4615
|
+
|
|
4616
|
+
// When age alone settles an unresolved group. Only a sealed group ages out;
|
|
4617
|
+
// an owner acknowledgement can settle either kind earlier.
|
|
4618
|
+
function debtGroupSettlesAt(group) {
|
|
4619
|
+
return group.sealed ? group.at + IDENTITY_UNCERTAIN_MAX_MS : Number.POSITIVE_INFINITY;
|
|
4620
|
+
}
|
|
4621
|
+
|
|
4622
|
+
function hasLiveProbeClaim(state, at) {
|
|
4623
|
+
return RATE_RESOURCES.some((resource) => state.probeClaims[resource]?.leaseUntil > at);
|
|
4624
|
+
}
|
|
4625
|
+
|
|
4626
|
+
// A journal with no free slot is a capacity condition, not an arithmetic one.
|
|
4627
|
+
// A retry can succeed once an unresolved group ages out or a live claim ends
|
|
4628
|
+
// and lets compaction run; the probe interval bounds the wait so a pane never
|
|
4629
|
+
// shows a retry hours away.
|
|
4630
|
+
function debtJournalFullFailure(state, nowMs) {
|
|
4631
|
+
const unblockAt = Math.min(
|
|
4632
|
+
...Object.values(state.debtGroups).filter((group) => !group.quiescent).map(debtGroupSettlesAt),
|
|
4633
|
+
...RATE_RESOURCES.map((resource) => state.probeClaims[resource]?.leaseUntil ?? Number.POSITIVE_INFINITY),
|
|
4634
|
+
nowMs + BUDGET_PROBE_MS,
|
|
4635
|
+
);
|
|
4636
|
+
return { ok: false, reason: "debt-journal-full", retryAt: Math.max(unblockAt, nowMs + 1000) };
|
|
4637
|
+
}
|
|
4638
|
+
|
|
4639
|
+
// Returns null when the charge is recorded, otherwise the failure to report.
|
|
4531
4640
|
function transferReceiptToDebt(state, receipt, nowMs, groupId = null) {
|
|
4532
4641
|
const units = Object.fromEntries(RATE_RESOURCES.map((resource) => [resource,
|
|
4533
4642
|
reservationCost(receipt, resource, state.leases, nowMs)]));
|
|
4534
|
-
if (RATE_RESOURCES.every((resource) => units[resource] === 0)) return
|
|
4643
|
+
if (RATE_RESOURCES.every((resource) => units[resource] === 0)) return null;
|
|
4535
4644
|
const id = debtGroupTarget(state, receipt, groupId);
|
|
4536
|
-
if (id === null) return
|
|
4645
|
+
if (id === null) return debtJournalFullFailure(state, nowMs);
|
|
4537
4646
|
const incomingBootId = receipt.receipt?.bootId ?? coordinationBootId();
|
|
4538
4647
|
const dedicated = id === groupId;
|
|
4539
|
-
const group = state.debtGroups[id] ?? {
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
units: { core: 0, graphql: 0 }, counts: { core: 0, graphql: 0 },
|
|
4544
|
-
at: nowMs, barrierAt: null,
|
|
4545
|
-
barrierRevisions: { core: 0, graphql: 0 },
|
|
4546
|
-
};
|
|
4547
|
-
if (group.quiescent) return false;
|
|
4648
|
+
const group = state.debtGroups[id] ?? newDebtGroup({
|
|
4649
|
+
bootId: incomingBootId, unknownChild: !dedicated || !receipt.receipt, at: nowMs,
|
|
4650
|
+
});
|
|
4651
|
+
if (group.quiescent) return ACCOUNTING_OVERFLOW;
|
|
4548
4652
|
if (receipt.receipt && dedicated) {
|
|
4549
|
-
if (group.ownerNonce !== null && group.ownerNonce !== receipt.receipt.ownerNonce) return
|
|
4653
|
+
if (group.ownerNonce !== null && group.ownerNonce !== receipt.receipt.ownerNonce) return ACCOUNTING_OVERFLOW;
|
|
4550
4654
|
group.ownerNonce = receipt.receipt.ownerNonce;
|
|
4551
4655
|
group.ownerPid = receipt.receipt.ownerPid;
|
|
4552
4656
|
group.ownerBirth = receipt.receipt.ownerBirth;
|
|
@@ -4561,7 +4665,7 @@ function transferReceiptToDebt(state, receipt, nowMs, groupId = null) {
|
|
|
4561
4665
|
!Number.isSafeInteger(group.units[resource] + amount) ||
|
|
4562
4666
|
!Number.isSafeInteger(state.debt[resource].unresolvedCount + (amount > 0 ? 1 : 0)) ||
|
|
4563
4667
|
!Number.isSafeInteger(state.debt[resource].revision + 1) ||
|
|
4564
|
-
!Number.isSafeInteger(group.counts[resource] + (amount > 0 ? 1 : 0))) return
|
|
4668
|
+
!Number.isSafeInteger(group.counts[resource] + (amount > 0 ? 1 : 0))) return ACCOUNTING_OVERFLOW;
|
|
4565
4669
|
}
|
|
4566
4670
|
for (const resource of RATE_RESOURCES) {
|
|
4567
4671
|
const amount = units[resource];
|
|
@@ -4575,7 +4679,7 @@ function transferReceiptToDebt(state, receipt, nowMs, groupId = null) {
|
|
|
4575
4679
|
// which reconcileBootDebt may treat the group as settled.
|
|
4576
4680
|
group.at = Math.max(group.at, nowMs);
|
|
4577
4681
|
state.debtGroups[id] = group;
|
|
4578
|
-
return
|
|
4682
|
+
return null;
|
|
4579
4683
|
}
|
|
4580
4684
|
|
|
4581
4685
|
function makeDebtGroupQuiescent(state, group, at) {
|
|
@@ -4602,23 +4706,25 @@ function makeDebtGroupQuiescent(state, group, at) {
|
|
|
4602
4706
|
return true;
|
|
4603
4707
|
}
|
|
4604
4708
|
|
|
4709
|
+
// Returns null when the receipt is retired, otherwise the failure to report.
|
|
4605
4710
|
function retireControlReceiptToDebt(state, resource, at) {
|
|
4606
4711
|
const control = state.controlReceipts[resource];
|
|
4607
|
-
if (!control) return
|
|
4712
|
+
if (!control) return null;
|
|
4608
4713
|
// A control receipt without an issued dispatch cannot have started HTTP.
|
|
4609
4714
|
// Removing it also fences a late owner before it can issue a child.
|
|
4610
4715
|
if (control.receipt.dispatches.length === 0) {
|
|
4611
4716
|
state.controlReceipts[resource] = null;
|
|
4612
|
-
return
|
|
4717
|
+
return null;
|
|
4613
4718
|
}
|
|
4614
4719
|
const generation = control.receipt.generation;
|
|
4615
|
-
|
|
4720
|
+
const failure = transferReceiptToDebt(state, control, at, generation);
|
|
4721
|
+
if (failure) return failure;
|
|
4616
4722
|
const group = state.debtGroups[generation];
|
|
4617
4723
|
if (group && !group.unknownChild && group.ownerPid !== null &&
|
|
4618
4724
|
group.ownerBirth !== null && group.bootId !== null &&
|
|
4619
|
-
!makeDebtGroupQuiescent(state, group, at)) return
|
|
4725
|
+
!makeDebtGroupQuiescent(state, group, at)) return ACCOUNTING_OVERFLOW;
|
|
4620
4726
|
state.controlReceipts[resource] = null;
|
|
4621
|
-
return
|
|
4727
|
+
return null;
|
|
4622
4728
|
}
|
|
4623
4729
|
|
|
4624
4730
|
// A reboot proves an unknown owner's children are gone. So does age: a group's
|
|
@@ -4630,16 +4736,49 @@ function reconcileBootDebt(state, at) {
|
|
|
4630
4736
|
for (const group of Object.values(state.debtGroups)) {
|
|
4631
4737
|
if (group.quiescent) continue;
|
|
4632
4738
|
const priorBoot = bootId !== null && group.bootId !== null && group.bootId !== bootId;
|
|
4633
|
-
const outlivedWindow =
|
|
4739
|
+
const outlivedWindow = at >= debtGroupSettlesAt(group);
|
|
4634
4740
|
if (!priorBoot && !outlivedWindow) continue;
|
|
4635
4741
|
if (!makeDebtGroupQuiescent(state, group, at)) {
|
|
4636
|
-
return
|
|
4742
|
+
return ACCOUNTING_OVERFLOW;
|
|
4637
4743
|
}
|
|
4638
4744
|
changed = true;
|
|
4639
4745
|
}
|
|
4640
4746
|
return { ok: true, changed };
|
|
4641
4747
|
}
|
|
4642
4748
|
|
|
4749
|
+
// Quiescent groups are frozen until a probe publication retires them, and a
|
|
4750
|
+
// publication needs a claim that first charges its control receipt to a group.
|
|
4751
|
+
// With the journal full of quiescent groups that charge has nowhere to go, so
|
|
4752
|
+
// the only operation that drains the journal is the one it blocks. Merging the
|
|
4753
|
+
// frozen groups keeps the totals and frees the slots. A claim compares the
|
|
4754
|
+
// snapshot it took against the groups it finds at publication, so the shape may
|
|
4755
|
+
// only change while no claim is live.
|
|
4756
|
+
function compactQuiescentDebtGroups(state, at) {
|
|
4757
|
+
if (Object.keys(state.debtGroups).length < DEBT_JOURNAL_COMPACT_AT || hasLiveProbeClaim(state, at)) {
|
|
4758
|
+
return { ok: true, changed: false };
|
|
4759
|
+
}
|
|
4760
|
+
const quiescent = Object.entries(state.debtGroups).filter(([, group]) => group.quiescent);
|
|
4761
|
+
let changed = false;
|
|
4762
|
+
for (const [bootId, entries] of Map.groupBy(quiescent, ([, group]) => group.bootId)) {
|
|
4763
|
+
if (entries.length < 2) continue;
|
|
4764
|
+
const groups = entries.map(([, group]) => group);
|
|
4765
|
+
const merged = newDebtGroup({ bootId, unknownChild: true, quiescent: true,
|
|
4766
|
+
at: Math.max(...groups.map((group) => group.at)),
|
|
4767
|
+
barrierAt: Math.max(0, ...groups.map((group) => group.barrierAt ?? 0)) });
|
|
4768
|
+
for (const resource of RATE_RESOURCES) {
|
|
4769
|
+
merged.units[resource] = groups.reduce((sum, group) => sum + group.units[resource], 0);
|
|
4770
|
+
merged.counts[resource] = groups.reduce((sum, group) => sum + group.counts[resource], 0);
|
|
4771
|
+
if (!Number.isSafeInteger(merged.units[resource]) ||
|
|
4772
|
+
!Number.isSafeInteger(merged.counts[resource])) return ACCOUNTING_OVERFLOW;
|
|
4773
|
+
merged.barrierRevisions[resource] = Math.max(...groups.map((group) => group.barrierRevisions[resource]));
|
|
4774
|
+
}
|
|
4775
|
+
for (const [id] of entries) delete state.debtGroups[id];
|
|
4776
|
+
state.debtGroups[merged.nonce] = merged;
|
|
4777
|
+
changed = true;
|
|
4778
|
+
}
|
|
4779
|
+
return { ok: true, changed };
|
|
4780
|
+
}
|
|
4781
|
+
|
|
4643
4782
|
function compactGovernorReceipts(state, nowMs) {
|
|
4644
4783
|
const sealed = new Set();
|
|
4645
4784
|
const full = Object.keys(state.reservations).length >= GOVERNOR_MAX_RESERVATIONS;
|
|
@@ -4663,18 +4802,16 @@ function compactGovernorReceipts(state, nowMs) {
|
|
|
4663
4802
|
const trial = structuredClone(state);
|
|
4664
4803
|
for (const reservation of candidates) {
|
|
4665
4804
|
const groupId = reservation.receipt?.generation ?? null;
|
|
4666
|
-
|
|
4667
|
-
if (
|
|
4668
|
-
|
|
4669
|
-
}
|
|
4805
|
+
const failure = transferReceiptToDebt(trial, reservation, nowMs, groupId);
|
|
4806
|
+
if (failure?.reason === "debt-journal-full") return { ok: true, changed: false };
|
|
4807
|
+
if (failure) return failure;
|
|
4670
4808
|
}
|
|
4671
4809
|
}
|
|
4672
4810
|
if (full) {
|
|
4673
4811
|
for (const [id, reservation] of Object.entries(state.reservations)) {
|
|
4674
4812
|
if (reservation.status !== "completed" || reservation.receipt) continue;
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
}
|
|
4813
|
+
const failure = transferReceiptToDebt(state, reservation, nowMs);
|
|
4814
|
+
if (failure) return failure;
|
|
4678
4815
|
delete state.reservations[id];
|
|
4679
4816
|
changed = true;
|
|
4680
4817
|
}
|
|
@@ -4695,9 +4832,9 @@ function compactGovernorReceipts(state, nowMs) {
|
|
|
4695
4832
|
for (const generation of sealed) {
|
|
4696
4833
|
for (const [id, reservation] of Object.entries(state.reservations)) {
|
|
4697
4834
|
if (reservation.receipt?.generation !== generation) continue;
|
|
4698
|
-
if (!(reservation.status === "started" && reservation.receipt.dispatches.length === 0)
|
|
4699
|
-
|
|
4700
|
-
|
|
4835
|
+
if (!(reservation.status === "started" && reservation.receipt.dispatches.length === 0)) {
|
|
4836
|
+
const failure = transferReceiptToDebt(state, reservation, nowMs, generation);
|
|
4837
|
+
if (failure) return failure;
|
|
4701
4838
|
}
|
|
4702
4839
|
delete state.reservations[id];
|
|
4703
4840
|
state.ownerGenerations[reservation.leaseId] = governorId();
|
|
@@ -4706,7 +4843,7 @@ function compactGovernorReceipts(state, nowMs) {
|
|
|
4706
4843
|
if (group && !group.unknownChild && group.ownerPid !== null &&
|
|
4707
4844
|
group.ownerBirth !== null && group.bootId !== null &&
|
|
4708
4845
|
!makeDebtGroupQuiescent(state, group, nowMs)) {
|
|
4709
|
-
return
|
|
4846
|
+
return ACCOUNTING_OVERFLOW;
|
|
4710
4847
|
}
|
|
4711
4848
|
}
|
|
4712
4849
|
retireOwnerGenerations(state);
|
|
@@ -4792,7 +4929,7 @@ function writeGovernorState(path, state) {
|
|
|
4792
4929
|
try {
|
|
4793
4930
|
if (state?.version === GOVERNOR_STATE_VERSION) {
|
|
4794
4931
|
if (!Number.isSafeInteger(state.revision) || state.revision === Number.MAX_SAFE_INTEGER) {
|
|
4795
|
-
return
|
|
4932
|
+
return ACCOUNTING_OVERFLOW;
|
|
4796
4933
|
}
|
|
4797
4934
|
state.revision += 1;
|
|
4798
4935
|
}
|
|
@@ -4822,7 +4959,7 @@ function writeGovernorQuotaState(path, state, nowMs) {
|
|
|
4822
4959
|
const normalized = normalizeGovernorState(state, nowMs, { prune: false });
|
|
4823
4960
|
if (!normalized) return { ok: false, reason: "corrupt" };
|
|
4824
4961
|
if (!governorAccountingSafe(normalized, nowMs)) {
|
|
4825
|
-
return
|
|
4962
|
+
return ACCOUNTING_OVERFLOW;
|
|
4826
4963
|
}
|
|
4827
4964
|
return writeGovernorState(path, normalized);
|
|
4828
4965
|
}
|
|
@@ -5136,17 +5273,19 @@ function mutateGovernor(scope, nowMs, mutate) {
|
|
|
5136
5273
|
const loaded = readGovernorState(scope.path, at, { persistMigration: false });
|
|
5137
5274
|
if (!loaded.ok) return loaded;
|
|
5138
5275
|
const state = loaded.value;
|
|
5139
|
-
if (!governorAccountingSafe(state, at)) return
|
|
5276
|
+
if (!governorAccountingSafe(state, at)) return ACCOUNTING_OVERFLOW;
|
|
5140
5277
|
const oldBootReconciled = reconcileBootDebt(state, at);
|
|
5141
5278
|
if (!oldBootReconciled.ok) return oldBootReconciled;
|
|
5142
5279
|
const compacted = compactGovernorReceipts(state, at);
|
|
5143
5280
|
if (!compacted.ok) return compacted;
|
|
5144
5281
|
const bootReconciled = reconcileBootDebt(state, at);
|
|
5145
5282
|
if (!bootReconciled.ok) return bootReconciled;
|
|
5283
|
+
const journal = compactQuiescentDebtGroups(state, at);
|
|
5284
|
+
if (!journal.ok) return journal;
|
|
5146
5285
|
// A mutator can reject without writing. Keep an untouched migration so
|
|
5147
5286
|
// that even that path upgrades the file once before releasing the lock.
|
|
5148
5287
|
const migration = loaded.migrated || compacted.changed ||
|
|
5149
|
-
oldBootReconciled.changed || bootReconciled.changed
|
|
5288
|
+
oldBootReconciled.changed || bootReconciled.changed || journal.changed
|
|
5150
5289
|
? structuredClone(state) : null;
|
|
5151
5290
|
const returnAfterMigration = (result) => {
|
|
5152
5291
|
if (migration === null) return result;
|
|
@@ -5155,7 +5294,7 @@ function mutateGovernor(scope, nowMs, mutate) {
|
|
|
5155
5294
|
};
|
|
5156
5295
|
const value = mutate(state, at);
|
|
5157
5296
|
if (value?.ok === false && value.write !== true) return returnAfterMigration(value);
|
|
5158
|
-
if (!governorAccountingSafe(state, at)) return returnAfterMigration(
|
|
5297
|
+
if (!governorAccountingSafe(state, at)) return returnAfterMigration(ACCOUNTING_OVERFLOW);
|
|
5159
5298
|
const normalized = normalizeGovernorState(state, at);
|
|
5160
5299
|
if (!normalized) return returnAfterMigration({ ok: false, reason: "corrupt" });
|
|
5161
5300
|
const written = writeGovernorState(scope.path, normalized);
|
|
@@ -5387,9 +5526,8 @@ function claimProbe(scope, leaseId, nowMs, resource) {
|
|
|
5387
5526
|
if (resource === "graphql" && state.controlReceipts.graphql) {
|
|
5388
5527
|
// The old claim's lease has expired. Keep an issued child charged as
|
|
5389
5528
|
// debt, and fence the old capability so a new observer can make progress.
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
}
|
|
5529
|
+
const failure = retireControlReceiptToDebt(state, "graphql", at);
|
|
5530
|
+
if (failure) return failure;
|
|
5393
5531
|
}
|
|
5394
5532
|
const nonce = randomUUID();
|
|
5395
5533
|
const startedReservationIds = Object.entries(state.reservations)
|
|
@@ -5408,7 +5546,7 @@ function claimProbe(scope, leaseId, nowMs, resource) {
|
|
|
5408
5546
|
const debtUnits = quiescentGroups.reduce((sum, group) => sum + group.units[resource], 0);
|
|
5409
5547
|
const debtCount = quiescentGroups.reduce((sum, group) => sum + group.counts[resource], 0);
|
|
5410
5548
|
if (!Number.isSafeInteger(debtUnits) || !Number.isSafeInteger(debtCount)) {
|
|
5411
|
-
return
|
|
5549
|
+
return ACCOUNTING_OVERFLOW;
|
|
5412
5550
|
}
|
|
5413
5551
|
const debtSnapshot = {
|
|
5414
5552
|
units: debtUnits,
|
|
@@ -5615,7 +5753,7 @@ function publishProbe(scope, leaseId, nonce, budgets, nowMs, resource) {
|
|
|
5615
5753
|
}
|
|
5616
5754
|
if (changedResources.length === 0) return { ok: false, reason: "corrupt" };
|
|
5617
5755
|
if (!retireClaimedQuiescentDebt(state, resource, claim)) {
|
|
5618
|
-
return
|
|
5756
|
+
return ACCOUNTING_OVERFLOW;
|
|
5619
5757
|
}
|
|
5620
5758
|
const completedBeforeClaim = Object.entries(state.reservations)
|
|
5621
5759
|
.filter(([, reservation]) =>
|
|
@@ -5735,9 +5873,8 @@ function failProbeClaim(scope, leaseId, nonce, nowMs, resource) {
|
|
|
5735
5873
|
const control = state.controlReceipts.graphql;
|
|
5736
5874
|
if (control.receipt.dispatches.length === 0) state.controlReceipts.graphql = null;
|
|
5737
5875
|
else if (control.receipt.dispatches.every((dispatch) => dispatch.terminalAt !== null)) {
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
}
|
|
5876
|
+
const failure = retireControlReceiptToDebt(state, "graphql", at);
|
|
5877
|
+
if (failure) return failure;
|
|
5741
5878
|
}
|
|
5742
5879
|
}
|
|
5743
5880
|
state.observers[resource] = {
|
|
@@ -6041,7 +6178,7 @@ function acknowledgeSealedGeneration(scope, capability, sealedNonce, nowMs) {
|
|
|
6041
6178
|
reservation.receipt?.generation === capability.generation)) {
|
|
6042
6179
|
return { ok: false, reason: "quiescence-unverified" };
|
|
6043
6180
|
}
|
|
6044
|
-
if (!makeDebtGroupQuiescent(state, group, at)) return
|
|
6181
|
+
if (!makeDebtGroupQuiescent(state, group, at)) return ACCOUNTING_OVERFLOW;
|
|
6045
6182
|
return { value: { status: "acknowledged", nonce: group.nonce } };
|
|
6046
6183
|
});
|
|
6047
6184
|
}
|
|
@@ -6300,13 +6437,16 @@ function governorHealth(result, nowMs = Date.now()) {
|
|
|
6300
6437
|
if (!result?.ok) return { status: `unavailable (${result?.reason ?? "unknown"})`, leases: 0, resources: {} };
|
|
6301
6438
|
const state = result.value;
|
|
6302
6439
|
let status = "healthy";
|
|
6303
|
-
if (
|
|
6440
|
+
if (hasLiveProbeClaim(state, nowMs)) status = "waiting for probe";
|
|
6304
6441
|
else if (RATE_RESOURCES.some((resource) => !state.budgets[resource] ||
|
|
6305
6442
|
nowMs - state.budgets[resource].observedAt > budgetSnapshotTtl(resource))) status = "stale";
|
|
6306
6443
|
else if (RATE_RESOURCES.some((resource) => state.budgets[resource].blockUntil > nowMs)) status = "blocked";
|
|
6444
|
+
const groups = Object.values(state.debtGroups);
|
|
6307
6445
|
return {
|
|
6308
6446
|
status,
|
|
6309
6447
|
leases: Object.keys(state.leases).length,
|
|
6448
|
+
debtGroups: groups.length,
|
|
6449
|
+
quiescentGroups: groups.filter((group) => group.quiescent).length,
|
|
6310
6450
|
resources: Object.fromEntries(RATE_RESOURCES.flatMap((resource) => {
|
|
6311
6451
|
const budget = state.budgets[resource];
|
|
6312
6452
|
return budget ? [[resource, {
|
|
@@ -6345,12 +6485,15 @@ function doctorCachedQuotaScope(host, { root = identityRegistryRoot(), nowMs = D
|
|
|
6345
6485
|
try {
|
|
6346
6486
|
if (!lstatSync(path).isFile()) return { ok: false, reason: "unsafe quota path", path };
|
|
6347
6487
|
} catch (error) {
|
|
6348
|
-
|
|
6488
|
+
if (error?.code !== "ENOENT") return { ok: false, reason: "cached quota unreadable", path };
|
|
6349
6489
|
}
|
|
6350
|
-
|
|
6490
|
+
// The runtime's own guard classifies the file: a missing ledger for a quota
|
|
6491
|
+
// key the registry still lists is corrupt, never a fresh start, so doctor
|
|
6492
|
+
// reports the same reason the panes act on.
|
|
6493
|
+
const result = readIdentityQuotaState(registry, { path, quotaKey }, nowMs, { persistMigration: false });
|
|
6351
6494
|
return result.ok && !result.missing
|
|
6352
6495
|
? { ok: true, result, path, quotaKey }
|
|
6353
|
-
: { ok: false, reason: result.
|
|
6496
|
+
: { ok: false, reason: result.ok ? "cached quota missing" : result.reason, path };
|
|
6354
6497
|
}
|
|
6355
6498
|
|
|
6356
6499
|
async function retryGovernorMutation(run, { now, wait, deadline, signal }) {
|
|
@@ -6977,6 +7120,10 @@ async function openInBrowser(tabKey, item, signal, _governor = null, { run = nul
|
|
|
6977
7120
|
// Returning quietly here is how a dead Enter key looks: the row has no usable
|
|
6978
7121
|
// page, and the user is entitled to know that rather than press it again.
|
|
6979
7122
|
if (!url) throw new Error("This row has no page to open yet; refresh and try again");
|
|
7123
|
+
return openBrowserUrl(url, signal, { run });
|
|
7124
|
+
}
|
|
7125
|
+
|
|
7126
|
+
async function openBrowserUrl(url, signal, { run = null } = {}) {
|
|
6980
7127
|
const opener = pick(BROWSER_OPENERS, process.platform, null);
|
|
6981
7128
|
if (!opener) throw new Error(`Opening a browser is not supported on ${process.platform}`);
|
|
6982
7129
|
const [command, ...prefix] = opener;
|
|
@@ -8003,7 +8150,7 @@ function mapWebhookInvalidations({ event, payload, config, enterpriseHost = null
|
|
|
8003
8150
|
? config.targets.map((target) => ({ ...target, resources: TAB_KEYS }))
|
|
8004
8151
|
: config.webhook.targets;
|
|
8005
8152
|
let targets = mappedTargets.filter((target) =>
|
|
8006
|
-
(!selectedHost || target.host === selectedHost) && (event === "installation" || names.has(target.repo)));
|
|
8153
|
+
(!selectedHost || target.host === selectedHost) && (event === "installation" || names.has(target.repo.toLowerCase())));
|
|
8007
8154
|
if (["installation", "installation_repositories"].includes(event)) {
|
|
8008
8155
|
targets = targets.filter((target) => {
|
|
8009
8156
|
const provider = config.providers[target.provider];
|
|
@@ -8013,7 +8160,10 @@ function mapWebhookInvalidations({ event, payload, config, enterpriseHost = null
|
|
|
8013
8160
|
}
|
|
8014
8161
|
if (!selectedHost && event !== "installation") {
|
|
8015
8162
|
const hostsByRepo = new Map();
|
|
8016
|
-
for (const target of targets)
|
|
8163
|
+
for (const target of targets) {
|
|
8164
|
+
const repo = target.repo.toLowerCase();
|
|
8165
|
+
hostsByRepo.set(repo, (hostsByRepo.get(repo) ?? new Set()).add(target.host));
|
|
8166
|
+
}
|
|
8017
8167
|
if ([...hostsByRepo.values()].some((hosts) => hosts.size !== 1)) return { ok: false, reason: "target" };
|
|
8018
8168
|
}
|
|
8019
8169
|
if (targets.length === 0) return ["installation", "installation_repositories"].includes(event)
|
|
@@ -11196,6 +11346,11 @@ async function runDoctor({ probeEndpoints = false } = {}) {
|
|
|
11196
11346
|
.filter(Boolean).sort((left, right) => right.at - left.at)[0] ?? null;
|
|
11197
11347
|
const lastFailedRecovery = [...(recoveryJournal?.entries ?? [])].reverse()
|
|
11198
11348
|
.find((entry) => entry.code !== "clear") ?? null;
|
|
11349
|
+
const ledgerPath = cachedQuota?.path ?? diagnosticScope?.path ?? null;
|
|
11350
|
+
// The journal cannot speak for a ledger that cannot be read, so the cached
|
|
11351
|
+
// read's own reason counts too; both use the governor's reason vocabulary.
|
|
11352
|
+
const hardRecovery = Boolean(ledgerPath) &&
|
|
11353
|
+
[currentRecovery?.code, cachedQuota?.reason].some((reason) => HARD_STORAGE_REASONS.includes(reason));
|
|
11199
11354
|
const retryDates = RATE_RESOURCES.flatMap((resource) => {
|
|
11200
11355
|
const active = recoveryJournal?.current[resource];
|
|
11201
11356
|
const observer = (probeEndpoints ? governorResult : cachedQuota?.result)?.value?.observers[resource];
|
|
@@ -11252,11 +11407,16 @@ async function runDoctor({ probeEndpoints = false } = {}) {
|
|
|
11252
11407
|
"",
|
|
11253
11408
|
...section("API governor"),
|
|
11254
11409
|
field("cached scope", probeEndpoints ? "verified by probe" : cachedQuota?.ok ? "read-only cached identity" : `unavailable (${cachedQuota?.reason ?? "unknown"})`),
|
|
11255
|
-
|
|
11256
|
-
|
|
11410
|
+
// The user has to find the file to move it.
|
|
11411
|
+
...(ledgerPath && cachedQuota?.ok === false || hardRecovery
|
|
11412
|
+
? [field("state file", `${join(dirname(ledgerPath), "quota-[redacted].json")} (quota fingerprint withheld)`)]
|
|
11257
11413
|
: []),
|
|
11414
|
+
...(hardRecovery ? [field("repair", LEDGER_REPAIR_ADVICE)] : []),
|
|
11258
11415
|
field("status", governor.status),
|
|
11259
11416
|
field("live leases", governor.leases),
|
|
11417
|
+
field("debt groups", governor.debtGroups == null
|
|
11418
|
+
? "unavailable"
|
|
11419
|
+
: `${governor.debtGroups}/${GOVERNOR_MAX_DEBT_GROUPS} (${governor.quiescentGroups} quiescent)`),
|
|
11260
11420
|
...RATE_RESOURCES.map((resource) => {
|
|
11261
11421
|
const detail = governor.resources[resource];
|
|
11262
11422
|
return field(
|
|
@@ -11571,8 +11731,10 @@ const KEY_TABLE = [
|
|
|
11571
11731
|
["Enter", "Open the selected item or accept a prompt"],
|
|
11572
11732
|
["r", "Refresh the current tab when a safe grant is available"],
|
|
11573
11733
|
["R", "Resynchronize the current tab, ignoring cached validators and backoff"],
|
|
11734
|
+
["s", "Actions: read-only setup check (standalone only; saved for five minutes)"],
|
|
11735
|
+
["g", "Actions: open the selected repository Actions page"],
|
|
11574
11736
|
["w", "Adjust table column widths"],
|
|
11575
|
-
["?", "
|
|
11737
|
+
["?", "Open / close contextual help; n / p changes page"],
|
|
11576
11738
|
["q / Esc / Ctrl+C", "Quit (Esc leaves width mode)"],
|
|
11577
11739
|
];
|
|
11578
11740
|
const KEY_COL = Math.max(...KEY_TABLE.map(([k]) => k.length)) + 3;
|
|
@@ -11594,6 +11756,8 @@ const HELP_PRIORITY = [
|
|
|
11594
11756
|
"Tab / Shift+Tab",
|
|
11595
11757
|
"PgUp / PgDn",
|
|
11596
11758
|
"R",
|
|
11759
|
+
"s",
|
|
11760
|
+
"g",
|
|
11597
11761
|
];
|
|
11598
11762
|
|
|
11599
11763
|
function helpLines(maxRows) {
|
|
@@ -15042,7 +15206,7 @@ function createAcquisitionEngine({
|
|
|
15042
15206
|
ledger.debt[resource].unresolvedUnits + ledger.debt[resource].quiescentUnits +
|
|
15043
15207
|
reservationCost(ledger.controlReceipts[resource], resource, ledger.leases, now()))]));
|
|
15044
15208
|
if (Object.values(units).some((value) => !Number.isSafeInteger(value) || value < 0)) {
|
|
15045
|
-
return
|
|
15209
|
+
return ACCOUNTING_OVERFLOW;
|
|
15046
15210
|
}
|
|
15047
15211
|
return transact((state) => {
|
|
15048
15212
|
const activeKeys = new Set(Object.values(state.subscriptions)
|
|
@@ -15623,6 +15787,14 @@ function coordinationNotice(reason) {
|
|
|
15623
15787
|
const RECOVERY_JOURNAL_MAX_ENTRIES = 128;
|
|
15624
15788
|
const RECOVERY_JOURNAL_MAX_BYTES = 128 * 1024;
|
|
15625
15789
|
const RECOVERY_ENTRY_MAX_BYTES = 1024;
|
|
15790
|
+
// Reasons that no retry can clear: the ledger itself must be repaired. The
|
|
15791
|
+
// runtime keeps them sticky and doctor names the manual remedy for them.
|
|
15792
|
+
const HARD_STORAGE_REASONS = Object.freeze(["corrupt", "unwritable", "capacity", "accounting-overflow"]);
|
|
15793
|
+
// A source failure on a tab may be superseded by these coordination causes.
|
|
15794
|
+
const STICKY_OVER_SOURCE_REASONS = Object.freeze([...HARD_STORAGE_REASONS, "receipt-retry", "interrupted-request"]);
|
|
15795
|
+
const LEDGER_REPAIR_ADVICE = "quit every gh-glance pane, then move the state file, its .recovery.json and " +
|
|
15796
|
+
"registry.json out of that directory and relaunch; a fresh ledger starts, and at most the receipt and " +
|
|
15797
|
+
"debt units below (when shown) may be spent twice this hour";
|
|
15626
15798
|
const RECOVERY_COPY = Object.freeze({
|
|
15627
15799
|
busy: ["Coordinating with your other panes", "Retry automatically", "Other pane; retry"],
|
|
15628
15800
|
stale: ["Coordinating with your other panes", "Retry automatically", "Other pane; retry"],
|
|
@@ -15632,6 +15804,7 @@ const RECOVERY_COPY = Object.freeze({
|
|
|
15632
15804
|
corrupt: ["Local coordination is corrupt", "Run gh-glance --doctor; preserve the reported state file", "State corrupt; doctor"],
|
|
15633
15805
|
capacity: ["Local coordination is full", "Run gh-glance --doctor; preserve the reported state file", "State full; doctor"],
|
|
15634
15806
|
"accounting-overflow": ["Local coordination accounting overflow", "Run gh-glance --doctor; preserve the reported state file", "Overflow; doctor"],
|
|
15807
|
+
"debt-journal-full": ["Local coordination journal is full", "Retry automatically; older requests settle within an hour", "Journal full; retry"],
|
|
15635
15808
|
"reservations-invalid": ["Local coordination reservation invalid", "Run gh-glance --doctor; preserve the reported state file", "Reservation; doctor"],
|
|
15636
15809
|
"external-factor-invalid": ["Local coordination budget invalid", "Run gh-glance --doctor; preserve the reported state file", "Budget invalid; doctor"],
|
|
15637
15810
|
"pacing-invalid": ["Local coordination pacing invalid", "Run gh-glance --doctor; preserve the reported state file", "Pacing invalid; doctor"],
|
|
@@ -15715,8 +15888,8 @@ function shouldClearRecoveryCause(cause, { sourceSuccess = false, tab = null, sc
|
|
|
15715
15888
|
|
|
15716
15889
|
function recoveryRetryAt(decision, budget, observer, nowMs) {
|
|
15717
15890
|
if (budget?.blockUntil > nowMs) return budget.blockUntil;
|
|
15718
|
-
if (["reservations-invalid", "external-factor-invalid", "pacing-invalid",
|
|
15719
|
-
"
|
|
15891
|
+
if ([...HARD_STORAGE_REASONS, "reservations-invalid", "external-factor-invalid", "pacing-invalid",
|
|
15892
|
+
"budget-resource"].includes(decision?.reason)) return null;
|
|
15720
15893
|
const resetHold = ["budget-reset", "reset", "rate-limit"].includes(decision?.reason);
|
|
15721
15894
|
if (decision?.mode === "probe" && !resetHold) {
|
|
15722
15895
|
return observer?.nextAt > nowMs ? observer.nextAt : null;
|
|
@@ -15760,12 +15933,14 @@ function cleanupQueueForResource(queue, resource) {
|
|
|
15760
15933
|
return count;
|
|
15761
15934
|
}
|
|
15762
15935
|
|
|
15763
|
-
function presentRecovery(cause, { nowMs = Date.now(), cols = 80 } = {}) {
|
|
15936
|
+
function presentRecovery(cause, { nowMs = Date.now(), cols = 80, hasSnapshot } = {}) {
|
|
15764
15937
|
if (!cause) return [];
|
|
15765
15938
|
const width = Math.max(1, Number.isSafeInteger(cols) ? cols - 1 : 79);
|
|
15766
15939
|
const resource = cause.resource === "graphql" ? "GraphQL" : cause.resource === "core" ? "Core" : "GitHub";
|
|
15767
|
-
const age =
|
|
15768
|
-
|
|
15940
|
+
const age = hasSnapshot === false ? "Not checked yet"
|
|
15941
|
+
: Number.isFinite(cause.sourceAt) && cause.sourceAt <= nowMs
|
|
15942
|
+
? `cached ${formatDuration(nowMs - cause.sourceAt)}`
|
|
15943
|
+
: hasSnapshot === true ? "Last-check time unavailable" : "cached age unavailable";
|
|
15769
15944
|
const retry = Number.isFinite(cause.retryAt)
|
|
15770
15945
|
? `; next ${statusInterval(cause.retryAt, nowMs)}` : "";
|
|
15771
15946
|
const summary = cause.code === "legacy-unresolved" && Number.isSafeInteger(cause.debtUnits)
|
|
@@ -15800,6 +15975,189 @@ function presentRecovery(cause, { nowMs = Date.now(), cols = 80 } = {}) {
|
|
|
15800
15975
|
`${cause.action}${retry}`.slice(0, width), age.slice(0, width)];
|
|
15801
15976
|
}
|
|
15802
15977
|
|
|
15978
|
+
// Presentation consumes source observations, never receipt or cache-load clocks.
|
|
15979
|
+
function dashboardPresentation({ tab, rows = null, hasSnapshot = rows !== null,
|
|
15980
|
+
sourceAt = null, nowMs = Date.now(), overdue = false, onDemand = false,
|
|
15981
|
+
requestState = "idle", error = false, recovery = null, disconnected = false,
|
|
15982
|
+
securityNotes = [], securityAllUnavailable = false, target = null, diagnostic = null } = {}) {
|
|
15983
|
+
const label = TABS.find((candidate) => candidate.key === tab)?.label ?? "Repository";
|
|
15984
|
+
const empty = { actions: "No workflow runs found", issues: "No open issues",
|
|
15985
|
+
prs: "No open pull requests", security: "No alerts returned by the checked sources" };
|
|
15986
|
+
const lastEmpty = { actions: "Last check found no workflow runs.", issues: "Last check found no open issues.",
|
|
15987
|
+
prs: "Last check found no open pull requests.", security: "Last check returned no alerts from the checked sources." };
|
|
15988
|
+
const sourceCheck = !hasSnapshot ? "Not checked yet"
|
|
15989
|
+
: !Number.isFinite(sourceAt) || sourceAt > nowMs ? "Last-check time unavailable"
|
|
15990
|
+
: (() => {
|
|
15991
|
+
const date = new Date(sourceAt);
|
|
15992
|
+
const today = new Date(nowMs);
|
|
15993
|
+
const day = date.toDateString() === today.toDateString() ? ""
|
|
15994
|
+
: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} `;
|
|
15995
|
+
return `Checked ${day}${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
|
15996
|
+
})();
|
|
15997
|
+
const showTable = Array.isArray(rows) && rows.length > 0;
|
|
15998
|
+
const failed = Boolean(error || disconnected);
|
|
15999
|
+
const held = Boolean(recovery) || requestState === "waiting";
|
|
16000
|
+
let state = "ready";
|
|
16001
|
+
let title = showTable ? label : empty[tab] ?? label;
|
|
16002
|
+
let explanation = tab === "actions"
|
|
16003
|
+
? "Actions shows automated jobs. A push starts a run only when a workflow has a matching trigger."
|
|
16004
|
+
: "GitHub returned this result for the selected repository.";
|
|
16005
|
+
let action = "Press r to refresh.";
|
|
16006
|
+
if (failed) {
|
|
16007
|
+
state = "failed";
|
|
16008
|
+
title = `Could not ${hasSnapshot ? "refresh" : "load"} ${label}`;
|
|
16009
|
+
} else if (held) {
|
|
16010
|
+
state = "waiting";
|
|
16011
|
+
title = hasSnapshot ? `Waiting to update ${label}` : `Waiting to check ${label}`;
|
|
16012
|
+
} else if (!hasSnapshot) {
|
|
16013
|
+
state = requestState === "loading" ? "loading" : "waiting";
|
|
16014
|
+
title = state === "loading" ? `Checking ${label}…` : `Waiting to check ${label}`;
|
|
16015
|
+
explanation = `Fetching this repository's ${tab === "actions" ? "workflow runs" : label.toLowerCase()}.`;
|
|
16016
|
+
} else if (overdue && !onDemand) {
|
|
16017
|
+
state = "overdue";
|
|
16018
|
+
title = `${label} needs an update`;
|
|
16019
|
+
} else if (onDemand) {
|
|
16020
|
+
state = "saved";
|
|
16021
|
+
title = `Saved ${label} view`;
|
|
16022
|
+
action = "Press r to check again. This view checks when opened or refreshed.";
|
|
16023
|
+
}
|
|
16024
|
+
if ((failed || held || state === "overdue" || state === "saved") && hasSnapshot && !showTable) {
|
|
16025
|
+
explanation = lastEmpty[tab] ?? "Showing the last checked result.";
|
|
16026
|
+
} else if ((failed || held) && !hasSnapshot) {
|
|
16027
|
+
explanation = "No successful check is available yet.";
|
|
16028
|
+
}
|
|
16029
|
+
if (recovery) {
|
|
16030
|
+
explanation = `${recovery.summary}. ${explanation}`;
|
|
16031
|
+
action = recovery.action + (Number.isFinite(recovery.retryAt)
|
|
16032
|
+
? `; next ${statusInterval(recovery.retryAt, nowMs)}` : "");
|
|
16033
|
+
}
|
|
16034
|
+
if (tab === "security" && securityAllUnavailable) {
|
|
16035
|
+
if (!failed && !held) title = "No alert sources available";
|
|
16036
|
+
explanation = `${recovery ? `${recovery.summary}. ` : ""}No alert source returned data for this check.`;
|
|
16037
|
+
}
|
|
16038
|
+
return { state, title, explanation, sourceCheck, action, showTable, target,
|
|
16039
|
+
notes: [...(diagnostic ? [diagnostic] : []), ...(tab === "security" ? securityNotes : [])] };
|
|
16040
|
+
}
|
|
16041
|
+
|
|
16042
|
+
// Panels contain sanitized display text. Width follows terminal cells, including
|
|
16043
|
+
// wide Unicode characters; combining marks stay with the preceding character.
|
|
16044
|
+
function panelCellWidth(char) {
|
|
16045
|
+
const code = char.codePointAt(0);
|
|
16046
|
+
return /\p{Mark}/u.test(char) || code === 0x200d || code === 0xfe0f ? 0
|
|
16047
|
+
: code >= 0x1100 && (code <= 0x115f || code === 0x2329 || code === 0x232a ||
|
|
16048
|
+
code >= 0x2e80 && code <= 0xa4cf || code >= 0xac00 && code <= 0xd7a3 ||
|
|
16049
|
+
code >= 0xf900 && code <= 0xfaff || code >= 0xfe10 && code <= 0xfe6f ||
|
|
16050
|
+
code >= 0xff00 && code <= 0xff60 || code >= 0xffe0 && code <= 0xffe6 || code >= 0x1f300) ? 2 : 1;
|
|
16051
|
+
}
|
|
16052
|
+
|
|
16053
|
+
function wrapPanelText(text, width) {
|
|
16054
|
+
const limit = Math.max(1, width);
|
|
16055
|
+
const lines = [];
|
|
16056
|
+
let line = "";
|
|
16057
|
+
let cells = 0;
|
|
16058
|
+
for (const char of safe(text)) {
|
|
16059
|
+
const size = panelCellWidth(char);
|
|
16060
|
+
if (cells + size > limit && line) {
|
|
16061
|
+
const space = line.lastIndexOf(" ");
|
|
16062
|
+
if (space > 0 && char !== " ") {
|
|
16063
|
+
lines.push(line.slice(0, space));
|
|
16064
|
+
line = line.slice(space + 1);
|
|
16065
|
+
// The retained word contains the same glyphs already measured above.
|
|
16066
|
+
cells = [...line].reduce((sum, glyph) => sum + panelCellWidth(glyph), 0);
|
|
16067
|
+
} else { lines.push(line.trimEnd()); line = ""; cells = 0; }
|
|
16068
|
+
}
|
|
16069
|
+
if (!line && char === " ") continue;
|
|
16070
|
+
line += char;
|
|
16071
|
+
cells += size;
|
|
16072
|
+
}
|
|
16073
|
+
if (line) lines.push(line.trimEnd());
|
|
16074
|
+
return lines;
|
|
16075
|
+
}
|
|
16076
|
+
|
|
16077
|
+
function contextualPanelLayout(view, { cols, rows } = {}) {
|
|
16078
|
+
const width = Math.max(1, cols - 5);
|
|
16079
|
+
const capacity = Math.max(1, rows - 6);
|
|
16080
|
+
const title = wrapPanelText(view.title, width);
|
|
16081
|
+
// Keep a short required remedy before explanation when the body is cramped.
|
|
16082
|
+
const action = capacity < 8 && /gh-glance --doctor/.test(view.action)
|
|
16083
|
+
? "gh-glance --doctor" : view.action;
|
|
16084
|
+
const lines = [view.title, action, view.sourceCheck, ...view.notes, view.explanation]
|
|
16085
|
+
.flatMap((line) => wrapPanelText(line, width));
|
|
16086
|
+
// A long title cannot consume the only row available for the recovery action.
|
|
16087
|
+
if (title.length >= capacity && capacity > 1) {
|
|
16088
|
+
return [...title.slice(0, capacity - 1), ...wrapPanelText(action, width).slice(0, 1)];
|
|
16089
|
+
}
|
|
16090
|
+
return lines.slice(0, capacity);
|
|
16091
|
+
}
|
|
16092
|
+
|
|
16093
|
+
// Selection is already resolved for acquisition. Displaying it must not ask gh
|
|
16094
|
+
// to resolve it again, or imply that the selected account can access it.
|
|
16095
|
+
function dashboardTarget({ repository = effectiveRuntimeRepository(), host = effectiveRuntimeHost() } = {}) {
|
|
16096
|
+
// These identifiers have a strict ASCII grammar. Validate the entire value
|
|
16097
|
+
// instead of applying the shorter retention limit for remote display fields.
|
|
16098
|
+
const slug = typeof repository === "string" && REPO_PATTERN.exec(repository)?.[0] === repository
|
|
16099
|
+
? repository : null;
|
|
16100
|
+
const selectedHost = normalizeHost(host);
|
|
16101
|
+
return { repository: slug, host: selectedHost,
|
|
16102
|
+
label: slug ? selectedHost && selectedHost !== "github.com" ? `${selectedHost}/${slug}` : slug
|
|
16103
|
+
: "Repository unresolved" };
|
|
16104
|
+
}
|
|
16105
|
+
|
|
16106
|
+
function clampHelpPage(index, count) {
|
|
16107
|
+
return Math.max(0, Math.min(Number.isSafeInteger(index) ? index : 0, Math.max(0, count - 1)));
|
|
16108
|
+
}
|
|
16109
|
+
|
|
16110
|
+
function contextHelpPages({ view, target, tab, remoteSetup = false }, keys = keyTableLines(), width = 75, height = 18) {
|
|
16111
|
+
const cols = Math.max(1, Math.trunc(width));
|
|
16112
|
+
const rows = Math.max(1, Math.trunc(height));
|
|
16113
|
+
const controlRows = cols >= 48 ? 1 : 2;
|
|
16114
|
+
const capacity = rows - controlRows;
|
|
16115
|
+
if (capacity < 1) return [[...wrapPanelText("q Quit", cols)].slice(0, rows)];
|
|
16116
|
+
const meaning = {
|
|
16117
|
+
actions: "Actions shows automated jobs. A push starts a run only when a workflow has a matching trigger.",
|
|
16118
|
+
issues: "Issues shows open issues in the selected repository.",
|
|
16119
|
+
prs: "Pull requests shows open requests to merge changes into the selected repository.",
|
|
16120
|
+
security: "Security shows alerts returned by the checked sources; unavailable sources can hide alerts.",
|
|
16121
|
+
};
|
|
16122
|
+
const title = remoteSetup ? "No GitHub remote found" : view.title;
|
|
16123
|
+
const action = remoteSetup ? "Close help with ?, then Enter to start gh repo create; or restart with gh-glance --repo owner/name."
|
|
16124
|
+
: view.action;
|
|
16125
|
+
// Wrap validated identity in bounded chunks so the remote-field sanitizer
|
|
16126
|
+
// cannot clip a long accepted target or consume its tail with a label prefix.
|
|
16127
|
+
const targetLines = [["Target", target.repository ?? target.label], ["Host", target.host ?? "unresolved"]]
|
|
16128
|
+
.flatMap(([label, value]) => {
|
|
16129
|
+
const text = `${label}: ${value}`;
|
|
16130
|
+
const wrapped = [];
|
|
16131
|
+
for (let offset = 0; offset < text.length; offset += MAX_FIELD_LENGTH) {
|
|
16132
|
+
wrapped.push(...wrapPanelText(text.slice(offset, offset + MAX_FIELD_LENGTH), cols));
|
|
16133
|
+
}
|
|
16134
|
+
return wrapped;
|
|
16135
|
+
});
|
|
16136
|
+
const context = [title, action, view.sourceCheck,
|
|
16137
|
+
...targetLines,
|
|
16138
|
+
"Target selection does not confirm access.",
|
|
16139
|
+
...(!target.repository ? ["Restart with gh-glance --repo owner/name."] : []),
|
|
16140
|
+
meaning[tab] === view.explanation ? null : meaning[tab], view.explanation, ...view.notes,
|
|
16141
|
+
"Other keys close help without performing their usual action.",
|
|
16142
|
+
"Keys (after closing help)", ...keys].filter(Boolean);
|
|
16143
|
+
const lines = context.flatMap((line) => wrapPanelText(line, cols));
|
|
16144
|
+
const chunks = [];
|
|
16145
|
+
// Narrow pages need room for navigation and quit. A compact first page keeps
|
|
16146
|
+
// the immediate state/remedy visible; all complete text follows unabridged.
|
|
16147
|
+
const priority = [title, action].flatMap((line) => wrapPanelText(line, cols));
|
|
16148
|
+
if (priority.length > capacity && capacity >= 2) {
|
|
16149
|
+
const shortAction = /gh-glance --doctor/.test(action) ? "gh-glance --doctor"
|
|
16150
|
+
: /gh auth/.test(action) ? "gh auth status" : remoteSetup ? "? then Enter"
|
|
16151
|
+
: /^Press r/.test(action) ? "Close ?, then r" : action;
|
|
16152
|
+
chunks.push([wrapPanelText(title, cols)[0], wrapPanelText(shortAction, cols)[0]]);
|
|
16153
|
+
}
|
|
16154
|
+
for (let offset = 0; offset < lines.length; offset += capacity) chunks.push(lines.slice(offset, offset + capacity));
|
|
16155
|
+
return chunks.map((chunk, index) => [...chunk,
|
|
16156
|
+
...(controlRows === 1
|
|
16157
|
+
? [`${index + 1}/${chunks.length} n next p prev ? close q/Esc/^C quit`]
|
|
16158
|
+
: [`${index + 1}/${chunks.length} n/p ?close`, "q/Esc/^C quit"])]);
|
|
16159
|
+
}
|
|
16160
|
+
|
|
15803
16161
|
function recoveryJournalPath(scope) {
|
|
15804
16162
|
return `${scope.path}.recovery.json`;
|
|
15805
16163
|
}
|
|
@@ -16406,6 +16764,7 @@ const KEY_HINTS = [
|
|
|
16406
16764
|
{ label: "Refresh", keys: "r" },
|
|
16407
16765
|
{ label: "Width", keys: "w" },
|
|
16408
16766
|
{ label: "Quit", keys: "q" },
|
|
16767
|
+
{ label: "Help", keys: "?" },
|
|
16409
16768
|
];
|
|
16410
16769
|
|
|
16411
16770
|
function activeKeyHints({
|
|
@@ -16428,6 +16787,7 @@ function activeKeyHints({
|
|
|
16428
16787
|
const REMOTE_SETUP_HINTS = [
|
|
16429
16788
|
{ label: "Create remote", keys: "Ent" },
|
|
16430
16789
|
{ label: "Quit", keys: "q" },
|
|
16790
|
+
{ label: "Help", keys: "?" },
|
|
16431
16791
|
];
|
|
16432
16792
|
|
|
16433
16793
|
const REMOTE_SETUP_LINES = [
|
|
@@ -16451,7 +16811,6 @@ const WIDE_STATE_WIDTH = REFRESH_STATUS_WIDTH + 1 + 12;
|
|
|
16451
16811
|
// StatusBar receives the drawable frame width, which is one column narrower
|
|
16452
16812
|
// than the terminal. A 44-cell threshold preserves the 45-column contract.
|
|
16453
16813
|
const WIDE_STATE_MIN_COLS = 44;
|
|
16454
|
-
const NOTICE_ROWS = 1;
|
|
16455
16814
|
|
|
16456
16815
|
const REFRESH_STATUS_GLYPHS = Object.freeze({
|
|
16457
16816
|
unicode: Object.freeze({
|
|
@@ -16473,7 +16832,7 @@ const REFRESH_STATUS_GLYPHS = Object.freeze({
|
|
|
16473
16832
|
});
|
|
16474
16833
|
|
|
16475
16834
|
function isMandatoryHint(hint) {
|
|
16476
|
-
return hint?.label === "Refresh" || hint?.label === "Quit";
|
|
16835
|
+
return hint?.label === "Refresh" || hint?.label === "Quit" || hint?.label === "Help";
|
|
16477
16836
|
}
|
|
16478
16837
|
|
|
16479
16838
|
function refreshStatus({
|
|
@@ -16597,7 +16956,7 @@ function statusBarLayout({
|
|
|
16597
16956
|
for (const [index, hint] of mandatory.entries()) {
|
|
16598
16957
|
// A full label is optional; every essential key is not. Reserve one
|
|
16599
16958
|
// separator and the key itself for each action still to come before taking
|
|
16600
|
-
// the current action's long form. This keeps
|
|
16959
|
+
// the current action's long form. This keeps r, q and ? at the 24-column
|
|
16601
16960
|
// terminal minimum, whose drawable frame is 23 cells wide.
|
|
16602
16961
|
const remainingMinimum = mandatory.slice(index + 1).reduce(
|
|
16603
16962
|
(total, candidate) => total + 1 + [...candidate.keys].length,
|
|
@@ -17006,10 +17365,13 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
17006
17365
|
// The `?` overlay. Renders only on a keypress, so consecutive idle frames are
|
|
17007
17366
|
// still byte-identical and the redraw suppression is untouched.
|
|
17008
17367
|
const [showHelp, setShowHelp] = useState(false);
|
|
17368
|
+
const [helpPage, setHelpPage] = useState(0);
|
|
17009
17369
|
// Set once, ICON_HINT_AFTER_MS into a first load that is still running. Never
|
|
17010
17370
|
// reset: it only gates a line that a resolved tab stops rendering anyway, and
|
|
17011
17371
|
// clearing it would cost a second state write for no visible difference.
|
|
17012
17372
|
const [iconHintDue, setIconHintDue] = useState(false);
|
|
17373
|
+
const [actionsSetup, setActionsSetup] = useState({});
|
|
17374
|
+
const actionsSetupRef = useRef(null);
|
|
17013
17375
|
const [errors, setErrors] = useState({ actions: null, issues: null, prs: null, security: null });
|
|
17014
17376
|
const [failureContext, setFailureContext] = useState(null);
|
|
17015
17377
|
const [loading, setLoading] = useState({ actions: false, issues: false, prs: false, security: false });
|
|
@@ -17064,6 +17426,9 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
17064
17426
|
// transient, and they are the ones worth the space. Derived here at render
|
|
17065
17427
|
// time rather than cached alongside the notes, because the unchanged-payload
|
|
17066
17428
|
// short-circuit can skip parse() entirely and a cached string would go stale.
|
|
17429
|
+
// Each unsuccessful source contributes one note; successful sources contribute
|
|
17430
|
+
// none (truncation is separate). Count before collapsing disabled-source notes.
|
|
17431
|
+
const securityAllUnavailable = securityNotes.length === ALERT_SOURCES.length;
|
|
17067
17432
|
const NOT_ENABLED = /not enabled/i;
|
|
17068
17433
|
const disabledNotes = securityNotes.filter((n) => NOT_ENABLED.test(n));
|
|
17069
17434
|
const otherNotes = securityNotes.filter((n) => !NOT_ENABLED.test(n));
|
|
@@ -17074,19 +17439,173 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
17074
17439
|
...otherNotes,
|
|
17075
17440
|
]
|
|
17076
17441
|
: securityNotes;
|
|
17077
|
-
|
|
17078
|
-
//
|
|
17079
|
-
|
|
17080
|
-
|
|
17081
|
-
|
|
17082
|
-
|
|
17083
|
-
|
|
17084
|
-
|
|
17085
|
-
|
|
17086
|
-
|
|
17087
|
-
|
|
17088
|
-
|
|
17089
|
-
|
|
17442
|
+
const admittedCadenceRef = useRef({});
|
|
17443
|
+
// Last *successful* poll per tab, wall-clock. Wall-clock on purpose: a laptop
|
|
17444
|
+
// sleeping is exactly the gap this is meant to report, and a monotonic clock
|
|
17445
|
+
// does not advance across suspend. Never written on the failure path, or a
|
|
17446
|
+
// persistently failing tab would report itself fresh forever.
|
|
17447
|
+
const lastOkRef = useRef(
|
|
17448
|
+
Object.fromEntries(
|
|
17449
|
+
TABS.flatMap((candidate) => {
|
|
17450
|
+
const cachedTab = cachedEntry?.tabs[candidate.key];
|
|
17451
|
+
const cachedAge = collectorCachedSourceAge(cachedTab);
|
|
17452
|
+
const lastOk = cachedAge === null ? cachedTab?.lastOk : Date.now() - cachedAge;
|
|
17453
|
+
return lastOk == null ? [] : [[candidate.key, lastOk]];
|
|
17454
|
+
}),
|
|
17455
|
+
),
|
|
17456
|
+
);
|
|
17457
|
+
const collectorSourceAgeRef = useRef(Object.fromEntries(TABS.flatMap((candidate) => {
|
|
17458
|
+
const cachedTab = cachedEntry?.tabs[candidate.key];
|
|
17459
|
+
const ageMs = collectorCachedSourceAge(cachedTab);
|
|
17460
|
+
return ageMs === null ? [] : [[candidate.key, { ageMs, measuredAt: performance.now() }]];
|
|
17461
|
+
})));
|
|
17462
|
+
const statusDisconnected = runtime.connect !== null
|
|
17463
|
+
? activeGovernorDecision?.disconnected === true
|
|
17464
|
+
: data[tab.key] !== null && !runtimeIdentityCoordinator?.current();
|
|
17465
|
+
const items = data[tab.key];
|
|
17466
|
+
const displayError = formatTabErrorForWidth(tabError, failureContext, Math.max(1, cols - 5));
|
|
17467
|
+
const coordinationError = activeGovernorDecision?.coordinationError && !remoteSetup;
|
|
17468
|
+
const coordinationReason = activeFailure ?? activeGovernorDecision?.reason ?? "unavailable";
|
|
17469
|
+
const noticeReason = activeRecovery?.code ??
|
|
17470
|
+
(tabError?.kind === "fetch" ? tabError.verdict : null) ??
|
|
17471
|
+
(coordinationError || activeGovernorDecision?.mode === "paused" ? coordinationReason : null) ??
|
|
17472
|
+
(statusDisconnected ? runtime.connect === null ? "identity-unavailable"
|
|
17473
|
+
: activeGovernorDecision?.reason === "app-auth" ? "app-auth" : "disconnected" : null);
|
|
17474
|
+
// A continuous fault stays sustained when its explanation becomes more
|
|
17475
|
+
// specific. Only losing the fault (or switching tabs) restarts the debounce.
|
|
17476
|
+
const coordinationCondition = !remoteSetup && noticeReason ? tab.key : null;
|
|
17477
|
+
useEffect(() => {
|
|
17478
|
+
setVisibleCoordinationCondition(null);
|
|
17479
|
+
if (coordinationCondition === null) return;
|
|
17480
|
+
const timer = setTimeout(
|
|
17481
|
+
() => setVisibleCoordinationCondition(coordinationCondition),
|
|
17482
|
+
COORDINATION_NOTICE_AFTER_MS,
|
|
17483
|
+
);
|
|
17484
|
+
return () => clearTimeout(timer);
|
|
17485
|
+
}, [coordinationCondition]);
|
|
17486
|
+
const showCoordinationNotice = coordinationCondition !== null &&
|
|
17487
|
+
visibleCoordinationCondition === coordinationCondition;
|
|
17488
|
+
const noticeResource = activeResource;
|
|
17489
|
+
const noticeSourceAt = collectorSourceAgeRef.current[tab.key]
|
|
17490
|
+
? now.getTime() - collectorSourceAgeRef.current[tab.key].ageMs -
|
|
17491
|
+
Math.max(0, performance.now() - collectorSourceAgeRef.current[tab.key].measuredAt)
|
|
17492
|
+
: lastOkRef.current[tab.key] ?? null;
|
|
17493
|
+
const noticeCause = activeRecovery ?? (noticeReason ? recoveryCause({
|
|
17494
|
+
reason: noticeReason === "other" ? "network-outage" : noticeReason,
|
|
17495
|
+
resource: noticeResource, sourceAt: noticeSourceAt,
|
|
17496
|
+
retryAt: activeGovernorDecision?.retryAt ?? activeGovernorDecision?.resetMs ??
|
|
17497
|
+
activeGovernorDecision?.notBefore ?? null,
|
|
17498
|
+
origin: tabError ? "source" : "coordination", tab: tabError ? tab.key : null,
|
|
17499
|
+
}) : null);
|
|
17500
|
+
const recoveryLines = showCoordinationNotice && noticeCause
|
|
17501
|
+
? presentRecovery(noticeCause, { nowMs: now.getTime(), cols, hasSnapshot: items !== null })
|
|
17502
|
+
: [!remoteSetup && displayError ? displayError : ""];
|
|
17503
|
+
const noticeTone = showCoordinationNotice ? ATTENTION : displayError ? ERROR_TEXT : undefined;
|
|
17504
|
+
// Data can be arbitrarily old without anything on screen saying so: a failing
|
|
17505
|
+
// poll only surfaces on the tab you have selected, and after a laptop sleep
|
|
17506
|
+
// every pane is plausible and wrong. Threshold-gated and minute-granular on
|
|
17507
|
+
// purpose -- see STALE_AFTER_MS.
|
|
17508
|
+
// Read from the ref written on every successful poll, not from meta.at, which
|
|
17509
|
+
// only moves when the *payload* changes -- see lastOkRef. Reading a ref during
|
|
17510
|
+
// render lags by one render, which is harmless here because `now` advances on
|
|
17511
|
+
// its own and the label is minute-granular by design.
|
|
17512
|
+
const collectorAge = collectorSourceAgeRef.current[tab.key];
|
|
17513
|
+
const sourceAgeMs = collectorAge
|
|
17514
|
+
? collectorAge.ageMs + Math.max(0, performance.now() - collectorAge.measuredAt)
|
|
17515
|
+
: null;
|
|
17516
|
+
const lastOk = sourceAgeMs === null ? lastOkRef.current[tab.key] : now.getTime() - sourceAgeMs;
|
|
17517
|
+
const staleFor = sourceAgeMs ?? (lastOk == null ? null : now.getTime() - lastOk);
|
|
17518
|
+
const staleAt = freshnessDeadline({
|
|
17519
|
+
lastOk,
|
|
17520
|
+
refreshMs: runtime.refreshMs,
|
|
17521
|
+
grantedMs: admittedCadenceRef.current[tab.key]?.grantedMs,
|
|
17522
|
+
governorDecision: activeGovernorDecision,
|
|
17523
|
+
currentEpochs: governorEpochs,
|
|
17524
|
+
});
|
|
17525
|
+
const staleLabel =
|
|
17526
|
+
staleFor != null && staleAt != null && now.getTime() > staleAt
|
|
17527
|
+
? formatDuration(staleFor)
|
|
17528
|
+
: null;
|
|
17529
|
+
// A quiet dashboard's display clock can precede a newly completed request.
|
|
17530
|
+
// Compare source clocks with actual render time, without adding a timer.
|
|
17531
|
+
const presentationNow = Date.now();
|
|
17532
|
+
const selectedTarget = dashboardTarget();
|
|
17533
|
+
const presentation = dashboardPresentation({
|
|
17534
|
+
tab: tab.key, rows: items, hasSnapshot: items !== null,
|
|
17535
|
+
sourceAt: sourceAgeMs === null ? lastOk : presentationNow - sourceAgeMs,
|
|
17536
|
+
nowMs: presentationNow, overdue: Boolean(staleLabel),
|
|
17537
|
+
onDemand: runtime.background === "on-demand" && tab.key !== "actions" &&
|
|
17538
|
+
items !== null && !loading[tab.key] && !activeRequestStatus,
|
|
17539
|
+
requestState: loading[tab.key] || activeRequestStatus ? "loading"
|
|
17540
|
+
: items === null && waiting[tab.key] ? "waiting" : "idle",
|
|
17541
|
+
error: tabError || activeFailure, recovery: noticeCause, diagnostic: displayError,
|
|
17542
|
+
disconnected: statusDisconnected, securityNotes: securityLines, securityAllUnavailable,
|
|
17543
|
+
target: selectedTarget.label,
|
|
17544
|
+
});
|
|
17545
|
+
const setupView = tab.key === "actions" ? actionsSetupPresentation(actionsSetup, presentationNow) : null;
|
|
17546
|
+
if (tab.key === "actions") {
|
|
17547
|
+
const url = actionsPageUrl(selectedTarget);
|
|
17548
|
+
if (runtime.connect !== null && setupView.title && (statusDisconnected || noticeCause || tabError)) {
|
|
17549
|
+
setupView.action = `${statusDisconnected ? "Restore connection;" : presentation.action} g Actions; ? Help`;
|
|
17550
|
+
}
|
|
17551
|
+
const showSetupPanel = !presentation.showTable && !remoteSetup &&
|
|
17552
|
+
((presentation.state === "ready" && !showCoordinationNotice) || runtime.connect !== null);
|
|
17553
|
+
presentation.notes.push(...(setupView.title && !showSetupPanel ? [setupView.title] : []), ...setupView.notes,
|
|
17554
|
+
"Close help with ?, then press s for a read-only setup check or g to open Actions.",
|
|
17555
|
+
...(url ? [url] : ["Restart with gh-glance --repo owner/name."]));
|
|
17556
|
+
if (showSetupPanel) {
|
|
17557
|
+
if (setupView.title) {
|
|
17558
|
+
presentation.notes.push(`Runs: ${presentation.title}`, presentation.action);
|
|
17559
|
+
presentation.title = setupView.title;
|
|
17560
|
+
}
|
|
17561
|
+
if (setupView.title) presentation.sourceCheck = `Runs: ${presentation.sourceCheck}`;
|
|
17562
|
+
if (setupView.title || presentation.state === "ready") presentation.action = setupView.action;
|
|
17563
|
+
}
|
|
17564
|
+
}
|
|
17565
|
+
if (USING_NERD_ICONS && iconHintDue && items === null && !tabError) {
|
|
17566
|
+
presentation.notes.push("Icons blank? GH_GLANCE_ICONS=unicode");
|
|
17567
|
+
}
|
|
17568
|
+
const panelLines = presentation.showTable ? [] : contextualPanelLayout(presentation, { cols, rows });
|
|
17569
|
+
|
|
17570
|
+
const tableVisible = Array.isArray(items) && items.length > 0;
|
|
17571
|
+
const tableNoticeCapacity = Math.max(0, rows - 9);
|
|
17572
|
+
const needsTableContext = ["failed", "overdue", "waiting"].includes(presentation.state);
|
|
17573
|
+
const shortAction = /gh-glance --doctor/.test(presentation.action) ? "gh-glance --doctor"
|
|
17574
|
+
: /gh auth/.test(presentation.action) ? "Run gh auth status" : presentation.action;
|
|
17575
|
+
const tableNotices = needsTableContext
|
|
17576
|
+
? tableNoticeCapacity < 4
|
|
17577
|
+
? tableNoticeCapacity === 1 ? [shortAction]
|
|
17578
|
+
: showCoordinationNotice && noticeCause
|
|
17579
|
+
? [noticeCause.summary, shortAction, presentation.sourceCheck]
|
|
17580
|
+
: [shortAction, presentation.sourceCheck, presentation.title]
|
|
17581
|
+
: [presentation.title, presentation.sourceCheck, ...recoveryLines.filter(Boolean)]
|
|
17582
|
+
: recoveryLines;
|
|
17583
|
+
const noticeLines = [...new Set([...(showCoordinationNotice ? tableNotices : []),
|
|
17584
|
+
...(setupView?.title ? [setupView.title, setupView.action, ...setupView.notes] : []),
|
|
17585
|
+
...tableNotices])].slice(0, tableNoticeCapacity);
|
|
17586
|
+
const visibleSecurityLines = tab.key === "security"
|
|
17587
|
+
? securityLines.slice(0, Math.max(0, tableNoticeCapacity - noticeLines.length)) : [];
|
|
17588
|
+
const extraLines = !showHelp && !remoteSetup && tableVisible
|
|
17589
|
+
? noticeLines.length + visibleSecurityLines.length : 0;
|
|
17590
|
+
// Tab strip, divider, frame edges, footer and terminal guard use six rows.
|
|
17591
|
+
// Table headers alone need two more. The same budget drives navigation.
|
|
17592
|
+
const bodyRows = Math.max(1, rows - 6 - (tableVisible && !showHelp ? 2 : 0) - extraLines);
|
|
17593
|
+
let helpPages = [];
|
|
17594
|
+
if (showHelp) {
|
|
17595
|
+
const fullDiagnostic = formatTabError(tabError, failureContext);
|
|
17596
|
+
const helpView = { ...presentation, notes: [
|
|
17597
|
+
...(fullDiagnostic ? [fullDiagnostic] : []),
|
|
17598
|
+
...presentation.notes.filter((note) => note !== displayError),
|
|
17599
|
+
] };
|
|
17600
|
+
helpPages = contextHelpPages({ view: helpView, target: selectedTarget,
|
|
17601
|
+
tab: tab.key, remoteSetup }, keyTableLines(), cols - 5, bodyRows);
|
|
17602
|
+
}
|
|
17603
|
+
const visibleHelpPage = clampHelpPage(helpPage, helpPages.length);
|
|
17604
|
+
const helpPageRef = useRef({ index: 0, count: 0 });
|
|
17605
|
+
helpPageRef.current = { index: visibleHelpPage, count: helpPages.length };
|
|
17606
|
+
useEffect(() => {
|
|
17607
|
+
if (helpPage !== visibleHelpPage) setHelpPage(visibleHelpPage);
|
|
17608
|
+
}, [helpPage, visibleHelpPage]);
|
|
17090
17609
|
|
|
17091
17610
|
// Read at fetch time rather than being a hook dependency, so dragging the
|
|
17092
17611
|
// pane wider doesn't cancel and restart in-flight requests -- the next tick
|
|
@@ -17109,28 +17628,8 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
17109
17628
|
ANIMATE && tab.key === "actions" && (data.actions ?? []).some((r) => r.status === "in_progress");
|
|
17110
17629
|
|
|
17111
17630
|
const inFlightRef = useRef({});
|
|
17112
|
-
const admittedCadenceRef = useRef({});
|
|
17113
17631
|
const rawRef = useRef({});
|
|
17114
17632
|
const entityRef = useRef(new Map());
|
|
17115
|
-
// Last *successful* poll per tab, wall-clock. Wall-clock on purpose: a laptop
|
|
17116
|
-
// sleeping is exactly the gap this is meant to report, and a monotonic clock
|
|
17117
|
-
// does not advance across suspend. Never written on the failure path, or a
|
|
17118
|
-
// persistently failing tab would report itself fresh forever.
|
|
17119
|
-
const lastOkRef = useRef(
|
|
17120
|
-
Object.fromEntries(
|
|
17121
|
-
TABS.flatMap((candidate) => {
|
|
17122
|
-
const cachedTab = cachedEntry?.tabs[candidate.key];
|
|
17123
|
-
const cachedAge = collectorCachedSourceAge(cachedTab);
|
|
17124
|
-
const lastOk = cachedAge === null ? cachedTab?.lastOk : Date.now() - cachedAge;
|
|
17125
|
-
return lastOk == null ? [] : [[candidate.key, lastOk]];
|
|
17126
|
-
}),
|
|
17127
|
-
),
|
|
17128
|
-
);
|
|
17129
|
-
const collectorSourceAgeRef = useRef(Object.fromEntries(TABS.flatMap((candidate) => {
|
|
17130
|
-
const cachedTab = cachedEntry?.tabs[candidate.key];
|
|
17131
|
-
const ageMs = collectorCachedSourceAge(cachedTab);
|
|
17132
|
-
return ageMs === null ? [] : [[candidate.key, { ageMs, measuredAt: performance.now() }]];
|
|
17133
|
-
})));
|
|
17134
17633
|
// Keep server source timestamps separate from client-local age estimates.
|
|
17135
17634
|
// An SSH receipt's age is measured on the client and cannot prove a new
|
|
17136
17635
|
// source observation when a cached snapshot is delivered again.
|
|
@@ -17426,6 +17925,25 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
17426
17925
|
const [openRequests] = useState(() => createOpenRequestRegistry());
|
|
17427
17926
|
useEffect(() => () => openRequests.abortAll(), [openRequests]);
|
|
17428
17927
|
|
|
17928
|
+
function openActionsPage() {
|
|
17929
|
+
const target = dashboardTarget();
|
|
17930
|
+
const url = actionsPageUrl(target);
|
|
17931
|
+
if (!url) {
|
|
17932
|
+
setActionsSetup((state) => ({ ...state, message: "Restart with gh-glance --repo owner/name." }));
|
|
17933
|
+
return;
|
|
17934
|
+
}
|
|
17935
|
+
const accessKey = runtimeIdentityCoordinator?.current()?.accessKey;
|
|
17936
|
+
openRequests.start(`actions-page:${url}`, ({ signal }) => openBrowserUrl(url, signal))?.then(() => {
|
|
17937
|
+
if (actionsPageUrl() === url && runtimeIdentityCoordinator?.current()?.accessKey === accessKey) {
|
|
17938
|
+
setActionsSetup((state) => ({ ...state, message: null, title: null, action: null }));
|
|
17939
|
+
}
|
|
17940
|
+
}).catch((err) => {
|
|
17941
|
+
if (err?.name === "AbortError" || actionsPageUrl() !== url ||
|
|
17942
|
+
runtimeIdentityCoordinator?.current()?.accessKey !== accessKey) return;
|
|
17943
|
+
setActionsSetup((state) => ({ ...state, title: "Actions page unavailable", action: "g Retry; ? Details", message: `Could not open Actions: ${shortErr(err)}. Press g to retry.` }));
|
|
17944
|
+
});
|
|
17945
|
+
}
|
|
17946
|
+
|
|
17429
17947
|
function openSelected() {
|
|
17430
17948
|
const { items, key: currentKey, tabKey } = navRef.current;
|
|
17431
17949
|
const item = items.find((i) => itemKey(i) === currentKey);
|
|
@@ -17480,13 +17998,19 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
17480
17998
|
flushWidthPreferences();
|
|
17481
17999
|
exit();
|
|
17482
18000
|
} else if (showHelpRef.current) {
|
|
17483
|
-
//
|
|
17484
|
-
//
|
|
17485
|
-
|
|
17486
|
-
|
|
17487
|
-
|
|
18001
|
+
// Paging belongs to the modal. Every other ordinary key closes it and
|
|
18002
|
+
// is consumed; quit remains global and width mode retains its ownership.
|
|
18003
|
+
if (input === "n" || input === "p") {
|
|
18004
|
+
setHelpPage(clampHelpPage(helpPageRef.current.index + (input === "n" ? 1 : -1),
|
|
18005
|
+
helpPageRef.current.count));
|
|
18006
|
+
} else setShowHelp(false);
|
|
17488
18007
|
} else if (input === "?") {
|
|
18008
|
+
setHelpPage(0);
|
|
17489
18009
|
setShowHelp(true);
|
|
18010
|
+
} else if (input === "s" && TABS[activeIndexRef.current].key === "actions") {
|
|
18011
|
+
void actionsSetupRef.current?.();
|
|
18012
|
+
} else if (input === "g" && TABS[activeIndexRef.current].key === "actions") {
|
|
18013
|
+
openActionsPage();
|
|
17490
18014
|
} else if (input === "w") {
|
|
17491
18015
|
enterWidthMode();
|
|
17492
18016
|
} else if (key.downArrow || input === "j") {
|
|
@@ -17510,7 +18034,7 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
17510
18034
|
// which is exactly when the key is most likely to be pressed.
|
|
17511
18035
|
const request = manualRefreshRequest(refreshIntent(input));
|
|
17512
18036
|
if (request) fetchTabRef.current?.(TABS[activeIndexRef.current].key, request);
|
|
17513
|
-
} else if (input >= "1" && input <= String(TABS.length)) {
|
|
18037
|
+
} else if (input.length === 1 && input >= "1" && input <= String(TABS.length)) {
|
|
17514
18038
|
setActiveIndex(Number(input) - 1);
|
|
17515
18039
|
} else if (key.tab && key.shift) {
|
|
17516
18040
|
setActiveIndex((i) => (i - 1 + TABS.length) % TABS.length);
|
|
@@ -17615,14 +18139,16 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
17615
18139
|
reason, resource, at, retryAt, sourceAt: lastOkRef.current[key] ?? null,
|
|
17616
18140
|
origin, tab: origin === "source" ? key : null,
|
|
17617
18141
|
});
|
|
17618
|
-
const hardFailure =
|
|
18142
|
+
const hardFailure = HARD_STORAGE_REASONS.includes(candidate.code);
|
|
18143
|
+
// Contention does not prove that previously unreadable storage recovered.
|
|
18144
|
+
// The existing healthy-control/source-success paths clear that remedy.
|
|
18145
|
+
if (!hardFailure && HARD_STORAGE_REASONS.includes(previous?.code)) return;
|
|
17619
18146
|
if (!hardFailure && previous?.code === "receipt-retry" &&
|
|
17620
18147
|
terminalizationBacklogForResource(terminalizationBacklog, scope?.hash, resource) > 0) return;
|
|
17621
18148
|
if (!hardFailure && previous?.code === "request-queue" &&
|
|
17622
18149
|
cleanupQueueForResource(cleanupQueue, resource) > 0) return;
|
|
17623
18150
|
if (previous?.origin === "source" && previous.tab === key && origin === "coordination" &&
|
|
17624
|
-
!
|
|
17625
|
-
"receipt-retry", "interrupted-request"].includes(candidate.code)) return;
|
|
18151
|
+
!STICKY_OVER_SOURCE_REASONS.includes(candidate.code)) return;
|
|
17626
18152
|
if (previous?.code === candidate.code && previous?.retryAt === candidate.retryAt &&
|
|
17627
18153
|
previous?.origin === candidate.origin && previous?.tab === candidate.tab &&
|
|
17628
18154
|
at - previous.at < 60_000) return;
|
|
@@ -17715,6 +18241,7 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
17715
18241
|
// scheduler out, and the Enter hint hands off to `gh repo create`.
|
|
17716
18242
|
function enterRemoteSetup() {
|
|
17717
18243
|
if (!runtimeHasNoRepositoryTarget()) return false;
|
|
18244
|
+
retireSetup();
|
|
17718
18245
|
const failure = toTabError(new Error(NO_REMOTE_ERROR_TEXT));
|
|
17719
18246
|
setErrors(Object.fromEntries(TABS.map((t) => [t.key, failure])));
|
|
17720
18247
|
return true;
|
|
@@ -17734,8 +18261,11 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
17734
18261
|
if (!refreshed?.ok && !tabReady) {
|
|
17735
18262
|
const resource = RATE_RESOURCES.find((candidate) => tabRequestCost(key)[candidate] > 0 &&
|
|
17736
18263
|
snapshot.value.observers[candidate]?.outcome !== "healthy");
|
|
18264
|
+
// A rejected mutation writes nothing, so the observer's stored nextAt
|
|
18265
|
+
// predates the failure; a failure that names its own retry time wins.
|
|
17737
18266
|
pauseCoordination(key, refreshed?.reason, "observer", false,
|
|
17738
|
-
|
|
18267
|
+
Number.isFinite(refreshed?.retryAt) ? refreshed.retryAt
|
|
18268
|
+
: resource ? snapshot.value.observers[resource]?.nextAt : null);
|
|
17739
18269
|
return;
|
|
17740
18270
|
}
|
|
17741
18271
|
publishGovernorEpochs(snapshot);
|
|
@@ -18110,7 +18640,7 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
18110
18640
|
.finally(() => {
|
|
18111
18641
|
inFlightRef.current[key] = false;
|
|
18112
18642
|
delete inFlightStartedAt[key];
|
|
18113
|
-
if (!currentAccess() && !Object.values(inFlightRef.current).some(Boolean) && runtimeIdentityCoordinator?.current()?.quotaKey !== settlementScope.quotaKey) releaseLease(settlementScope, leaseId);
|
|
18643
|
+
if (!currentAccess() && !setupFlight && !Object.values(inFlightRef.current).some(Boolean) && runtimeIdentityCoordinator?.current()?.quotaKey !== settlementScope.quotaKey) releaseLease(settlementScope, leaseId);
|
|
18114
18644
|
manualInFlight.delete(key);
|
|
18115
18645
|
onSettled?.();
|
|
18116
18646
|
if (!cancelled) {
|
|
@@ -18136,6 +18666,23 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
18136
18666
|
}
|
|
18137
18667
|
|
|
18138
18668
|
const leaseId = governorId();
|
|
18669
|
+
let setupFlight = null;
|
|
18670
|
+
let setupGeneration = 0;
|
|
18671
|
+
let setupPartition = null;
|
|
18672
|
+
let setupState = {};
|
|
18673
|
+
function publishSetup(next) {
|
|
18674
|
+
setupState = next;
|
|
18675
|
+
if (!cancelled) setActionsSetup(next);
|
|
18676
|
+
}
|
|
18677
|
+
function retireSetup() {
|
|
18678
|
+
setupGeneration += 1;
|
|
18679
|
+
if (setupFlight) {
|
|
18680
|
+
setupFlight.retired = true;
|
|
18681
|
+
setupFlight.controller.abort();
|
|
18682
|
+
}
|
|
18683
|
+
setupPartition = null;
|
|
18684
|
+
publishSetup({});
|
|
18685
|
+
}
|
|
18139
18686
|
const pending = new Map();
|
|
18140
18687
|
const cleanupQueue = new Set();
|
|
18141
18688
|
let lastWallAt = Date.now();
|
|
@@ -18258,6 +18805,7 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
18258
18805
|
}
|
|
18259
18806
|
}
|
|
18260
18807
|
|
|
18808
|
+
actionsSetupRef.current = () => publishSetup({ title: "Standalone only", action: "g Actions; ? Help", message: "Setup checks are available in standalone mode. Press g to open Actions." });
|
|
18261
18809
|
if (runtime.connect !== null) {
|
|
18262
18810
|
const repository = effectiveRuntimeRepository({ remoteUrls: runtimeRemoteUrls });
|
|
18263
18811
|
const host = effectiveRuntimeHost({ remoteUrls: runtimeRemoteUrls });
|
|
@@ -18651,13 +19199,14 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
18651
19199
|
}
|
|
18652
19200
|
|
|
18653
19201
|
function retireCurrentScope() {
|
|
19202
|
+
retireSetup();
|
|
18654
19203
|
if (!cleanupScope || !registeredScopeHash) return;
|
|
18655
19204
|
for (const key of [...pending.keys()]) cancelPending(key, cleanupScope);
|
|
18656
19205
|
for (const subscription of acquisitionSubscriptions.values()) {
|
|
18657
19206
|
acquisitionEngine.unsubscribe(subscription.id);
|
|
18658
19207
|
}
|
|
18659
19208
|
acquisitionSubscriptions.clear();
|
|
18660
|
-
if (!Object.values(inFlightRef.current).some(Boolean)) releaseLease(cleanupScope, leaseId);
|
|
19209
|
+
if (!setupFlight && !Object.values(inFlightRef.current).some(Boolean)) releaseLease(cleanupScope, leaseId);
|
|
18661
19210
|
}
|
|
18662
19211
|
|
|
18663
19212
|
// discardData separates the two reasons this runs. An account *change* must
|
|
@@ -18733,6 +19282,7 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
18733
19282
|
const activeTab = TABS[activeIndexRef.current].key;
|
|
18734
19283
|
const kept = maintainControlLease(scope, leaseId, runtime.refreshMs, activeTab, nowMs);
|
|
18735
19284
|
if (kept.ok) return scope;
|
|
19285
|
+
retireSetup();
|
|
18736
19286
|
registeredScopeHash = null;
|
|
18737
19287
|
if (governorRef.current?.leaseId === leaseId) governorRef.current = null;
|
|
18738
19288
|
return null;
|
|
@@ -19126,6 +19676,85 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
19126
19676
|
} else await transport;
|
|
19127
19677
|
return { persisted: true, retry: false, kind, key };
|
|
19128
19678
|
}
|
|
19679
|
+
actionsSetupRef.current = async () => {
|
|
19680
|
+
if (cancelled || setupFlight) return;
|
|
19681
|
+
const nowMs = Date.now();
|
|
19682
|
+
const target = dashboardTarget();
|
|
19683
|
+
if (remoteSetupRef.current || !actionsPageUrl(target)) {
|
|
19684
|
+
publishSetup({ message: "Restart with gh-glance --repo owner/name." });
|
|
19685
|
+
return;
|
|
19686
|
+
}
|
|
19687
|
+
const capturedScope = ensureScope(nowMs);
|
|
19688
|
+
if (!capturedScope) {
|
|
19689
|
+
publishSetup({ ...setupState, error: "Setup check unavailable while account coordination recovers.",
|
|
19690
|
+
retryAt: actionsSetupRetryAt(nowMs) });
|
|
19691
|
+
return;
|
|
19692
|
+
}
|
|
19693
|
+
const partition = JSON.stringify(["standalone", target.host, target.repository, capturedScope.accessKey]);
|
|
19694
|
+
if (setupPartition !== partition) { retireSetup(); setupPartition = partition; }
|
|
19695
|
+
if (setupState.retryAt > nowMs) { publishSetup({ ...setupState }); return; }
|
|
19696
|
+
if (actionsSetupFresh(setupState.result, nowMs)) {
|
|
19697
|
+
publishSetup({ ...setupState, saved: true, error: null, message: null });
|
|
19698
|
+
return;
|
|
19699
|
+
}
|
|
19700
|
+
const generation = setupGeneration;
|
|
19701
|
+
const flight = { controller: new AbortController(), scope: { ...capturedScope, identityProvider: null }, retired: false };
|
|
19702
|
+
setupFlight = flight;
|
|
19703
|
+
const setupSignal = AbortSignal.any([controller.signal, flight.controller.signal]);
|
|
19704
|
+
const current = () => !cancelled && !setupSignal.aborted && generation === setupGeneration &&
|
|
19705
|
+
setupPartition === partition && identity()?.accessKey === capturedScope.accessKey &&
|
|
19706
|
+
actionsPageUrl() === actionsPageUrl(target) && !remoteSetupRef.current;
|
|
19707
|
+
publishSetup({ ...setupState, busy: true, error: null, saved: false, message: null });
|
|
19708
|
+
try {
|
|
19709
|
+
const admitted = await runAdmittedOperation({ scope: capturedScope, leaseId,
|
|
19710
|
+
operation: "setup:actions-workflows", priority: "manual", signal: setupSignal,
|
|
19711
|
+
waitMs: GOVERNOR_ADMISSION_WAIT_MS,
|
|
19712
|
+
run: async (signal) => {
|
|
19713
|
+
const response = await ghApi(actionsSetupArgs(target), {
|
|
19714
|
+
operation: "setup:actions-workflows", signal, includeHeaders: true,
|
|
19715
|
+
});
|
|
19716
|
+
return { ...response, observation: parseActionsSetup(response.body, response.headers), checkedAt: Date.now() };
|
|
19717
|
+
},
|
|
19718
|
+
});
|
|
19719
|
+
if (!current()) return;
|
|
19720
|
+
if (!admitted.ok) {
|
|
19721
|
+
const error = admitted.error;
|
|
19722
|
+
const failedAt = Date.now();
|
|
19723
|
+
let snapshot = inspectGovernor(capturedScope, failedAt);
|
|
19724
|
+
const throttle = classifyThrottle({ ...error?.apiResponse, stderr: errText(error) });
|
|
19725
|
+
let holdUntil = 0;
|
|
19726
|
+
if (classify(error) === "rate-limited" || throttle.kind !== "none") {
|
|
19727
|
+
const budget = snapshot.value?.budgets?.core;
|
|
19728
|
+
const resetMs = throttle.kind === "primary" ? throttle.resetMs
|
|
19729
|
+
: throttle.kind === "secondary" ? transportCooldownDeadline({ ...throttle, at: failedAt })
|
|
19730
|
+
: budget?.resetMs ?? null;
|
|
19731
|
+
holdUntil = resetMs ?? 0;
|
|
19732
|
+
handleRateLimitBlocks("actions", [{ resource: "core", resetMs,
|
|
19733
|
+
epoch: budget?.epoch ?? null }], failedAt);
|
|
19734
|
+
snapshot = inspectGovernor(capturedScope, failedAt);
|
|
19735
|
+
}
|
|
19736
|
+
const retryAt = actionsSetupRetryAt(failedAt, Math.max(
|
|
19737
|
+
holdUntil, snapshot.value?.budgets?.core?.blockUntil ?? 0, admitted.decision?.retryAt ?? 0,
|
|
19738
|
+
admitted.decision?.value?.retryAt ?? 0, admitted.decision?.value?.notBefore ?? 0,
|
|
19739
|
+
admitted.decision?.value?.resetMs ?? 0,
|
|
19740
|
+
error?.retryAt ?? 0, failedAt + githubAppResponseRetryAfter(error?.apiResponse, failedAt)));
|
|
19741
|
+
publishSetup({ ...setupState, busy: false, error: shortErr(error), retryAt });
|
|
19742
|
+
return;
|
|
19743
|
+
}
|
|
19744
|
+
publishSetup({ result: { ...admitted.value.observation, checkedAt: admitted.value.checkedAt }, busy: false });
|
|
19745
|
+
if (terminalizationBacklogForResource(terminalizationBacklog, capturedScope.hash, "core") > 0) {
|
|
19746
|
+
recordRuntimeRecovery("actions", "receipt-retry");
|
|
19747
|
+
}
|
|
19748
|
+
} finally {
|
|
19749
|
+
if (setupFlight === flight) setupFlight = null;
|
|
19750
|
+
if ((cancelled || (flight.retired && flight.scope.hash !== scope?.hash)) &&
|
|
19751
|
+
!Object.values(inFlightRef.current).some(Boolean)) {
|
|
19752
|
+
releaseLease(flight.scope, leaseId);
|
|
19753
|
+
}
|
|
19754
|
+
if (current() && setupState.busy) publishSetup({ ...setupState, busy: false });
|
|
19755
|
+
}
|
|
19756
|
+
};
|
|
19757
|
+
|
|
19129
19758
|
fetchTabRef.current = (key, { force = false, kind = force ? "manual" : "active" } = {}) => {
|
|
19130
19759
|
if (kind === "tab-switch") {
|
|
19131
19760
|
// A newly active subscription is not evidence of quiet: the tab you
|
|
@@ -19513,7 +20142,9 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
19513
20142
|
acquisitionEngine.unsubscribe(subscription.id);
|
|
19514
20143
|
}
|
|
19515
20144
|
acquisitionSubscriptions.clear();
|
|
19516
|
-
|
|
20145
|
+
retireSetup();
|
|
20146
|
+
actionsSetupRef.current = null;
|
|
20147
|
+
if (cleanupScope && registeredScopeHash && !setupFlight) releaseLease(cleanupScope, leaseId);
|
|
19517
20148
|
if (governorRef.current?.leaseId === leaseId) governorRef.current = null;
|
|
19518
20149
|
if (contextCoordinatorRef.current === coordinator) contextCoordinatorRef.current = null;
|
|
19519
20150
|
// `cancelled` stops state updates from a promise that already resolved;
|
|
@@ -19551,9 +20182,6 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
19551
20182
|
TABS.map((t) => [t.key, data[t.key] == null && !errors[t.key] && loading[t.key]]),
|
|
19552
20183
|
);
|
|
19553
20184
|
const anyFirstLoad = Object.values(firstLoad).some(Boolean);
|
|
19554
|
-
const statusDisconnected = runtime.connect !== null
|
|
19555
|
-
? activeGovernorDecision?.disconnected === true
|
|
19556
|
-
: data[tab.key] !== null && !runtimeIdentityCoordinator?.current();
|
|
19557
20185
|
let semanticStatus = refreshStatus({
|
|
19558
20186
|
widthMode: resizeRef.current.active,
|
|
19559
20187
|
remoteSetup,
|
|
@@ -19593,45 +20221,6 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
19593
20221
|
return () => clearTimeout(timer);
|
|
19594
20222
|
}, [anyFirstLoad, iconHintDue]);
|
|
19595
20223
|
|
|
19596
|
-
const items = data[tab.key];
|
|
19597
|
-
const displayError = formatTabErrorForWidth(tabError, failureContext, Math.max(1, cols - 5));
|
|
19598
|
-
const coordinationError = activeGovernorDecision?.coordinationError && !remoteSetup;
|
|
19599
|
-
const coordinationReason = activeFailure ?? activeGovernorDecision?.reason ?? "unavailable";
|
|
19600
|
-
const noticeReason = activeRecovery?.code ??
|
|
19601
|
-
(tabError?.kind === "fetch" ? tabError.verdict : null) ??
|
|
19602
|
-
(coordinationError || activeGovernorDecision?.mode === "paused" ? coordinationReason : null) ??
|
|
19603
|
-
(statusDisconnected ? runtime.connect === null ? "identity-unavailable"
|
|
19604
|
-
: activeGovernorDecision?.reason === "app-auth" ? "app-auth" : "disconnected" : null);
|
|
19605
|
-
// A continuous fault stays sustained when its explanation becomes more
|
|
19606
|
-
// specific. Only losing the fault (or switching tabs) restarts the debounce.
|
|
19607
|
-
const coordinationCondition = !remoteSetup && noticeReason ? tab.key : null;
|
|
19608
|
-
useEffect(() => {
|
|
19609
|
-
setVisibleCoordinationCondition(null);
|
|
19610
|
-
if (coordinationCondition === null) return;
|
|
19611
|
-
const timer = setTimeout(
|
|
19612
|
-
() => setVisibleCoordinationCondition(coordinationCondition),
|
|
19613
|
-
COORDINATION_NOTICE_AFTER_MS,
|
|
19614
|
-
);
|
|
19615
|
-
return () => clearTimeout(timer);
|
|
19616
|
-
}, [coordinationCondition]);
|
|
19617
|
-
const showCoordinationNotice = coordinationCondition !== null &&
|
|
19618
|
-
visibleCoordinationCondition === coordinationCondition;
|
|
19619
|
-
const noticeResource = activeResource;
|
|
19620
|
-
const noticeSourceAt = collectorSourceAgeRef.current[tab.key]
|
|
19621
|
-
? now.getTime() - collectorSourceAgeRef.current[tab.key].ageMs -
|
|
19622
|
-
Math.max(0, performance.now() - collectorSourceAgeRef.current[tab.key].measuredAt)
|
|
19623
|
-
: lastOkRef.current[tab.key] ?? null;
|
|
19624
|
-
const noticeCause = activeRecovery ?? (noticeReason ? recoveryCause({
|
|
19625
|
-
reason: noticeReason === "other" ? "network-outage" : noticeReason,
|
|
19626
|
-
resource: noticeResource, sourceAt: noticeSourceAt,
|
|
19627
|
-
retryAt: activeGovernorDecision?.retryAt ?? activeGovernorDecision?.resetMs ??
|
|
19628
|
-
activeGovernorDecision?.notBefore ?? null,
|
|
19629
|
-
origin: tabError ? "source" : "coordination", tab: tabError ? tab.key : null,
|
|
19630
|
-
}) : null);
|
|
19631
|
-
const noticeLines = showCoordinationNotice && noticeCause
|
|
19632
|
-
? presentRecovery(noticeCause, { nowMs: now.getTime(), cols })
|
|
19633
|
-
: [!remoteSetup && displayError ? displayError : ""];
|
|
19634
|
-
const noticeTone = showCoordinationNotice ? ATTENTION : displayError ? ERROR_TEXT : undefined;
|
|
19635
20224
|
const spin = SPINNER[frame % SPINNER.length];
|
|
19636
20225
|
|
|
19637
20226
|
const counts = Object.fromEntries(
|
|
@@ -19673,37 +20262,12 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
19673
20262
|
TABS.map((t) => [t.key, Boolean(errors[t.key]) && errors[t.key]?.verdict !== "no-remote"]),
|
|
19674
20263
|
);
|
|
19675
20264
|
|
|
19676
|
-
// Data can be arbitrarily old without anything on screen saying so: a failing
|
|
19677
|
-
// poll only surfaces on the tab you have selected, and after a laptop sleep
|
|
19678
|
-
// every pane is plausible and wrong. Threshold-gated and minute-granular on
|
|
19679
|
-
// purpose -- see STALE_AFTER_MS.
|
|
19680
|
-
// Read from the ref written on every successful poll, not from meta.at, which
|
|
19681
|
-
// only moves when the *payload* changes -- see lastOkRef. Reading a ref during
|
|
19682
|
-
// render lags by one render, which is harmless here because `now` advances on
|
|
19683
|
-
// its own and the label is minute-granular by design.
|
|
19684
|
-
const collectorAge = collectorSourceAgeRef.current[tab.key];
|
|
19685
|
-
const sourceAgeMs = collectorAge
|
|
19686
|
-
? collectorAge.ageMs + Math.max(0, performance.now() - collectorAge.measuredAt)
|
|
19687
|
-
: null;
|
|
19688
|
-
const lastOk = sourceAgeMs === null ? lastOkRef.current[tab.key] : now.getTime() - sourceAgeMs;
|
|
19689
|
-
const staleFor = sourceAgeMs ?? (lastOk == null ? null : now.getTime() - lastOk);
|
|
19690
|
-
const staleAt = freshnessDeadline({
|
|
19691
|
-
lastOk,
|
|
19692
|
-
refreshMs: runtime.refreshMs,
|
|
19693
|
-
grantedMs: admittedCadenceRef.current[tab.key]?.grantedMs,
|
|
19694
|
-
governorDecision: activeGovernorDecision,
|
|
19695
|
-
currentEpochs: governorEpochs,
|
|
19696
|
-
});
|
|
19697
|
-
const staleLabel =
|
|
19698
|
-
staleFor != null && staleAt != null && now.getTime() > staleAt
|
|
19699
|
-
? formatDuration(staleFor)
|
|
19700
|
-
: null;
|
|
19701
20265
|
if (staleLabel && (["watching", "shared"].includes(semanticStatus.kind) ||
|
|
19702
20266
|
activeFailure && semanticStatus.kind === "paused" &&
|
|
19703
20267
|
activeGovernorDecision?.cause)) {
|
|
19704
20268
|
semanticStatus = refreshStatus({
|
|
19705
20269
|
stale: true,
|
|
19706
|
-
disconnected:
|
|
20270
|
+
disconnected: statusDisconnected,
|
|
19707
20271
|
});
|
|
19708
20272
|
}
|
|
19709
20273
|
|
|
@@ -19713,6 +20277,7 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
19713
20277
|
semanticStatus = refreshStatus({ onDemand: true });
|
|
19714
20278
|
}
|
|
19715
20279
|
|
|
20280
|
+
|
|
19716
20281
|
// A settled on-demand view has no data wake to advance its visible age.
|
|
19717
20282
|
// Tick only its display clock; source timestamps and acquisition stay idle.
|
|
19718
20283
|
const showCachedAge = semanticStatus.kind === "cached" && staleFor !== null;
|
|
@@ -19763,7 +20328,7 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
19763
20328
|
// Bottom-right of the frame, lazygit style: how much of the tab you can
|
|
19764
20329
|
// currently see out of how much there is.
|
|
19765
20330
|
const countLabel =
|
|
19766
|
-
|
|
20331
|
+
!tableVisible
|
|
19767
20332
|
? null
|
|
19768
20333
|
: tabOffset > 0
|
|
19769
20334
|
? `${tabOffset + 1}-${tabOffset + visibleItems.length} of ${counts[tab.key]}`
|
|
@@ -19777,22 +20342,9 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
19777
20342
|
// terminal is doing the clipping.
|
|
19778
20343
|
const frameCols = Math.max(1, cols - 1);
|
|
19779
20344
|
|
|
19780
|
-
//
|
|
19781
|
-
//
|
|
19782
|
-
|
|
19783
|
-
// several panes side by side plus `--repo` from anywhere -- so telling them
|
|
19784
|
-
// apart meant quitting and running --doctor. Only the explicit cases are shown:
|
|
19785
|
-
// resolving the *inferred* repo would cost a subprocess call at startup for a
|
|
19786
|
-
// label that, by definition, names the directory you are already sitting in.
|
|
19787
|
-
//
|
|
19788
|
-
// GH_REPO is read straight from the environment and never went through
|
|
19789
|
-
// parseRepoTarget, so unlike runtime.repo it is unvalidated -- safe() before it
|
|
19790
|
-
// is drawn, on the same rule as every other string this app does not own.
|
|
19791
|
-
// Dropped before the tab name rather than with it: PanelEdge drops its whole
|
|
19792
|
-
// label when it cannot seat one, so a single concatenated string would take the
|
|
19793
|
-
// tab name down with the target at ordinary widths.
|
|
19794
|
-
const target = runtime.repo ?? safe(process.env.GH_REPO ?? "");
|
|
19795
|
-
const withTarget = target ? `${tab.label} · ${target}` : tab.label;
|
|
20345
|
+
// The existing resolver also supplies inferred targets without another lookup.
|
|
20346
|
+
// Complete target and host remain reachable in help when this edge is short.
|
|
20347
|
+
const withTarget = `${tab.label} · ${selectedTarget.label}`;
|
|
19796
20348
|
const topLabel = frameCols - 3 - withTarget.length >= 0 ? withTarget : tab.label;
|
|
19797
20349
|
|
|
19798
20350
|
// Fixed columns deliberately do not shrink, so BRANCH and TIME stay readable
|
|
@@ -19822,8 +20374,8 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
19822
20374
|
// avoid, and it was reachable simply by dragging a sidebar narrow. The guard
|
|
19823
20375
|
// sits after usableSize(), which substitutes DEFAULT_COLS for a 0 reported
|
|
19824
20376
|
// mid-resize, so a transient zero cannot be mistaken for a genuinely tiny pane.
|
|
19825
|
-
const tooNarrow = frameCols < MIN_COMPACT_WIDTH;
|
|
19826
|
-
const fullHeaderVisible = !compact && !showHelp && !tooNarrow && !remoteSetup;
|
|
20377
|
+
const tooNarrow = tableVisible && frameCols < MIN_COMPACT_WIDTH;
|
|
20378
|
+
const fullHeaderVisible = tableVisible && !compact && !showHelp && !tooNarrow && !remoteSetup;
|
|
19827
20379
|
const selectedWidthKey = selectedWidthKeyByTab[tab.key] ?? null;
|
|
19828
20380
|
const selectedWidthColumn = effectiveHeader?.find(
|
|
19829
20381
|
(column) => column.key === selectedWidthKey,
|
|
@@ -19913,23 +20465,22 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
19913
20465
|
// recovers immediately -- this is a render-time branch and touches no state.
|
|
19914
20466
|
// Short enough to survive at the widths it actually appears at -- a message
|
|
19915
20467
|
// about the pane being too narrow that is itself truncated would be a joke.
|
|
19916
|
-
//
|
|
19917
|
-
//
|
|
19918
|
-
// truncate-end so it degrades rather than wraps in a narrow pane. Every
|
|
19919
|
-
// glyph in KEY_TABLE is plain ASCII, which is what keeps it safe here.
|
|
20468
|
+
// Help is cell-wrapped and paginated within bodyRows, with navigation
|
|
20469
|
+
// and quit reserved on every page. Rendering never adds another wrap.
|
|
19920
20470
|
showHelp &&
|
|
19921
|
-
|
|
20471
|
+
(helpPages[visibleHelpPage] ?? [])
|
|
19922
20472
|
.map((line, i) => e(Text, { key: `help${i}`, wrap: "truncate-end" }, line)),
|
|
19923
20473
|
!showHelp &&
|
|
19924
20474
|
tooNarrow &&
|
|
19925
20475
|
e(Text, { dimColor: true, wrap: "truncate-end" }, "too narrow"),
|
|
19926
|
-
...noticeLines.map((line, index) =>
|
|
20476
|
+
...(showHelp || !tableVisible ? [] : noticeLines).map((line, index) =>
|
|
19927
20477
|
e(Text, { key: `recovery:${index}`, color: noticeTone, wrap: "truncate-end" }, line)),
|
|
19928
20478
|
!showHelp &&
|
|
19929
20479
|
!tooNarrow &&
|
|
19930
20480
|
!remoteSetup &&
|
|
20481
|
+
tableVisible &&
|
|
19931
20482
|
tab.key === "security" &&
|
|
19932
|
-
|
|
20483
|
+
visibleSecurityLines.map((note, i) =>
|
|
19933
20484
|
e(Text, { key: i, dimColor: true, wrap: "truncate-end" }, note),
|
|
19934
20485
|
),
|
|
19935
20486
|
!showHelp &&
|
|
@@ -19952,6 +20503,7 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
19952
20503
|
!showHelp &&
|
|
19953
20504
|
!tooNarrow &&
|
|
19954
20505
|
!remoteSetup &&
|
|
20506
|
+
tableVisible &&
|
|
19955
20507
|
e(MemoHeaderCells, {
|
|
19956
20508
|
cells: header,
|
|
19957
20509
|
selectedWidthKey: resizeRef.current.active ? selectedWidthKey : null,
|
|
@@ -19978,49 +20530,23 @@ function App({ onCreateRemote = () => {} } = {}) {
|
|
|
19978
20530
|
}),
|
|
19979
20531
|
);
|
|
19980
20532
|
}),
|
|
19981
|
-
|
|
19982
|
-
|
|
19983
|
-
|
|
19984
|
-
|
|
19985
|
-
|
|
19986
|
-
// an error and has never resolved: the error line directly above already
|
|
19987
|
-
// says what happened, and "no runs" underneath it reads as a fact about the
|
|
19988
|
-
// repository rather than the absence of an answer.
|
|
19989
|
-
!showHelp &&
|
|
19990
|
-
!tooNarrow &&
|
|
19991
|
-
!remoteSetup &&
|
|
19992
|
-
visibleItems.length === 0 &&
|
|
19993
|
-
!(tabError && items == null) &&
|
|
19994
|
-
e(
|
|
19995
|
-
Text,
|
|
19996
|
-
{ dimColor: true },
|
|
19997
|
-
firstLoad[tab.key]
|
|
19998
|
-
? `${showSpinner ? spin : SPINNER[0]} loading ${tab.label.toLowerCase()}…` +
|
|
19999
|
-
// Appended only once a first fetch has been running for
|
|
20000
|
-
// ICON_HINT_AFTER_MS, so an ordinary start never shows it: the
|
|
20001
|
-
// remedy is for someone looking at a pane of blank boxes, and by
|
|
20002
|
-
// then they have been looking long enough to want an explanation.
|
|
20003
|
-
// Suppressed once icons are already ASCII, since then there is
|
|
20004
|
-
// nothing to fix.
|
|
20005
|
-
(USING_NERD_ICONS && iconHintDue ? " (icons blank? GH_GLANCE_ICONS=unicode)" : "")
|
|
20006
|
-
: semanticStatus.kind === "paused" || waiting[tab.key]
|
|
20007
|
-
? "waiting for API budget…"
|
|
20008
|
-
: tab.key === "security" && securityNotes.length === ALERT_SOURCES.length
|
|
20009
|
-
? "no alert sources available"
|
|
20010
|
-
: `no ${tab.countLabel}`,
|
|
20011
|
-
),
|
|
20533
|
+
!showHelp && !remoteSetup && !tableVisible &&
|
|
20534
|
+
panelLines.map((line, index) => e(Text, {
|
|
20535
|
+
key: `context:${index}`, wrap: "truncate-end",
|
|
20536
|
+
bold: index === 0, color: index === 0 ? TITLE_COLOR : undefined,
|
|
20537
|
+
}, line)),
|
|
20012
20538
|
// Pushes the panel's bottom edge down to the foot of the pane, so the
|
|
20013
20539
|
// frame stays put instead of closing up under the column headers on a
|
|
20014
20540
|
// tab that only has a handful of rows.
|
|
20015
20541
|
e(Box, { flexGrow: 1 }),
|
|
20016
20542
|
),
|
|
20017
|
-
e(PanelEdge, { width: frameCols, top: false, label: countLabel, labelColor: BORDER_COLOR }),
|
|
20543
|
+
e(PanelEdge, { width: frameCols, top: false, label: showHelp ? null : countLabel, labelColor: BORDER_COLOR }),
|
|
20018
20544
|
e(StatusBar, {
|
|
20019
20545
|
status: semanticStatus,
|
|
20020
20546
|
detail: activeGovernorDecision,
|
|
20021
20547
|
spin: showSpinner ? spin : null,
|
|
20022
|
-
stale: semanticStatus.kind === "cached" && staleFor
|
|
20023
|
-
? formatDuration(
|
|
20548
|
+
stale: semanticStatus.kind === "cached" && Number.isFinite(staleFor) && staleFor >= 0
|
|
20549
|
+
? formatDuration(staleFor) : staleLabel,
|
|
20024
20550
|
nowMs: now.getTime(),
|
|
20025
20551
|
interactive,
|
|
20026
20552
|
cols: frameCols,
|
|
@@ -20375,6 +20901,9 @@ export {
|
|
|
20375
20901
|
GOVERNOR_MAX_LEASES,
|
|
20376
20902
|
GOVERNOR_MAX_INTENTS,
|
|
20377
20903
|
GOVERNOR_MAX_RESERVATIONS,
|
|
20904
|
+
GOVERNOR_MAX_DEBT_GROUPS,
|
|
20905
|
+
DEBT_JOURNAL_COMPACT_AT,
|
|
20906
|
+
UNKNOWN_DEBT_GROUP_ID,
|
|
20378
20907
|
GOVERNOR_LOCK_ORPHAN_MS,
|
|
20379
20908
|
GOVERNOR_LOCK_WAIT_MS,
|
|
20380
20909
|
GOVERNOR_PROBE_DRAIN_MS,
|
|
@@ -20390,6 +20919,7 @@ export {
|
|
|
20390
20919
|
normalizeGovernorState,
|
|
20391
20920
|
serializeGovernorState,
|
|
20392
20921
|
readGovernorState,
|
|
20922
|
+
readIdentityQuotaState,
|
|
20393
20923
|
writeGovernorState,
|
|
20394
20924
|
writeGovernorQuotaState,
|
|
20395
20925
|
pidIsDead,
|
|
@@ -20465,6 +20995,12 @@ export {
|
|
|
20465
20995
|
ghApi,
|
|
20466
20996
|
actionsRunsArgs,
|
|
20467
20997
|
actionsWorkflowsArgs,
|
|
20998
|
+
actionsSetupArgs,
|
|
20999
|
+
parseActionsSetup,
|
|
21000
|
+
actionsSetupFresh,
|
|
21001
|
+
actionsSetupRetryAt,
|
|
21002
|
+
actionsPageUrl,
|
|
21003
|
+
openBrowserUrl,
|
|
20468
21004
|
parseActionsBodies,
|
|
20469
21005
|
parseActionsRuns,
|
|
20470
21006
|
parseWorkflowCatalog,
|
|
@@ -20633,6 +21169,12 @@ export {
|
|
|
20633
21169
|
collectorSourceAdvanced,
|
|
20634
21170
|
cleanupQueueForResource,
|
|
20635
21171
|
presentRecovery,
|
|
21172
|
+
dashboardPresentation,
|
|
21173
|
+
contextualPanelLayout,
|
|
21174
|
+
dashboardTarget,
|
|
21175
|
+
contextHelpPages,
|
|
21176
|
+
clampHelpPage,
|
|
21177
|
+
wrapPanelText,
|
|
20636
21178
|
recordRecoveryTransition,
|
|
20637
21179
|
safeRecordRecoveryTransition,
|
|
20638
21180
|
readRecoveryTransitions,
|