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
+4 -6
View File
@@ -4,8 +4,8 @@
{% for item in events %}
{% set e = item.event %}
<tr>
<td style="white-space:nowrap;" title="{{ e.timestamp.strftime('%Y-%m-%d %H:%M:%S UTC') }}">{{ e.timestamp|ago }}</td>
<td style="font-size:0.85rem;">{{ e.username }}</td>
<td class="nowrap">{{ e.timestamp|when }}</td>
<td class="nowrap">{{ e.username }}</td>
<td title="{{ e.action }}">{{ item.action or e.action }}</td>
<td>
{% if item.entity %}
@@ -14,11 +14,9 @@
{% if item.entity.gone %}<span class="field-hint">(deleted)</span>{% endif %}
{% elif e.entity_type %}<span class="field-hint">{{ e.entity_type|replace('_', ' ') }}</span>{% endif %}
</td>
<td style="font-size:0.8rem;color:var(--text-muted);max-width:340px;">
<td class="audit-facts">
{% for f in item.facts %}
<span style="color:var(--text-dim);">{{ f.label }}</span>
{% if f.url %}<a href="{{ f.url }}">{{ f.text }}</a>{% else %}{{ f.text }}{% endif %}
{% if not loop.last %} &nbsp;·&nbsp; {% endif %}
<div><span class="tone-dim">{{ f.label }}</span> {% if f.url %}<a href="{{ f.url }}">{{ f.text }}</a>{% else %}{{ f.text }}{% endif %}</div>
{% endfor %}
</td>
</tr>
+5 -3
View File
@@ -1,10 +1,12 @@
{# audit/list.html — who changed what, newest first, each moment exact in the
reader's own zone (milestone 490): a record is read for when, not how long ago. #}
{% extends "base.html" %}
{% from "_macros.html" import page_header, empty_state %}
{% block title %}Audit log — Steward{% endblock %}
{% block content %}
{% call page_header("Audit log", "Who changed what, newest first.") %}
<form method="get" style="display:flex;gap:0.5rem;align-items:center;">
<select name="action" aria-label="Show only" style="font-size:0.85rem;" onchange="this.form.submit()">
<form class="filter-bar" method="get" action="/audit/">
<select name="action" aria-label="Show only" onchange="this.form.submit()">
<option value="">Everything</option>
{% for prefix, label in areas %}
<option value="{{ prefix }}" {% if action_filter == prefix %}selected{% endif %}>{{ label }}</option>
@@ -19,7 +21,7 @@
<table class="table">
<thead>
<tr>
<th>When</th>
<th>When ({{ tz_label() }})</th>
<th>User</th>
<th>Did</th>
<th>What</th>