Technical Handbook

How this dashboard works — and how to fix & improve it

Written for someone brand new to all of this. You don't need a computer-science background or years of experience — you need curiosity and a bit of care. Read it top to bottom once and you'll get how the whole thing fits together; come back to any section when you need it. Nothing here is as complicated as the words make it sound.

New to all this? Start here. Three things will make everything below click:
  • You will not be writing code by hand. You'll mostly ask an AI assistant (Claude Code) to do things in plain English, and check its work.
  • You almost can't break anything permanently. Every change is saved with a full history, so anything can be undone. That means you're free to try things.
  • When a word looks scary (commit, push, deploy, API), it has a simple meaning — they're all explained the first time they appear. You don't need to memorize anything.
Take your time. If you get stuck, the answer is usually "ask Claude Code to explain it" or "ask Janet."

1. The big picture

This is a reporting dashboard for a marketing agency that runs ad campaigns for private K–12 schools. Every two weeks each school gets a performance report. The dashboard's job: pull the ad numbers automatically, draft the report with AI, let a human review & send it, and keep a permanent record.

Three pieces make it work, and you can think of them like this:

  • GitHub = the filing cabinet. It stores all the code and all the data (each school's numbers live in a plain text file).
  • Vercel = the engine room. It takes what's in GitHub and runs it as a live website, and runs the daily automation.
  • Claude Code = your assistant. It's how you (or a developer) make changes to the code by typing requests in plain English.

The live site is reports.schoolstorytellers.com. There is no separate database, no server to manage — it's all files in GitHub, served by Vercel.

2. The three tools, in plain terms

Claude Code

Claude Code is an AI assistant for working on code. The easiest way to use it is the Claude desktop app (Mac or Windows) — you open this project in it and chat with it like a normal conversation. You describe what you want — "the AI summaries aren't loading, can you fix it" — and it reads the code, finds the problem, makes the change, and can even test it. You don't have to know how to program; you have to be able to describe the problem clearly and review what it did.

How you'll actually use it: open the Claude app, point it at this project, and type your request in plain English. It shows you each change before or after making it. When you're happy, you tell it to save (commit) and publish (push) — in your own words, you don't type any commands yourself. It explains everything as it goes.

(There's also a terminal/command-line version of Claude Code for developers who prefer it — but the desktop app does everything you'll need and is far simpler to start with. Download it from claude.com/product/claude-code.)

One problem per chat — and name the chat. Start a fresh Claude Code conversation for each separate task, and give it a clear name (e.g. "Fix Quaker GA4 import", "Lauralton Meta backfill"). Two reasons: a chat has a limited memory, so a long one that drifts across many problems eventually runs out and gets confused; and a named, single-topic chat is easy to come back to and easy for someone else to follow. When you finish a task — or it starts feeling long or muddled — close it and open a new one for the next thing. You lose nothing by starting fresh; all the actual work is saved in GitHub, not in the chat.

Good requests are specific: "On Sacred Heart's report, the Google Ads box shows 3 leads but the summary says 0 — can you find why and fix it?" beats "the dashboard is broken." If something was working yesterday and isn't today, say that — the history is recoverable.

GitHub

GitHub stores the project as a set of files and remembers every version of every file, forever. Every time the dashboard saves a report, generates a draft, or the daily automation runs, it writes a new version to GitHub. That history is your safety net: nothing is ever truly lost, and any past state can be restored.

TermPlain meaning
Repository (repo)The project folder. Ours is janet-sm/schoolstorytellersdashboard.
CommitOne saved change, with a note describing it. Like a save point in a game.
PushSending your saved changes up to GitHub so they go live.
mainThe one official version everything runs from.

You can browse all of this at github.com/janet-sm/schoolstorytellersdashboard — click any file to read it, or the "History" button to see every change.

Vercel

Vercel watches the GitHub repo. The moment anything is pushed to main, Vercel rebuilds and publishes the live site automatically — usually within a minute. You never manually "deploy"; pushing to GitHub is deploying.

Vercel also runs two things that plain files can't:

The Vercel project is in the agency's Vercel account (team self-made1). That's also where the secrets (API keys) are stored — see §8. The custom domain reports.schoolstorytellers.com points here.

3. How data flows (the pipeline)

This is the heart of the system. Once a day (and on demand via the "Run pipeline now" button), this sequence runs for every active school:

1. Pull → reads Google Ads, Meta, and GA4 numbers straight from each platform's API (with the school's Coefficient Google Sheet kept as an automatic backup)
2. Calculate → totals the spend, leads/clicks, cost-per-lead, and all the period-over-period comparisons
3. Refresh → writes those numbers into the school's data file in GitHub
4. Roll over → when a 2-week period ends, it snapshots the report, archives it, and starts the next period
5. Draft → asks Claude (the AI) to write the report summary, channel notes, next steps, and client email
6. Human step → a person verifies form fills, edits the draft, and sends it; then clicks "Mark sent"

As of the API cutover (Phase 6), the dashboard pulls each channel directly from the platform's own API — Google Ads, Meta, and GA4 — for all four active schools. Coefficient is still there as an automatic safety net: if an API call fails, that channel silently falls back to the school's Google Sheet for that run, so a number is never lost. The system tells you which source actually served each channel and warns when a credential has a problem (see per-channel health & token alerts below). GA4 is live on the API for Lauralton and Maclay; Sacred Heart hides website traffic, and Quaker still reads GA4 from its sheet until its API access is granted.

4. How each school works

Every school is one file: schools/<slug>.json (a "slug" is the short name in the URL, e.g. maclay-school). The file holds everything — the school's info, which Google Sheet to read, the current period, all the numbers, the AI text, and the full history of past reports.

SchoolTypeJudged onNotes
Sacred Heart ChicagoLeadCost per lead (CPL)Verify form fills (login in LastPass)
Maclay SchoolLeadCost per lead (CPL)Verify form fills; has a Loom walkthrough
Lauralton HallTrafficCost per click (CPC)No form fills
The Quaker SchoolTrafficCost per click (CPC)No form fills; GA4 daily tab not yet in its sheet
Tandem FriendsArchivedDone. Excluded from everything.
Sample Prep SchoolTemplateDemo report for prospects + onboarding template

Key rules baked into the system

Where the numbers come from — the APIs (with Coefficient as backup)

For each channel the dashboard tries the platform's own API first, and only falls back to the Google Sheet if that fails. So the chain per channel is:

Platform API (Google Ads / Meta / GA4)  →  if it fails →   the school's Google Sheet (filled by Coefficient)  →  the dashboard

Which source a school uses per channel is set in its data file under school.sources (e.g. "meta_ads": ["meta-api"]); Coefficient is always appended as the final fallback, so a misconfigured or down API can never lose data. The API credentials live in Vercel environment variables (not the repo) — Janet owns those.

Coefficient still matters for two reasons: it's the automatic fallback, and it's how Quaker's GA4 is still read (until its API access lands). It's a Google Sheets add-on you set up inside the sheet: connect a data source, choose fields, and it creates an auto-refreshing "import" tab. The PM can do this — point-and-click, no code. The catch is the dashboard expects specific tab names and column names; if they don't match, that channel silently shows nothing (this is why Quaker's GA4 is blank — its sheet has no daily GA4 import tab).

The exact setup the dashboard expects. Don't worry about memorizing this — it's a reference card for when you (or Claude Code) set up a sheet. Each school's sheet needs up to three "import tabs." The tab's name gets recorded in the school's data file; the column names have to match exactly (Coefficient lets you rename columns while you build the import). If a name is off, that channel just shows nothing — no error — which is the #1 thing to check when a number is missing.
ChannelTab (any name, recorded in the JSON)Required columns
Google Adse.g. Google AdsDate, Impressions, Clicks, Cost Micros (this holds daily $ spend), Conversions (= leads). Optional Campaign Name for the per-campaign table.
Meta (FB/IG)e.g. FB AdsDate, Impressions, Link clicks, Amount spent, Leads. Optional Campaign name / Ad name.
GA4 (website)GA4: Total users by DateDate, Sessions, Landing page (one row per date × landing page; the dashboard sums them for the period).

Two Coefficient jobs you can do yourself

These are both done inside Coefficient in the Google Sheet (login in LastPass) — no code. Open the sheet, open the Coefficient sidebar, and you're editing the imports.

1. Add Quaker's missing GA4 import

Quaker's report shows no website-sessions number because its sheet never had a daily GA4 import. Fix it once:

  1. Open Quaker's Google Sheet → Extensions → Coefficient.
  2. Create a new import from Google Analytics (GA4), connected to Quaker's GA4 property.
  3. Set it up "by date" with these fields: Date, Sessions, and Landing page (so there's one row per date × landing page).
  4. Name the import tab exactly GA4: Total users by Date (the dashboard looks for that exact name — the other schools use it too).
  5. Set it to auto-refresh daily, run it once, and you're done. The sessions number appears on Quaker's report after the next pipeline run.

2. Fix the Lauralton Meta sync (a failed/stalled import)

When the dashboard shows an amber "Meta data behind" flag on a school, its Coefficient Meta import has stopped updating. To fix it you backfill the gap, then let the daily schedule take over again:

  1. Open Lauralton's Google Sheet → the Meta Ads import → Coefficient sidebar.
  2. Look at the last date already in the tab (the most recent row). That's where the data stopped.
  3. Re-run the import for the gap: set its date range to start from that last date and end at the day before yesterday (not today, not yesterday). Run it. This fills in every missing day up to the point where the normal daily refresh picks up.
  4. Make sure the import's scheduled daily refresh is turned back on — once the gap is filled, the regular schedule keeps it current from there.
  5. Back on the dashboard, click "Run pipeline now"; the amber flag clears once the sheet is caught up.
Why "the day before yesterday," not today? Ad platforms take a day or two to finalize each day's numbers. If you pull right up to today, you'd grab incomplete figures. Ending the backfill one day before yesterday leaves the most recent (still-settling) days to the normal daily sync, which re-pulls them once they're final — so the numbers stay accurate and the schedule stays smooth.

Onboarding a new school

The easiest way is the “+ Add school” button at the top of the dashboard — no files, no code. It opens a short form (name → auto-slug, the campaign objective — which picks the lead/traffic template and stamps the campaign label — first-period dates, location, and a row per inquiry form with its landing-page URL), builds schools/<slug>.json correctly, and writes it for you. It then shows a “Hand to Janet” card with the one step it can't do.

That handoff is the only manual part: Janet grants the ad-account / GA4 read access and adds the account IDs to the Vercel env maps (GOOGLE_ADS_CUSTOMER_IDS, META_AD_ACCOUNTS, GA4_PROPERTY_IDS), each keyed by the new slug. Until then the school shows empty stats — the pipeline fills them on its next run. (The form deliberately can't do this: API credentials are account-level and stay with Janet.)

The per-form landing-page URLs you enter power that school's Landing Page Conversion funnel automatically, once its GA4 is connected. Forms can be added or retired any time from the report itself.

Advanced: the command-line scaffolder

The button calls the same builder as the CLI. To script it (or hand it to Claude Code — "onboard a new school called X, lead, first period …"): node scripts/new-school.mjs --slug=… --name="…" --type=lead|traffic --start=… --end=… --form="Form name|https://…/lp-path" (--form repeatable). Then refresh the registry (node scripts/registry.mjs) and commit. A Coefficient sheet is optional now (APIs are primary) — wire one only if you want the backup, via --sheet/--*-tab.

Where each source stands now

The direct-API path is built and live — it's the primary source. Coefficient has moved from "the source" to "the backup + onboarding helper." Status today:

ApproachStatus
Google Ads API directLive, primary for all 4 schools. OAuth + developer token; credentials in the GOOGLE_ADS_* Vercel secrets.
Meta API directLive, primary for all 4 schools. Needs each school's ad-account ID in META_AD_ACCOUNTS. (The "Pull live from Meta API" button still exists as a manual one-off.)
GA4 API directLive for Lauralton & Maclay. Service account with Viewer on each GA4 property + property IDs in GA4_PROPERTY_IDS. Quaker is pending its access grant; Sacred Heart hides traffic.
Coefficient (backup)The automatic fallback when an API call fails, and still how Quaker's GA4 is read. The PM can set up / fix imports without code.
The plan (Phase 6 cutover): once a full reporting cycle confirms the APIs match the sheets (the drift check), and Janet signs off, the Coefficient dependency and subscription get retired. Until that sign-off, leave Coefficient running — it's the safety net. Don't cancel it.

Per-channel health & token alerts

Because the dashboard now depends on API credentials, it watches each channel and tells you when one needs attention — so an expired token can't quietly rot behind the sheet fallback.

Drift check (proving the APIs match the sheets). Before retiring Coefficient, run the parity check: open /api/drift-check?key=sst-drift-check (add &start=YYYY-MM-DD&end=YYYY-MM-DD for a settled past period) — it reads each channel from both the API and the sheet and reports any metric that differs by more than 2%. All-green across a full cycle is the evidence for Janet's cutover sign-off. (Developers can run the same thing locally with node scripts/api-drift.mjs if the credentials are in their shell.)

5. What's in the project

Folder / fileWhat it is
dashboards/internal.htmlThe home view staff see — the school cards, schedule, content engine, guide.
dashboards/client.htmlThe actual report page (both the staff-editable and client versions are this one file).
dashboards/onboarding.htmlThe PM onboarding guide (/onboarding).
dashboards/tech-handbook.htmlThis page (/handbook).
schools/*.jsonOne file per school — all data and history.
schools/snapshots/*.jsonFrozen copies of each finished period (the "View →" links).
content/content-log.jsonClient questions & blog ideas logged from the home view.
api/pipeline-run.jsThe daily pipeline (the §3 sequence). The most important file.
api/generate-draft.jsCalls Claude to write the report draft.
api/meta-refresh.jsThe "Pull live from Meta API" backup.
api/save.js, advance-period.js, content-log.jsSave edits, roll a period forward, log content ideas.
vercel.jsonRouting (which URL goes where) + the cron schedule.

6. Getting set up, then making a change

Most fixes need no setup at all

Before anything else: a lot of "the dashboard isn't working right" fixes happen right in the dashboard in your browser — no code, no Git, no setup. Re-running the pipeline, editing a draft, fixing a form-fill number, publishing a report, spotting a "data behind" flag. Do those there first. You only touch the code when something needs to change in how the dashboard itself works.

One-time setup (for actual code/data changes)

Your question: clone a local copy, or work directly on GitHub? Both exist, and here's the honest answer:

The first-time clone, step by step (you do this once):

  1. Get added to the repo. Ask Janet to add your GitHub account to janet-sm/schoolstorytellersdashboard.
  2. Sign in to GitHub on your computer. Easiest: install the GitHub CLI and run gh auth login (it walks you through signing in via the browser). This is what lets your computer push changes up.
  3. Let Claude Code do the clone. Open the Claude desktop app and tell it: "Clone the GitHub repo janet-sm/schoolstorytellersdashboard to my computer and open it." It runs the clone for you and opens the project. (If you'd rather click buttons, GitHub Desktop has a "Clone a repository" button that does the same thing — then open that folder in Claude Code.)
  4. That's it. From now on you just open that folder in Claude Code and start asking for changes.
Think of it like this: clone = download a working copy once. commit = save a change with a note. push = send your saved changes up to GitHub (which makes them go live). Claude Code does the commit and push for you — you just say "save and publish."

The everyday loop

The normal loop, whether you do it yourself or ask Claude Code to:

  1. Describe the change to Claude Code in plain English.
  2. Review what it did — it shows the edits and usually tests them in a preview.
  3. Save & publish — it commits to GitHub and pushes; Vercel republishes in ~1 minute.
  4. Check the live site to confirm.
Nothing you do is permanent-bad. Every change is a commit in GitHub. If a change makes things worse, tell Claude Code "undo the last change" or "go back to how this looked yesterday" — the history makes it recoverable. When in doubt, ask before pushing.
One real gotcha: the dashboard writes data to GitHub on its own (every save, every pipeline run). So your local copy can fall behind. Before editing data files, pull the latest. Claude Code handles this for you, but it's the one thing that causes confusing "my change disappeared" moments.

7. Troubleshooting the common stuff

AI summaries are blank or won't generate

The draft generator calls the Claude API with a specific model name. If Anthropic retires that model, every draft fails with a "model not found" error and summaries go blank. This exact thing happened — the code used claude-sonnet-4-20250514, which was retired. The fix was a one-line change in api/generate-draft.js to a current model (claude-sonnet-4-6). If summaries break again after working fine, this is the first thing to check: ask Claude Code to "check the model name in generate-draft.js against the current Claude models." Also confirm the ANTHROPIC_API_KEY secret is still set in Vercel.

A report's numbers look wrong or stale

The numbers come from the Coefficient Google Sheet. If they're behind, the report shows an amber "the Coefficient sheet only has data through [date]" note. Fix: re-run the Coefficient import on that sheet. For Meta specifically, click "Pull live from Meta API" for an instant refresh. Then click "Run pipeline now" on the home view to recompute.

The numbers don't match up — and how you fix it yourself

This is one you can and should fix without any code — it's all in the dashboard. "Don't match up" is usually one of these:

What you're seeingWhat to do (in the dashboard)
Form fills / leads are wrong, so the CPL is wrong. The number of inquiries doesn't match what you counted in the school's form dashboard.Form fills are the one number entered by hand, and they drive the CPL. On the current report, fix it in the Inquiry Reconciliation box. For a past report, open it (Past reports → "View →"), update the form fills there, and the CPL/status recompute and save automatically. The blended number always uses your verified form fills, not the platform's leads.
The report's numbers don't match what you see in Google/Meta today.The report now pulls from the platform APIs, so it should match closely (ad platforms still finalize the last 1–2 days, so tiny recent differences are normal). Click "Run pipeline now" to recompute. If a school shows a red "token expired" flag, the API couldn't be read and it's on the sheet fallback — text Janet to re-authenticate. To confirm API-vs-sheet parity, run the drift check.
The channel total and its campaign table disagree (e.g. the Google box says one spend, the campaign row another).This is a stale-data symptom too — the pipeline rebuilds the campaign table each run. Re-sync the sheet and "Run pipeline now." If they still disagree after a fresh run, the math itself may be off — that's a "tell Claude Code or Janet" case, not something to hand-edit.
The written summary disagrees with the numbers.Once someone edits a summary, that text is frozen — if the numbers move afterward, the words don't auto-update. Just re-edit the summary text on the report. (The AI draft regenerates with fresh numbers; a human-edited version is kept on purpose.)
Rule of thumb: if a number is wrong because of form fills or stale data, that's yours to fix right in the dashboard — edit the form fills, or re-sync + re-run the pipeline. If the numbers still don't add up after a clean refresh (the calculation looks broken), that's when you loop in Claude Code or Janet. You won't break anything by re-running the pipeline or editing form fills — both are safe to try.

A school shows in the wrong week

The home view buckets schools by their next due date. A report that's been marked sent is "done" and jumps to its next two-week cycle. If something looks off, confirm the school's current_period dates and report_status — Claude Code can read these out for you.

"My change disappeared" / data reverted

Almost always the local-copy-behind issue above, or the daily pipeline overwriting a manual change with sheet data. Past versions are in GitHub history — ask Claude Code to restore the version from a specific time.

A page won't load / shows an error

Check the deploy in Vercel (was the last push successful?) and the browser console for errors. Ask Claude Code to "start the preview and check the console for errors on [page]."

8. Secrets & access

The API functions need a few secret keys, stored as environment variables in Vercel (Project → Settings → Environment Variables). They are never in the code or GitHub.

SecretWhat it's for
GH_TOKENLets the site read & write the school data files in GitHub.
GH_REPOWhich repo to write to (janet-sm/schoolstorytellersdashboard).
ANTHROPIC_API_KEYLets the site call Claude to write report drafts.
META_ACCESS_TOKENReads Meta ad data (now the primary Meta source, not just the manual button).
META_AD_ACCOUNTSMaps each school to its Meta ad account ID.
GOOGLE_ADS_*The Google Ads API credentials — developer token, OAuth client ID/secret + refresh token, MCC login-customer-id, and per-school customer IDs. Primary Google source for all schools.
GA4_SERVICE_ACCOUNT_KEY / GA4_PROPERTY_IDSThe GA4 service-account key (base64 JSON) and the slug→property-ID map. Primary website-traffic source where a school is on the GA4 API.
DRIFT_CHECK_KEY (optional)The key for /api/drift-check. If unset it defaults to sst-drift-check.

Token expiry: these API credentials can expire or be revoked. When that happens the home view shows a red "<API> token expired — re-authenticate (Janet)" flag and the run summary lists it under alerts. Refreshing a credential is account-level work — Janet's job, not the PM's.

Logins you'll want, and where to get them (ask Janet — hello@jgstudio.co — for any you're missing):

LoginForWhere it is
CoefficientSetting up / fixing the data syncs in each school's sheetShared in LastPass
LastPassHolds the Coefficient login + each school's form-dashboard passwordSign up free, Janet shares passwords to your account
GitHubBrowsing the code/data and its historyAsk Janet to add you to the repo
VercelSeeing deploys, logs, and the secret keysAsk Janet to add you to the self-made1 team
AnthropicThe Claude API key + AI billingAsk Janet

9. Where to take the automation next

Monitoring agents (built)

Three scheduled Claude agents now watch the dashboard so problems surface on their own. They run automatically in Claude Code (Scheduled section in the sidebar) and are read-only — they report, they don't change anything, matching the "alerts, never auto-act" rule. The daily pipeline still does the actual data refresh; these are the judgment layer on top of it.

AgentWhenWhat it flags
Health sweepDaily, ~morning (after the 12:00 UTC pipeline)Expired tokens, stale channels, overdue reports, narrative-vs-numbers drift, missing inquiry counts — grouped “Needs Janet / PM action / Clear”.
Config & drift auditWeekly (Monday)Config errors (the registry validator) and API-vs-sheet divergence (the drift check) — catches slow-burn problems before a client report.
Pre-send QADaily, afternoonLooks ahead: any report due within 2 days that isn't ready (counts entered, narrative fresh, no data-behind flags, email drafted), with who fixes each gap.

To change one, edit it from the Scheduled sidebar (or its SKILL.md). Natural next step: once trusted, let the health sweep auto-fix the reversible things (e.g. regenerate a stale not-yet-sent draft) while still only flagging the irreversible ones. Wiring a digest to Slack/text is a "later if it helps" add-on.

Other realistic next steps, in rough order of value:

Who does what. The PM's job here is to spot and fix the small stuff when the dashboard isn't behaving — a stale sheet to re-sync, a number that looks off, re-running the pipeline, fixing a typo, editing a draft. Anything that needs account-level setup or new credentials (like Google Ads API authentication) is Janet's. When unsure which bucket something is in, text Janet — that's the right call, not a bother.
The way to do any of these: open Claude Code in the project and describe the goal. It can scope the work, tell you what access/keys are needed, build it, and test it. Start small, review each step, and push often.
The one-paragraph summary: The data and code live in GitHub. Vercel turns that into the live site and runs a daily job that pulls ad numbers and drafts reports with AI. You change things by asking Claude Code in plain English, then saving (commit) and publishing (push) — Vercel redeploys in a minute. Every change is reversible because GitHub keeps all history. The humans' job is to verify form fills, polish the AI draft, and send.