The audit's second tier: values that were measurements of the author's desktop, shipped to every machine as if they were defaults. Settings greeted every human as Gabriel; it now greets whoever accountsservice says is signed in, and nobody when it says nothing. The weather shipped his home coordinates and confidently reported his forecast anywhere on earth; it now ships unset, fetches nothing until a location is chosen, and the location row says so. The GTK bookmarks carried seven /home/gib paths and his file server into every file dialog; they are now generated per machine from a template and gitignored -- Nautilus edits the instance freely, the way settings.ini already worked one file over. Web search routed through his personal bang redirector; the engine is now the webSearchUrl preference with a DuckDuckGo default, read by both the script command and the suggestions extension, which the launcher-search contract already pins to one another. The GPU vitals path defaulted to his card1 and lost the readout on any machine enumerated differently; a machine with exactly one GPU now adopts it. And the Containers and Snapshots pages hide once a scan proves their backing stack absent, instead of rendering permanently empty on machines that never had podman or snapper. Lesser residue swept in the same pass: the DP-2 hyprpaper block one machine needed, the author's username-typo expansions (moved to his personal seed in user/, where personal content belongs), a capture fallback into /home/gib, and a parity table asserting one machine's hardware as fact. Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
300 lines
12 KiB
QML
300 lines
12 KiB
QML
// Home — the page you open to do something, not to read a report.
|
|
//
|
|
// It used to lead with a diagram of the attached monitor: DP-2, 4500 x 3000,
|
|
// 1.13x scale, XRGB2101010. That is Displays-page data, it was the largest
|
|
// thing on the screen, and nobody has ever needed it here. Below it sat a
|
|
// permanently open search field for a weather location that is set about once a
|
|
// year, and then roughly half a page of nothing.
|
|
//
|
|
// What Panama already knows is the useful material: whether anything needs
|
|
// attention, what is running, and the two or three things worth doing next. A
|
|
// finding is shown when there is one and the page is quiet when there is not,
|
|
// which is the same shape the Firewall and Containers pages use.
|
|
//
|
|
// Weather stays. It is the one genuinely ambient thing here, and it belongs
|
|
// next to a greeting rather than in a card of its own with a search box open.
|
|
|
|
import QtQuick
|
|
import qs.config
|
|
import qs.services
|
|
|
|
SettingsPage {
|
|
id: root
|
|
|
|
readonly property int openedHour: new Date().getHours()
|
|
readonly property string greeting: openedHour < 12
|
|
? "Good morning"
|
|
: (openedHour < 18 ? "Good afternoon" : "Good evening")
|
|
|
|
// Findings, in the order they deserve attention. Each names the page that
|
|
// can actually resolve it, because a home page that reports a problem it
|
|
// cannot help with is just an alarm.
|
|
readonly property var findings: {
|
|
const found = [];
|
|
const exposed = Firewall.exposedDataStores ?? [];
|
|
if (exposed.length > 0) {
|
|
found.push({
|
|
label: exposed.length === 1
|
|
? "A database is reachable from your network"
|
|
: "Databases are reachable from your network",
|
|
detail: exposed.map(entry => String(entry.name)).join(" and ")
|
|
+ ", published on every interface",
|
|
page: "firewall",
|
|
action: "Review"
|
|
});
|
|
}
|
|
if (Updates.securityCount > 0) {
|
|
found.push({
|
|
label: Updates.securityCount + " update"
|
|
+ (Updates.securityCount === 1 ? "" : "s") + " carry a security advisory",
|
|
detail: "Worth installing before the rest",
|
|
page: "updates",
|
|
action: "Open"
|
|
});
|
|
}
|
|
if (Updates.rebootNeeded) {
|
|
found.push({
|
|
label: "A newer kernel is installed than the one running",
|
|
detail: "Restart to use it",
|
|
page: "updates",
|
|
action: "Open"
|
|
});
|
|
}
|
|
if (Health.summary?.errors > 0 || Health.summary?.warnings > 0) {
|
|
const count = Number(Health.summary?.errors ?? 0) + Number(Health.summary?.warnings ?? 0);
|
|
found.push({
|
|
label: count + " health check" + (count === 1 ? "" : "s") + " need attention",
|
|
detail: "Something the desktop owns is not in its expected state",
|
|
page: "services",
|
|
action: "Open"
|
|
});
|
|
}
|
|
return found;
|
|
}
|
|
|
|
// One line of everything that is fine, so the quiet case still says
|
|
// something rather than showing an empty page.
|
|
readonly property string reassurance: {
|
|
const parts = [];
|
|
if (Health.checks.length > 0)
|
|
parts.push(Health.checks.length + " health checks pass");
|
|
if (Disks.rootFilesystem)
|
|
parts.push(Disks.formatBytes(Number(Disks.rootFilesystem.available ?? 0)) + " free");
|
|
const newest = Snapshots.configs.length > 0
|
|
? (Snapshots.configs[0].snapshots ?? [])[0]
|
|
: null;
|
|
if (newest)
|
|
parts.push("snapshots ran at " + root.clockOf(String(newest.date ?? "")));
|
|
return parts.join(" · ");
|
|
}
|
|
|
|
// snapper prints a full date; only the time is wanted in a summary line.
|
|
function clockOf(stamp: string): string {
|
|
const match = /(\d{1,2}:\d{2})(?::\d{2})?\s*(AM|PM)?/i.exec(stamp);
|
|
if (!match)
|
|
return stamp;
|
|
return match[2] ? match[1] + " " + match[2].toUpperCase() : match[1];
|
|
}
|
|
|
|
// Greets whoever is signed in, from the same accountsservice record the
|
|
// lock screen shows. This once hardcoded the author's first name, which
|
|
// made the first screen of Settings wrong for every other human.
|
|
readonly property string greetingName: {
|
|
if (!UserAccounts.me)
|
|
return "";
|
|
const full = UserAccounts.displayName(UserAccounts.me);
|
|
return full.split(" ")[0] || "";
|
|
}
|
|
|
|
title: root.greetingName !== "" ? `${root.greeting}, ${root.greetingName}` : root.greeting
|
|
lede: Weather.available
|
|
? Math.round(Weather.temperature) + Weather.unitSuffix + " and "
|
|
+ Weather.description.toLowerCase() + " in " + Settings.weatherLocation
|
|
: "Your desktop is configured and ready."
|
|
|
|
Component.onCompleted: {
|
|
if (!Firewall.scanned)
|
|
Firewall.refresh();
|
|
if (!Containers.scanned)
|
|
Containers.refresh();
|
|
if (!Snapshots.scanned)
|
|
Snapshots.refresh();
|
|
if (!Disks.scanned)
|
|
Disks.refresh();
|
|
}
|
|
|
|
// ── What you came here to do ────────────────────────────────────────────
|
|
|
|
Grid {
|
|
id: doing
|
|
|
|
width: parent.width
|
|
columns: width >= 720 ? 2 : 1
|
|
columnSpacing: 16
|
|
rowSpacing: 16
|
|
|
|
SettingsCard {
|
|
width: doing.columns === 2
|
|
? (doing.width - doing.columnSpacing) / 2
|
|
: doing.width
|
|
title: "Focus"
|
|
subtitle: FocusSession.active
|
|
? "A session is running · " + FocusSession.remainingText + " left"
|
|
: (FocusModes.active
|
|
? FocusModes.activeName + " is on because " + FocusModes.activeReason
|
|
: (Notifs.doNotDisturb
|
|
? "Do Not Disturb is on"
|
|
: "Nothing is quieting this machine"))
|
|
|
|
SettingRow {
|
|
label: FocusSession.active ? "End the session" : "Start a focus session"
|
|
detail: FocusSession.active
|
|
? "Puts Do Not Disturb and Caffeine back as they were"
|
|
: Settings.focusDurationMinutes + " minutes · Super+Shift+F"
|
|
controlWidth: 120
|
|
|
|
SettingsButton {
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: FocusSession.active ? "End" : "Start"
|
|
tone: FocusSession.active ? "normal" : "accent"
|
|
onClicked: FocusSession.active ? FocusSession.end(false) : FocusSession.startDefault()
|
|
}
|
|
}
|
|
|
|
SettingRow {
|
|
label: "Do Not Disturb"
|
|
detail: FocusModes.active
|
|
? "Held on by the " + FocusModes.activeName + " mode"
|
|
: "Banners are held; the notification centre still fills"
|
|
divider: false
|
|
controlWidth: 48
|
|
|
|
SettingsToggle {
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
enabled: !FocusModes.active && !FocusSession.active
|
|
checked: Notifs.doNotDisturb
|
|
onToggled: value => Notifs.doNotDisturb = value
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
width: doing.columns === 2
|
|
? (doing.width - doing.columnSpacing) / 2
|
|
: doing.width
|
|
title: "Right now"
|
|
subtitle: Containers.running > 0
|
|
? Containers.running + " of " + Containers.total + " containers running"
|
|
: "No containers are running"
|
|
|
|
TextRow {
|
|
label: Containers.running > 0
|
|
? Containers.projects.filter(project => project.running > 0)
|
|
.map(project => String(project.title)).join(", ")
|
|
: "Nothing to report"
|
|
detail: Containers.running > 0
|
|
? "Started for development, still up"
|
|
: "Start a stack from the Containers page"
|
|
value: ""
|
|
}
|
|
|
|
ActionRow {
|
|
label: "Containers"
|
|
detail: "Projects, what they expose, and what they cost"
|
|
action: "Open"
|
|
divider: false
|
|
onTriggered: ShellState.settingsPage = "containers"
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── This machine ────────────────────────────────────────────────────────
|
|
|
|
SettingsCard {
|
|
title: "This machine"
|
|
subtitle: root.reassurance
|
|
|
|
TextRow {
|
|
visible: root.findings.length === 0
|
|
label: "Nothing needs your attention"
|
|
detail: "Anything worth acting on would appear here."
|
|
value: ""
|
|
divider: false
|
|
}
|
|
|
|
Repeater {
|
|
model: root.findings
|
|
|
|
delegate: SettingRow {
|
|
required property var modelData
|
|
required property int index
|
|
width: parent.width
|
|
label: String(modelData.label ?? "")
|
|
detail: String(modelData.detail ?? "")
|
|
controlWidth: 110
|
|
divider: index < root.findings.length - 1
|
|
|
|
SettingsButton {
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: String(modelData.action ?? "Open")
|
|
onClicked: ShellState.settingsPage = String(modelData.page)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Worth doing, when there is something ────────────────────────────────
|
|
|
|
SettingsCard {
|
|
visible: Updates.total > 0 || Containers.reclaimable > 0
|
|
title: "Do next"
|
|
subtitle: "Small things this machine is waiting on."
|
|
|
|
ActionRow {
|
|
visible: Updates.total > 0
|
|
label: "Install " + Updates.total + " update" + (Updates.total === 1 ? "" : "s")
|
|
detail: "Packages, applications and firmware, from wherever each comes from"
|
|
action: "Open"
|
|
divider: Containers.reclaimable > 0
|
|
onTriggered: ShellState.settingsPage = "updates"
|
|
}
|
|
|
|
ActionRow {
|
|
visible: Containers.reclaimable > 0
|
|
label: "Reclaim " + Containers.formatBytes(Containers.reclaimable)
|
|
detail: "Container images and volumes nothing references"
|
|
action: "Review"
|
|
divider: false
|
|
onTriggered: ShellState.settingsPage = "containers"
|
|
}
|
|
}
|
|
|
|
// ── Weather ─────────────────────────────────────────────────────────────
|
|
|
|
SettingsCard {
|
|
title: "Weather"
|
|
subtitle: "Shown above and in the date menu."
|
|
|
|
// The location is set roughly once a year, so the search that changes it
|
|
// is behind a press rather than permanently open.
|
|
PickerRow {
|
|
id: locationPicker
|
|
|
|
label: "Location"
|
|
detail: "Only the search term is sent; the name is a label kept on this machine"
|
|
value: Settings.weatherLocation !== "" ? Settings.weatherLocation : "Not set — choose one to see weather"
|
|
|
|
LocationPicker {
|
|
width: parent.width
|
|
onPicked: locationPicker.collapse()
|
|
}
|
|
}
|
|
|
|
ChoiceRow { setting: "temperatureUnit" }
|
|
SliderRow { setting: "weatherRefreshMinutes"; divider: false }
|
|
}
|
|
}
|