feat: times in the reader's own zone; the audit log shows exact moments (milestone 490)
- users.time_zone (0034). An account without one takes the browser's zone on its first page (POST /account/time-zone/adopt, never over a choice made by hand); the session follows the account on every request. - /account: who you are and the zone every page writes its times in, reached from your name in the sidebar. - formatting.when: 'Today 06:58', 'Yesterday 23:41', 'Tue 6 Oct 14:12', '6 Oct 2025 14:12', 24-hour, in the reader's zone; |ago's hover is the exact local moment instead of UTC; tz_label() for column headers. - Audit log: When (EDT) with exact times, one detail fact a line, on classes. - tzdata in the image, and the airgapped smoke checks a zone resolves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -86,3 +86,24 @@ async def test_the_row_says_what_was_done_not_its_code(app):
|
||||
assert 'title="monitor.disabled">paused monitor</td>' in page
|
||||
assert "dry run</span>" in page and "False" not in page
|
||||
assert '<option value="monitor." selected>Monitors</option>' in page
|
||||
|
||||
|
||||
async def test_each_entry_shows_its_exact_moment_in_the_readers_zone(app):
|
||||
"""Milestone 490: a record is read for when, not how long ago."""
|
||||
from quart import session
|
||||
from steward.core import formatting as f
|
||||
events = [{"event": _ev("host", "h1", "host.renamed"), "detail": {}, "entity": None,
|
||||
"facts": [{"label": "was", "text": "web-0", "url": None},
|
||||
{"label": "AI token", "text": "#1", "url": None}]}]
|
||||
async with app.test_request_context("/audit/"):
|
||||
session["tz"] = "America/New_York"
|
||||
page = await render_template("audit/list.html", events=events, after=None, action_filter="")
|
||||
ny = f.zone("America/New_York")
|
||||
assert f"<th>When ({f.tz_label(ny)})</th>" in page
|
||||
# 12:00 UTC is 08:00 in New York, and the hover names the zone.
|
||||
assert f'<td class="nowrap">{f.when(events[0]["event"].timestamp, ny)}</td>' in page
|
||||
assert "08:00:00 EDT" in page and " ago<" not in page
|
||||
# One fact a line, labels quiet, and the table on classes.
|
||||
assert '<div><span class="tone-dim">was</span> web-0</div>' in page
|
||||
table = page[page.index('<table class="table">'):]
|
||||
assert "style=" not in table
|
||||
|
||||
@@ -44,17 +44,59 @@ def test_duration(secs, text):
|
||||
(NOW + timedelta(minutes=5), "in 5m"),
|
||||
(NOW, "just now"),
|
||||
])
|
||||
def test_ago_reads_relative_and_titles_the_absolute_utc_time(when, text):
|
||||
def test_ago_reads_relative_and_titles_the_exact_moment(when, text):
|
||||
out = str(f.ago(when, now=NOW))
|
||||
stamp = when.replace(tzinfo=timezone.utc) if when.tzinfo is None else when
|
||||
assert out == (f'<time datetime="{stamp:%Y-%m-%dT%H:%M:%SZ}" '
|
||||
f'title="{stamp:%Y-%m-%d %H:%M:%S} UTC">{text}</time>')
|
||||
f'title="{stamp:%a} {stamp.day} {stamp:%b %Y %H:%M:%S} UTC">{text}</time>')
|
||||
|
||||
|
||||
def test_ago_of_nothing_and_of_another_zone():
|
||||
assert str(f.ago(None)) == "—"
|
||||
est = timezone(timedelta(hours=-4))
|
||||
assert 'title="2026-10-06 18:00:00 UTC"' in str(f.ago(NOW.astimezone(est), now=NOW))
|
||||
assert 'title="Tue 6 Oct 2026 18:00:00 UTC"' in str(f.ago(NOW.astimezone(est), now=NOW))
|
||||
|
||||
|
||||
# Milestone 490: a moment in the reader's own zone, on a 24-hour clock.
|
||||
NY = f.zone("America/New_York")
|
||||
|
||||
|
||||
def test_ago_hovers_the_moment_in_the_readers_zone():
|
||||
assert 'title="Tue 6 Oct 2026 13:57:00 EDT">3m ago<' in str(
|
||||
f.ago(NOW - timedelta(minutes=3), now=NOW, tz=NY))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("moment, text, title", [
|
||||
(NOW - timedelta(hours=1), "Today 13:00", "Tue 6 Oct 2026 13:00:00 EDT"),
|
||||
# 03:30 UTC on the 6th is 23:30 on the 5th in New York: the zone moves the day.
|
||||
(datetime(2026, 10, 6, 3, 30, tzinfo=timezone.utc), "Yesterday 23:30", "Mon 5 Oct 2026 23:30:00 EDT"),
|
||||
(datetime(2026, 10, 7, 6, 0, tzinfo=timezone.utc), "Tomorrow 02:00", "Wed 7 Oct 2026 02:00:00 EDT"),
|
||||
(datetime(2026, 9, 30, 12, 0, tzinfo=timezone.utc), "Wed 30 Sep 08:00", "Wed 30 Sep 2026 08:00:00 EDT"),
|
||||
# Before this year the year shows; in winter the zone is EST, an hour further back.
|
||||
(datetime(2025, 12, 25, 12, 0, tzinfo=timezone.utc), "25 Dec 2025 07:00", "Thu 25 Dec 2025 07:00:00 EST"),
|
||||
(datetime(2026, 10, 6, 21, 5), "Today 17:05", "Tue 6 Oct 2026 17:05:00 EDT"), # naive is UTC
|
||||
])
|
||||
def test_when_writes_the_exact_moment_in_the_readers_zone(moment, text, title):
|
||||
stamp = moment.replace(tzinfo=timezone.utc) if moment.tzinfo is None else moment
|
||||
assert str(f.when(moment, NY, now=NOW)) == (
|
||||
f'<time datetime="{stamp:%Y-%m-%dT%H:%M:%SZ}" title="{title}">{text}</time>')
|
||||
|
||||
|
||||
def test_when_is_a_24_hour_clock_and_utc_without_a_zone():
|
||||
assert ">Today 18:00<" in str(f.when(NOW, now=NOW))
|
||||
assert ">Today 23:59<" in str(f.when(NOW.replace(hour=23, minute=59), now=NOW))
|
||||
assert str(f.when(None)) == "—"
|
||||
|
||||
|
||||
def test_zone_falls_back_to_utc_and_names_itself():
|
||||
assert f.zone(None) is timezone.utc and f.zone("") is timezone.utc
|
||||
assert f.zone("Not/AZone") is timezone.utc and f.zone("../etc/passwd") is timezone.utc
|
||||
assert f.tz_label(NY, now=NOW) == "EDT"
|
||||
assert f.tz_label(NY, now=datetime(2026, 1, 15, tzinfo=timezone.utc)) == "EST"
|
||||
assert f.tz_label(None) == "UTC"
|
||||
names = f.zone_names()
|
||||
assert "America/New_York" in names and "Europe/London" in names and "UTC" in names
|
||||
assert list(names) == sorted(names)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value, kind, cls", [
|
||||
|
||||
Reference in New Issue
Block a user