I was sitting in my office, on full wifi, on a connection fast enough to stream four things at once, and my own app was telling me I was offline. Worse than that, it was lying to me politely. I'd typed up a session record, hit save, and nothing happened. So I hit it again. And again. I'll be honest with you, I was spamming that save button like a man trying to summon a lift that's already on its way.
Then I scrolled down, found a little banner I'd half-forgotten existed, and said the thing every developer says right before they lose an afternoon: "why am I offline?" I wasn't offline. The app just thought I was, and it had decided the most helpful thing to do about that was nothing at all, very quietly.
What the app is actually for
A bit of context. The thing throwing the tantrum is my Forest School App, a record book for forest school practitioners. The whole point of it is that it works in the woods. No bars, no wifi, a phone in someone's coat pocket while they're running a session with a dozen children and a fire. You record the session there and then, it saves locally, and it syncs back up when you're somewhere with a signal again.
Under the bonnet it's a .NET MAUI Blazor Hybrid app with a local SQLite mirror and a sync engine on top. Offline-first by design. So you'd think the one thing it would be relaxed about is being offline. That is, after all, the entire premise. The irony of an offline-first app falling over because it incorrectly believed it was offline was not lost on me, mostly because I was the one staring at it.
What I'd assumed was one bug turned out to be three, all tangled together, each one happily reinforcing the others so that the symptom looked like a single thing. Nobody writes about this part, so let me.
Bug one: the banner was lying before it had even checked
The offline banner had a default value, and that default was "Offline". Which sounds harmless until you realise what that means in practice. On the sign-in screen, before the app had run a single real connectivity check, the banner was already sitting there declaring me offline. It wasn't reporting a fact. It was reporting an assumption it had made before it had any facts at all.
So the very first thing a user saw, every single time, was a confident statement about their connection that hadn't been verified. Sometimes it was even right, by accident, the way a stopped clock is. But mostly it was just the default leaking out into the UI and looking like a diagnosis.
The fix here was the boring, correct one: actually probe the server on init. A login-safe check that doesn't need an auth token, because at the sign-in screen you obviously don't have one yet. Don't show a verdict until you've earned it. A status indicator that displays a value it hasn't checked yet isn't a status indicator, it's a decoration.
Bug two: the operating system was lying too
This is the one that genuinely changed how I think about the whole problem. While digging through the connectivity code, I found a comment the AI had left in earlier, and it was one of those comments that's worth more than the code around it:
"On Windows desktop, Connectivity.NetworkAccess frequently mis-reports 'not Internet' even with a working connection."
And that turned out to be the heart of it. The platform connectivity API lies in both directions. On Windows it was flapping and reporting no internet access when there was plenly of it. And in the other direction, a captive portal, the kind of wifi that wants you to tick a box and accept terms before it lets you through, will happily report the radio as "up" while your actual server is completely unreachable. The radio being on tells you the radio is on. It tells you nothing whatsoever about whether you can reach the thing you actually want to talk to.
So I had the app asking the operating system "are we connected?" and treating the answer as gospel, when the honest answer to that question is "connected to what, exactly?" The radio state and the ability to reach my API are two completely different facts, and I'd been conflating them.
The fix was to stop gating on the radio state at all. Not weight it less, not check it as a hint. Stop. Instead, trust a real probe: a HEAD /api/ping against my own server with a short timeout. The method ended up being MauiReachability.CanReachServerAsync, and the deliberate decision in there is that it does not look at RadioConnected whatsoever. It doesn't care what the OS thinks. It asks the only question that matters, which is "can I actually reach the server right now?", and it believes the answer to that and nothing else.
In an offline-first app, "offline" is a UI state you compute from a real probe, not a fact the operating system hands you. The OS will tell you what it thinks, confidently, and it will be wrong often enough to ruin your day.
Bug three: "nothing happens" was itself two things
So that explains the lying banner. But it didn't fully explain the saves vanishing, because even after I'd sorted the connectivity reporting, I could still reproduce a save that appeared to do nothing. The "nothing happens" symptom was hiding two separate problems behind one frustrated user.
The first was almost embarrassingly physical. The save button had scrolled off the bottom of the screen. The session form had grown, lots of CPD notes and observation content stacked up, and the button I was hammering had quietly drifted below the fold. So part of "nothing happened" was simply that nothing visible happened, because the button doing the work wasn't on screen at the moment I expected feedback. Ouch. Also fair.
The second was the real saboteur. A stale auth token. The refresh token had rotated and expired, the call to get a fresh token failed, and the write got queued silently. No error, no shout, no banner, just a write quietly parked in a queue waiting for an auth state that was never coming back on its own.
The tell, the thing that proved it was auth and not connectivity, was beautifully simple: re-logging-in fixed it instantly. If it had genuinely been a network problem, a fresh login wouldn't have changed a thing. The fact that signing in again made every queued save go through immediately told me the network had been fine the whole time. The app had been failing an authentication check and reporting it, to the extent it reported anything, as if it were a connectivity check.
Why the AI kept fixing the wrong layer
Here's the thing though. When I first described the symptom to the AI, "I save and nothing happens, and it says I'm offline", the first explanation it reached for was the network. Which is completely reasonable. It's the most plausible single cause, it matches the on-screen evidence, and it's the layer the user is pointing at. So it dove straight into the connectivity code and started improving it.
And the connectivity code did need improving. But it wasn't the whole story, and if I'd let it stop there I'd have shipped a beautifully reliable connectivity check sitting on top of an auth bug that was still eating saves. The plausible explanation was real. It just wasn't complete.
What I learned to do was refuse the first answer until it had ruled out the other layers. Make it account for auth. Make it account for where the button actually is on screen. Make it account for the write queue and what happens to things sitting in it. When a user says "nothing happens", the AI's first plausible explanation is usually pointed at the wrong layer, because the most visible cause and the actual cause are rarely the same thing. I'm the product manager here. The AI is brilliant at executing a theory, but the theory of what's actually broken has to come from someone willing to be suspicious of the obvious answer.
Make your failures loud
The deepest lesson out of all this had nothing to do with connectivity APIs. It was about silence. Every single one of these three bugs shared the same flaw: they failed quietly. The banner asserted a state without telling you it hadn't checked. The OS reported a connection it couldn't actually deliver. The auth failure queued my work without a single word about why. Three different layers, all failing politely, all conspiring to make a working app look broken in a way that pointed at the wrong cause.
If even one of them had been loud, "couldn't reach the server, your save is queued" or "your session has expired, please sign in again", I'd have known what was wrong in about four seconds instead of an afternoon. Silent failure is the most expensive kind, because it spends your time pretending nothing is wrong.
What I'd tell you to do
If you're building anything offline-first, take this away from my afternoon of button-spamming. Compute "offline" yourself, from a real probe to your own server, with a short timeout, and don't let the operating system's radio state anywhere near the decision. The OS is answering a different question than the one you're asking, and it's confident about it.
Never let a status indicator show a value it hasn't actually checked. A default of "Offline" is a lie waiting to happen. Default to "checking" if you must default to anything, and earn the verdict before you show it.
And when a user tells you "nothing happens", treat it as a symptom with at least three suspects: the network, the UI, and auth. Make whichever tool you're working with rule out all three before it touches the obvious one. Then go back through and make every one of those failure paths shout. The goal isn't an app that never fails. It's an app that, when it does fail, tells you the truth about it immediately. Mine had been doing the opposite, very politely, and politeness is the last thing you want from an error.