From 9fc6ca0d9a9cdba9d5808951ce290e559a9e8554 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Sun, 13 Sep 2026 14:02:05 +0200 Subject: feat(mail-overview): show mail-watcher health in the drawer The watcher already writes ~/.local/state/mail-watcher.heartbeat every 60s, so the drawer reads it and shows a dot: green when idling, yellow when a folder gave up, red when the heartbeat is stale or missing. Backoff stays green because it is normal recovery and the watcher's own health check treats it that way. The staleness rule is reimplemented in a few lines rather than shelling out to --status; the heartbeat file is the documented cheap-to-poll surface. Read once per open rather than watched: the heartbeat is written by atomic replace, so an inotify watch held on the old inode dies with it. "Sync now" is removed with its Process. The watcher triggers syncs, the cron tick is the backstop, and waybar's right-click still runs mailsync.sh. --- mail-overview/MailPanel.qml | 78 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 6 deletions(-) (limited to 'mail-overview/MailPanel.qml') diff --git a/mail-overview/MailPanel.qml b/mail-overview/MailPanel.qml index 7457fb2..0a7f36a 100644 --- a/mail-overview/MailPanel.qml +++ b/mail-overview/MailPanel.qml @@ -10,6 +10,7 @@ // GNU General Public License for more details. import Quickshell +import Quickshell.Io import Quickshell.Wayland import QtQuick @@ -20,6 +21,45 @@ Scope { property string monitor: "DP-1" property bool open: false + // mail-watcher's heartbeat, written every 60s. Read once per open: the + // drawer is a LazyLoader, so closing and reopening rebuilds the FileView + // and reads the current file. Watch is deliberately not used, because the + // heartbeat is written by atomic replace (tmpfile + rename) and an inotify + // watch held on the old inode dies with it. + property bool watcherAlive: false + property int watcherDead: 0 + property int watcherExpected: 0 + + // The same staleness rule as mail-watcher's heartbeat_is_healthy: dead + // threads or a heartbeat older than 300s mean the watcher needs a look. + // Backoff is healthy, so it never turns the dot. + function readHeartbeat(payload) { + root.watcherAlive = false; + root.watcherDead = 0; + root.watcherExpected = 0; + + let data = null; + try { data = JSON.parse(payload); } catch (e) { return; } + if (!data || typeof data.ts !== "string") return; + + const ts = Date.parse(data.ts); + if (isNaN(ts) || (Date.now() - ts) / 1000 > 300) return; + + root.watcherAlive = true; + root.watcherDead = Number(data.dead) || 0; + root.watcherExpected = Number(data.expected) || 0; + } + + readonly property color watcherColor: + !watcherAlive ? Theme.red + : watcherDead > 0 ? Theme.yellow + : Theme.green + + readonly property string watcherText: + !watcherAlive ? "watcher not running" + : watcherDead > 0 ? `${watcherDead} folder(s) dead, check the log` + : `watcher ok ยท ${watcherExpected} folders` + readonly property var screenObj: Quickshell.screens.find(s => s.name === root.monitor) ?? Quickshell.screens[0] @@ -95,6 +135,13 @@ Scope { // the drawer. MouseArea { anchors.fill: parent } + FileView { + id: heartbeat + path: `${Quickshell.env("HOME")}/.local/state/mail-watcher.heartbeat` + onLoaded: root.readHeartbeat(text()) + onLoadFailed: root.readHeartbeat("") + } + Column { id: content anchors { left: parent.left; right: parent.right; top: parent.top; margins: 16 } @@ -230,16 +277,35 @@ Scope { Rectangle { width: parent.width; height: 1; color: Qt.alpha(Theme.text, 0.12) } + // Watcher health, above the button. Green when idling, + // yellow when a folder gave up, red when there is no fresh + // heartbeat at all. + Item { + width: parent.width + implicitHeight: 22 + + Rectangle { + id: watcherDot + anchors.verticalCenter: parent.verticalCenter + width: 8; height: 8; radius: 4 + color: root.watcherColor + } + + Text { + anchors { left: watcherDot.right; leftMargin: 10; verticalCenter: parent.verticalCenter } + text: root.watcherText + color: Theme.subtext + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize - 2 + } + } + + Rectangle { width: parent.width; height: 1; color: Qt.alpha(Theme.text, 0.12) } + Row { anchors.right: parent.right spacing: 10 - Button { - text: Accounts.syncing ? "Syncing..." : "Sync now" - enabled: !Accounts.syncing - onClicked: Accounts.sync() - } - Button { text: "Open qtmaildir" onClicked: { Accounts.openClient(); root.close(); } -- cgit v1.2.3