Medium Dashboards system

Pipeline Aging Board

A stage-by-stage aging board that shows how long each open lead has been sitting in its current pipeline stage, with buckets (0-2 days, 3-7, 8-14, 14+) and per-stage medians, so stale deals stop hiding in a flat list. It computes time-in-stage from CRM status/stage-change timestamps and surfaces the oldest untouched leads first. The point is an action queue for follow-up, not a vanity funnel chart.

4 to 7 days
timeline
Medium
complexity
5
tools
4
steps

Built with real HMX dashboard tool paths

Supabase PostgresPostgres triggersSQL window functionsNext.js 16 server componentsTypeScriptSupabase PostgresPostgres triggersSQL window functionsNext.js 16 server componentsTypeScript

01 // System facts

System facts

Pipeline Aging Board uses a reporting model and review layer for Dashboards. A stage-by-stage aging board that shows how long each open lead has been sitting in its current pipeline stage, with buckets (0-2 days, 3-7, 8-14,... The architecture connects establish a stage-entry, supabase postgres, postgres triggers, and dashboard action with an explicit control path.

Outcome

Stale deals become visible and sortable by neglect, so the team works the oldest-at-risk leads first instead of always touching the newest — fewer leads silently rotting in a middle stage.

Main risk

Without true stage-change history, 'time in stage' is wrong (it reflects record age, not stage age), so the board points people at the wrong deals.

Prevention

Capture stage-entry timestamps explicitly (trigger/history row) and validate a handful of known leads by hand before trusting the board; show 'data since <date>' so pre-history leads aren't misread.

Fallback

If stage history can't be reconstructed, ship an interim 'days since last activity/update' aging view (using updated_at) clearly labeled as last-touch aging, and add proper stage-entry capture as the follow-up.

System architecture

Pipeline Aging Board Architecture

6 nodes
Establish a stage-entry
SQL that computes age = now
Supabase Postgres
Postgres triggers
Review Queue
Dashboard Action
  1. 01Establish a stage-entry

    A stage-by-stage aging board that shows how long each open lead has been sitting in its current pipeline stage, with buckets (0-2 days, 3-7, 8-14,...

  2. 02SQL that computes age = now

    Write SQL that computes age = now() - stage_entered_at per open lead, bucketed, plus per-stage median age and a count of leads past an SLA threshold.

  3. 03Supabase Postgres

    Supabase Postgres contributes the trusted model for Pipeline Aging Board so metrics are defined before they are visualized.

  4. 04Postgres triggers

    Build the board as columns per stage with the oldest leads at top, an age badge, and a clear 'past threshold' highlight; default to open leads only (exclude won/lost/spam/partial).

  5. 05Review Queue

    If stage history can't be reconstructed, ship an interim 'days since last activity/update' aging view (using updated_at) clearly labeled as last-to...

  6. 06Dashboard Action

    Stale deals become visible and sortable by neglect, so the team works the oldest-at-risk leads first instead of always touching the newest — fewer...

How it is built

Build steps

A stage-by-stage aging board that shows how long each open lead has been sitting in its current pipeline stage, with buckets (0-2 days, 3-7, 8-14, 14+) and per-stage medians, so stale deals stop hiding in a flat list. It computes time-in-stage from CRM status/stage-change timestamps and surfaces the oldest untouched leads first. The point is an action queue for follow-up, not a vanity funnel chart.

  1. 01Establish a stage-entry timestamp: if the CRM only stores current status, add a lightweight stage-history capture (a trigger or an updated-at-per-status field) so 'time in current stage' is computable rather than estimated from created_at.
  2. 02Write SQL that computes age = now() - stage_entered_at per open lead, bucketed, plus per-stage median age and a count of leads past an SLA threshold.
  3. 03Build the board as columns per stage with the oldest leads at top, an age badge, and a clear 'past threshold' highlight; default to open leads only (exclude won/lost/spam/partial).
  4. 04Add a per-owner filter and a daily 'aging summary' count so the same view drives both the live board and a short standup number.

Tools

Workflow surface

  • Supabase Postgres
  • Postgres triggers
  • SQL window functions
  • Next.js 16 server components
  • TypeScript
  • Inputs layer: Establish a stage-entry timestamp: if the CRM only stores current status, add a lightweight stage-history capture (a trigger or an updated-at-per-status field) so 'time in current stage' is computable rather than estimated from created_at.
  • Transform layer: Write SQL that computes age = now() - stage_entered_at per open lead, bucketed, plus per-stage median age and a count of leads past an SLA threshold.
  • Metrics layer: Supabase Postgres contributes the trusted model for Pipeline Aging Board so metrics are defined before they are visualized.
  • Visualization layer: Postgres triggers handles refresh, review, or reporting delivery while capture stage-entry timestamps explicitly (trigger/history row) and validate a handful of known leads by hand before trusting the board; show 'data...
  • Action layer: Stale deals become visible and sortable by neglect, so the team works the oldest-at-risk leads first instead of always touching the newest — fewer...

Data flow

  1. 01Establish a stage-entry timestamp: if the CRM only stores current status, add a lightweight stage-history capture (a trigger or an updated-at-per-status field) so 'time in current stage' is computable rather than estimated from created_at.
  2. 02Write SQL that computes age = now() - stage_entered_at per open lead, bucketed, plus per-stage median age and a count of leads past an SLA threshold.
  3. 03Build the board as columns per stage with the oldest leads at top, an age badge, and a clear 'past threshold' highlight; default to open leads only (exclude won/lost/spam/partial).
  4. 04Add a per-owner filter and a daily 'aging summary' count so the same view drives both the live board and a short standup number.

Controls and fallbacks

  • Without true stage-change history, 'time in stage' is wrong (it reflects record age, not stage age), so the board points people at the wrong deals.
  • Capture stage-entry timestamps explicitly (trigger/history row) and validate a handful of known leads by hand before trusting the board; show 'data...
  • If stage history can't be reconstructed, ship an interim 'days since last activity/update' aging view (using updated_at) clearly labeled as last-to...