Notechondria

Version: 0.1.75 Build Date: 2026-04-26T20:00

What's Changed

Multi-device session manager — frontend (editor)

0.1.65 shipped the backend: creators.Session model, the MultiSessionAuthentication DRF class, auth_payload augmented to return {multi_device, other_sessions_count, session:{id, device_label, …}}, and two new endpoints (GET /api/v1/auth/sessions/ and DELETE /api/v1/auth/sessions/<id>/). Frontend was tracked in docs/TODO.md as still-to-do; this round delivers it for editor_app and stages the shared building blocks so planner_app / portal_app can adopt the same UI when their Settings pages get the Apple-style sub-page treatment.

Shared AuthClient interface — two new methods

  • Future<Map<String, dynamic>> listSessions(String token) and Future<void> revokeSession(String token, int sessionId) added to notechondria_shared/lib/src/app_shell/auth_client.dart. Each app's NotechondriaClient already declares implements AuthClient, so the new methods auto-inherit as abstract. (Note: main 0.1.67 separately landed equivalent declarations on the shared interface; this round consolidates them with the same signatures so behaviour is unchanged.)

Concrete HTTP client implementations (all three apps)

  • editor_app/lib/core/http_client.dart, planner_app/lib/core/client.dart, and portal_app/lib/core/client.dart each gain the two @override implementations:
    • listSessionsGET /auth/sessions/ then _decode.
    • revokeSessionDELETE /auth/sessions/<id>/ then _decode. The 204 No Content response decodes to {}; non-2xx surfaces as an exception just like every other client method.

applyAuthPayload captures multi-device metadata

  • editor_app/lib/app_shell.dart adds three new fields on _AppShellState:

    • int? _currentSessionId — id of the row that owns the current bearer token. Used by ActiveSessionsCard to flag "This device" without re-asking the backend.
    • bool _multiDevicetrue when the current user has more than one active session. Drives the warning banner above the Settings menu.
    • int _otherSessionsCount — display string for the banner.

    applyAuthPayload reads these from the 0.1.65 payload shape; unknown fields (older backend) silently default to null/false/0 so the app keeps working against pre-0.1.65 deployments. logout resets all three so a stale "1 other device" banner doesn't linger after sign-out.

ActiveSessionsCard widget (shared)

  • New notechondria_shared/lib/src/components/active_sessions_card.dart. Self-contained: takes onListSessions / onRevokeSession / onCurrentRevoked callbacks and handles its own loading / error / refresh state. Each row shows the device label (icon-mapped from User-Agent: iOS / Android / iPad / Mac / Windows / Linux / generic), last-seen + created timestamps via formatCompactTimestamp, an IP-fingerprint hint, a "This device" pill on the current session, and a trash button that opens a confirm-with-context dialog before calling onRevokeSession. After a non-current revoke the card re-fetches; after revoking the caller's own session it fires onCurrentRevoked so the host can run its local sign-out flow.

Editor _SignInSecurityPage hosts the card

  • editor_app/lib/modules/settings_pages.dart_SignInSecurityPage now renders ActiveSessionsCard above the existing _ConnectedAccountsSection. Hidden when signed out (onListSessions == null). The card's onCurrentRevoked callback drops the local token, resets all three new session fields, clears the persisted session, and calls _loadInitialData() so the app drops back to the anonymous view immediately.

Multi-device warning banner

  • editor_app/lib/modules/settings.dart_buildMultiDeviceBanner(context) renders a tertiaryContainer-tinted Card above the Settings menu when _multiDevice && _otherSessionsCount > 0. Tap dives into the _SignInSecurityPage so the user can audit + revoke without hunting through the menu. Mirrors iOS' "Signed in on N other devices" pattern. Hidden completely (no spacing) when the user is on a single device, signed out, or running against an older backend that doesn't return the flag.

Renumbering note

This round was originally authored on the codex branch as 0.1.71 alongside the four cross-branch round-logs (0.1.71–0.1.74). Per the mapping documented in docs/versions/0.1.74.md, codex 0.1.71 (multi-device frontend) lands on main as 0.1.75 to preserve TODO rule #6 (third-digit monotonic across the repo history). Content is unchanged from the original codex 0.1.71 commit.

Files Changed

  • frontend/notechondria_shared/lib/src/app_shell/auth_client.dartlistSessions + revokeSession declarations (deduped against main 0.1.67's equivalent additions).
  • frontend/notechondria_shared/lib/src/components/active_sessions_card.dart (new) — the card.
  • frontend/notechondria_shared/lib/notechondria_shared.dart — exports ActiveSessionsCard.
  • frontend/editor_app/lib/core/http_client.dartlistSessions + revokeSession overrides (deduped against main 0.1.67's equivalent additions; kept the variant with the 204-No-Content comment).
  • frontend/planner_app/lib/core/client.dart — same dedup.
  • frontend/portal_app/lib/core/client.dart — same dedup.
  • frontend/editor_app/lib/app_shell.dart_currentSessionId, _multiDevice, _otherSessionsCount fields; payload reads in applyAuthPayload; reset in logout.
  • frontend/editor_app/lib/core/build_helpers.dartonListSessions, onRevokeSession, onCurrentSessionRevoked, multiDevice, otherSessionsCount props threaded into _SettingsPage.
  • frontend/editor_app/lib/modules/settings.dart — three new _SettingsPage props; _buildMultiDeviceBanner helper; banner placement in build().
  • frontend/editor_app/lib/modules/settings_pages.dart_SignInSecurityPage renders ActiveSessionsCard.
  • docs/TODO.md — multi-device frontend item rewritten to scope only planner_app / portal_app.
  • VERSION — 0.1.74 → 0.1.75.

Notes

  • All four packages (editor / planner / portal / shared) pass their smoke tests after this round. All editor module files remain under the §1.5 1000-line cap.
  • planner_app and portal_app's Settings pages still use the pre-0.1.68 flat layout; dropping the ActiveSessionsCard there is blocked on porting the sub-page architecture, which is tracked separately in docs/TODO.md.