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.dart exposes four byte-identical helpers as concrete mixin methods on HttpNotechondriaClient:
    • 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 with httpLogTagPrefix so editor emits Editor.HTTP/..., planner Planner.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 to notechondria_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 shared http.Client instance for outbound requests.
  • void Function(DebugLogLevel, String, String)? get logger — the optional debug-log sink.
  • ValueNotifier<ApiDebugSnapshot?> get debugSnapshot — already a final ValueNotifier field on the host; just needs an @override annotation.
  • ValueNotifier<List<ApiDebugSnapshot>> get debugHistory — same pattern.
  • String get httpLogTagPrefix'Editor' / 'Planner' / 'Portal'. Drives the per-app log tag in send.

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?

  • _uri would force exposing _baseUrl as a getter. Trivial, but offers ~3 lines of dedup. Skipped.
  • _get / _post / _patch / _delete are 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 with http.Client.get / http.Client.post at 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:

  • HttpNotechondriaClient declared as class HttpNotechondriaClient with HttpClientInternalsMixin implements NotechondriaClient.
  • Five @override annotations:
    • 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.dart reduced from ~250 lines (per app) to ~70 lines, keeping only _uri + four verb wrappers. The verb wrappers now call send(...) and headers(...) (no leading underscore) on the host instead of _send / _headers.
  • Call sites of _send / _decode / _headers / _shapedErrorMessage in each app's main client file (core/http_client.dart for editor; core/client.dart for 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 — exports HttpClientInternalsMixin.
  • frontend/editor_app/lib/core/http_client.dart — host class declares with HttpClientInternalsMixin; 5 override annotations; _send / _decode / _headers / _shapedErrorMessage call 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"; HttpClientInternalsMixin removed from the pending list.

Notes

  • All four packages (editor / planner / portal / shared) pass flutter analyze (zero errors) and flutter test smoke suites.
  • §1.5 1000-LOC cap respected. Largest file in the repo unchanged this round: portal app_shell.dart at 971 lines (mixin work doesn't touch app_shell). Editor http_client.dart grew 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 _handleDestinationSelected unused; deprecated surfaceVariant / withOpacity; applyAuthPayload missing @override) carried over from earlier rounds — addressed in the upcoming AppShellSessionMixin round which deletes applyAuthPayload outright.