Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 43 additions & 23 deletions bin/pdrive-ui
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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


Expand All @@ -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(
Expand All @@ -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]:
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion docs/EVERYDAY_USE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 5 additions & 4 deletions docs/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 24 additions & 3 deletions tests/test-ui-preferences.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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,
Expand Down