Finish the wonderland: System told truthfully, in eight tabs instead of ten
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -27,13 +27,19 @@ Singleton {
|
||||
property bool postRepairScanPending: false
|
||||
|
||||
readonly property bool actionable: root.status === "warning" || root.status === "error"
|
||||
readonly property bool busy: scanProcess.running || repairProcess.running || root.postRepairScanPending
|
||||
readonly property bool busy: scanProcess.running || repairProcess.running
|
||||
|| singleCheckProcess.running || root.postRepairScanPending
|
||||
readonly property string helperPath: Quickshell.env("PANAMA_HEALTH_HELPER")
|
||||
|| Quickshell.shellDir + "/scripts/panama-doctor"
|
||||
readonly property var statuses: ["ok", "warning", "error", "unconfigured"]
|
||||
readonly property var groups: ["desktop-foundation", "input-media", "integrations", "panama-tools"]
|
||||
readonly property var overallStatuses: ["healthy", "warning", "error"]
|
||||
readonly property var settingsTargets: ["my-home", "datetime"]
|
||||
// Every page a check's "open" action is allowed to send someone to. This
|
||||
// list and the doctor's authored targets are one change, not two: a target
|
||||
// the doctor emits but this does not accept fails validAction, and a single
|
||||
// rejected action invalidates the WHOLE snapshot -- so half of the pair
|
||||
// does not degrade the row, it blanks the page.
|
||||
readonly property var settingsTargets: ["my-home", "datetime", "updates"]
|
||||
readonly property var instructionTargets: ["ddc-permissions"]
|
||||
|
||||
Process {
|
||||
@@ -113,6 +119,54 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: singleCheckProcess
|
||||
|
||||
property string checkId: ""
|
||||
property int baseGeneration: 0
|
||||
property string outputText: ""
|
||||
property int exitCode: -1
|
||||
property bool exited: false
|
||||
property bool streamFinished: false
|
||||
property bool settled: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
singleCheckProcess.outputText = this.text;
|
||||
singleCheckProcess.streamFinished = true;
|
||||
root.settleSingleCheck();
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
singleCheckProcess.exitCode = exitCode;
|
||||
singleCheckProcess.exited = true;
|
||||
root.settleSingleCheck();
|
||||
}
|
||||
}
|
||||
|
||||
// `tee` rather than a shell redirect: the path is a value, not a fragment
|
||||
// of a command line, so nothing about it can be read as syntax.
|
||||
Process {
|
||||
id: saveProcess
|
||||
|
||||
property string payload: ""
|
||||
property string targetPath: ""
|
||||
|
||||
stdinEnabled: true
|
||||
stdout: StdioCollector {}
|
||||
onStarted: {
|
||||
saveProcess.write(saveProcess.payload);
|
||||
saveProcess.stdinEnabled = false;
|
||||
}
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
root.lastSaveResult = exitCode === 0
|
||||
? "Report saved to " + saveProcess.targetPath + "."
|
||||
: "Could not save the health report.";
|
||||
saveProcess.payload = "";
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: failureNotification
|
||||
}
|
||||
@@ -229,6 +283,11 @@ Singleton {
|
||||
};
|
||||
if (candidate.action !== undefined)
|
||||
check.action = root.safeAction(candidate.action);
|
||||
// Carried through rather than reconstructed: the page shows the exact
|
||||
// command a repair will run before running it, and the helper is the
|
||||
// only thing that knows what that is.
|
||||
if (candidate.repairCommand !== undefined)
|
||||
check.repairCommand = candidate.repairCommand;
|
||||
return check;
|
||||
}
|
||||
|
||||
@@ -343,6 +402,154 @@ Singleton {
|
||||
&& candidate.message.length > 0;
|
||||
}
|
||||
|
||||
// ── Re-checking one row ─────────────────────────────────────────────────
|
||||
//
|
||||
// A full scan runs thirty probes. Asking again about the one row somebody
|
||||
// just repaired should not cost the other twenty-nine, so the helper is
|
||||
// asked for that check alone and the answer is spliced into the accepted
|
||||
// snapshot. The reply arrives in the full snapshot shape, which means it
|
||||
// goes through exactly the same validation as a whole scan -- an invalid
|
||||
// single-check reply leaves the existing row alone rather than replacing a
|
||||
// good answer with a bad one.
|
||||
|
||||
property string refreshingId: ""
|
||||
|
||||
readonly property bool refreshingCheck: singleCheckProcess.running
|
||||
|
||||
function refreshCheck(id: string): bool {
|
||||
if (root.busy || singleCheckProcess.running)
|
||||
return false;
|
||||
if (!root.checks.some(candidate => candidate.id === id))
|
||||
return false;
|
||||
|
||||
root.refreshingId = id;
|
||||
singleCheckProcess.checkId = id;
|
||||
singleCheckProcess.baseGeneration = root.acceptedGeneration;
|
||||
singleCheckProcess.outputText = "";
|
||||
singleCheckProcess.exitCode = -1;
|
||||
singleCheckProcess.exited = false;
|
||||
singleCheckProcess.streamFinished = false;
|
||||
singleCheckProcess.settled = false;
|
||||
singleCheckProcess.exec([root.helperPath, "check", id]);
|
||||
return true;
|
||||
}
|
||||
|
||||
function settleSingleCheck(): void {
|
||||
if (singleCheckProcess.settled || !singleCheckProcess.exited
|
||||
|| !singleCheckProcess.streamFinished)
|
||||
return;
|
||||
singleCheckProcess.settled = true;
|
||||
root.finishSingleCheck(singleCheckProcess.exitCode, singleCheckProcess.checkId,
|
||||
singleCheckProcess.baseGeneration,
|
||||
singleCheckProcess.outputText);
|
||||
}
|
||||
|
||||
function finishSingleCheck(exitCode: int, id: string, baseGeneration: int, text: string): bool {
|
||||
root.refreshingId = "";
|
||||
// A full scan that landed while this one row was being re-checked is
|
||||
// the newer answer for every row including this one. Splicing a stale
|
||||
// row back into it would undo part of a scan nobody asked to undo.
|
||||
if (baseGeneration !== root.acceptedGeneration)
|
||||
return false;
|
||||
if (exitCode !== 0) {
|
||||
root.lastError = "That check could not be re-run.";
|
||||
return false;
|
||||
}
|
||||
|
||||
let candidate;
|
||||
try {
|
||||
candidate = JSON.parse(text.trim());
|
||||
} catch (error) {
|
||||
root.lastError = "That check returned an unreadable response.";
|
||||
return false;
|
||||
}
|
||||
if (!root.validSnapshot(candidate) || candidate.checks.length !== 1
|
||||
|| candidate.checks[0].id !== id) {
|
||||
root.lastError = "That check returned an invalid response.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const replacement = root.safeCheck(candidate.checks[0]);
|
||||
const merged = root.checks.map(check => check.id === id ? replacement : check);
|
||||
root.checks = merged;
|
||||
root.summary = root.countsFor(merged);
|
||||
root.status = root.summary.status;
|
||||
root.snapshot = Object.assign({}, root.snapshot, {
|
||||
checks: merged,
|
||||
summary: root.summary
|
||||
});
|
||||
root.lastError = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
// The same arithmetic the helper does, applied to a list that has had one
|
||||
// row replaced. Recomputed rather than left alone: a warning that repaired
|
||||
// itself must leave the headline count, not just its own row.
|
||||
function countsFor(checks: var): var {
|
||||
const counts = { ok: 0, warning: 0, error: 0, unconfigured: 0 };
|
||||
for (const check of checks)
|
||||
counts[check.status] += 1;
|
||||
return {
|
||||
status: counts.error > 0 ? "error" : counts.warning > 0 ? "warning" : "healthy",
|
||||
healthy: counts.ok,
|
||||
warnings: counts.warning,
|
||||
errors: counts.error,
|
||||
unconfigured: counts.unconfigured
|
||||
};
|
||||
}
|
||||
|
||||
// ── Saving the report ───────────────────────────────────────────────────
|
||||
|
||||
property string lastSaveResult: ""
|
||||
|
||||
readonly property string defaultReportPath:
|
||||
(Quickshell.env("HOME") ?? "") + "/panama-health-report.txt";
|
||||
|
||||
// Plain text rather than the JSON copyReport puts on the clipboard: a file
|
||||
// somebody saves is a file somebody opens, and a report they can read
|
||||
// without a JSON viewer is worth more than one that round-trips.
|
||||
function reportText(): string {
|
||||
const lines = [
|
||||
"Panama system health",
|
||||
"Generated " + String(root.snapshot?.generatedAt ?? "at an unknown time"),
|
||||
"Status: " + root.status
|
||||
+ " (" + root.summary.healthy + " ok, "
|
||||
+ root.summary.warnings + " warnings, "
|
||||
+ root.summary.errors + " errors, "
|
||||
+ root.summary.unconfigured + " unconfigured)",
|
||||
""
|
||||
];
|
||||
for (const version of root.snapshot?.context?.versions ?? [])
|
||||
lines.push(version.id + ": " + version.version);
|
||||
lines.push("");
|
||||
for (const check of root.checks) {
|
||||
lines.push("[" + check.status + "] " + check.title + " (" + check.id + ")");
|
||||
lines.push(" " + check.detail);
|
||||
if (check.repairCommand)
|
||||
lines.push(" repair: " + check.repairCommand);
|
||||
}
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
function saveReport(path: string): bool {
|
||||
if (saveProcess.running)
|
||||
return false;
|
||||
const target = path && path.length > 0 ? path : root.defaultReportPath;
|
||||
if (!target || target.indexOf("/") !== 0) {
|
||||
root.lastSaveResult = "That is not a path this can write to.";
|
||||
return false;
|
||||
}
|
||||
root.lastSaveResult = "";
|
||||
saveProcess.targetPath = target;
|
||||
saveProcess.payload = root.reportText();
|
||||
// Re-arm stdin: the previous run closed it, and a disabled channel
|
||||
// stays closed even after being set back to true mid-run. Same
|
||||
// discipline as copyProcess above.
|
||||
saveProcess.stdinEnabled = true;
|
||||
saveProcess.exec(["tee", target]);
|
||||
return true;
|
||||
}
|
||||
|
||||
function copyReport(): bool {
|
||||
if (copyProcess.running)
|
||||
return false;
|
||||
@@ -366,7 +573,9 @@ Singleton {
|
||||
generation: root.generation,
|
||||
acceptedGeneration: root.acceptedGeneration,
|
||||
repairingId: root.repairingId,
|
||||
refreshingId: root.refreshingId,
|
||||
lastRepair: root.lastRepair,
|
||||
lastSaveResult: root.lastSaveResult,
|
||||
lastError: root.lastError,
|
||||
checks: root.checks.map(check => check.id),
|
||||
checkStates: root.checks.map(check => ({ id: check.id, status: check.status }))
|
||||
@@ -427,6 +636,10 @@ Singleton {
|
||||
|| typeof candidate.title !== "string" || candidate.title.length === 0
|
||||
|| typeof candidate.detail !== "string" || candidate.detail.length === 0)
|
||||
return false;
|
||||
if (candidate.repairCommand !== undefined
|
||||
&& (typeof candidate.repairCommand !== "string"
|
||||
|| candidate.repairCommand.length === 0))
|
||||
return false;
|
||||
return candidate.action === undefined || root.validAction(candidate.action);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user