Eleven parts ago, ClinicLive lived on a kiosk and a wall. This season made the trilogy's third promise: put it in the patient's pocket with AI assistance and hide nothing — every prompt verbatim, every native feature behind an interface each host answers honestly, every screenshot from a real emulator or a real desktop, every dead end kept in. The meter has stopped. Time to look at what we made, and at what we couldn't prove.
The gallery
This is the payoff post, so the pictures go first. The season's claim was one set of screens, three hosts, so wherever the bench allowed it, a row is the same Razor component photographed on a phone, on a desk and in a browser.
Phone
Desk
Browser
Phone
Desk
BrowserSettings.razor
asks IPlatformInfo where it is and prints the answer. The Windows frame is from
Part 5 — the phone-layout-on-a-monitor era — and it shows.
Phone
Desk
Browser
Phone
The wall TV
Part 5
Part 7
Part 8
Before the fix
After
The ticket
The scanner
Offline
Back onlineThe bill, honestly
Same rules as season one's retro and season two's: cumulative API spend from the build log as each part landed. Part 1 was the plan, with no commit, so it rides inside Part 2's reading:
| Part | What we built | Running total |
|---|---|---|
| 1–2 | The plan; contracts, the shared RCL, two hosts, the first capability; Android's insets | ≈ $1.30 |
| 3 | /api/pocket, the typed client, the real Visit screen | ≈ $1.90 |
| 4 | The phone joins the hub; lifecycle; the reconnect that never gives up | ≈ $2.60 |
| 5 | Two verbs of haptics, notifications in context — and the Windows dead end | ≈ $3.40 |
| 6 | Firebase push to a closed app; the stopped-state lesson | ≈ $4.20 |
| 7 | Geolocation, a testable haversine, package visibility | ≈ $4.70 |
| 8 | QR on the ticket, the native scanner — and the poster hunt | ≈ $5.60 |
| 9 | Connectivity, two-tier storage, the remembered and cached visit | ≈ $6.10 |
| 10 | A desk-sized window, one breakpoint, the waiting-room page | ≈ $6.50 |
| 11 | Signed Android, self-contained Windows, CI, the runbook | ≈ $7.20 |
The meter, final reading: ≈ $7.20 for the whole companion app — nine capability interfaces, three hosts, real push, real haptics, a real scanner, offline, a signed build and CI. Season one's $6.20 built ClinicLive; season two's $3.60 designed it; $7.20 put it in a pocket. Seventeen dollars for the trilogy. The two big dead ends — Part 5's toast chase and Part 8's poster hunt — are in that number; everything else was proportionate.
How the meter is measured: the build's actual token consumption priced at the API list rates of the models used (the 🧠 boxes name them). On a subscription plan your bill won't literally say $7.20 — but the tokens burned are the same, so it's the honest apples-to-apples number.
And the caveat, one last time, because it was truer this season than either before: the dollars are not the real cost. The real cost was the bench — a GPS fix to fake, airplane mode to toggle, a vibrator log to read, a Firebase console to watch, a desktop to photograph without photographing anything else. The meter bought code; the proving is where the season went.
The complete mistake log
Every 🤖 from eleven parts in one table, including two that were about the process, not the code:
| Part | What went wrong | Caught by |
|---|---|---|
| 1 — setup | The Windows target failed with MSB3030 copying a Windows App SDK DLL. The scratch path was about 270 characters; the App SDK's copy step dies silently past MAX_PATH. Ten seconds to build at a short path. | A shorter folder name |
| 2 — hosts | A purple status bar: the template's colors.xml still painted it. CSS tokens don't reach Android's window chrome. | The first Android screenshot |
| 2 — hosts | Header and tab labels missing on Android: the WebView draws under the system bars and env(safe-area-inset-*) reads 0 inside it. Fixed natively with an insets listener. | The Android shot beside the Windows one |
| 2 — hosts | View is ambiguous inside a MAUI project. | The compiler |
| 2 — hosts | The h1 focus ring was back on Windows — FocusOnNavigate's tabindex="-1" matched the ring rule. Season two's bug, new face. | The Windows screenshot |
| 2 — hosts | The web host served every static file empty: no environment set means Production, and MapStaticAssets wanted published assets. | A screenshot that looked like 1996 |
| 2 — hosts | The harness passed -nologo into the app's own arguments; Chromium screenshotted a directory listing. | The screenshot |
| 2 — hosts | The Windows window opened maximized with the phone layout stretched across a 4K monitor. Deferred to Part 10. | The screenshot |
| 3 — API | The "tomorrow" test booked the slot season one's booking test owns in the shared fixture; the old test went red. | The old test |
| 3 — API | postgres:18 images refuse a volume at the old data path; compose stopped working. | Compose failing to start |
| 3 — API | Builds failed on DLL locks while the hosts were still running. | The build |
| 3 — API | --no-launch-profile means Production — for the server, no demo seed. | An empty API |
| 4 — live | Cold start grew from about 4 s to 7.5 s with the SignalR client; adb taps at four seconds were dropped. A slow app looked like a broken one. | logcat |
| 4 — live | The emulator dimmed for inactivity. Gray screenshots, no bug. | A gray screenshot, then skepticism |
| 4 — live | "Now serving David C.." — the masked name already ends with a full stop. | The screenshot |
| 4 — live | Part 2's insets code left two warnings. | The compiler |
| 4 — live | A minute after the server came back, the phone still said "reconnecting…": WithAutomaticReconnect() gives up after four tries, forever. Replaced with a keep-trying policy. | The screenshot a minute after restart |
| 5 — nudges | Windows toasts never displayed: registration OK, setting Enabled, Show() returned, no banner — even a PowerShell toast to that identity was silent. Open all season. | The "Allowed, nothing shown" screenshot |
| 5 — nudges | Windows reported haptics "available" — IsSupported is optimistic on a desktop. Cosmetic. | The Settings screenshot |
| 5 — process | The first full-screen capture photographed the entire desktop, every other window included. Deleted before any commit; the harness now refuses full-screen unless the app is maximized, and excludes the taskbar. | A human, looking before committing |
| 5 — process | A "diagnostic" Notepad the harness opened was a tab in the user's own Notepad window — Windows 11 Notepad is single-instance — and closing it closed theirs. Never automate other people's apps. | The person whose window it was |
| 6 — push | D8: FragmentKt defined twice — Firebase's fragment-ktx 1.8.8 against MAUI 10's 1.9.0. Pinned. | The build |
| 6 — push | "App closed" was simulated with am force-stop: Firebase said 1 delivered, the phone showed nothing. Force-stop is Android's stopped state, where push is never delivered; swiping an app away isn't. Redone with HOME plus am kill. | A phone showing nothing after "1 delivered" |
| 6 — push | "Push is on" missing from the first shot — the token round-trip takes seconds. Not a bug. | An early screenshot |
| 6 — push | Two identical "You're next": the check-in reload and the hub reload raced. Loads serialized. | The notification shade |
| 6 — push | IsInForeground defaulted to true; a process Firebase starts has no window at all. | Reading the closed-app path |
| 7 — location | "No maps app" with Google Maps installed: Android 11's package visibility needs a <queries> block in the manifest. | The screenshot, with Maps right there |
| 7 — location | Maps then launched and crashed — the emulator's stale build. The intent resolved; the crash was photographed, not hidden. | The emulator |
| 7 — location | The test asserted Greenwich to the Eiffel Tower is about 341 km; the haversine's 333.8 was right. The expectation was fixed, not the formula. | A failing test, checked by hand |
| 7 — location | Test builds blocked by the running server's DLL lock. | The build |
| 8 — scanner | The emulator never rendered its QR posters — forty minutes sweeping the room. A limit of the bench; the decode is proven by a test instead. | Forty minutes of looking |
| 8 — scanner | The first web ticket shot was mid-animation, QR not yet loaded. The harness now waits. | The screenshot |
| 8 — scanner | .CacheOutput() without output-cache middleware — swapped for a Cache-Control header before it could throw. | Review, before runtime |
| 8 — scanner | [A-HJ-NP-Z2-9] let the banned L back in — J to N spans J, K, L, M, N — so a cereal-box URL parsed as ticket CEREAL. | The new test |
| 8 — scanner | ZXing.Net and ZXing.ImageSharp both define BarcodeReader<T>. | The compiler |
| 9 — offline | SecureStorage.GetAsync throws after a keystore reset; Home would have crashed on a restored phone. | Reading the restore path |
| 9 — offline | Storage read in OnInitialized — no browser under the web host's prerender. Moved to OnAfterRender. | The web host |
| 9 — offline | The native checkbox was Android-blue against petrol. One accent-color line. | The Android screenshot |
| 10 — desk | No code catch. The honest entry is what stayed unfixed — toasts, because the answer is identity, not C#. | Named, not fixed |
| 10 — process | The narrow-window frame was re-shot later, and the file that came back was not the app: Windows had refused the script's request to bring the window forward, so the capture copied whatever was in front of it — an unrelated window. Deleted before any commit; the harness now refuses to shoot unless the app window is in front and owns every corner of the frame. | A human, looking at the image before writing its caption |
| 11 — ship | -r win-x64 makes NuGet restore every target for that runtime, and Android's Mono runtime has no win-x64 pack (NU1102). Drop the flag. | The restore |
| 11 — ship | The MSIX build reported success and produced no sideloadable package outside obj/. | Looking in the output folder |
| 11 — ship | apksigner.bat exits non-zero after printing the certificates; the harness read "failed" for a success. | Reading the output, not the exit code |
| 11 — ship (after publishing) | The workflow's first real run failed its Android job with NETSDK1147: it installed the android workload, and a MAUI project needs maui-android. One word, fixed on top of the tag; the re-run passed on all three targets. | The first real CI run |
Read the right-hand column and a third pattern appears. Season one's mistakes were about context — your rules, your timezone — and were caught by reading code against a spec. Season two's were about looking, and the screenshot loop caught them. This season's were about the platform underneath the code: insets, stopped states, package visibility, MAX_PATH, restore semantics, exit codes. Almost none are visible in a diff, and a screenshot alone often wasn't enough either — the catch needed a screenshot plus a skeptic: a gray frame that was only a dimmed emulator, a "1 delivered" that meant nothing, an exit code that lied in both directions. The two process rows cost no tokens and taught the most: a screenshot harness needs a privacy guard, not just a crop, and it must never touch a window it didn't open.
What the AI never caught alone
The table undersells the human half again, because the biggest contributions were decisions, and this season they lived in the prompts. Two verbs of haptics, because an app that vibrates for everything gets muted. Permission at check-in, never at launch. "A second front door must not grow a second set of rules." "The code is the credential." "Stale means read-only." "Prove as much as the bench allows" and its twin, "fake nothing." Each was a sentence a person wrote before the model started, and each would have shipped as something plausible and worse without it. The two process incidents were human catches too: the model doesn't know what else is on your desktop, or whose Notepad that is. And Part 7's wrong "known distance" was caught by a test the model wrote but decided by a human working the numbers by hand — a failing test is a question, not an order.
What the AI got wrong, the season in one sentence: it knew the APIs and
not the platform. It wrote NotificationCompat from memory and didn't know a
force-stopped app never receives a push; it wired a ZXing camera view in an afternoon and
didn't know the emulator couldn't show it a poster; it signed an APK and didn't know
apksigner exits non-zero on success. Platform knowledge only comes from
being bitten — and a bench, a log and a skeptic are how you get bitten on purpose,
cheaply, before a patient does.
What the AI was brilliant at
Fairness cuts both ways, and this season's wins were the biggest of the three:
- The capability-interface pattern, nine times over. From
IPlatformInfotoIAppStorage, each a small interface in the shared project, answered by MAUI next to the platform it talks to and by the web host honestly —null,false, "not here". Introduced once in Part 2, never bent, so the screens never learned which host they were on. - Diagnoses that would have cost a person a day. Safe-area insets reading zero inside a WebView. A 270-character path killing a Windows build. Android 11's package visibility. The AndroidX version dance. Each named from a screenshot or a build log of the symptom.
- Proof by other means. When the emulator couldn't be shown a QR, it proposed the test that decodes the server's PNG back through the parser — the whole chain except the lens. When no screenshot can show a vibration, it went to the OS's vibrator log: 182 ms, 102 ms, 403 ms. It looked for evidence instead of settling for a claim.
- Honest no-ops. "iOS: not built here." "Windows: no push." "Browser: not a scanner." The season's least glamorous code and its most trustworthy.
- The runbook.
docs/pocket.md, written in the same session as the code it describes — the only time anyone ever writes one.
Model pick, the season's rule: Opus wherever the question was about semantics — foreground versus background versus stopped, what "resumed" must mean for a socket, whether stale data is read-only, what not to fake; Sonnet wherever Essentials or a documented toolchain did the heavy lifting — geolocation, storage, CSS, signing, YAML. Same principle as the two seasons before, on a new axis: in a mobile app, the consequence of a cheap-model mistake is a notification that never arrives.
The three things the bench couldn't prove
Every part photographed what it claimed, and three claims could not be photographed. They belong together, so nobody has to hunt for them:
- Windows toast notifications never displayed. Not in Part 5, not in Part
10, not from Part 11's published build. Registration succeeds, the OS setting says
Enabled,
Show()returns, and no banner appears. The cause is app identity — the unpackaged build has none — and the fix is MSIX packaging or an installer-made shortcut with an explicit AppUserModelID, neither of which this series demonstrates, because sideloading an MSIX means trusting a home-made certificate. The Windows app says "Allowed" and shows nothing. That's the honest state. - No on-device QR decode was photographed. The emulator never rendered its
virtual-scene posters, so the scanner was only ever pointed at a virtual living room. The
chain — the server's PNG, read by ZXing back to
cliniclive://visit/CODE, through the parser — is proven byTicketQrTests, which runs in CI. Everything except the lens. - Google Maps crashed on the emulator. After the package-visibility fix the directions intent resolved and Maps launched — then died, the emulator's stale Maps build without a Play sign-in. What we control worked; what a real phone does next was not shown.
Windows: Allowed, nothing shown
The emulator's MapsWas Blazor Hybrid the right bet?
Part 1 made the case before any code existed: for a companion to an existing Blazor product, the screens would be the cheap part and the platform edges the whole cost, so sharing the screens would pay for itself. The retro gets to check it.
The screens were cheaper than cheap. Five pages written once; the Settings triplet above is the proof they never learned where they were. Season two's tokens carried straight over, dark mode included. The waiting-room page was forty lines because the live plumbing already existed. And the party trick paid off exactly where it mattered: a native camera page pushed over the WebView, because the WebView is just one page in a native app.
The edges cost what edges cost, and the table above is that bill: insets CSS can't see, a status bar the tokens can't reach, a cold start three and a half seconds slower with the SignalR client, a 33.8 MB APK and a 235 MB Windows folder, the AndroidX version dance, toasts that need an identity, iOS compiling but never built for want of a Mac. None of that is Blazor's fault — and, the honest point, none of it would have been avoided by going native. A Kotlin app meets package visibility and the stopped state too; a WinUI app meets the same identity rule. Native would have paid the same edge bill and rebuilt the screens, three times.
So: yes, for this app — a small team, an existing Blazor product, screens that are forms, cards and one big number. Where the bet goes the other way: an app whose product is the camera, or gesture-heavy motion, or platform idioms polished enough to sell a store listing. There the WebView is the wrong stage and shared screens the wrong savings. Know which app you have before you place it.
The tests postscript
The number that grew all season: 9 tests from season one, 13 after Part 3's API tests against a real Postgres, 15 after Part 6's push test with a fake sender, 20 after Part 7's haversine, 28 after Part 8's QR round-trip and parser — and 28 green through Parts 9, 10 and 11, which changed nothing the tests can see. Two of them caught the AI red-handed: the wrong "known distance" and the regex that let a banned letter back in. The harness built in season one is now what lets a phone, a desk and a browser share one set of rules without anyone re-checking them by hand. Three seasons in, the compounding return on those first nine tests is the least surprising and most useful fact in the trilogy.
Checkpoint, the last one: you've now shipped a companion app, not a demo of one — shared screens on three hosts, nine capabilities answered honestly, a live queue, push to a closed phone, haptics proven by the kernel, a QR pipeline proven by a test, offline that tells the truth about its age, a signed build and CI. And you know exactly which three things it can't yet prove. Here's what's next: pick one screen of an app you own, put its native needs behind a capability interface, and run it in a second host. You'll meet your first edge within the hour — write it down.
Where next 🎉
Twelve parts, one companion app, $7.20. The whole three-season arc — every prompt, every tag,
every honest screenshot — lives in the
ClinicLive repo; the README's season-three table maps pocket-02 through
pocket-11 to their parts, and docs/pocket.md takes you from a fresh
clone to a signed APK.
If you arrived through the pocket door, the story starts two seasons earlier: From Prompt to Production, Part 1 builds ClinicLive from a spec to a VPS, and From Prompt to Polish, Part 1 makes it something people would want to use. The full catalog of everything on this site is on the series page.
And that's the trilogy complete. Season one proved AI can build software with you. Season two proved it can design with you, steered. This season proved it can reach the parts of a product that live on someone else's operating system — and that the steering matters more there, not less, because the mistakes are no longer in the code where a diff can show them. A prompt that says what not to fake. A bench that toggles airplane mode. A log that keeps the dead ends next to the wins. A skeptic reading the exit codes. The tools got the app onto the phone; the honesty is what makes it worth trusting there.
Thanks for coming all the way — three seasons, thirty-six parts, one clinic. If any of it helped you ship something, or you hit an edge this series didn't, the contact page is open and I'd genuinely like to hear about it. Now go put something of yours in someone's pocket. 📱