Notechondria
Version: 0.1.81 Build Date: 2026-04-27T01:00
What's Changed
HttpClientInternalsMixin — seventh shared mixin shipped
Continues the cross-app deduplication started in 0.1.54. Adds the
seventh shared mixin out of the eight planned in docs/TODO.md;
one remains (AppShellSessionMixin).
Unlike the prior six, this mixin is on HttpNotechondriaClient
(the per-app HTTP client class) rather than on _AppShellState.
That's why it lives under lib/src/http/ instead of
lib/src/app_shell/.
-
New
frontend/notechondria_shared/lib/src/http/http_client_internals_mixin.dartexposes four byte-identical helpers as concrete mixin methods onHttpNotechondriaClient:send(method, uri, operation, {requestBytes})— request wrapper that emits a DEBUG line on dispatch and an INFO/WARNING line on response (status-driven). On failure, records a debug snapshot and rethrows wrapped. Logs prefixed withhttpLogTagPrefixso editor emitsEditor.HTTP/..., plannerPlanner.HTTP/..., etc.decode(response, {uri, method})— JSON decode with HTML-detection, debug-snapshot recording, and 4xx/5xx →Exception(shapedErrorMessage(...))reshape. Empty 2xx bodies decode to{}.headers({token, includeJsonContentType})— Authorization- Accept + optional Content-Type header builder.
shapedErrorMessage({statusCode, uri, method, data})— wraps 401/403 errors in the AGENTS.md §1.7 shape so no bare backend error reaches the UI. Pre-shaped backend messages (containing an em-dash) pass through verbatim.
-
Five internal helpers (
_stringifyErrors,_previewBody,_formatDecodeError,_recordDebugSnapshot, top-level_levelForStatus) ride along inside the mixin file. They're underscored — library-private tonotechondria_shared— so per-app code can't reach them and the host class's public surface stays clean.
Five abstract host requirements
The mixin needs read access to a few host-class fields. Each
app's HttpNotechondriaClient overrides them:
http.Client get httpClient— the sharedhttp.Clientinstance for outbound requests.void Function(DebugLogLevel, String, String)? get logger— the optional debug-log sink.ValueNotifier<ApiDebugSnapshot?> get debugSnapshot— already afinal ValueNotifierfield on the host; just needs an@overrideannotation.ValueNotifier<List<ApiDebugSnapshot>> get debugHistory— same pattern.String get httpLogTagPrefix—'Editor'/'Planner'/'Portal'. Drives the per-app log tag insend.
What stayed per-app
_uri (URL constructor reading the private _baseUrl field)
and the four verb wrappers (_get / _post / _patch /
_delete) live on per-app extensions in
core/http_client_internals.dart. Verb wrappers internally call
send(...) from the shared mixin.
Why not also share these?
_uriwould force exposing_baseUrlas a getter. Trivial, but offers ~3 lines of dedup. Skipped._get/_post/_patch/_deleteare tiny but called at 600+ sites repo-wide. Renaming would be mechanical (sed) but would land massive call-site churn for marginal dedup. Worse, the bare verb names (get/post) collide visually withhttp.Client.get/http.Client.postat call sites and would hurt readability. Deferred.
If a future round wants to lift these, the mixin signature stays
stable — only the per-app core/http_client_internals.dart
shrinks further.
Three apps ported
Each app's wiring follows the same shape:
-
HttpNotechondriaClientdeclared asclass HttpNotechondriaClient with HttpClientInternalsMixin implements NotechondriaClient. -
Five
@overrideannotations:final ValueNotifier<ApiDebugSnapshot?> debugSnapshot(was already a public field; added@override).final ValueNotifier<List<ApiDebugSnapshot>> debugHistory(same).- Three new getters:
httpClient,logger,httpLogTagPrefix.
-
core/http_client_internals.dartreduced from ~250 lines (per app) to ~70 lines, keeping only_uri+ four verb wrappers. The verb wrappers now callsend(...)andheaders(...)(no leading underscore) on the host instead of_send/_headers. -
Call sites of
_send/_decode/_headers/_shapedErrorMessagein each app's main client file (core/http_client.dartfor editor;core/client.dartfor planner / portal) renamed via sed to drop the leading underscore.
Files Changed
VERSION— 0.1.80 → 0.1.81.frontend/notechondria_shared/lib/src/http/http_client_internals_mixin.dart— new shared mixin (313 lines, well under §1.5 1000-LOC cap).frontend/notechondria_shared/lib/notechondria_shared.dart— exportsHttpClientInternalsMixin.frontend/editor_app/lib/core/http_client.dart— host class declareswith HttpClientInternalsMixin; 5 override annotations;_send/_decode/_headers/_shapedErrorMessagecall sites renamed.frontend/editor_app/lib/core/http_client_internals.dart— trimmed from ~273 to ~80 lines (kept_uri+ verbs only).frontend/planner_app/lib/core/client.dart— same wiring shape as editor.frontend/planner_app/lib/core/http_client_internals.dart— trimmed.frontend/portal_app/lib/core/client.dart— same wiring shape.frontend/portal_app/lib/core/http_client_internals.dart— trimmed.docs/TODO.md— mixin entry rewritten "2 of 8" → "1 of 8";HttpClientInternalsMixinremoved from the pending list.
Notes
- All four packages (editor / planner / portal / shared) pass
flutter analyze(zero errors) andflutter testsmoke suites. - §1.5 1000-LOC cap respected. Largest file in the repo unchanged
this round: portal
app_shell.dartat 971 lines (mixin work doesn't touch app_shell). Editorhttp_client.dartgrew slightly (~14 lines) from the new wiring, now at 922. - Behavioral surface unchanged: every API call still routes
through
send(...)→decode(...)→headers(...)with the same per-app log-tag prefix. Verified by smoke tests on all three apps. - Pre-existing unrelated warnings (planner/portal
app_shell_handleDestinationSelectedunused; deprecatedsurfaceVariant/withOpacity;applyAuthPayloadmissing@override) carried over from earlier rounds — addressed in the upcomingAppShellSessionMixinround which deletesapplyAuthPayloadoutright.