// 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]; } title: `${root.greeting}, Gabriel` 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 LocationPicker { width: parent.width onPicked: locationPicker.collapse() } } ChoiceRow { setting: "temperatureUnit" } SliderRow { setting: "weatherRefreshMinutes"; divider: false } } }