There was a ghost on the production server. A component called InsightSummaryCard.vue that didn't exist in any local source file, rendering a card that said "Current revenue is $0" on a dashboard where revenue was decidedly not zero.
Tracing it took SSH access and a grep through the deployed build. A previous Claude session had pushed thirty commits directly to production — code that the local repo never received. The local main branch had diverged with its own WIP commit, creating a split timeline where two versions of the codebase existed simultaneously. The ghost component lived in one timeline. The fix lived in the other.
The root cause of the $0 was almost comically mundane: dashboard.revenue was an object with {value, change} properties, but the formatter was treating it as a plain number. Feed an object to a currency formatter and you get $0. Not null, not an error, not a blank — zero. The most misleading possible output, because zero looks like an answer.
After reconciling the codebases, the real work began: period-over-period comparisons across all six Jobber integration tabs.
Every analytics endpoint gained a prior-period query using the existing priorPeriod() helper. The API returns both current and prior values. The frontend computes the delta and renders ↑ 12% or ↓ 8% beside each KPI. Revenue, collection rate, close rate, average ticket, job duration, team utilization — every number that matters now has a direction. A number without context is trivia. A number with a trend is intelligence.
The Team tab got rebuilt from a PrimeVue DataTable into individual tech cards — each technician gets a name, a revenue headline, and a six-stat grid: jobs completed, average ticket, revenue per hour, utilization rate, jobs per day, trend. The utilization bar shows real context: "32.5 hours booked of 176 available." Not a percentage floating in space, but a sentence that tells you whether someone is stretched thin or has room.
The Customers tab reorganized its KPIs into two tiers. Primary: active count, new versus returning with revenue tooltips, average lifetime value, average ticket. Secondary: repeat rate, average days between jobs, churn risk, six-month retention. Every label has a hover tooltip explaining what it measures and why it matters. The reactivation alert became a proper callout card — not a vague "you have at-risk customers" but specific names, their lifetime value, how many days since their last visit. Actionable, not informational.
The Jobs tab expanded from three KPIs to seven. Completion rate now shows both the percentage and the raw fraction — "87% (142 of 163)" — because percentages lie about small samples. Average duration gained directional messaging: "2.1 hours (12% faster)" tells a different story than just "2.1 hours." Status distribution moved to its own full-width row with hover tooltips.
Five bugs fixed along the way. The overview insight buttons had been placeholder TODOs since the first build. The Jobs tab KPIs didn't respond to the period toggle because the selected period wasn't being forwarded to the API — a wiring oversight invisible until someone actually switched from 30 days to 7 days and noticed nothing changed. A duplicate class attribute was breaking the Vite build. Confusing double bars on the tech cards — utilization next to revenue proportion — got simplified to just utilization.
The lesson from the ghost code is worth remembering: Quick Deploy plus multiple Claude sessions can create timeline divergence. Always git fetch before starting work. And the pattern that emerged — prior-period objects in API responses, client-side delta computation — should be the standard for every integration dashboard going forward. A number alone is a fact. A number with direction is a decision.