Delivery Operations Platform

A live-data dashboard for B2B SaaS customer success — SLA tracking, PM scorecards, automated Slack digests, internal ticketing, AI-assisted triage. ~10K LOC across single-page web app + Python automation + Cloudflare Workers + GitHub Actions cron.

Vanilla JS SPA Python automation Cloudflare Workers GitHub Actions cron Free-tier infrastructure $0/month Co-built with Claude
About the screenshots: Every screenshot below is from a fully-functional local demo using entirely synthetic data — fake clients (Meridian Financial, Atlas Recovery, Acme Collections…), fake PMs (Priya Anand, Marcus Rivera, James Okafor), fake ticket IDs (DEMO-#### / INT-####). The production system this mirrors is closed-source. Architecture, math, and workflows are real; nothing identifiable has been published.

This is the delivery-operations tool I co-built with Claude (Anthropic) as pair-programmer over 50+ iterations during my time as Customer Operations Lead. It replaced ~150 hours/month of manual collation across three spreadsheets, eliminated the daily-digest authoring task entirely, and surfaces SLA breach risk before it lands on the CS team's radar.

Day to day, the platform handles a portfolio of 50+ enterprise clients on a Voice AI system that runs 1M+ calls per day. The dashboard is what the CS team and PMs actually look at — SLA breach tracking, ticket assignment, follow-up history, weekly PM accountability metrics, and the daily Slack digest preview before it ships.

The home view — KPI tiles, filter pills, ticket table

Open the dashboard and the first thing you see is the state of the whole portfolio. KPI tiles up top, status filters on the left, and a paginated table with full client/PM/priority/RAG-status filtering, search, and sortable columns. State (filter + sort + tab) persists across sessions in localStorage.

Active tickets — default landing view

60 total tickets, 40 actively open, 25 with at least one SLA clock breached, 11 clients with current load. Each row shows ticket ID, summary, client, priority, status, age, PM assignment, and SLA RAG status. The colored stripe on each KPI tile reflects the metric’s health.

Active tickets landing view

All tickets — including closed

Switches scope to all tickets (Open + Reopened + Closed). The same filter and sort affordances apply. Closed tickets stay searchable for post-mortems and historical client checks.

All tickets view

One-click filter by client

Click a client name in the sidebar (or pick from the dropdown) and the table narrows to that account. Useful for “what does Meridian look like right now?” conversations during the weekly client review.

Filtered by client

Three SLA clocks per ticket, business-hours-aware

The SLA engine tracks three independent clocks per external ticket (First Response, First Update, Time to Resolve) against per-priority targets, with a 12-hour business-hours window (14:00–02:00 IST, Mon–Fri). Internal tickets get two clocks (no First Response — they bypass that contract). Auto-pause when state flips to awaiting_customer; auto-resume when the customer replies.

SLA-breached filter — the daily triage view

One click on “SLA Breached” narrows to tickets that have blown at least one clock. Red chips and per-clock overshoot labels (“+1.5d”, “+14h”) make the worst offenders obvious. PMs start their day here.

SLA-breached tickets only

At-Risk filter — before it breaches

“At Risk” surfaces tickets that have used 80%+ of their SLA target but haven’t breached yet. A breach-warner DM also pings the assigned PM in Slack so they can act before the chip turns red.

At-risk tickets

Ticket drawer — AI triage + 3-clock SLA panel

Click any ticket and a side drawer slides out. Top section shows the AI-generated “What happened” summary plus a “Recommended Next Step” that Claude synthesizes from the comment thread. Below that: full metadata, three SLA clocks with per-target overshoot, and the PM assignment / ETA / action editor.

Ticket drawer external

Drawer scrolled — assignment, RCA notes, csm_handled flag

Scroll the drawer down and you get the PM owner / ETA / action editor, the “PM response not required” (csm_handled) toggle for small or questionnaire-type tickets the CSM resolves without escalating, the RCA / resolution-notes thread, the related-links field, and a “Post to #ops-channel” button that drops a status update into the ticket’s Slack thread.

Drawer assignment + RCA

Per-PM, per-client, per-priority scorecard — what PMs use to track their own work

The Scorecard tab is what each PM opens to see how they’re tracking against their SLA contract. Switch the grouping between PM / Client / Priority. Switch the window between 7d / 30d / 90d / Lifetime / Custom range. Switch the workspace scope between Jira-only / Internal-only / Both combined. Tickets where the CSM handled without needing PM escalation are auto-excluded from PM accountability metrics so the numbers reflect actual PM workload — not random small things the CSM closed.

By PM — the weekly accountability view

Per PM: tickets handled, SLA breached, % First Response Met, % Resolution Met, MTTR, reopens, reopen rate. Bar charts below show open tickets per PM and per-PM First Response / Resolution pass rates side by side. Weekly snapshot runs every Thursday and posts to the team channel.

Scorecard by PM

By Client — the client-health view

Same metrics, regrouped per client. Surfaces which accounts are running hot (lots of tickets, breaches piling up) vs which are quiet. Useful during QBR prep and renewal conversations.

Scorecard by client

By Priority — the SLA-contract view

Group by P0 / P1 / P2 / P3 to see how the team is performing against each priority’s SLA tier. P0 / P1 should be near-100% First Response Met; lower priorities have more headroom. Exposes whether priority calibration is honest.

Scorecard by priority

Row drill-down — click a PM to see their tickets

Expand any row to see the ticket-by-ticket list that fed the numbers above. Each row links straight to the ticket drawer; the outcome chip (met / breached / in progress) matches what got counted.

Scorecard drilldown

Separate workspace for issues the CSM raises herself

Internal tickets are issues a CSM or QA team member spots before any customer Jira exists — routing bugs, audio quality regressions, campaign config glitches. Lives in its own workspace with its own KPI tiles, filter sidebar, table, scorecard, and analytics. Two SLA clocks (no First Response). Never auto-pauses on awaiting_customer because there is no customer in the loop.

Internal tickets — same UI, separate state

10 active internal tickets across Ironwood Collections, Cascade Community Bank, Beacon Credit Co. Each ticket carries a “raised by” field so the team can ask the original raiser for context.

Internal tickets workspace

Raise Internal Ticket — modal form

Click “Raise Internal Ticket” from the table header to open this modal. Client + summary + description are required; priority defaults to Medium; ETA is optional; attachments and related links supported. A “Resolved → close” toggle on each ticket closes it inline once the fix lands. State is persisted to internal_tickets.json via a Cloudflare Worker write-proxy, so no GitHub token needed at the browser.

Raise internal ticket modal

Daily ticket flow, priority mix, time-windowed KPIs

Switch to the Analytics tab for the time-windowed view. Daily raised-vs-closed flow chart, priority distribution donut, KPI tiles for the selected window. Useful in the monthly delivery review to spot trends — is the team raising more tickets than they’re closing? Which priority is growing?

Analytics — 30-day window

60 tickets raised, 20 closed, 40 still open across the 30-day window. Average ticket age: 17 days. Priority distribution shows Medium dominating, with a smaller High and Low slice.

Analytics 30 days

Analytics — 90-day view (full scroll)

Longer time horizon. Same charts, broader trend. The window switcher snaps between 7 days / 30 days / 90 days / All Time without re-fetching anything — all rendered client-side from the cached data.json.

Analytics 90 days full

The dashboard talks to Slack so nobody has to write a digest by hand

A Python script (build_digest.py) assembles the daily digest from the live ticket state, the script (post_digest.py) ships it to Slack via chat.postMessage, and per-ticket sub-threading means replies stay organized. The dashboard exposes a preview-before-post modal so the CS lead can sanity-check the digest before it lands in the team channel.

Daily Slack digest — preview before posting

Opens the exact message that will land in the team channel at 2 PM IST. Intro paragraph first (summary of state), then one top-level Slack message per open ticket with status, age, ETA, and SLA chip. Replies thread under each ticket. The “Post Live to Slack” button ships the digest; “Copy Markdown” copies the raw mrkdwn for pasting elsewhere.

Slack digest preview

EOD Summary — end-of-day team check-in

A second canned summary the lead can paste into the #ops channel at end of day. Shows tickets that moved status, tickets that breached an SLA, and tickets that closed in the last 24 hours. Skips the noisy stuff (filter changes, comment-only updates).

EOD summary

Every feature that ships in the production system

Beyond the screenshots above, here’s the full surface area. Some of these are small quality-of-life features, some are background automation, some are dashboard ergonomics — together they’re what makes the system actually save 150+ hours/month.

SLA & clock math

core

Three-clock SLA contract

External tickets track First Response (CSM-owned), First Update (PM-owned), and Time to Resolve (PM-owned) independently. Internal tickets track two (no First Response — they bypass that contract).

core

Business-hours-aware SLA math

A business day is 12 hours (14:00–02:00 IST, Mon–Fri). All SLA targets count business minutes, not wall-clock. The math lives in sla_tracker.py with a regression test suite.

automation

Auto SLA pause on awaiting_customer

State machine watches the ticket’s awaiting field. The moment it flips to awaiting_customer, the SLA clocks pause and the “PAUSED” chip appears. Auto-resumes on the next customer comment.

automation

Manual SLA pause with reason picker

For situations the state machine can’t infer (e.g. waiting on legal review). The drawer offers a Pause button with a preset reason list. Pauses persist in manual_fields.json across syncs.

automation

Pause queue self-heal

If a pause write fails (Worker offline, GitHub API hiccup), the dashboard keeps the pending pause in a localStorage queue and retries on the next sync. Surfaces a one-click “sync pauses now” banner.

automation

Internal tickets never auto-pause

Internal tickets have no customer in the loop, so awaiting_customer can’t happen. The state machine hard-disables auto-pause for INT-* IDs to prevent the clock-frozen bug we used to see.

automation

Auto-priority decider

Priority level (P0 / P1 / P2 / P3) is derived from the ticket’s label set, summary keywords, and client tier. PMs can override in the drawer; overrides persist immutably in manual_fields.json.

notification

80% SLA breach warning DMs

The breach_warner script DMs the assigned PM when a ticket crosses 80% of its First Update target. One DM per ticket per clock — idempotent via a dedup ledger.

Ticket workflow

core

Resolved → close toggle (internal tickets)

Each internal ticket has a one-click “Resolved — close ticket” checkbox. Flipping it sets status to Closed, stamps a closing timestamp, and adds a resolution note to the audit trail.

core

csm_handled (PM response not required)

A drawer toggle for tickets where the CSM resolved without needing the PM. Hides the ticket from the PM scorecard so accountability metrics reflect actual PM work, not random small things the CSM closed.

core

Awaiting-state derivation

The state_machine.py module reads the comment thread and computes whether the ticket is awaiting_internal (us) or awaiting_customer (them). Drives the auto-pause logic.

core

RCA / resolution-notes thread

Each ticket has an in-drawer notes thread for findings, hypotheses, and the final RCA. Stamped with author + timestamp, persisted to manual_fields.json, and surfaced in the daily digest.

core

Related links + attachments

Each ticket can carry related links (Jira sister tickets, Confluence runbooks, dashboards) and attachments (screenshots, log files). Inline edit; multi-line entry.

core

Immutable manual fields

PM, ETA, action remarks, RCA, sla_pause all live in manual_fields.json and merge spread-style on every write. Prevents the “PM update wiped my pause” class of bug.

Scorecard & reporting

core

Per-PM scorecard with bar charts

Tickets handled, breached, % First Response Met, % Resolution Met, MTTR, reopens, reopen rate. Bar charts below for open tickets / FR met / Resolution met per PM.

core

Per-Client & per-Priority groupings

Same metrics regrouped — one switch flips between PM / Client / Priority views. Exposes account-level hotspots and SLA-tier honesty in seconds.

core

Time-window scoping

7d / 30d / 90d / Lifetime / Custom (date-range picker). Custom range re-aggregates lifetime rows client-side; presets read straight from scorecard.json.

core

Workspace scope

Jira-only / Internal-only / Both combined. PMs default to their workspace; the “Both” mode is for delivery review where total throughput matters.

automation

Weekly per-PM scorecard auto-post

Every Thursday build_pm_scorecard.py assembles the 7-day per-PM scorecard and posts it to the leadership channel. Same view PMs can pull on demand.

automation

csm_handled tickets auto-excluded from PM metrics

The scorecard pipeline filters out tickets flagged csm_handled before aggregating PM-side metrics. PMs aren’t penalized for tickets the CSM closed without their involvement.

UX

Row drill-down

Click any row in the scorecard table to expand a ticket-by-ticket list with the outcome chip per ticket. Links straight into the ticket drawer.

UX

CSV export

Every scorecard view exports to CSV with one click for downstream Excel / Power BI / Google Sheets work.

AI-assisted triage

AI

“What happened” summary

Each ticket gets a Claude-generated plain-English summary of the issue, regenerated whenever new comments land. Lives in the drawer header so PMs can triage in 10 seconds without reading the full thread.

AI

“Recommended next step”

Below the summary, Claude proposes the next action — “PM to confirm settlement amount; CSM to close the loop” or similar. Saves the “OK what do I do now” pause.

AI

Carry-forward thread context

The fetch_thread_context.py pipeline pulls PM replies from Slack threads back into the daily digest so context carries from yesterday to today.

AI

Regenerate summary on demand

A button in the drawer kicks off a fresh summary if the existing one is stale or wrong — useful after big comment-thread updates.

Slack & notifications

notification

Daily digest at 2 PM IST

Cron fires build_digest.py + post_digest.py. One top-level Slack message per open ticket; intro paragraph first; per-ticket replies thread underneath.

notification

Per-ticket sub-threading

Each digest ticket lands as its own top-level message; reply with PM updates, RCA, “we’re on it”. The next digest carries the latest reply forward.

notification

Silent-thread follow-up nudges

followup_poker.py looks for tickets with no PM reply in 24h+ and nudges the assignee in DM. Idempotent — one nudge per clock per ticket.

notification

New-ticket alerts with dedup ledger

Every newly-synced ticket gets a one-shot Slack alert. A persistent ledger (slack_alerted_tickets.json) prevents duplicate alerts if sync runs twice.

UX

Per-ticket Slack post from drawer

Drawer button posts a status update straight into the ticket’s Slack thread without opening Slack. PM mentions tag the right person automatically via PM_SLACK_IDS.

UX

Bot-message delete from drawer

If a digest message was wrong, one-click delete from the dashboard removes it from Slack via chat.delete. No alt-tabbing.

Filters, search & UX

UX

Filter by client / PM / priority / RAG status

Four orthogonal dropdowns + sidebar pills. Compound filters supported (“Atlas, P1, breached” in three clicks). State persists in localStorage.

UX

Search by ID / client / summary

Top-bar search box. Press / from anywhere to focus. Matches across ticket ID, client, and summary text.

UX

Sortable columns

Click any column header to sort by it. Default: created date descending. Sticky across sessions.

UX

Pagination (25 per page)

Standard pager. Reset on every filter change so the user lands on page 1 of the filtered set.

UX

Sticky filter state

Tab, sub-tab, all filters, sort column, sort direction, scorecard window, scorecard grouping all persist per browser. Open the dashboard tomorrow and you’re where you left off.

UX

Live-sync indicator

Top-right shows when the dashboard last successfully synced. Hover for the exact timestamp + the source freshness (data.json / internal_tickets.json / scorecard.json) per stream.

Automation & infrastructure

infra

30-min sync during business hours

A GitHub Actions workflow runs cloud_sync.py every 30 min from 14:00 to 02:00 IST on weekdays. Off-hours and weekends: 60 min cadence.

infra

One-click manual “Sync now”

Top-right button in the dashboard fires a workflow_dispatch via the Cloudflare Worker proxy. New data lands in ~30 seconds.

infra

JSON-as-database

All state lives in JSON files committed back to a GitHub repo — no DB. Every sync does atomic git pull --rebase -X theirs + write to avoid the 30-min cron silently dying on conflicts.

infra

Cloudflare Worker write-proxy

Browser doesn’t need a GitHub PAT — writes go through a Worker that validates a shared secret then commits via the GitHub Contents API. PAT only lives server-side.

infra

Quota monitor across 5 free tiers

Cron pulls quota usage from GitHub Actions, Cloudflare Workers, Cloudflare Pages, Resend, and Sarvam. Email + Slack alert at 80% to prevent silent outages.

infra

[skip ci] on auto-commits

Sync-generated commits include [skip ci] in the message so Cloudflare Pages doesn’t fire a deploy every 30 minutes and burn the free-tier build cap.

infra

$0/month total cost

Everything fits in free tiers: GitHub Actions (2000 min/mo), Cloudflare Workers (100K req/day), Cloudflare Pages (500 builds/mo), Resend (3K mails/mo), GitHub Pages (1GB).

Auth & security

auth

Per-user email-OTP gate

Visiting any path redirects to an email-entry login. OTP via Resend / Gmail OAuth, 10-min TTL, HMAC-signed 24h session cookie. Rate-limited (5 attempts) and the “does this email exist” lookup leaks nothing.

auth

Email allowlist by domain

Only specific approved emails can ever sign in — even if the team password leaks. Domain-level allowlists for the broader org plus per-user approval for cross-org guests.

auth

Admin approval flow + login notifications

New-user sign-ins fire a Slack DM to the admin with an Approve / Deny pair. Until approved, new users hit a holding page. Successful logins also DM the admin so credential takeover is visible.

auth

One-click sign-in links

For frequently-active whitelisted users, the system can mint single-use magic-link sign-ins delivered via email instead of OTP. 10-minute window; one redemption.

auth

HMAC-signed session cookies

Sessions are JWT-style HMAC-signed (COOKIE_SECRET, 32+ chars). Tampering invalidates. Cookie expires 24h after issue.

auth

Worker-side admin gate

Any write call to the Worker re-checks the email is on the admin list before committing. Even a user with a valid session can’t escalate via direct API call.

What it’s built with

Frontend
Single-page web app, vanilla JavaScript (no framework), Chart.js for analytics. ~10K LOC in one HTML file. Optimized for the “one CS lead + one PM scrolling on a laptop” UX.
Backend automation
Python: cloud_sync.py (ticketing sync), sla_tracker.py (business-hours SLA math), state_machine.py (awaiting-state derivation), build_digest.py + post_digest.py (Slack digest), fetch_thread_context.py (carry-forward PM replies), followup_poker.py (silent-thread nudges), breach_warner.py (80% SLA DMs), build_pm_scorecard.py (weekly scorecard).
Edge + cron
Cloudflare Worker handles scheduled triggers (cron), proxies dashboard writes back to GitHub, and routes Slack events. GitHub Actions runs the long-running sync pipelines on a free-tier schedule.
Storage
Plain JSON files committed to a GitHub repo — data.json, internal_tickets.json, manual_fields.json, scorecard.json, sla_events.json, slack_alerted_tickets.json, client_pm_defaults.json. No database; atomic git pull --rebase -X theirs on every write.
Auth
Per-user email-OTP on the Cloudflare Pages deployment. Resend / Gmail OAuth for the OTP send. KV store for OTP TTLs and admin allowlist. HMAC-signed session cookies.
Notifications
Slack chat.postMessage with retry-on-thread (per-ticket sub-threading for replies), chat.delete for cleanup, DMs for breach warnings and follow-up nudges. Email via Resend (transactional) and Gmail OAuth (production sends).
AI
Claude (Anthropic) generates the per-ticket “What happened” summary and “Recommended next step” from the comment thread. Same model + pair-programmer role used to build the system.
Cost
$0/month. Fits entirely within Cloudflare Workers free tier, Cloudflare Pages free tier, GitHub Actions 2000-min/mo free tier, Resend 3K-mails/mo free tier, GitHub Pages 1GB free tier. Quota monitor pings before any cap.