Notechondria
Version: 0.1.50 Build Date: 2026-04-20T11:00
What's Changed
Four user-reported symptoms, one commit:
- The bare string
"Invalid token."still reached the login UI (an AGENTS.md §1.7 violation that slipped past the 0.1.46 fix). - First login always felt like it failed — the dialog closed, the UI flickered, and then the app silently logged out moments later.
- After login there was no confirmation that the session had actually started.
- 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
VERSION: 0.1.49 → 0.1.50.- Three client files gain
_shapedErrorMessage+setLogger+ DEBUG-logging_send: - Three app_shells tighten
sessionRejected, inline the post-login push, add the success SnackBar, and bind the HTTP logger:
Verification
editor_app/planner_app/portal_app:flutter analyzeissue counts unchanged vs 0.1.49 (54 / 70 / 68); no errors.flutter test test/smoke_test.dartpasses 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.
ApiDebugSnapshotalready keeps a truncated response body for the API debug card; that surface is gated and intentional.