fix: Test Connection names the volume a /health 503 says refused #143
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/connection-test-names-the-volume-that-refused"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What's in
/health503 with the documented body readsReachable, but the server cannot write to its images volume (/app/data/images): <detail>, one line per refusing directory (It also cannot …for the second and later). Adbentry whose detail starts withSQLite Errororjournal_mode stayed(the two shapes DEPLOYMENT.md §7 documents) readscannot host its database on the db volume (<path>): <detail>; adbentry the filesystem refused reads like any other volume. A #112-era entry withoutdetailstill names the volume, with no dangling colon.Reachable, but the server answered HTTP 503 without saying which volume refused …). Every other status and every transport failure keeps today's message.BannerTone.warning(Icons.warning_amber,colorScheme.secondary) on the sharedInlineBanner, so it is visibly different from the red "could not connect" banner.HealthRepository.checkkeeps returningFuture<bool>;DioHealthRepositorythrows a typed domainServerNotReadyException(message),_testConnectionEpicmaps it toConnectionTestResultAction(ok: false, error, serverNotReady: true), andSettingsStategainedserverNotReady(reset byTestConnectionActionandSaveSettingsAction). The regeneratedapp_state.freezed.dartis committed./healthcontract is read, not changed) and no newDOUJIN_MANAGER_*variable. README's Settings sentence gained the Test Connection clause; the #112 backlog item that asked for this is removed.Verification
scripts/verify.sh app, run from the worktree root at the final commit (b6ea1e6) and waited to completion: submodule check,flutter pub get,build_runner clean+buildwith the generated-file checksum guard (no drift reported),flutter analyze→No issues found! (ran in 2.8s),flutter test→00:47 +997: All tests passed!,== verify OK (app) ==, exit 0. The backend is untouched, soverify.sh backendwas not run.DioHealthRepositoryagainst a dio fake adapter with real JSON decoding, in thea 503 from /healthgroup ofapp/test/data_layer_test.dart(volume + path + detail; db SQLite vs. filesystem wording; onlydbgets the host wording; several refusals; a body dio left as text; the detail-less #112 body anddetail: ''; an entry missing name and path; other-shape JSON object and array; text/html body; nothing refusing; a 500 with the same body stays the plain HTTP message). Reducer —app/test/app_test.dart(result action carriesserverNotReady;TestConnectionActionandSaveSettingsActionclear it). Epic throughcreateStoreand the connection-section widget —app/test/settings_connection_test.dart(not-ready reaches state as such; a plain failure stays a failure; a later 200 clears the verdict; the widget renders the message with the warning tone; an error stays an error; warning and error differ in icon and colour). Widget tests use bounded pumps, nopumpAndSettle. No REST, use-case or persistence layer changed.git checkoutrestore; each went red on exactly the named tests and nothing else, tree clean afterwards):if (code == 503)disabled → 7 red; the SQLite-wording heuristic forced (name == dbalone) and negated (false) → the two db wording tests;clauses.skip(clauses.length)→ "several refusals are one line each"; the String-body path returning null → "a body dio left as text"; theclauses.isEmptyguard removed → "nothing refusing is generic";detail == null || detail.isEmpty ? '' : ': $detail'collapsed → "a refusal without a reason still names the volume"; thename == _kDbDirectory &&conjunct removed → "only the db volume can fail to host the database";body is! Mapanddirectories is! Listguards turned into throws → "JSON of another shape is generic"; the null-path and null-name fallbacks removed → "a refusing entry missing name and path still reads"; the reducer'sserverNotReady:lines removed from the result,TestConnectionActionandSaveSettingsActionarms → the three reducer tests; the epic'son ServerNotReadyExceptionarm removed → 3 red insettings_connection_test; the page tone collapsed to success/error → 2 red; the banner's warning arm styled as error → 1 red. A second, independent probe pass atb6ea1e6re-ran the second-round probes and the epic/page-arm probes and confirmed each red on the same test.DataDirectoryHealthDtoin the RestAdapter and DEPLOYMENT.md §7, and the #112-era detail-less shape fromgit show fc50854. Two conditionals in the malformed/partial-body class remain unpinned (probes stayed green): thedetail != null &&conjunct of the SQLite heuristic (a #112-era server whosedbvolume refuses readscannot write to its db volume, which is the intended fallback) and theentry is Mapguard on a directories element.Follow-ups
Exception: Could not connect to …because_testConnectionEpicinapp/lib/presentation/middleware/epics.dartstorese.toString(); the epic should store the message alone (pre-existing, noticed while Test Connection learned to read the 503 body).🤖 Generated with Claude Code
Flutter Coverage
Total: 83.0% (9024 of 10876)
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! The client finally listens to what the server is telling it~ ♡ A 503 that used to collapse into
Server returned HTTP 503now names the exact volume, its path, and the refusing party's own words — one line per refusal. That's the difference between "something is broken" and "go fix your CIFS mount on the NAS." This is what diagnostics are for~ ♪Verdict: ✅ Looks good to me~
Contract verification. I read both ends of the wire myself:
HealthDtos.cs(name/path/writable/detail, camelCase viaJsonPropertyName) and DEPLOYMENT.md §7's table on thedbvolume._kSqliteDetailPrefixes = ['SQLite Error', 'journal_mode stayed']matches §7's documented shapes exactly, and thedb-alone hosting distinction mirrors the DTO doc's "just hosted a SQLite database there" wording. The #112-era detail-less body (git show fc50854— three fields, nodetail) is handled without a dangling colon, and the PR-body claim that pre-#112 servers never answered 503 holds: the 503 came with #112 itself. ♡Layering. Textbook: typed domain exception (
ServerNotReadyException) in the domain layer, thrown by the data layer, mapped in the epic — the interface staysFuture<bool>and no caller parses text. Redux threading is complete:serverNotReadyis reset in all three arms (TestConnectionAction,SaveSettingsAction, result action), so a stale amber banner can't survive a new test or a save. The regenerated freezed file is committed with the field threaded through equality/hashCode/toString/copyWith — no drift.Fail-safe parsing. Every malformed-shape arm returns the generic "reached, answered 503, no volume named" message rather than crashing: non-Map bodies, non-List directories, text/html proxy pages, nothing-refusing bodies, entries missing
name/path. I probed this personally — see below~Mutation probes (mine, independent, at head
b6ea1e6):a db volume SQLite refused cannot host the databasewent red. ✓name == _kDbDirectoryconjunct removed (any volume's SQLite words get host wording) →only the db volume can fail to host the databasewent red. ✓on ServerNotReadyExceptionarm removed → exactly 3 red insettings_connection_test.dart. ✓ (My first run of this probe stayed green — that was CPU contention from a concurrent full-suite run in my sandbox, not the code; clean rerun went red exactly as the PR body documents. I re-verified after restoring.)detail != null &&conjunct: I tried to remove it — it doesn't compile (String?null-safety atdetail.startsWith). The residual is type-system-pinned. Fufu, that's a lovely property~entry is Mapguard: removed it and fed a"not-a-map"string element →type 'String' is not a subtype of type 'int' of 'index'— confirming the guard is what keeps garbage elements from crashing the parse. It's genuinely defensive, not dead code.Tree restored pristine after every probe (
git status0 changes).💡 Little ideas (non-blocking)~
clauses.skip(1)) — the "It also …" continuation lines are worded for a body-text banner but joined with\n; ifInlineBannerever hard-wraps oddly on narrow windows the multi-line shape is the place to look. Purely speculative polish, no action needed.Exception: Could not connect to …prefix leak in_testConnectionEpic's generic catch sits right next to this code and is now more visible next to the worded banner; the parked backlog item is the right home for it. Next to the new amber banner the prefix does stand out a little, but it's pre-existing and documented.✅ What I liked~
contentTypeknob precisely so the undecoded-String path (no content type) is exercised against the real dio decode pipeline. Testing the transport reality, not a fantasy.DEPLOYMENT.md §7is read, not changed; ADR 0010's contract is honored; no new env vars; README's one-sentence amendment is exactly the right size. Scope control is immaculate.settings_reducer.dart100%,settings_layout.dart100%,settings_page.dart99.3%,health_repository.dart87.0% (the 7 uncovered lines are the pre-existing transport-error arms, untouched by this PR).Automated review by Jibril · 2026-09-18 20:37 CEST
CI/CD: passed for head
b6ea1e6(analyze + 997 tests + coverage comment 8094) · Local checks: flutter analyze clean; mutation probes 5/5 at head; full local suite deferred to CI (sandbox CPU contention made local timing unrepresentative)