- 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.
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.
(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.)
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.
| Term | Plain meaning |
|---|---|
| Repository (repo) | The project folder. Ours is janet-sm/schoolstorytellersdashboard. |
| Commit | One saved change, with a note describing it. Like a save point in a game. |
| Push | Sending your saved changes up to GitHub so they go live. |
| main | The 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 API endpoints (the
/api/...functions) — small programs that pull ad data, generate AI drafts, and save changes back to GitHub. - The daily cron job — an alarm clock that runs the whole pipeline once a day at
12:00 UTC(~8am ET) with no human involved.
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:
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.
| School | Type | Judged on | Notes |
|---|---|---|---|
| Sacred Heart Chicago | Lead | Cost per lead (CPL) | Verify form fills (login in LastPass) |
| Maclay School | Lead | Cost per lead (CPL) | Verify form fills; has a Loom walkthrough |
| Lauralton Hall | Traffic | Cost per click (CPC) | No form fills |
| The Quaker School | Traffic | Cost per click (CPC) | No form fills; GA4 daily tab not yet in its sheet |
| Tandem Friends | Archived | — | Done. Excluded from everything. |
| Sample Prep School | Template | — | Demo report for prospects + onboarding template |
Key rules baked into the system
- Lead schools are scored on CPL (strong <$150, alert >$300). Traffic schools on CPC (strong <$0.50, alert >$2).
- Verified form fills are the source of truth for lead schools — the blended CPL uses spend ÷ form fills, not the platform's reported leads. Form fills are the one number entered by hand.
- Periods run Monday–Sunday, two weeks each, staggered so you're never writing all four reports in the same week.
- Internal vs. client view: staff see every period (even unsent ones) plus action buttons; clients only see reports that have been published. The
?view=clientURL or any/report/...link is the client view.
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:
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).
| Channel | Tab (any name, recorded in the JSON) | Required columns |
|---|---|---|
| Google Ads | e.g. Google Ads | Date, Impressions, Clicks, Cost Micros (this holds daily $ spend), Conversions (= leads). Optional Campaign Name for the per-campaign table. |
| Meta (FB/IG) | e.g. FB Ads | Date, Impressions, Link clicks, Amount spent, Leads. Optional Campaign name / Ad name. |
| GA4 (website) | GA4: Total users by Date | Date, 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:
- Open Quaker's Google Sheet → Extensions → Coefficient.
- Create a new import from Google Analytics (GA4), connected to Quaker's GA4 property.
- Set it up "by date" with these fields: Date, Sessions, and Landing page (so there's one row per date × landing page).
- Name the import tab exactly
GA4: Total users by Date(the dashboard looks for that exact name — the other schools use it too). - 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:
- Open Lauralton's Google Sheet → the Meta Ads import → Coefficient sidebar.
- Look at the last date already in the tab (the most recent row). That's where the data stopped.
- 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.
- 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.
- Back on the dashboard, click "Run pipeline now"; the amber flag clears once the sheet is caught up.
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:
| Approach | Status |
|---|---|
| Google Ads API direct | Live, primary for all 4 schools. OAuth + developer token; credentials in the GOOGLE_ADS_* Vercel secrets. |
| Meta API direct | Live, 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 direct | Live 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. |
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.
- Automatic retries. A momentary network blip or rate-limit is retried a few times before anything falls back — so brief hiccups don't even show up.
- 🗝 Token expired (red flag on the school card). When a platform rejects the credential (an expired/revoked token), the home view shows "<API> token expired … — re-authenticate (Janet)". It notes whether the sheet fallback covered the numbers ("showing Coefficient data") or not ("no data"). Re-authenticating is Janet's job (it's account-level credentials) — the PM's step is to text Janet.
- Amber "data behind" / "fell back" flags still work as before for stale sheets and non-credential API hiccups.
- The pipeline run summary lists any token problems under
alerts, so they're visible in the run output too.
/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 / file | What it is |
|---|---|
dashboards/internal.html | The home view staff see — the school cards, schedule, content engine, guide. |
dashboards/client.html | The actual report page (both the staff-editable and client versions are this one file). |
dashboards/onboarding.html | The PM onboarding guide (/onboarding). |
dashboards/tech-handbook.html | This page (/handbook). |
schools/*.json | One file per school — all data and history. |
schools/snapshots/*.json | Frozen copies of each finished period (the "View →" links). |
content/content-log.json | Client questions & blog ideas logged from the home view. |
api/pipeline-run.js | The daily pipeline (the §3 sequence). The most important file. |
api/generate-draft.js | Calls Claude to write the report draft. |
api/meta-refresh.js | The "Pull live from Meta API" backup. |
api/save.js, advance-period.js, content-log.js | Save edits, roll a period forward, log content ideas. |
vercel.json | Routing (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 normal way is a local copy (a "clone"). Claude Code works on a copy of the project that lives on your computer. You clone it once, then work from that folder from then on. This is what you'll do for anything real, because Claude Code needs the files in front of it to read, edit, and test them.
- You can edit directly on GitHub's website for a tiny one-off — open the file on github.com, click the pencil ✏️, change a word, click "Commit changes." It deploys the same way. Good for a typo when you're not at your computer, but you don't get Claude Code's help, so it's the exception, not the routine.
The first-time clone, step by step (you do this once):
- Get added to the repo. Ask Janet to add your GitHub account to
janet-sm/schoolstorytellersdashboard. - 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. - 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.)
- That's it. From now on you just open that folder in Claude Code and start asking for changes.
The everyday loop
The normal loop, whether you do it yourself or ask Claude Code to:
- Describe the change to Claude Code in plain English.
- Review what it did — it shows the edits and usually tests them in a preview.
- Save & publish — it commits to GitHub and pushes; Vercel republishes in ~1 minute.
- Check the live site to confirm.
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 seeing | What 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.) |
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.
| Secret | What it's for |
|---|---|
GH_TOKEN | Lets the site read & write the school data files in GitHub. |
GH_REPO | Which repo to write to (janet-sm/schoolstorytellersdashboard). |
ANTHROPIC_API_KEY | Lets the site call Claude to write report drafts. |
META_ACCESS_TOKEN | Reads Meta ad data (now the primary Meta source, not just the manual button). |
META_AD_ACCOUNTS | Maps 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_IDS | The 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):
| Login | For | Where it is |
|---|---|---|
| Coefficient | Setting up / fixing the data syncs in each school's sheet | Shared in LastPass |
| LastPass | Holds the Coefficient login + each school's form-dashboard password | Sign up free, Janet shares passwords to your account |
| GitHub | Browsing the code/data and its history | Ask Janet to add you to the repo |
| Vercel | Seeing deploys, logs, and the secret keys | Ask Janet to add you to the self-made1 team |
| Anthropic | The Claude API key + AI billing | Ask 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.
| Agent | When | What it flags |
|---|---|---|
| Health sweep | Daily, ~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 audit | Weekly (Monday) | Config errors (the registry validator) and API-vs-sheet divergence (the drift check) — catches slow-burn problems before a client report. |
| Pre-send QA | Daily, afternoon | Looks 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:
- Sending the report email stays manual on purpose — Janet reviews each draft and copies it into her own inbox (the report has "Copy subject" / "Copy email body" buttons). This is a deliberate choice to avoid a mistaken send to a client, not a missing feature.
- Retire Coefficient (Phase 6 cutover, in progress). Google Ads, Meta, and GA4 now pull from their APIs, with the sheet as fallback. The last steps are: run the drift check across a full cycle, get Janet's sign-off, then remove the sheet dependency and cancel the Coefficient subscription. Until that sign-off, keep Coefficient running.
- Finish GA4 on the API for the last schools — Quaker needs its GA4 access grant; Sacred Heart hides website traffic so it doesn't need one.
- Form-fill automation — the one manual number. If each school's form tool has an API or export, this could be pulled in.
- Token-expiry & data-behind alerts already exist on-screen (the red "token expired" flag, the amber "data behind" flag, red CPL chips, the due banner) plus the run-summary
alertslist — a quick text to Janet covers re-auth. A Slack/email auto-notifier is still a "later if it helps" item, not a gap. - The content engine (client questions → blog ideas) could feed an AI blog drafter.