left4me/docs/superpowers/plans/2026-05-16-timeago-shared-display.md
mwiegand fdcefcfec6
plan(timeago-shared-display): nine-task TDD migration to a Jinja filter
Lays out the file-by-file migration from the current three time-display
styles to the unified timeago filter from the design spec. TDD ordering
with tests-first, per-task commits, line-numbered locators, and an
explicit verification pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 11:05:42 +02:00

26 KiB

timeago Shared Display Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Unify all user-facing datetime rendering in l4d2web behind a single timeago Jinja filter that returns a <time> element with a relative label and a precise UTC tooltip.

Architecture: Two callables in l4d2web/l4d2web/services/timeago.pyhumanize_delta (pure text, source of truth for the relative-label ladder) and format_time_html (wraps the text in a <time> element). The latter is registered as Jinja filter timeago in the Flask app factory. Templates and routes migrate from raw datetime repr and bespoke inline math to {{ ts | timeago }}.

Tech Stack: Python 3.13, Flask, Jinja2, markupsafe.Markup, pytest.

Reference spec: docs/superpowers/specs/2026-05-16-timeago-shared-display-design.md


File Structure

File Action Responsibility
l4d2web/l4d2web/services/timeago.py Rewrite humanize_delta (new symmetric ladder) + new format_time_html
l4d2web/l4d2web/app.py Modify Register timeago filter in create_app
l4d2web/tests/test_timeago.py Create Unit tests for both helpers + Flask smoke test
l4d2web/l4d2web/templates/admin_users.html Modify Use filter for created_at / updated_at
l4d2web/l4d2web/templates/blueprints.html Modify Use filter for created_at / updated_at
l4d2web/l4d2web/templates/_job_table.html Modify Use filter for created_at / finished_at (with None guard)
l4d2web/l4d2web/templates/job_detail.html Modify Use filter for created_at / started_at / finished_at
l4d2web/l4d2web/templates/_live_state.html Modify Replace inline (now - x).total_seconds() with filter
l4d2web/l4d2web/templates/_server_actions.html Modify Switch from latest_job_when (string) to latest_job_at | timeago
l4d2web/l4d2web/templates/_overlay_build_status.html Modify Switch from latest_build_when to latest_build_at | timeago
l4d2web/l4d2web/routes/page_routes.py Modify Drop humanize_delta imports; pass raw datetime as latest_job_at / latest_build_at
l4d2web/l4d2web/routes/server_routes.py Modify Remove now-dead now= kwarg from _live_state.html render call

Task 1: Rewrite humanize_delta with the new ladder

Files:

  • Modify: l4d2web/l4d2web/services/timeago.py
  • Create: l4d2web/tests/test_timeago.py

The current ladder uses just now under 45s and clamps future deltas. The new ladder is symmetric, has second precision, and uses day-month (with year if different) for ≥7 days. Spec table in section "Ladder (long form, symmetric for past and future)".

  • Step 1: Create the test file with parameterised boundary tests

Create l4d2web/tests/test_timeago.py with:

from datetime import UTC, datetime, timedelta

import pytest

from l4d2web.services.timeago import humanize_delta


NOW = datetime(2026, 5, 16, 12, 0, 0, tzinfo=UTC)


@pytest.mark.parametrize(
    ("delta", "expected"),
    [
        # zero
        (timedelta(0), "now"),
        # past, seconds
        (timedelta(seconds=1), "1 second ago"),
        (timedelta(seconds=2), "2 seconds ago"),
        (timedelta(seconds=59), "59 seconds ago"),
        # past, minutes
        (timedelta(seconds=60), "1 minute ago"),
        (timedelta(minutes=1), "1 minute ago"),
        (timedelta(minutes=2), "2 minutes ago"),
        (timedelta(minutes=59), "59 minutes ago"),
        # past, hours
        (timedelta(minutes=60), "1 hour ago"),
        (timedelta(hours=1), "1 hour ago"),
        (timedelta(hours=2), "2 hours ago"),
        (timedelta(hours=23), "23 hours ago"),
        # past, days
        (timedelta(hours=24), "1 day ago"),
        (timedelta(days=1), "1 day ago"),
        (timedelta(days=2), "2 days ago"),
        (timedelta(days=6), "6 days ago"),
        # past, date fallback same year (now = 16 May 2026)
        (timedelta(days=7), "9 May"),
        (timedelta(days=30), "16 Apr"),
        (timedelta(days=120), "16 Jan"),
        # past, date fallback different year
        (timedelta(days=365), "16 May 2025"),
        (timedelta(days=400), "11 Apr 2025"),
    ],
)
def test_humanize_delta_past(delta, expected):
    then = NOW - delta
    assert humanize_delta(then, now=NOW) == expected


@pytest.mark.parametrize(
    ("delta", "expected"),
    [
        # future, seconds
        (timedelta(seconds=1), "in 1 second"),
        (timedelta(seconds=2), "in 2 seconds"),
        (timedelta(seconds=59), "in 59 seconds"),
        # future, minutes
        (timedelta(seconds=60), "in 1 minute"),
        (timedelta(minutes=2), "in 2 minutes"),
        (timedelta(minutes=59), "in 59 minutes"),
        # future, hours
        (timedelta(hours=1), "in 1 hour"),
        (timedelta(hours=23), "in 23 hours"),
        # future, days
        (timedelta(days=1), "in 1 day"),
        (timedelta(days=6), "in 6 days"),
        # future, date fallback same year
        (timedelta(days=7), "23 May"),
        (timedelta(days=30), "15 Jun"),
        # future, date fallback different year
        (timedelta(days=365), "16 May 2027"),
    ],
)
def test_humanize_delta_future(delta, expected):
    then = NOW + delta
    assert humanize_delta(then, now=NOW) == expected


def test_humanize_delta_accepts_naive_input_as_utc():
    then_naive = (NOW - timedelta(minutes=5)).replace(tzinfo=None)
    assert humanize_delta(then_naive, now=NOW) == "5 minutes ago"


def test_humanize_delta_accepts_naive_now_as_utc():
    then = NOW - timedelta(minutes=5)
    now_naive = NOW.replace(tzinfo=None)
    assert humanize_delta(then, now=now_naive) == "5 minutes ago"


def test_humanize_delta_default_now_is_datetime_now_utc():
    then = datetime.now(UTC) - timedelta(seconds=3)
    assert humanize_delta(then) in {"3 seconds ago", "2 seconds ago", "4 seconds ago"}


def test_humanize_delta_year_boundary_includes_year_when_years_differ():
    now = datetime(2026, 1, 2, 12, 0, 0, tzinfo=UTC)
    then = datetime(2025, 12, 30, 12, 0, 0, tzinfo=UTC)
    assert humanize_delta(then, now=now) == "30 Dec 2025"
  • Step 2: Run the new tests to verify they fail against the current implementation

Run: pytest l4d2web/tests/test_timeago.py -v Expected: most past tests FAIL (current implementation returns just now under 45s, no singular 1 second ago); all future tests FAIL (current clamps to 0 → just now); date-fallback tests FAIL (current returns ISO 2025-04-21 not 9 May).

  • Step 3: Rewrite humanize_delta to satisfy the tests

Replace the entire contents of l4d2web/l4d2web/services/timeago.py with:

from datetime import UTC, datetime


_MONTHS = (
    "Jan", "Feb", "Mar", "Apr", "May", "Jun",
    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
)


def _ensure_utc(dt: datetime) -> datetime:
    if dt.tzinfo is None:
        return dt.replace(tzinfo=UTC)
    return dt


def _format_date(then: datetime, now: datetime) -> str:
    month = _MONTHS[then.month - 1]
    if then.year == now.year:
        return f"{then.day} {month}"
    return f"{then.day} {month} {then.year}"


def _relative_label(seconds: int, past: bool) -> str:
    if seconds < 60:
        unit, n = "second", seconds
    elif seconds < 3600:
        unit, n = "minute", seconds // 60
    elif seconds < 86400:
        unit, n = "hour", seconds // 3600
    else:
        unit, n = "day", seconds // 86400
    plural = "" if n == 1 else "s"
    if past:
        return f"{n} {unit}{plural} ago"
    return f"in {n} {unit}{plural}"


def humanize_delta(then: datetime, now: datetime | None = None) -> str:
    if now is None:
        now = datetime.now(UTC)
    then = _ensure_utc(then)
    now = _ensure_utc(now)

    delta_seconds = int((now - then).total_seconds())
    abs_seconds = abs(delta_seconds)

    if abs_seconds == 0:
        return "now"

    if abs_seconds >= 7 * 86400:
        return _format_date(then, now)

    return _relative_label(abs_seconds, past=(delta_seconds > 0))
  • Step 4: Run the tests to verify they pass

Run: pytest l4d2web/tests/test_timeago.py -v Expected: all tests PASS.

  • Step 5: Run the full test suite to check for regressions in callers of humanize_delta

Run: pytest l4d2web/tests -q Expected: all tests pass. If any pre-existing test asserts on the legacy "just now" / 7-day ISO fallback strings via latest_job_when rendering, update those assertions to match the new format (e.g. "1 second ago", "9 May"). Note in commit message which tests were updated and why.

  • Step 6: Commit
git add l4d2web/l4d2web/services/timeago.py l4d2web/tests/test_timeago.py
git commit -m "feat(timeago): symmetric ladder with second precision and date fallback

Rewrite humanize_delta as a symmetric past/future ladder with
sub-minute precision. Replace the bare ISO date fallback after 7 days
with a day-month form (year suppressed when same as now). Refs spec
docs/superpowers/specs/2026-05-16-timeago-shared-display-design.md."

Task 2: Add format_time_html returning a <time> element

Files:

  • Modify: l4d2web/l4d2web/services/timeago.py

  • Modify: l4d2web/tests/test_timeago.py

  • Step 1: Append tests for format_time_html to the test file

Append to l4d2web/tests/test_timeago.py:

from markupsafe import Markup

from l4d2web.services.timeago import format_time_html


def test_format_time_html_returns_markup():
    then = NOW - timedelta(minutes=5)
    out = format_time_html(then, now=NOW)
    assert isinstance(out, Markup)


def test_format_time_html_contains_time_element_with_attrs():
    then = datetime(2026, 5, 16, 14, 32, 11, tzinfo=UTC)
    now = then + timedelta(minutes=5)
    out = str(format_time_html(then, now=now))
    assert out.startswith("<time ")
    assert out.endswith("</time>")
    assert 'datetime="2026-05-16T14:32:11+00:00"' in out
    assert 'title="2026-05-16 14:32:11 UTC"' in out
    assert ">5 minutes ago<" in out


def test_format_time_html_label_matches_humanize_delta():
    then = NOW - timedelta(hours=2)
    label = humanize_delta(then, now=NOW)
    out = str(format_time_html(then, now=NOW))
    assert f">{label}<" in out


def test_format_time_html_normalises_naive_input_to_utc():
    then_naive = datetime(2026, 5, 16, 14, 32, 11)
    now = datetime(2026, 5, 16, 14, 37, 11, tzinfo=UTC)
    out = str(format_time_html(then_naive, now=now))
    assert 'datetime="2026-05-16T14:32:11+00:00"' in out
    assert 'title="2026-05-16 14:32:11 UTC"' in out
  • Step 2: Run the new tests to verify they fail

Run: pytest l4d2web/tests/test_timeago.py -v -k format_time_html Expected: FAIL with ImportError: cannot import name 'format_time_html'.

  • Step 3: Implement format_time_html in timeago.py

Append to l4d2web/l4d2web/services/timeago.py:

from markupsafe import Markup, escape


def format_time_html(then: datetime, now: datetime | None = None) -> Markup:
    if now is None:
        now = datetime.now(UTC)
    then_utc = _ensure_utc(then).astimezone(UTC)
    now = _ensure_utc(now)

    label = humanize_delta(then_utc, now=now)
    iso = then_utc.isoformat()
    title = then_utc.strftime("%Y-%m-%d %H:%M:%S UTC")
    return Markup(
        f'<time datetime="{escape(iso)}" title="{escape(title)}">'
        f"{escape(label)}</time>"
    )

Note: place the from markupsafe import Markup, escape import at the top of the file alongside the existing from datetime import ... line — don't leave it inline as written above.

  • Step 4: Run the tests to verify they pass

Run: pytest l4d2web/tests/test_timeago.py -v Expected: all tests PASS.

  • Step 5: Commit
git add l4d2web/l4d2web/services/timeago.py l4d2web/tests/test_timeago.py
git commit -m "feat(timeago): add format_time_html returning a <time> element

Wrap humanize_delta in an HTML <time> element with datetime= and
title= attributes carrying the precise UTC value, so hovering surfaces
the exact timestamp regardless of the relative label."

Task 3: Register timeago Jinja filter in the Flask app factory

Files:

  • Modify: l4d2web/l4d2web/app.py:37-58

  • Modify: l4d2web/tests/test_timeago.py

  • Step 1: Add a Flask smoke test for the filter

There is no shared app fixture in this codebase — each test instantiates create_app directly (see l4d2web/tests/test_health.py for the minimal pattern). Append to l4d2web/tests/test_timeago.py:

from flask import render_template_string

from l4d2web.app import create_app


def test_timeago_filter_registered_on_app():
    app = create_app({"TESTING": True, "SECRET_KEY": "test"})
    with app.app_context():
        rendered = render_template_string(
            "{{ ts | timeago }}",
            ts=datetime.now(UTC) - timedelta(minutes=3),
        )
    assert "<time " in rendered
    assert "&lt;time" not in rendered
    assert "3 minutes ago" in rendered
  • Step 2: Verify the fixture and the failing assertion

Run: pytest l4d2web/tests/test_timeago.py::test_timeago_filter_registered_on_app -v Expected: FAIL with a Jinja TemplateSyntaxError: No filter named 'timeago' (or similar), confirming the filter is not yet registered.

  • Step 3: Register the filter in create_app

In l4d2web/l4d2web/app.py:

Add the import near the other from l4d2web... imports at the top:

from l4d2web.services.timeago import format_time_html

Inside create_app, register the filter immediately after init_db() runs and before the @app.before_request definitions. Add a single line:

    app.add_template_filter(format_time_html, "timeago")
  • Step 4: Run the smoke test to verify it passes

Run: pytest l4d2web/tests/test_timeago.py::test_timeago_filter_registered_on_app -v Expected: PASS.

  • Step 5: Run the full test suite to confirm nothing else broke

Run: pytest l4d2web/tests -q Expected: all tests pass.

  • Step 6: Commit
git add l4d2web/l4d2web/app.py l4d2web/tests/test_timeago.py
git commit -m "feat(app): register timeago Jinja filter

Templates can now call {{ ts | timeago }} directly without route-side
precomputation."

Task 4: Migrate admin_users.html and blueprints.html

Files:

  • Modify: l4d2web/l4d2web/templates/admin_users.html:25-26
  • Modify: l4d2web/l4d2web/templates/blueprints.html:17-18

Both templates render created_at / updated_at as raw Python datetime repr. No None guard needed — these columns are nullable=False in models.py.

  • Step 1: Modify admin_users.html

Replace lines 25-26 of l4d2web/l4d2web/templates/admin_users.html:

        <td>{{ user.created_at }}</td>
        <td>{{ user.updated_at }}</td>

with:

        <td>{{ user.created_at | timeago }}</td>
        <td>{{ user.updated_at | timeago }}</td>
  • Step 2: Modify blueprints.html

Replace lines 17-18 of l4d2web/l4d2web/templates/blueprints.html:

        <td>{{ blueprint.created_at }}</td>
        <td>{{ blueprint.updated_at }}</td>

with:

        <td>{{ blueprint.created_at | timeago }}</td>
        <td>{{ blueprint.updated_at | timeago }}</td>
  • Step 3: Run the existing tests for these pages

Run: pytest l4d2web/tests/test_admin_users.py l4d2web/tests/test_blueprints.py -q Expected: all tests pass. If a test asserts on the raw datetime string in the rendered HTML, update it to assert the presence of <time for the same row instead.

  • Step 4: Commit
git add l4d2web/l4d2web/templates/admin_users.html l4d2web/l4d2web/templates/blueprints.html
git commit -m "refactor(templates): use timeago filter for admin/blueprint timestamps"

Task 5: Migrate _job_table.html and job_detail.html (with None guards)

Files:

  • Modify: l4d2web/l4d2web/templates/_job_table.html:22-23
  • Modify: l4d2web/l4d2web/templates/job_detail.html:24-26

In models.py, Job.started_at and Job.finished_at are nullable; Job.created_at is not. Preserve the existing - placeholder for nullable columns.

  • Step 1: Modify _job_table.html

Replace lines 22-23 of l4d2web/l4d2web/templates/_job_table.html:

      <td>{{ job.created_at }}</td>
      <td>{{ job.finished_at or "-" }}</td>

with:

      <td>{{ job.created_at | timeago }}</td>
      <td>{% if job.finished_at %}{{ job.finished_at | timeago }}{% else %}-{% endif %}</td>
  • Step 2: Modify job_detail.html

Replace lines 24-26 of l4d2web/l4d2web/templates/job_detail.html:

      <tr><th>Created</th><td>{{ job.created_at }}</td></tr>
      <tr><th>Started</th><td>{{ job.started_at or "-" }}</td></tr>
      <tr><th>Finished</th><td>{{ job.finished_at or "-" }}</td></tr>

with:

      <tr><th>Created</th><td>{{ job.created_at | timeago }}</td></tr>
      <tr><th>Started</th><td>{% if job.started_at %}{{ job.started_at | timeago }}{% else %}-{% endif %}</td></tr>
      <tr><th>Finished</th><td>{% if job.finished_at %}{{ job.finished_at | timeago }}{% else %}-{% endif %}</td></tr>
  • Step 3: Run the job-related tests

Run: pytest l4d2web/tests/test_job_logs.py l4d2web/tests/test_pages.py -q Expected: all tests pass. Update assertions that pin raw-datetime substrings to instead assert <time ; the - placeholder for nullable fields must still render in the absence of started_at / finished_at.

  • Step 4: Commit
git add l4d2web/l4d2web/templates/_job_table.html l4d2web/l4d2web/templates/job_detail.html
git commit -m "refactor(templates): use timeago filter for job timestamps

Preserves the existing '-' placeholder for nullable started_at /
finished_at columns."

Task 6: Migrate _live_state.html

Files:

  • Modify: l4d2web/l4d2web/templates/_live_state.html:9-11, 30-33, 53-56

Three call sites; all use bespoke (now - x).total_seconds() // … math. Replace with the filter. The now template variable becomes unused inside this file after the rewrite.

  • Step 1: Replace the polled Ns ago line (lines 9-11)

In l4d2web/l4d2web/templates/_live_state.html, find:

    <small class="muted">
      polled {{ ((now - snapshot.last_seen_at).total_seconds() | int) }}s ago
    </small>

Replace with:

    <small class="muted">
      polled {{ snapshot.last_seen_at | timeago }}
    </small>
  • Step 2: Replace the joined Nm ago line (line 31)

Find:

        <span class="meta">
          joined {{ ((now - session.joined_at).total_seconds() // 60) | int }}m ago
          · ping {{ session.min_ping }}-{{ session.max_ping }}ms
        </span>

Replace with:

        <span class="meta">
          joined {{ session.joined_at | timeago }}
          · ping {{ session.min_ping }}-{{ session.max_ping }}ms
        </span>
  • Step 3: Replace the last seen Nm ago line (line 55)

Find:

        <span class="meta">
          last seen {{ ((now - row.last_seen).total_seconds() // 60) | int }}m ago
        </span>

Replace with:

        <span class="meta">
          last seen {{ row.last_seen | timeago }}
        </span>
  • Step 4: Run the live-state tests

Run: pytest l4d2web/tests/test_servers.py -q Expected: tests pass. The two tests test_servers_index_renders_live_state_badge and test_live_state_fragment_renders_current_and_recent (server_routes.py:449, 513) render this fragment. If they assert on Nm ago substrings, replace those assertions with checks for <time or for the new long-form output (e.g. joined 5 minutes ago).

  • Step 5: Commit
git add l4d2web/l4d2web/templates/_live_state.html
git commit -m "refactor(templates): use timeago filter in _live_state.html

Replaces three bespoke (now - x).total_seconds() expressions with the
shared filter, unifying vocabulary (no more '0m ago' inside the first
minute) and adding the UTC tooltip."

Task 7: Migrate _server_actions.html + _overlay_build_status.html + page_routes.py

Files:

  • Modify: l4d2web/l4d2web/routes/page_routes.py:240-305, 442-484
  • Modify: l4d2web/l4d2web/templates/_server_actions.html:25-32
  • Modify: l4d2web/l4d2web/templates/_overlay_build_status.html:7-14

The two route helpers currently precompute a string via humanize_delta. Replace with raw datetime passed under a new key, let the template apply the filter.

  • Step 1: Update _build_server_actions_context in page_routes.py

In l4d2web/l4d2web/routes/page_routes.py, replace the block at lines 239-305 (function body of _build_server_actions_context) so that:

  • Line 240 — remove from l4d2web.services.timeago import humanize_delta.
  • Line 284 — rename latest_job_when: str | None = None to latest_job_at: datetime | None = None.
  • Line 294 — replace latest_job_when = humanize_delta(ref_time) with latest_job_at = ref_time.
  • Line 303 — update the returned dict key from "latest_job_when": latest_job_when to "latest_job_at": latest_job_at.

datetime is already imported at page_routes.py:2 (from datetime import UTC, datetime, timedelta) — no import change needed.

  • Step 2: Update _build_overlay_build_status_context in page_routes.py

In the same file, replace the block at lines 442-484 (function body of _build_overlay_build_status_context) so that:

  • Line 443 — remove from l4d2web.services.timeago import humanize_delta.

  • Line 467 — rename latest_build_when: str | None = None to latest_build_at: datetime | None = None.

  • Line 475 — replace latest_build_when = humanize_delta(ref_time) with latest_build_at = ref_time.

  • Line 481 — update the returned dict key from "latest_build_when": latest_build_when to "latest_build_at": latest_build_at.

  • Step 3: Update _server_actions.html

In l4d2web/l4d2web/templates/_server_actions.html, line 29, replace:

    {{ latest_job_when }}

with:

    {{ latest_job_at | timeago }}
  • Step 4: Update _overlay_build_status.html

In l4d2web/l4d2web/templates/_overlay_build_status.html, line 11, replace:

    {{ latest_build_when }}

with:

    {{ latest_build_at | timeago }}
  • Step 5: Run the test suite to catch context-key mismatches

Run: pytest l4d2web/tests -q Expected: tests pass. The most likely failure point is tests that check the rendered server actions fragment (test_servers.py) or overlay build status fragment. If any test asserts the old latest_job_when string output, update it to look for <time or the new long-form output (e.g. 12 minutes ago).

  • Step 6: Commit
git add l4d2web/l4d2web/routes/page_routes.py l4d2web/l4d2web/templates/_server_actions.html l4d2web/l4d2web/templates/_overlay_build_status.html
git commit -m "refactor(page_routes): pass datetime to templates for timeago filter

Drop the inline humanize_delta imports and string-precomputation; pass
the raw datetime as latest_job_at / latest_build_at and let the
template apply the timeago filter. One fewer code path computing
relative-time strings."

Task 8: Drop the dead now= kwarg from _live_state.html render call

Files:

  • Modify: l4d2web/l4d2web/routes/server_routes.py:266-275

After Task 6, _live_state.html no longer reads now. Remove the kwarg from the only render_template call that passes it.

  • Step 1: Confirm no other template uses the now context variable

Run: grep -rn "\\bnow\\b" l4d2web/l4d2web/templates/ Inspect the output. The only references should be in template files that we have already migrated. Expected: no remaining (now - …) or bare {{ now }} references in any template.

  • Step 2: Remove the now= kwarg

In l4d2web/l4d2web/routes/server_routes.py, at line 273 inside the render_template("_live_state.html", …) call, remove the line:

        now=datetime.now(UTC).replace(tzinfo=None),
  • Step 3: Check whether datetime and UTC are still used in the file

If lines 210 and 234 still reference datetime.now(UTC).replace(tzinfo=None) (for the cutoff and recent_cutoff variables), the imports stay. Don't remove them speculatively.

  • Step 4: Run the test suite

Run: pytest l4d2web/tests -q Expected: all tests pass. If a test passes a fake now into the live-state context expecting it to be respected, that test relied on dead code and should be updated to assert against <time output relative to a real datetime.now(UTC) reference.

  • Step 5: Commit
git add l4d2web/l4d2web/routes/server_routes.py
git commit -m "refactor(server_routes): drop unused 'now' kwarg from _live_state render

After the timeago migration, the live-state template no longer reads
'now' — it computes relative labels through the filter, which derives
its own reference time."

Task 9: End-to-end verification

Files: none — verification only.

  • Step 1: Run the entire test suite

Run: pytest l4d2web/tests -q Expected: all tests pass.

  • Step 2: Run ruff if it's part of the project's check workflow

Run: ruff check l4d2web/ Expected: no new violations. The .ruff_cache/ directory at the project root suggests ruff is in active use.

  • Step 3: Confirm no remaining raw-datetime renders or bespoke inline-time math

Run: grep -rn -E "\\{\\{ [a-z_.]+\\.(created_at|updated_at|started_at|finished_at|joined_at|last_seen|last_seen_at)" l4d2web/l4d2web/templates/ Expected: every match is followed by | timeago or | timeago }}{% else %}…{% endif %}. No bare {{ x.created_at }} should remain.

Run: grep -rn "(now -" l4d2web/l4d2web/templates/ Expected: no matches.

  • Step 4: Manual UI smoke (developer-side, optional but recommended)

Start the dev server (see README.md for the exact command) and log in:

  • Visit /admin/usersCreated / Updated columns render <time> elements; hovering shows UTC.

  • Visit /blueprints — same.

  • Visit /jobs and a single job detail — Created / Started / Finished use the filter; null Finished shows -.

  • Open a server with live state — polled N seconds ago, joined N minutes ago, last seen N minutes ago; check that page-source shows <time markup, not literal &lt;time&gt;.

  • Step 5: No-op commit not required — work is already committed across Tasks 1-8.

End of plan.