Plate № 25 · UX primitives · time

A pattern from the gf.cx specimen book

Human-friendly relative time · days ago, exact on hover

Source · Dan, 2026-07-18

"Represent as 'days ago' and have a data-tool-tip that, on hover, as per standard, tells you the exact time / date / year."

Why it's human-centric — first

A timestamp like 2026-07-18T07:12:49-04:00 is correct and nearly unreadable. To answer the only question a reader actually has — is this fresh or stale? — they have to subtract that instant from now in their head, across a timezone, maybe across a date boundary. That's arithmetic the page can do for them, and should.

People don't reason about the recent past in ISO-8601. They reason in elapsed terms: "a minute ago", "this morning", "yesterday", "last week". Relative time answers the question directly3d ago lands as freshness at a glance, no mental math, no locale in the reader's head. It's the humane default because it matches how attention already works: recency is a feeling of distance, not a coordinate.

But relative time trades away precision — 3d ago can't tell you which day, or the exact minute an incident fired, and that precision genuinely matters when you're correlating logs or filing a report. The move is not to choose. Show the human read in the cell; hang the exact instant on a hover tooltip. Progressive disclosure of precision: human-scale by default, machine-exact on demand. Nobody has to trade legible for precise — the glance is free, the detail is one hover away.

The cell answers "how long ago?"; the hover answers "exactly when?" — the same datum, read at two distances.

The shape — two reads of one instant

One stored timestamp, rendered two ways that occupy the same DOM node — the visible text is the human read, the data-tip attribute is the exact read. Three tiny stdlib-only helpers, one shared parser.

HelperReadsOutput
fmt_age(iso)the cell — the human glance7s / 12m / 3h / 5d ago
fmt_exact(iso)the data-tip — precision on demand18 Jul 2026, 07:12:49 -0400
parse_ts(iso)the shared foundation both calla datetime, or None on garbage

The element, in-page

Below is the exact markup a rendered cell produces — the human age in the text node, the exact instant in data-tip. The tooltip CSS is already loaded by this page, so hovering the cell shows the styled card live (dotted underline = "there's more here"):

Last run  3d ago

The reference implementation

Stateless, stdlib-only, and — this is the load-bearing rule — one parser. Every humanised time on the surface flows through parse_ts, so the "what does a malformed / oddly-offset timestamp mean?" decision lives in exactly one place. From ~/bin/gfcx_hub_format.py:

import re
from datetime import datetime, timezone

def parse_ts(iso):
    """The ONE place ISO parsing lives. Tolerates a trailing Z and a
    colon-less TZ offset. Returns None on empty/garbage input so callers
    stop each hand-rolling fromisoformat()+except."""
    if not iso:
        return None
    s = str(iso).strip().replace("Z", "+00:00")
    s = re.sub(r"([+-]\d{2})(\d{2})$", r"\1:\2", s)   # -0400 -> -04:00
    try:
        return datetime.fromisoformat(s)
    except (ValueError, TypeError, AttributeError):
        return None

def fmt_age(iso):
    """The human read for the cell: '7s/12m/3h/5d ago'."""
    ts = parse_ts(iso)
    if ts is None:
        return iso            # degrade to the raw string, never crash
    s = int((datetime.now(timezone.utc) - ts).total_seconds())
    if s < 60:    return f"{s}s ago"
    if s < 3600:  return f"{s // 60}m ago"
    if s < 86400: return f"{s // 3600}h ago"
    return f"{s // 86400}d ago"

def fmt_exact(iso):
    """The precise read for the tooltip: '18 Jul 2026, 07:12:49 -0400'.
    '' when unparseable, so the caller then omits the tooltip."""
    ts = parse_ts(iso)
    if ts is None:
        return ""
    return ts.strftime("%d %b %Y, %H:%M:%S %z").strip()

Wiring the pair into a cell — the age is the text, the exact instant is the data-tip, and the tooltip is omitted (not blank) when the instant can't be parsed:

exact = fmt_exact(iso)
tip   = f' data-tip="{html_escape(exact)}"' if exact else ''
cell  = f'<span class="sched-age"{tip}>{fmt_age(iso)}</span>'

The trap — the colon-less offset

The subtle failure that motivated pulling parsing into one function. A shell producing timestamps with date +%z emits an offset with no colon-0400, not -04:00. datetime.fromisoformat rejected that before Python 3.11.

So on a status board, one row — the one job whose script stamped %z — silently showed a raw 2026-07-18T07:12:49-0400 in a column of tidy "Xh ago" cells, because parse_ts returned None and fmt_age degraded to the raw string. Worse: it rendered fine on the dev machine (Python 3.14) and only broke under the scheduled render on an older interpreter — the classic generated-artifact revert.

Fix: normalise the offset inside the one shared parser — re.sub(r"([+-]\d{2})(\d{2})$", r"\1:\2", s) — and every caller, on every Python version, is fixed at once. The one-parser discipline is what made a one-line fix possible.

When exactly one row of a humanised-time table shows a raw timestamp, suspect a timezone-offset or format the parser silently rejected — and fix the shared parser, not the row.

Client-side equivalent

When the cell is rendered in the browser instead of server-side, the same two-read shape in a few lines of JS — Intl gives you the exact read for free, locale-aware:

function fmtAge(iso) {
  const s = Math.floor((Date.now() - new Date(iso)) / 1000);
  if (s < 60)    return `${s}s ago`;
  if (s < 3600)  return `${Math.floor(s/60)}m ago`;
  if (s < 86400) return `${Math.floor(s/3600)}h ago`;
  return `${Math.floor(s/86400)}d ago`;
}
function cell(iso) {
  const exact = new Date(iso).toLocaleString(undefined,
    { day:'2-digit', month:'short', year:'numeric',
      hour:'2-digit', minute:'2-digit', second:'2-digit', timeZoneName:'short' });
  const el = document.createElement('span');
  el.textContent = fmtAge(iso);
  el.setAttribute('data-tip', exact);   // assets.gf.cx/tooltip picks it up
  return el;
}

JS's new Date() parses the colon-less offset fine, so the trap above is Python-specific — but keep the raw-string fallback either way: a cell should never render NaN ago.

Import

The tooltip is the shared portfolio primitive — one stylesheet, keyed on the [data-tip] attribute. The relative-time helpers are copy-in (no runtime dependency).

<link rel="stylesheet" href="https://assets.gf.cx/tooltip/tooltip.css">

Limitations

Where it applies

Reusable elements

The named, copyable pieces — lift any one without the others:

Reference

Source
Dan, 2026-07-18 — a nit-pick on a status-board bug: a GitHub off-site backup row showing a raw ISO stamp instead of "days ago", plus the standing rule that the exact time/date/year must live on a hover tooltip.
In use / example
status.gf.cx — the backup and job tables render every LAST RUN cell as fmt_age + a data-tip exact instant. Hover any age to see the styled card live.
Reusable elements
parse_ts · fmt_age · fmt_exact in gfcx_hub_format.py · the two-read cell idiom · tooltip.css (listed above).
Origin
A humanised-time landmine, 2026-07-18 — one row of raw ISO in a column of "Xh ago" cells, traced to a colon-less %z offset the parser rejected. The fix (normalise in the one shared parser) crystallised the deeper pattern: readable by default, precise on demand, parsed in exactly one place.