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:
@@ -0,0 +1,32 @@
|
||||
{# account/index.html — the signed-in user's own account (milestone 490): who
|
||||
they are, and the time zone every page writes its times in. #}
|
||||
{% extends "base.html" %}
|
||||
{% from "_macros.html" import page_header %}
|
||||
{% block title %}Your account — Steward{% endblock %}
|
||||
{% block content %}
|
||||
{{ page_header("Your account") }}
|
||||
|
||||
<p class="page-verdict">
|
||||
Signed in as <strong>{{ user.username }}</strong> ({{ user.role.value }}) with {{ source }}
|
||||
· times show in <strong>{{ zone_name }}</strong> ({{ zone_label }})
|
||||
</p>
|
||||
|
||||
<form method="post" action="/account/" class="form-page">{{ csrf_field() }}
|
||||
<section class="card form-section" aria-labelledby="acct-time">
|
||||
<h2 id="acct-time" class="section-title">Time zone</h2>
|
||||
{% if error %}<p class="field-error" role="alert">{{ error }}</p>{% endif %}
|
||||
<div class="field">
|
||||
<label class="field-label" for="acct-tz">Show times in</label>
|
||||
<select class="input" id="acct-tz" name="time_zone">
|
||||
{% for region, names in zones %}
|
||||
{% if region %}<optgroup label="{{ region }}">{% endif %}
|
||||
{% for z in names %}<option value="{{ z }}"{% if z == zone_name %} selected{% endif %}>{{ z.split('/', 1)[1]|replace('_', ' ') if region else z }}</option>{% endfor %}
|
||||
{% if region %}</optgroup>{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<span class="field-hint">Every page writes its times in this zone, on a 24-hour clock.</span>
|
||||
</div>
|
||||
</section>
|
||||
<button type="submit" class="btn">Save</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -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 %} · {% 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -68,8 +68,9 @@
|
||||
{% endif %}
|
||||
</nav>
|
||||
<div class="side-user">
|
||||
<a href="/account/" title="Your account and time zone"{% if _p.startswith('/account') %} aria-current="page"{% endif %}>{{ session.username }}</a>
|
||||
<form method="post" action="/logout">{{ csrf_field() }}
|
||||
<button type="submit" title="Log out">⏻ {{ session.username }}</button>
|
||||
<button type="submit" title="Log out" aria-label="Log out">⏻</button>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -106,6 +107,25 @@ document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && document.body.classList.contains('nav-open')) setNavOpen(false);
|
||||
});
|
||||
</script>
|
||||
{% if session.user_id and not session.tz %}
|
||||
<script>
|
||||
// An account with no time zone yet takes this browser's, once (milestone 490);
|
||||
// the page then reloads so its times are written in it.
|
||||
(function () {
|
||||
var tz;
|
||||
try { tz = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (e) { return; }
|
||||
if (!tz) return;
|
||||
fetch('/account/time-zone/adopt', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content},
|
||||
body: JSON.stringify({time_zone: tz})
|
||||
}).then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) { if (d && d.adopted) location.reload(); })
|
||||
.catch(function () {});
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<main{% block main_class %}{% endblock %}>
|
||||
{% if plugin_failures and session.user_role == 'admin' %}
|
||||
|
||||
Reference in New Issue
Block a user