From 6fe62e9a5baae324326f93247c341629b1a3a5b6 Mon Sep 17 00:00:00 2001 From: Claudiu Schuster Date: Fri, 28 Aug 2026 23:49:28 +0200 Subject: [PATCH] Sort issue review by displayed time --- bin/pdrive-ui | 66 +++++++++++++++++++++++------------- docs/EVERYDAY_USE.md | 2 +- docs/OPERATIONS.md | 9 ++--- tests/test-ui-preferences.sh | 27 +++++++++++++-- 4 files changed, 73 insertions(+), 31 deletions(-) diff --git a/bin/pdrive-ui b/bin/pdrive-ui index 8abd23f..02aa392 100755 --- a/bin/pdrive-ui +++ b/bin/pdrive-ui @@ -828,6 +828,32 @@ def load_preferences() -> dict[str, Any]: return preferences +def issue_event_time( + event: dict[str, Any], + timestamp_keys: tuple[str, ...], +) -> dt.datetime: + """Return the first usable event timestamp for filtering and ordering.""" + + event_timestamp = next((str(event.get(key) or "") for key in timestamp_keys if event.get(key)), "") + try: + event_time = dt.datetime.fromisoformat(event_timestamp) + except ValueError: + return dt.datetime.min.replace(tzinfo=dt.timezone.utc) + if event_time.tzinfo is None: + event_time = event_time.replace(tzinfo=dt.timezone.utc) + return event_time + + +def sort_issue_events(events: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Order issue rows by their visible timestamp, newest first.""" + + return sorted( + events, + key=lambda event: issue_event_time(event, ("resolved_at", "last_seen", "timestamp")), + reverse=True, + ) + + def event_is_after_review( preferences: dict[str, Any], event: dict[str, Any], @@ -842,15 +868,9 @@ def event_is_after_review( reviewed_time = dt.datetime.fromisoformat(reviewed_at) except ValueError: reviewed_time = dt.datetime.min.replace(tzinfo=dt.timezone.utc) - event_timestamp = next((str(event.get(key) or "") for key in timestamp_keys if event.get(key)), "") - try: - event_time = dt.datetime.fromisoformat(event_timestamp) - if event_time.tzinfo is None: - event_time = event_time.replace(tzinfo=dt.timezone.utc) - if reviewed_time.tzinfo is None: - reviewed_time = reviewed_time.replace(tzinfo=dt.timezone.utc) - except ValueError: - return False + event_time = issue_event_time(event, timestamp_keys) + if reviewed_time.tzinfo is None: + reviewed_time = reviewed_time.replace(tzinfo=dt.timezone.utc) return event_time > reviewed_time @@ -868,16 +888,16 @@ def unreviewed_events(preferences: dict[str, Any], issues: dict[str, Any]) -> li continue if event_is_after_review(preferences, event, ("last_seen", "timestamp")): selected.append(event) - return selected + return sort_issue_events(selected) def resolved_events(issues: dict[str, Any], limit: int = 5) -> list[dict[str, Any]]: raw_events = issues.get("events", []) if not isinstance(raw_events, list): return [] - return [ - event for event in raw_events if isinstance(event, dict) and str(event.get("lifecycle") or "") == "resolved" - ][:limit] + return sort_issue_events( + [event for event in raw_events if isinstance(event, dict) and str(event.get("lifecycle") or "") == "resolved"] + )[:limit] def unreviewed_recovery_events( @@ -890,13 +910,15 @@ def unreviewed_recovery_events( raw_events = issues.get("events", []) if not isinstance(raw_events, list): return [] - return [ - event - for event in raw_events - if isinstance(event, dict) - and str(event.get("lifecycle") or "") == "resolved" - and event_is_after_review(preferences, event, ("resolved_at", "last_seen", "timestamp")) - ][:limit] + return sort_issue_events( + [ + event + for event in raw_events + if isinstance(event, dict) + and str(event.get("lifecycle") or "") == "resolved" + and event_is_after_review(preferences, event, ("resolved_at", "last_seen", "timestamp")) + ] + )[:limit] def issues_since_review(preferences: dict[str, Any], issues: dict[str, Any]) -> tuple[int, int]: @@ -5386,9 +5408,7 @@ class PDriveWindow(Gtk.ApplicationWindow): notices=notices, ) ) - for event in events: - self.issue_list.add(self.issue_row(event)) - for event in recoveries: + for event in sort_issue_events(events + recoveries): self.issue_list.add(self.issue_row(event)) if not events: self.issue_list.add( diff --git a/docs/EVERYDAY_USE.md b/docs/EVERYDAY_USE.md index 82f5f16..dd8c683 100644 --- a/docs/EVERYDAY_USE.md +++ b/docs/EVERYDAY_USE.md @@ -99,7 +99,7 @@ affected paths and suggested next steps. Opening the list does not acknowledge anything; mark it reviewed only after reading the evidence. The button is also available when only informational recovery records are waiting. Marking them reviewed removes them from this list while their history and log evidence remain -available. +available. The newest event or recovery is shown first. ## Before shutdown, update or uninstall diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 364dc09..3e44129 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -254,10 +254,11 @@ checks prove authentication, mount and RC health. Routine bandwidth-control notices do not enter issue review. Automatically resolved incidents remain visible as a single informational recovery record until they are marked reviewed. The review button remains -available when only these recovery records are waiting. A recovered root-level -session refresh keeps its displayed time anchored to the final related 401 log -event instead of changing with each status poll. An old message or a path merely -disappearing from the queue is never enough to declare success. +available when only these recovery records are waiting. Review rows are ordered +by their displayed event or recovery time with the newest first. A recovered +root-level session refresh keeps its displayed time anchored to the final related +401 log event instead of changing with each status poll. An old message or a path +merely disappearing from the queue is never enough to declare success. Each retained incident shows its local timestamp, severity, category, affected path or component, sanitized rclone context and a suggested next step. Its diff --git a/tests/test-ui-preferences.sh b/tests/test-ui-preferences.sh index 68cc0f1..7163a61 100755 --- a/tests/test-ui-preferences.sh +++ b/tests/test-ui-preferences.sh @@ -163,6 +163,13 @@ issue_events = [ {"timestamp": "2026-08-24T11:30:00+02:00", "level": "notice", "message": "old notice"}, {"timestamp": "2026-08-24T13:00:00+02:00", "level": "error", "message": "new error"}, {"timestamp": "2026-08-24T13:15:00+02:00", "level": "notice", "message": "new notice"}, + { + "timestamp": "2026-08-24T14:00:00+02:00", + "resolved_at": "2026-08-24T12:30:00+02:00", + "level": "notice", + "lifecycle": "resolved", + "message": "older recovery", + }, { "timestamp": "2026-08-24T11:45:00+02:00", "resolved_at": "2026-08-24T13:30:00+02:00", @@ -178,11 +185,25 @@ selected, missing = module.unreviewed_issue_events( reviewed, issue_payload, ) -assert [event["message"] for event in selected] == ["new error", "new notice"] +assert [event["message"] for event in selected] == ["new notice", "new error"] assert missing == 0 -assert [event["message"] for event in module.resolved_events(issue_payload)] == ["automatic recovery"] +assert [event["message"] for event in module.resolved_events(issue_payload)] == [ + "automatic recovery", + "older recovery", +] assert [event["message"] for event in module.unreviewed_recovery_events(reviewed, issue_payload)] == [ - "automatic recovery" + "automatic recovery", + "older recovery", +] +review_rows = module.sort_issue_events( + module.unreviewed_events(reviewed, issue_payload) + + module.unreviewed_recovery_events(reviewed, issue_payload) +) +assert [event["message"] for event in review_rows] == [ + "automatic recovery", + "new notice", + "new error", + "older recovery", ] selected, missing = module.unreviewed_issue_events( reviewed,