Notechondria

Version: 0.1.82 Build Date: 2026-04-27T01:30

What's Changed

AppShellSessionMixin — eighth and final cross-app mixin shipped

Closes the 0.1.52 → 0.1.82 cross-app deduplication arc. All eight mixins planned in docs/TODO.md are now in notechondria_shared/lib/src/app_shell/ (with one in lib/src/http/). The largest single body consolidated this round: applyAuthPayload was ~120 lines per app and logout was ~35 lines — the biggest dedup target left after the 0.1.78–0.1.81 sweep handled the smaller mixins.

  • New frontend/notechondria_shared/lib/src/app_shell/app_shell_session_mixin.dart exposes two concrete public methods (the only auth surface the per-app code calls into):
    • applyAuthPayload(payload) — establishes a new session after password login / OAuth / email-verify / session-restore. Fetches the user's server /settings/, reconciles client-vs-server app_settings timestamps, applies local theme + log-preference settings, stamps token / profile / settings on the State, fires loadInitialData(), and pushes any locally-created courses + drafts. On a 401 / 5xx during settings fetch, falls back to cached local settings so the user still gets a usable session.
    • logout() — best-effort cloud logout, then clears token / profile / settings / deletedNotes plus per-app session metadata, drops the persisted session, and refires loadInitialData() so the UI lands on the anonymous front page.

Five hooks for per-app session-shape divergences

The TODO underestimated how much the three apps' bodies actually diverged. These hooks isolate the differences:

  • applySessionMetadata(payload) — editor populates _currentSessionId / _multiDevice / _otherSessionsCount from the 0.1.65 multi-device payload shape. Planner / portal default no-op.
  • clearSessionMetadata() — editor clears those three fields on logout. Planner / portal no-op.
  • clearAppSpecificSessionFields() — planner + portal use this to reset _plannerEvents on logout. Editor no-op.
  • persistSession(token, user) — editor calls _LocalAppStore.saveSession. Planner / portal default no-op (they don't persist the session token client-side; every cold boot re-authenticates).
  • clearPersistedSession() — editor calls _LocalAppStore.clearSession. Same per-app pattern.

Per-app abstract surface (forwarders to existing helpers)

Five method hooks each app's State overrides to forward to the existing per-app core/...dart helpers — no new code, just public-named entry points the mixin can call:

  • currentAppSettingsPayload({themePreset, themeMode, apiBaseUrl})_currentAppSettingsPayload(...)
  • applyLocalAppSettings(settings, {persist})_applyLocalAppSettings(...)
  • loadInitialData()_loadInitialData()
  • syncAllLocalCourses()_syncAllLocalCourses()
  • syncAllLocalDrafts()_syncAllLocalDrafts()

Plus the read-write field surface this mixin needs:

  • set token / get profile + set / get settings + set / get deletedNotes + set — all one-liners on each State.

Behavioral change: editor's api_base_url safety promoted to all apps

Editor's 0.1.66 fix — api_base_url is CLIENT-side state and the server's stored value (which defaults to http://localhost:9080/api/v1) must NEVER overwrite the user's real URL on login — was promoted to the shared mixin. Planner + portal now get the same protection for free. Their old applyAuthPayload bodies used settings['api_base_url'] as the fallback in the build-from-defaults branch, which would have silently broken any planner / portal user whose server app_settings was empty (e.g. first login from a fresh device). Strict improvement, but worth flagging.

Style change: setStaterefreshState

Planner + portal's old applyAuthPayload wrapped the field mutation in setState(() { _token = token; ... }). Editor used inline assign + a separate refreshState() call. The mixin can't call protected setState, so it uses refreshState() (equivalent to if (mounted) setState(() {})). Functionally the same — the rebuild fires after the assignments either way.

_logout extension renamed logout (planner + portal)

Planner + portal's old logout flow lived in core/logout.dart as an underscored extension method _logout. The mixin's public logout() replaces both. Two places needed updating:

  • core/logout.dart reduced to a documentation shim (file kept so the parts manifest in lib/main.dart doesn't break).
  • One call site per app: onLogout: _logoutonLogout: logout.

Files Changed

  • VERSION — 0.1.81 → 0.1.82.
  • frontend/notechondria_shared/lib/src/app_shell/app_shell_session_mixin.dart — new shared mixin (~340 lines, well under §1.5 1000-LOC cap).
  • frontend/notechondria_shared/lib/notechondria_shared.dart — exports AppShellSessionMixin.
  • frontend/editor_app/lib/app_shell.dart — mixin slot + 14 override one-liners; applyAuthPayload + logout deleted inline (~165 lines removed); stale _parseUpdatedAt helper was already in settings_helpers.dart so unaffected.
  • frontend/planner_app/lib/app_shell.dart — mixin slot + 12 override one-liners (no multi-device); applyAuthPayload + _parseUpdatedAt deleted inline (~120 lines).
  • frontend/planner_app/lib/core/logout.dart — reduced to a documentation shim.
  • frontend/portal_app/lib/app_shell.dart — same shape as planner.
  • frontend/portal_app/lib/core/logout.dart — shim.
  • docs/TODO.md — mixin entry rewritten "1 of 8" → "8 of 8 COMPLETE"; whole bullet checked off.

Notes

  • All eight cross-app mixins now shipped. The remaining work in app_shell/-style code is per-app (UI surfaces, app-specific state, _LocalAppStore schemas) and shouldn't have further byte-identical chunks across the three apps.
  • All four packages (editor / planner / portal / shared) pass flutter analyze (zero errors) and flutter test smoke suites.
  • §1.5 1000-LOC cap respected — and notably, both planner and portal app_shell.dart SHRANK by ~67 lines this round (971 → 904 portal; 969 → 902 planner). The cap pressure flagged in 0.1.80.md is now resolved. Largest file in the repo is learner_note_editor.dart at 939 (planner / portal).
  • Behavioral micro-changes in planner + portal:
    • api_base_url no longer falls back to the server's value in the build-from-defaults branch of applyAuthPayload (now always uses the local value, matching editor's 0.1.66 fix).
    • Field mutations during applyAuthPayload use refreshState() instead of an explicit setState(() { ... }) block; same end result. Both changes are net-positive correctness improvements; flagged here so they don't surprise anyone reviewing the diff.
  • Pre-existing unrelated warnings on planner / portal app_shell (_handleDestinationSelected unused; deprecated surfaceVariant / withOpacity) carried over — separate from this round.
  • The two core/logout.dart shim files in planner / portal can be deleted entirely once a future round regenerates the parts manifest in each app's lib/main.dart. Left for now to keep this round's diff focused on the mixin port.