Notechondria

Version: 0.1.50 Build Date: 2026-04-20T11:00

What's Changed

Four user-reported symptoms, one commit:

  1. The bare string "Invalid token." still reached the login UI (an AGENTS.md §1.7 violation that slipped past the 0.1.46 fix).
  2. First login always felt like it failed — the dialog closed, the UI flickered, and then the app silently logged out moments later.
  3. After login there was no confirmation that the session had actually started.
  4. No DEBUG-level instrumentation for request/response bodies — the user couldn't tell which HTTP call was misbehaving.

1. Client-side 401/403 reshape

DRF raises bare-string AuthenticationFailed("Invalid token."). The detail field landed verbatim in the frontend exception message and from there into the login dialog's FeedbackText. All three apps' core/client.dart now wrap 401/403 bodies at _decode time via a new _shapedErrorMessage helper:

  • 401"Session rejected: Backend.Auth/<METHOD> <path> \u2014 <cause>"
  • 403"Request forbidden: Backend.Auth/<METHOD> <path> \u2014 <cause>"
  • Already-shaped backend messages (containing \u2014) pass through unchanged so we never double-wrap.

Users see the new shape end-to-end. The sessionRejected detector in _loadInitialData still matches (the reshape keeps "invalid token" inside the cause) plus the new "session rejected:" phrase is added to its substring list.

2. "First login fails" root-causes

Two overlapping bugs in _applyAuthPayload:

(a) Double bootstrap race. The handler called _loadInitialData() directly, then _syncAllLocalData(showMessage: false) which also called _loadInitialData(). Both bootstraps hit ~8 authenticated endpoints. If ANY of them returned 401 (server revoke, rate limit, a post-login propagation race), the old sessionRejected check — which fired on a single 401 — wiped the freshly-issued token and kicked the user back to login.

Fix: replaced await _syncAllLocalData(showMessage: false) with inlined _syncAllLocalCourses() + _syncAllLocalDrafts() so the second _loadInitialData() never runs post-login. Wrapped in a try/catch that logs a shaped warning instead of cascading.

(b) sessionRejected was too aggressive. A single flaky 401 should not nuke a session. Tightened the predicate to require AT LEAST TWO 401-shaped errors before clearing _token/_profile. Matched substrings widened to include the new "session rejected:" phrase.

Both fixes applied to all three apps (editor_app/lib/app_shell.dart, planner_app/lib/app_shell.dart, portal_app/lib/app_shell.dart).

3. Login-success SnackBar

_applyAuthPayload now calls _showMessage('Signed in as <username>.') after the bootstrap finishes in every app. The existing _log line under <App>.Auth/applyAuthPayload is still emitted — the SnackBar is a separate visible confirmation that survives after the dialog closes.

4. Per-request DEBUG logs

HttpNotechondriaClient in all three apps now has an optional _logger callback settable via setLogger(...). The _send dispatch wraps every HTTP call with three log points:

  • Before dispatch (DEBUG): "HTTP request sent: <App>.HTTP/request \u2014 <METHOD> <path> (<N>B payload)"
  • Response received: "HTTP response received: <App>.HTTP/response \u2014 <METHOD> <path> \u2192 <status> (<ms>ms, <bytes>B)" at DEBUG for 2xx/3xx, INFO for 4xx, WARNING for 5xx.
  • Network failure (WARNING): "HTTP request failed: <App>.HTTP/request_failed \u2014 <METHOD> <path> (<ms>ms, exc=<ExceptionType>)"

_post and _patch pre-compute the JSON body so the request-sent line can include the exact payload byte count. The logger callback is wired at initState() in each app_shell and feeds the shared DebugLogController, so every HTTP round-trip now appears in the Debug log card alongside the existing UI events.

This answers the "I need to see the results for each backend-frontend communication in logs with level debug" ask directly. The Debug log card's default filter already shows DEBUG entries; no UI change needed.

Files Changed

New

  • docs/versions/0.1.50.md (this file).

Modified

Verification

  • editor_app / planner_app / portal_app: flutter analyze issue counts unchanged vs 0.1.49 (54 / 70 / 68); no errors. flutter test test/smoke_test.dart passes on all three.

Notes / follow-ups

  • Debug log access. The Debug log card still lives inside the Settings tab only. A later round should expose a floating debug button or keyboard shortcut so the user can see what's happening without navigating away from the current surface.
  • sessionRejected ≥2 threshold. If two independent endpoints genuinely 401 during bootstrap (real revoke, not a race), the second bootstrap attempt is gone so recovery needs a manual sign out / sign in. If this becomes a problem, add a one-shot token probe (GET /auth/session/) that's authoritative for the sessionRejected decision instead of inferring from N endpoints.
  • Request body body-preview. We log byte count, not content — tokens and personal notes go over this channel and a body preview in the log card would be a PII leak. ApiDebugSnapshot already keeps a truncated response body for the API debug card; that surface is gated and intentional.