Academyby Dasow

Stop 8 of 10 · Weekly

Client-facing surfaces

The delivered report, the portal page and the monthly summary, all built from an allowlist, with a test that proves what a client can never see.

Watch first · 0:50

Three surfaces, one source

Everything a client sees now comes out of the same run data: the monthly report from stop three, a portal page they can open any day, and a short summary in the email that carries the report. One source means the three cannot quietly disagree, which used to happen every month when somebody updated a slide and not the sheet.

Surface Built from Cadence
The report metrics.json and the reviewed draft Monthly
The portal page The same metrics.json, allowlisted fields Rebuilt daily
The summary The approved report, three paragraphs With the report

What a client never sees

Write this list into the code, not into a policy document.

The last three are the interesting ones. They are not secrets exactly, they are workings, and a client reading workings acts on a finding your team was about to discard.

Build the publish step
Write publish.py. For an account code it reads the latest metrics.json and the approved report, and writes a static HTML page to portal/<code>/index.html.

Every value rendered comes from PORTAL_FIELDS, an explicit list of field names at the top of the module. Anything not in the list is dropped before the template sees it. The account code in the output path must match the account_code in the payload, and the function raises if it does not. The page shows spend, conversions, cost per conversion and the month over month change per platform, plus the date of the data and a line naming any platform that was unavailable.

The page is served behind a per-client link with an expiry. Tell me what you used and where I confirm it is right for our host.
The leak test
Write test_no_leak.py. Build a poisoned payload: a valid A09 metrics.json with extra keys margin, retainer, agency_cost, model_cost and internal_note, plus a block of A12 rows and a customer email column.

Assert none of those keys, none of their values, the string A12 and no email appears anywhere in the rendered A09 page. Add a test that a field added to the payload but not to PORTAL_FIELDS never renders. Then add a test that PORTAL_FIELDS itself contains no name matching margin, cost, fee or internal. Run it.

The last assertion catches the future. Somebody will one day add agency_cost_per_report to the allowlist while tidying, and the test says no.

The monthly summary

The summary is three paragraphs at the top of the email, written from the approved report rather than from the numbers, so it cannot say something the report does not.

The summary the client reads first
Read the approved report for [paste the account code] and write the three paragraphs that open the email.

First paragraph: the result against the goal in one sentence, then the single number that explains it. Second: the one decision we made this month, what it cost, and what it bought. Third: the one thing we need from the client, with a date.

No adjectives about performance. Every figure must appear in the report. If the month was poor, say so in the first sentence rather than the third paragraph.
The client question you cannot answer with a chart
[paste the client's email question about their results]
Answer this using only the approved report and metrics.json for their account. Where the answer needs data we do not have, say precisely which data and what it would take to get it. Where the honest answer is that a change we made caused this, say that first. Draft it as a reply I can send, under two hundred words.

Quick check

Try it

Report a bug or share feedback