mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 22:47:13 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
96 lines
2.9 KiB
Python
96 lines
2.9 KiB
Python
"""The decision-history payload, with timestamps rendered server-side.
|
|
|
|
Split out of ``_web_payload.py`` to keep every file under the 250-line cap.
|
|
|
|
The local-time rendering lives here rather than in the browser because
|
|
LibreWolf's ``privacy.resistFingerprinting`` pins JavaScript's ``Date`` to
|
|
UTC: on 2026-08-30 a 14:00 CEST decision rendered as "12:00 PM" beside a
|
|
correct, server-computed "6m ago", and the two-hour disagreement made a live
|
|
page look stale.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
import logging
|
|
from typing import Any
|
|
|
|
from screen_locker._decision_log import read_decisions
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
# How many decisions the Why view gets by default. The trail holds 3000;
|
|
# the browser only ever needs the recent tail to answer "why did nothing
|
|
# happen".
|
|
DEFAULT_DECISION_LIMIT = 200
|
|
|
|
|
|
def build_decisions_payload(
|
|
*,
|
|
limit: int = DEFAULT_DECISION_LIMIT,
|
|
) -> dict[str, Any]:
|
|
"""Build the decision-history payload, newest first.
|
|
|
|
Args:
|
|
limit: Maximum number of decisions to return.
|
|
|
|
Returns:
|
|
The recent decisions and the total the trail holds.
|
|
"""
|
|
decisions = read_decisions()
|
|
tail = decisions[-limit:] if limit > 0 else decisions
|
|
return {
|
|
"total": len(decisions),
|
|
"returned": len(tail),
|
|
"decisions": [_with_local_stamps(entry) for entry in reversed(tail)],
|
|
}
|
|
|
|
|
|
def _local_stamp(value: object) -> str | None:
|
|
"""Render one ISO timestamp in this machine's local time.
|
|
|
|
Formatted server-side on purpose. The browser used to do it with
|
|
``toLocaleString``, which LibreWolf's ``privacy.resistFingerprinting``
|
|
pins to UTC -- so on 2026-08-30 a 14:00 CEST decision rendered as
|
|
"12:00 PM", two hours stale-looking, right next to a correct
|
|
server-computed "6m ago".
|
|
|
|
Args:
|
|
value: The stored ISO-8601 timestamp, if it is one.
|
|
|
|
Returns:
|
|
A local-time string, or None when there is nothing to render.
|
|
"""
|
|
if not isinstance(value, str):
|
|
return None
|
|
try:
|
|
moment = datetime.fromisoformat(value)
|
|
except ValueError:
|
|
_logger.warning(
|
|
"Decision timestamp %r is unparsable — leaving it unformatted "
|
|
"rather than showing a wrong local time",
|
|
value,
|
|
)
|
|
return None
|
|
return moment.astimezone().strftime("%b %d, %H:%M")
|
|
|
|
|
|
def _with_local_stamps(entry: dict[str, object]) -> dict[str, object]:
|
|
"""Return *entry* with server-rendered local-time fields added.
|
|
|
|
Args:
|
|
entry: One decision record from the trail.
|
|
|
|
Returns:
|
|
The record plus ``local_time`` (and ``local_last_time`` for a
|
|
collapsed row).
|
|
"""
|
|
enriched = dict(entry)
|
|
local = _local_stamp(entry.get("timestamp"))
|
|
if local is not None:
|
|
enriched["local_time"] = local
|
|
last_local = _local_stamp(entry.get("last_timestamp"))
|
|
if last_local is not None:
|
|
enriched["local_last_time"] = last_local
|
|
return enriched
|