Files
Panama/config/dot/quickshell/modules/settings/UpdatesPage.qml
T
Gabriel Brown 8f0fe23377 Add Software Update, across packages, applications and firmware
Three sources that fail independently, so they are counted and applied
separately: a flatpak mirror being down says nothing about whether a
kernel security fix is waiting. Blending them into one number would hide
exactly the case that matters.

Checking costs about nine seconds, which is too long to spend every time
a page opens, so the page opens on the last result and says when it was
taken. A first visit with nothing cached goes and finds out rather than
showing a confident "up to date" it has no basis for.

Installing packages takes a snapshot first, named after what is about to
happen, so Snapshots shows "before 32 package updates" rather than a
timestamp. Best effort: a machine without snapper still updates, because
an update that refuses to run when a nicety fails would be worse than
one without a restore point.

Automatic updates cover applications only, through a Panama-owned user
timer running daily with a randomized delay. Packages still ask, and
dnf-automatic is reported as absent rather than offered, because
installing software is not a settings action.

Health gained a check, and that is where the bug was: it first returned
status "degraded", which is not in the doctor's vocabulary of ok,
warning, error and unconfigured. It was counted as nothing at all while
the summary still said healthy -- the same silent no-op this codebase
keeps relearning. A contract now asserts every status a check can return
is one the doctor counts, and the doctor's own contract knows about the
new check rather than failing on its arrival.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-19 17:25:11 -04:00

259 lines
9.7 KiB
QML

// Software updates.
//
// Three sources that fail independently -- packages, applications, firmware --
// so each is counted and applied on its own. Blending them into one number
// would hide the case that matters: a flatpak mirror being down says nothing
// about whether a security fix is waiting.
//
// Applying packages takes a snapshot first, named after what is about to
// happen, so the Snapshots page shows "before 32 package updates" rather than a
// timestamp. That is the thing neither macOS nor Windows does cleanly, and it
// is nearly free here.
import Quickshell
import QtQuick
import qs.config
import qs.services
SettingsPage {
id: root
objectName: "updates"
title: "Software Update"
lede: "Packages, applications, and firmware, each from the place it actually comes from."
property string expandedSource: ""
Component.onCompleted: {
Updates.refresh();
// A first visit with nothing cached should not show a confident "up to
// date" it has no basis for, so it goes and finds out.
if (!Updates.everChecked && !Updates.checking)
Updates.check();
}
TextRow {
visible: Updates.lastError !== ""
label: "Updates need attention"
detail: Updates.lastError
value: ""
divider: false
}
TextRow {
visible: Updates.lastApplied !== null
label: "Updated"
detail: Updates.lastApplied
? Updates.sourceLabel(String(Updates.lastApplied.source ?? ""))
+ (String(Updates.lastApplied.restorePoint ?? "") !== ""
? " · a snapshot was taken first, number "
+ String(Updates.lastApplied.restorePoint)
: "")
: ""
value: ""
divider: false
}
// ── The headline ─────────────────────────────────────────────────────────
SettingsCard {
Column {
width: parent.width
spacing: 10
Row {
width: parent.width
spacing: 12
Text {
text: Updates.checking ? "Checking…" : Updates.summary()
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeTitle
font.weight: Font.DemiBold
}
Text {
anchors.verticalCenter: parent.verticalCenter
visible: Updates.securityCount > 0
text: Updates.securityCount + " carry a security advisory"
color: Theme.warn
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
}
}
Text {
text: Updates.lastCheckedText()
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
ActionRow {
label: "Check for updates"
detail: "Refreshes package metadata, application remotes, and firmware. Takes a few seconds."
action: Updates.checking ? "Checking…" : "Check now"
enabled: !Updates.busy
divider: Updates.rebootNeeded
onTriggered: Updates.check()
}
// The honest version of "restart required": the running kernel is not
// the newest installed one, so a reboot would change which kernel runs.
TextRow {
visible: Updates.rebootNeeded
label: "Restart to finish"
detail: "A newer kernel is installed than the one running. "
+ String(Updates.kernel?.running ?? "") + " → "
+ String(Updates.kernel?.newestInstalled ?? "")
value: "Restart needed"
divider: false
}
}
// ── One card per source ──────────────────────────────────────────────────
SettingsCard {
title: "System packages"
subtitle: Updates.dnf?.available === false
? "dnf is not available on this machine."
: (Number(Updates.dnf?.count ?? 0) === 0
? "Nothing waiting."
: Updates.dnf.count + " package"
+ (Updates.dnf.count === 1 ? "" : "s") + " ready to install"
+ (Updates.securityCount > 0
? ", " + Updates.securityCount + " carrying an advisory" : ""))
ActionRow {
visible: Number(Updates.dnf?.count ?? 0) > 0
label: "Install package updates"
detail: "Asks for your password, and takes a snapshot first so this can be undone"
action: Updates.applying ? "Working…" : "Install"
enabled: !Updates.busy
onTriggered: Updates.apply("dnf")
}
ActionRow {
visible: Number(Updates.dnf?.count ?? 0) > 0
label: "What would change"
detail: root.expandedSource === "dnf"
? "Every package that would be replaced"
: Updates.dnf.count + " packages"
action: root.expandedSource === "dnf" ? "Hide" : "Show"
divider: root.expandedSource === "dnf"
onTriggered: root.expandedSource = root.expandedSource === "dnf" ? "" : "dnf"
}
Repeater {
model: root.expandedSource === "dnf" ? (Updates.dnf?.packages ?? []) : []
delegate: TextRow {
required property var modelData
required property int index
width: parent.width
label: String(modelData.name ?? "")
detail: String(modelData.repository ?? "")
value: String(modelData.version ?? "")
divider: index < (Updates.dnf?.packages ?? []).length - 1
}
}
TextRow {
visible: Number(Updates.dnf?.count ?? 0) === 0 && Updates.everChecked
label: "Packages are current"
detail: "Nothing from the system repositories is waiting"
value: ""
divider: false
}
}
SettingsCard {
title: "Applications"
subtitle: Updates.flatpak?.available === false
? "Flatpak is not installed."
: (Number(Updates.flatpak?.count ?? 0) === 0
? "Nothing waiting."
: Updates.flatpak.count + " application"
+ (Updates.flatpak.count === 1 ? "" : "s") + " ready to update")
Repeater {
model: Updates.flatpak?.applications ?? []
delegate: TextRow {
required property var modelData
required property int index
width: parent.width
label: String(modelData.id ?? "")
detail: "Flatpak"
value: String(modelData.version ?? "")
divider: true
}
}
ActionRow {
visible: Number(Updates.flatpak?.count ?? 0) > 0
label: "Update applications"
detail: "Needs no password: these are installed for your account"
action: Updates.applying ? "Working…" : "Update"
enabled: !Updates.busy
onTriggered: Updates.apply("flatpak")
}
SwitchRow {
label: "Update applications automatically"
detail: "Once a day, in the background. Applications are not a security boundary the way packages are, so this is safe to leave on; packages still ask."
checked: Updates.automatic?.flatpakEnabled === true
enabled: !Updates.busy && Updates.automatic?.flatpakAvailable === true
divider: false
onToggled: value => Updates.setAutomaticFlatpak(value)
}
}
SettingsCard {
title: "Firmware"
subtitle: Updates.firmware?.available === false
? "Firmware updating is not available on this machine."
: (Number(Updates.firmware?.count ?? 0) === 0
? "No firmware updates are offered for this hardware."
: Updates.firmware.count + " device"
+ (Updates.firmware.count === 1 ? "" : "s") + " have firmware available")
Repeater {
model: Updates.firmware?.devices ?? []
delegate: TextRow {
required property var modelData
required property int index
width: parent.width
label: String(modelData.name ?? "")
detail: String(modelData.version ?? "") + " → " + String(modelData.target ?? "")
+ (modelData.needsReboot ? " · installs on restart" : "")
value: ""
divider: true
}
}
ActionRow {
visible: Number(Updates.firmware?.count ?? 0) > 0
label: "Install firmware"
detail: "Some devices only finish updating after a restart"
action: Updates.applying ? "Working…" : "Install"
enabled: !Updates.busy
divider: false
onTriggered: Updates.apply("firmware")
}
// Reported rather than offered. dnf-automatic is a package this machine
// does not have, and installing software is not a settings action.
TextRow {
visible: Updates.automatic?.dnfAutomaticAvailable === false
label: "Automatic package updates"
detail: "Not set up. dnf-automatic is not installed, and Settings does not install software."
value: "Off"
divider: false
}
}
}