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
This commit is contained in:
@@ -122,6 +122,7 @@ Rectangle {
|
||||
case "applications": return applicationsPage;
|
||||
case "storage": return storagePage;
|
||||
case "snapshots": return snapshotsPage;
|
||||
case "updates": return updatesPage;
|
||||
case "users": return usersPage;
|
||||
case "sharing": return sharingPage;
|
||||
case "printers": return printersPage;
|
||||
@@ -165,6 +166,7 @@ Rectangle {
|
||||
Component { id: applicationsPage; ApplicationsPage {} }
|
||||
Component { id: storagePage; StoragePage {} }
|
||||
Component { id: snapshotsPage; SnapshotsPage {} }
|
||||
Component { id: updatesPage; UpdatesPage {} }
|
||||
Component { id: usersPage; UsersPage {} }
|
||||
Component { id: sharingPage; SharingPage {} }
|
||||
Component { id: printersPage; PrintersPage {} }
|
||||
|
||||
@@ -42,6 +42,7 @@ Rectangle {
|
||||
{ page: "power", label: "Power & Lock", icon: "\u{F0425}" },
|
||||
{ page: "datetime", label: "Date & Time", icon: "\u{F0954}" },
|
||||
{ page: "applications", label: "Applications", icon: "\u{F003B}" },
|
||||
{ page: "updates", label: "Software Update", icon: "\u{F06B0}" },
|
||||
{ page: "storage", label: "Storage", icon: "\u{F02CA}" },
|
||||
{ page: "snapshots", label: "Snapshots", icon: "\u{F0954}" },
|
||||
{ page: "users", label: "Users", icon: "\u{F0004}" },
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ LockScreenPreview 1.0 LockScreenPreview.qml
|
||||
PowerPage 1.0 PowerPage.qml
|
||||
DateTimePage 1.0 DateTimePage.qml
|
||||
AccessibilityPage 1.0 AccessibilityPage.qml
|
||||
UpdatesPage 1.0 UpdatesPage.qml
|
||||
UsersPage 1.0 UsersPage.qml
|
||||
WallpaperPicker 1.0 WallpaperPicker.qml
|
||||
WallpaperControls 1.0 WallpaperControls.qml
|
||||
|
||||
Reference in New Issue
Block a user