Runtime date counters — variations + portfolio-wide pattern
Date: 2026-05-21 · Status: sketched · Build effort: ~30 min for v1 (single widget script), ~1 hr for full portfolio retrofit
The principle (per Dan, 2026-05-21)
“For everything, that countdown timer, or conversely, a count-up timer, days passed since the deadline, like -15 days, etc. … it’s a foundational pattern across everything — one of the coolest, simple things.”
Every dated reference on a static page wants to express time-distance from now to that date, computed at read time, not at publish time. Build-time bake-ins drift; runtime recalculation is always accurate.
The four canonical states
PAST NOW FUTURE
─────────────────────|─────────────────────
│
"overdue 15d" │ "today" "in 54d"
"paid 30d ago" │ "renews in 90d"
"expired 90d" │ "expires in 30d"
"filed 2y ago" │ "due 14:30"
│
[count-up] [zero] [count-down]
Six concrete variations the portfolio surfaces want to express:
| # | State | Distance | UI form | Example |
|---|---|---|---|---|
| 1 | Countdown — future deadline | -N days | “54d left” | claim deadlines, contract renewals, gate-service intervals |
| 2 | Overdue — past deadline, unresolved | +N days | “15d overdue” / “-15d” | unpaid invoices, missed service, expired insurance |
| 3 | Today — happening now | 0 | “today” / “due today” | calendar items, scheduled cron tomorrow |
| 4 | Recency — past event, just record-keeping | +N days | “30d ago” / “30 days ago” | last service, last touched, last contact |
| 5 | Anniversary — past event with periodic meaning | +N days, normalised | “3.2y ago” / “1 year, 47 days” | original purchase date, account opened |
| 6 | Time-of-day countdown — sub-day precision | hours, minutes | “in 3h 22m” / “5h overdue” | same-day deadlines, meeting starts |
Markup pattern (canonical)
Same shape works for all six variations. Authors declare the anchor date + interpretive mode, widget renders the appropriate label:
<!-- Variation 1: countdown -->
<span data-when="2026-07-14" data-mode="countdown">…</span>
<!-- Variation 2: overdue-aware countdown (auto-flips after target) -->
<span data-when="2026-04-15" data-mode="deadline">…</span>
<!-- Variation 4: recency / "ago" -->
<span data-when="2026-05-17" data-mode="ago">…</span>
<!-- Variation 5: anniversary (with auto-unit selection) -->
<span data-when="2016-02-15" data-mode="age">…</span>
<!-- Variation 6: sub-day precision -->
<span data-when="2026-05-21T18:00:00-04:00" data-mode="precise">…</span>
The static HTML body is the fallback (what shows if JS is disabled or hasn’t loaded yet). The widget replaces with computed value on page load + on visibility events (so opening yesterday’s tab today recalculates).
The widget — single source of truth
~/bin/portfolio_date_counter.js (or hosted at gf.cx/widgets/date-counter.js) — one drop-in script that handles all six modes. Embed via the same data-cfasync="false" pattern as ask-opus:
<script src="https://gf.cx/widgets/date-counter.js" defer data-cfasync="false"></script>
Pseudocode of the core dispatch:
function format(anchor, mode, now = new Date()) {
const target = new Date(anchor);
const ms = target - now;
const days = Math.round(ms / 86400000);
switch (mode) {
case "countdown":
return days > 0 ? `${days}d left` :
days === 0 ? "today" :
`${-days}d overdue`;
case "deadline": // alias for countdown, semantically clearer
return format(anchor, "countdown", now);
case "ago":
return days < 0 ? `in ${-days}d` :
days === 0 ? "today" :
days === 1 ? "yesterday" :
days < 30 ? `${days}d ago` :
days < 365 ? `${Math.round(days/30)}mo ago` :
`${(days/365).toFixed(1)}y ago`;
case "age": // sentimental / record-keeping precision
const years = days / 365.25;
return years < 1 ? `${days}d` :
years < 10 ? `${years.toFixed(1)} years` :
`${Math.floor(years)} years`;
case "precise": // sub-day for same-day events
const hours = Math.round(ms / 3600000);
if (Math.abs(hours) < 24) {
return hours > 0 ? `in ${hours}h` :
hours === 0 ? "now" :
`${-hours}h overdue`;
}
return format(anchor, "countdown", now);
}
}
Visual treatment by state
Same data-mode + state-derived CSS class for color-coding without needing extra attributes:
[data-when][data-state="overdue"] { color: var(--accent-alert); font-weight: 700; }
[data-when][data-state="today"] { color: var(--accent-warn); font-weight: 700; }
[data-when][data-state="soon"] { color: var(--accent-warn); } /* <7 days */
[data-when][data-state="future"] { color: var(--ink); }
[data-when][data-state="past"] { color: var(--ink-soft); }
The widget sets data-state after computing the distance:
�STASH8�
Auto-tick (optional, off by default)
For long-lived tabs / surfaces, recalculate periodically:
�STASH9�
Off by default — adds complexity. Most surfaces don’t need it (reload-on-revisit is enough). Enable on long-lived dashboards via data-tick="60" attribute.
Where this lifts across the portfolio (today, no retrofit yet)
pa.gf.cx
────────
claim cockpits deadline pills countdown / overdue
equipment records last-service date ago
household records service-due date countdown
vehicles records registration expiry countdown
contractors records last-contacted date ago
personal records purchase date age
claim.gf.cx
────────────
active-claims index deadlines per claim countdown
opened-date ago
archive section resolved-date ago
dare.co.uk
──────────
case studies published date ago (multi-unit)
methods archive authored date age
field notes dated entries ago
dogwood.house
─────────────
booking confirmations check-in date countdown
member dog journal last stay ago
service expiry next renewal countdown
audreyinc.com
─────────────
product service last polish date ago
journal entries posted date ago
warranty cards registered date age
The retrofit strategy
Same pattern as the Q&A retrofit script (per feedback_clean_sweep_retrofit_scripts.md). Build the widget + a clean-sweep injector:
- Phase 1 (~30 min) — widget + first integration
- Author
~/bin/portfolio_date_counter.js- Deploy asgf.cx/widgets/date-counter.js(same hosting model as ask-opus widget) - Updatepa_claim_cockpit_render.pyto emit<span data-when="..." data-mode="deadline">in the deadlines bar - Verify on the live cockpit - Phase 2 (~30 min) — pa.gf.cx retrofit
- Asset records (equipment / vehicles / household / personal / contractors) all get the widget script + dated spans
- Either via
pa_qa_block_inject.py-style injector OR via the render scripts emitting it natively - Phase 3 (variable) — other portfolio domains - dare.co.uk archive — script-generate ago labels for case studies / methods / field notes - dogwood + audrey — similar - Each portfolio adoption is its own micro-build
Source of truth + click-through schema
Every countdown badge is bound to a canonical record that owns the anchor date. The badge knows its source via two extra attributes:
<span data-when="2026-07-14"
data-mode="deadline"
data-source="/equipment/john-deere-z665-eztrak-zero-turn-mower#service-log"
data-source-label="Z665 · next service">54d left</span>
The widget makes the badge clickable when data-source is present — clicking the badge navigates to that record’s deep-link (the #anchor jumps to the relevant section).
The four schema fields
| Attribute | Required | Purpose |
|---|---|---|
data-when |
✓ | ISO date — the anchor (the only thing the widget needs to compute the label) |
data-mode |
✓ | One of countdown / deadline / ago / age / precise — interpretive mode |
data-source |
optional | Canonical record URL + #anchor to deep-link the source-of-truth record |
data-source-label |
optional | Short human label shown on hover / on the rollup page (e.g. “Z665 · next service”) |
If data-source is absent, the badge is read-only (no link). If data-source is present, the badge becomes a hyperlink with a small ↗ arrow on hover.
Authoring discipline
The author of each canonical record is responsible for emitting the badges that reference it. So pa_claim_cockpit_render.py emits the deadlines bar with data-source pointing back at the cockpit itself + an anchor to the relevant deadline line. The equipment service-due badge is emitted by the equipment record itself OR by a referrer page (e.g., the equipment landing showing “next service” for each item).
The Z665 record IS the source of truth ──────────────┐
│
/equipment/john-deere-z665 │
┌──────────────────────────────────────┐ │
│ Last service: 17 May 2026 │ │
│ Hour meter: ~350h │ │
│ Next service due: 14 Jul 2026 ◀────┼──────────────┘
│ (every 50h or annual, whichever │
│ comes first) │
└──────────────────────────────────────┘
│
│ emitted by the same render script,
│ flows as <span data-when ... data-source>
▼
/equipment/ (landing — card grid)
┌──────────────────────────────────────┐
│ John Deere Z665 │
│ Status: serviced 17 May 2026 │
│ ⏳ Next service: 54d left ◀──── (click → jumps to source)
└──────────────────────────────────────┘
The portfolio-wide rollup — pa.gf.cx/countdown
The big move Dan called out:
“pa.gf.cx/countdown — could be what’s happening and what’s expected, throughout the seasonal shifts, as per the records…”
A unified seasonal dashboard that scans every static record for data-when attributes, aggregates them into one page, groups by urgency / season / category, and surfaces what’s coming up across all assets — pool opening, gate quote due, watch service follow-up, vehicle registration renewal, driveway sealing, equipment service, claim deadlines, all in one view.
How it gets built (build-time scan)
A new ~/bin/pa_countdown_render.py that:
- Walks the
pa/static-HTML tree - Parses every page for
data-whenattributes - Extracts: anchor date, mode, source URL, source label, page title/category
- Sorts by chronological proximity to today
- Groups by user-meaningful buckets (overdue / this week / this month / this quarter / this year / archive)
- Emits
pa/countdown/index.htmlwith the rollup
┌─── Build pipeline ────────────────────────────────────┐
│ │
│ Static records countdown scanner │
│ ──────────── ──────────────── │
│ /equipment/*.html ──┐ │
│ /vehicles/*.html ──┤ pa_countdown_render.py │
│ /household/*.html ──┼───▶ scans data-when │
│ /personal/*.html ──┤ attributes │
│ /property/*.html ──┤ │
│ /contractors/*.html──┘ │
│ │ │
│ ▼ │
│ pa/countdown/index.html │
│ │
│ Each record stays the source of truth — the │
│ rollup never owns the date, just aggregates the │
│ references. │
└───────────────────────────────────────────────────────┘
What the rollup page looks like
pa.gf.cx / countdown (seasonal · cyclical · what's expected)
──────────────────────────────────────────────────────────────────────
Last regenerated · 2026-05-21 · 47 dated references across 6 sections
⚠ Overdue (3)
──────────────────
-8d Miele Complete C3 Calima [household] →
Service overdue · last "next due" 5 Jun 2023
-2d Husqvarna 150BT pickup [equipment] →
Repair pickup scheduled 19 May 2026
today Wife's classic Omega quote review [personal] →
Awaiting first WatchRepair.net call back
📅 This week (5)
──────────────────
+2d Husqvarna 150BT estimate [equipment] →
+4d Jorge garden-waste quote review [contractors] →
+5d Claim 046414618 contractor mtg [claim] →
+7d Sofia time-zone meeting [calendar] → time.dare.co.uk
+7d Z665 oil-check (50h interval) [equipment] →
📅 This month (8)
──────────────────
+14d BMW registration audit [vehicles] →
+21d Pool opening target [property] →
+28d F-250 wrap quote follow-up [vehicles] →
...
📅 This quarter (12)
──────────────────
+54d Z665 next service interval [equipment] →
+67d Strong Fence gate decision [property] →
...
📅 Beyond / annual reminders (19)
──────────────────
+120d Driveway sealcoating (3-year) [property] →
+9.2mo Newtown Homeware Miele service [household] →
+1.5y Vehicle inspection cycle [vehicles] →
...
Each row is clickable → jumps to the source record’s data-source URL + anchor. The dashboard is read-only — never edits dates, only aggregates and shows them.
Why this works architecturally
┌──────────────────────────────────────────────────────┐
│ Single source of truth per date │
│ │
│ Z665 next-service date lives ONLY on the Z665 │
│ record. The /countdown rollup READS it and │
│ references it. The cockpit deadlines bar READS it │
│ and references it. The home grid READS it and │
│ references it. │
│ │
│ Change the Z665 date in ONE place → cascades to │
│ every reader on next publish. │
└──────────────────────────────────────────────────────┘
Same pattern as the contractors directory (canonical record + many references). Dates are first-class entities, not duplicated strings.
Build cadence
/countdown is regenerated on every publish (since the publish step already walks the file tree). It’s also the natural home for a morning “what’s today + this week” digest that gets read at 07:00 ET (per user_workday_starts_7am_et.md) — could even be a clean-sweep email at 07:00 of just the overdue + this-week sections.
Future: seasonal awareness
The “seasonal shifts” Dan mentioned could be encoded as named buckets:
spring-opening (March 1 – April 30) pool, driveway thaw inspection, garden equipment
summer-ops (May 1 – August 31) mowing schedule, blower service, pool chemistry
autumn-prep (September 1 – October 31) leaf clearance, gate winterization, equipment storage
winter-quiet (November 1 – February 28) indoor service interval, watch overhauls, tax prep
A data-season attribute on records (or auto-inferred from data-when date) lets the rollup group by season as a second dimension alongside urgency.
Anti-patterns to avoid
- Don’t tick more than once per minute. Sub-second updates create cognitive noise. Days-precision UI tolerates 60-second refresh fine.
- Don’t recalculate offscreen. The visibility check above prevents wasted CPU on hidden tabs.
- Don’t use just
date - datemath without timezone awareness. ISO dates without timezone get interpreted as UTC; for end-of-day deadlines, appendT23:59:59in the source. - Don’t break SSR / no-JS fallback. The static text inside the span is the read-on-print fallback. Widget enhances; static text is the baseline.
- Don’t break print. Add a
@media print { [data-when]::after { content: " · " attr(data-when); } }so paper output shows the actual anchor date alongside the relative label.
Sibling memories + sketches
user_static_site_plus_living_brains_stack.md— the build-time/runtime distinction this resolvesfeedback_last_touched_freshness_pattern.md— adjacent pattern (static-baked dates that don’t need recalculation; this widget makes them runtime-accurate)feedback_clean_sweep_retrofit_scripts.md— the retrofit strategy this followsparked_sketch_portfolio_running_cost_calculator_2026-05-21.md— adjacent runtime-widget design (task #46)feedback_qa_capture_pattern.md— the original drop-in widget pattern this lifts from
The “future-time status page” framing (Dan, 2026-05-21)
“It’s a slice of future-time status page.”
That’s the precise mental model. pa.gf.cx/countdown is to TIME what an infrastructure status page is to SYSTEMS:
Conventional status page /countdown future-time status page
──────────────────────── ─────────────────────────────────
What's broken NOW? What's expected SOON?
Per-service degradation level Per-record urgency level
Heatmap of components Heatmap of dated commitments
Read-only, aggregated Read-only, aggregated
Source: monitoring probes Source: per-record data-when attrs
Same affordances: - At-a-glance: is anything red (overdue)? - Drill-down: click an entry to jump to the source record (the “incident” view) - Roll-up: weekly digest of upcoming + new entries since last check - History: archive section shows the past 30 days of resolved entries
The portfolio-wide implication: every brand surface gets one.
pa.gf.cx/countdown— country-house future-time statusdare.co.uk/upcoming— content + client-engagement future-time statusdogwood.house/calendar— booking + service future-time status (already exists in calendar.dogwood.house but currently a different beast)audreyinc.com/upcoming— product release + restock future-time status
One template, one widget, one render script. Brand-stem-prefixed + same canonical pattern.
The aphorism
Every dated reference wants to know “how far from now?” — not “how far from when this was last published.” Bake the anchor; compute the distance at read time. The HTML is the date; the widget is the relativity. Aggregate the dated references into one view and you’ve built a future-time status page — a slice of the operating system from a temporal angle.