feat: times in the reader's own zone; the audit log shows exact moments (milestone 490)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m0s
CI / unit (push) Failing after 1m23s
CI / publish (push) Skipped

- 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:
2026-10-08 07:56:27 -04:00
co-authored by Claude Opus 5.5
parent 955450d33f
commit 6a81cf4aac
19 changed files with 425 additions and 25 deletions
+21
View File
@@ -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
+45 -3
View File
@@ -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", [
@@ -0,0 +1,82 @@
"""Integration: the zone a user reads times in (milestone 490), against real
Postgres. The browser's zone is saved for an account without one and never
over one set by hand; the Account page shows and saves it, refuses a zone
Steward does not know, and a change reaches the session on the next request.
Requires STEWARD_DATABASE_URL."""
from __future__ import annotations
import asyncio
import os
import time
import uuid
import pytest
pytestmark = pytest.mark.integration
@pytest.fixture
def app():
if not os.environ.get("STEWARD_DATABASE_URL"):
pytest.skip("needs Postgres")
from steward.app import create_app
return create_app(testing=False)
def test_the_browser_zone_is_adopted_once_and_the_account_page_changes_it(app):
from steward.models.users import User, UserRole
async def _go():
tag = uuid.uuid4().hex[:8]
uid = str(uuid.uuid4())
async with app.db_sessionmaker() as s:
async with s.begin():
s.add(User(id=uid, username=f"tz-{tag}", email=f"tz-{tag}@test",
password_hash="x", role=UserRole.viewer, is_active=True))
client = app.test_client()
async with client.session_transaction() as sess:
sess.update(user_id=uid, user_role="viewer", username=f"tz-{tag}",
login_at=time.time(), _csrf="test-token")
csrf = {"X-CSRF-Token": "test-token"}
async def zone():
async with app.db_sessionmaker() as s:
return (await s.get(User, uid)).time_zone
out = {}
# No zone yet: every page carries the script that reports the browser's.
out["before"] = await (await client.get("/account/")).get_data(as_text=True)
out["bogus"] = (await client.post("/account/time-zone/adopt", json={"time_zone": "Mars/Olympus"},
headers=csrf)).status_code
out["adopt"] = await (await client.post("/account/time-zone/adopt",
json={"time_zone": "America/New_York"},
headers=csrf)).get_json()
out["after_adopt"] = await zone()
# A second browser elsewhere does not replace it.
out["again"] = await (await client.post("/account/time-zone/adopt",
json={"time_zone": "Europe/Berlin"},
headers=csrf)).get_json()
out["page"] = await (await client.get("/account/")).get_data(as_text=True)
resp = await client.post("/account/", form={"time_zone": "Europe/London"}, headers=csrf)
out["save"] = (resp.status_code, resp.headers.get("Location"))
out["after_save"] = await zone()
resp = await client.post("/account/", form={"time_zone": "Nowhere"}, headers=csrf)
out["refused"] = (resp.status_code, await resp.get_data(as_text=True))
out["after_refused"] = await zone()
async with client.session_transaction() as sess:
out["session_tz"] = sess.get("tz")
return out
out = asyncio.run(_go())
assert "/account/time-zone/adopt" in out["before"]
assert out["bogus"] == 400
assert out["adopt"] == {"adopted": True} and out["after_adopt"] == "America/New_York"
assert out["again"] == {"adopted": False}
page = out["page"]
assert "times show in <strong>America/New_York</strong> (E" in page # EDT or EST
assert '<option value="America/New_York" selected>New York</option>' in page
assert "/account/time-zone/adopt" not in page # nothing left to adopt
assert out["save"] == (302, "/account/") and out["after_save"] == "Europe/London"
status, body = out["refused"]
assert status == 400 and "Nowhere is not a time zone Steward knows." in body
assert out["after_refused"] == "Europe/London" and out["session_tz"] == "Europe/London"
+1
View File
@@ -18,6 +18,7 @@ TOP_LEVEL = {
"steward/templates/ansible/index.html",
"steward/templates/audit/list.html",
"steward/templates/ai_access/index.html",
"steward/templates/account/index.html",
"plugins/docker/templates/docker/index.html",
"plugins/snmp/templates/snmp/index.html",
"plugins/traefik/templates/traefik/index.html",