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.

Home on the Android emulator: the 'Your visit, in your pocket' hero, a 'Find my appointment' card with a six-dot code field and a Show my visit button, four tabs along the bottomPhone
Home in the Windows app: a dark 1100 by 760 window with a five-entry side rail on the left and the same hero and code card in a wider columnDesk
Home in a browser at phone width: the same hero and code card, light theme, bottom tab barBrowser
Home. Part 2's phone and browser frames beside Part 10's desk frame — dark because the desktop's OS theme is, and the tokens follow it. Same component; only the width and the host's answers change.
The Settings screen's About card on Android: Running on Android 13, Host Native app, Layout Phone (one hand, thumbs)Phone
The Settings screen on Windows, dark theme, with a Nudges card and an About card: Running on WinUI 10.0.26200.9168, Host Native app, Layout Large screenDesk
The Settings screen in a browser: Running on Web (server: Win32NT), Host Browser, Layout Large screenBrowser
The capability-interface pattern, photographed: one Settings.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.
The Waiting room page on Android: NOW SERVING Ava C., Up next 1 Noah B., five tabs at the bottomPhone
The Waiting room page in the Windows window with the side rail, dark theme, Ava C. now serving and Noah B. up nextDesk
The Waiting room page in a browser at 1280 pixels, light theme, side rail on the leftBrowser
The waiting room, Part 10: Ava C. being served, Noah B. next, on all three — the desk and the browser both wearing the rail that one 900-pixel media query hands to anything wide enough.
My visit on Android after a phone check-in: Hi Maria, Checked in, a huge number 1, 'You're the only one waiting', and Part 3's Refresh buttonPhone
The waiting-room TV board in its dark signage palette: UP NEXT 1 Maria G., clock reading 06:38The wall TV
Part 3's moment: Maria checked in from a phone and appeared on the TV built two seasons earlier, because the app's check-in is literally the kiosk's method. That Refresh button died in Part 4.
Android's notification permission dialog, 'Allow ClinicLive Pocket to send you notifications?', over Liam's booked visitPart 5
Android's location dialog with Precise and Approximate options over the Find us pagePart 7
Android's camera permission dialog, 'Allow ClinicLive Pocket to take pictures and record video?', over the Home screenPart 8
Permission in context, three times: notifications at check-in, location on "Use my location", the camera on "Scan my ticket". Never at launch. The dialogs are Android's; the timing is ours.
My visit for Emma with a coral 'reconnecting…' pill beside the heading and 'It's your turn — please go through' in the cardBefore the fix
The same screen for Emma with no pill: the server is back and the connection recoveredAfter
Part 4's evidence. Left: a full minute after the server came back, the default reconnect policy had given up for good. Right: the keep-trying policy — 2, 4, 8, 16 seconds, then every 30, never null. A browser tab gets refreshed; a phone in a car park doesn't.
The clinic website's booking ticket for Leo: Thursday 3 September at 15:30, the code 5GYZRH in large letters, a QR code on a white tile and copy inviting a scan with the Pocket appThe ticket
The native scanner page on the emulator: a live camera view of the emulator's virtual room — a bookshelf, a low-poly cat, a TV showing a checkerboard — with 'Point the camera at the QR on your ticket' and a Cancel buttonThe scanner
Part 8: a QR on the ticket and a native ZXing page pushed over the Blazor WebView. The camera is looking at the emulator's virtual living room, not at a ticket — more on that below.
Airplane mode: an offline strip under the header, a notice 'Showing what the clinic said at 13:34 — we can't reach it right now', Leo's cached visit and a disabled check-in buttonOffline
Signal back: the same visit for Leo with no strip, no notice, and the check-in button enabledBack online
Part 9: a code in the keystore, a queue state cached with the time it was true. Stale means read-only — you can see the button but you can't press it.

The 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:

What each part of the companion app cost in API spend (cumulative)
PartWhat we builtRunning total
12The 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
4The phone joins the hub; lifecycle; the reconnect that never gives up≈ $2.60
5Two verbs of haptics, notifications in context — and the Windows dead end≈ $3.40
6Firebase push to a closed app; the stopped-state lesson≈ $4.20
7Geolocation, a testable haversine, package visibility≈ $4.70
8QR on the ticket, the native scanner — and the poster hunt≈ $5.60
9Connectivity, two-tier storage, the remembered and cached visit≈ $6.10
10A desk-sized window, one breakpoint, the waiting-room page≈ $6.50
11Signed 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:

What the AI got wrong, part by part
PartWhat went wrongCaught by
1 — setupThe 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 — hostsA 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 — hostsHeader 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 — hostsView is ambiguous inside a MAUI project.The compiler
2 — hostsThe 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 — hostsThe web host served every static file empty: no environment set means Production, and MapStaticAssets wanted published assets.A screenshot that looked like 1996
2 — hostsThe harness passed -nologo into the app's own arguments; Chromium screenshotted a directory listing.The screenshot
2 — hostsThe Windows window opened maximized with the phone layout stretched across a 4K monitor. Deferred to Part 10.The screenshot
3 — APIThe "tomorrow" test booked the slot season one's booking test owns in the shared fixture; the old test went red.The old test
3 — APIpostgres:18 images refuse a volume at the old data path; compose stopped working.Compose failing to start
3 — APIBuilds 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 — liveCold 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 — liveThe 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 — livePart 2's insets code left two warnings.The compiler
4 — liveA 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 — nudgesWindows 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 — nudgesWindows reported haptics "available" — IsSupported is optimistic on a desktop. Cosmetic.The Settings screenshot
5 — processThe 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 — processA "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 — pushD8: 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 — pushTwo identical "You're next": the check-in reload and the hub reload raced. Loads serialized.The notification shade
6 — pushIsInForeground 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 — locationMaps then launched and crashed — the emulator's stale build. The intent resolved; the crash was photographed, not hidden.The emulator
7 — locationThe 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 — locationTest builds blocked by the running server's DLL lock.The build
8 — scannerThe 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 — scannerThe 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 — scannerZXing.Net and ZXing.ImageSharp both define BarcodeReader<T>.The compiler
9 — offlineSecureStorage.GetAsync throws after a keystore reset; Home would have crashed on a restored phone.Reading the restore path
9 — offlineStorage read in OnInitialized — no browser under the web host's prerender. Moved to OnAfterRender.The web host
9 — offlineThe native checkbox was Android-blue against petrol. One accent-color line.The Android screenshot
10 — deskNo code catch. The honest entry is what stayed unfixed — toasts, because the answer is identity, not C#.Named, not fixed
10 — processThe 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 — shipThe MSIX build reported success and produced no sideloadable package outside obj/.Looking in the output folder
11 — shipapksigner.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 IPlatformInfo to IAppStorage, 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:

  1. 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.
  2. 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 by TicketQrTests, which runs in CI. Everything except the lens.
  3. 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.
The Windows app maximized, dark theme, on the Settings page: Haptics 'Available on this device', Notifications 'Allowed', the three test buttons — and no toast banner anywhere on the screenWindows: Allowed, nothing shown
An Android system dialog reading 'Maps keeps stopping' with App info and Close app options, over the Find us pageThe emulator's Maps
Two of the three, exactly as they happened. Left: the season's only full-screen Windows capture — taken with the app maximized so nothing else could be in frame — and the banner that isn't in it. Right: Maps, resolved and crashed. The third has no picture, which is rather the point.

Was 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. 📱