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:

  1. Phase 1 (~30 min) — widget + first integration - Author ~/bin/portfolio_date_counter.js - Deploy as gf.cx/widgets/date-counter.js (same hosting model as ask-opus widget) - Update pa_claim_cockpit_render.py to emit <span data-when="..." data-mode="deadline"> in the deadlines bar - Verify on the live cockpit
  2. 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
  3. 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:

  1. Walks the pa/ static-HTML tree
  2. Parses every page for data-when attributes
  3. Extracts: anchor date, mode, source URL, source label, page title/category
  4. Sorts by chronological proximity to today
  5. Groups by user-meaningful buckets (overdue / this week / this month / this quarter / this year / archive)
  6. Emits pa/countdown/index.html with 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

Sibling memories + sketches

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.

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.

Source: parked_sketch_runtime_date_counters_2026-05-21.md · Rendered 2026-05-21 11:36