Notechondria

Version: 0.1.48 Build Date: 2026-04-20T09:00

What's Changed

Frontend: phased status indicator replaces the login spinner

User complaint: generic spinners don't reveal which step is stuck. A CircularProgressIndicator tells you "something is happening" but not "we're waiting on the network vs. waiting on disk vs. waiting on the user's CPU to decrypt the response." The user asked for a per-step label that counts up seconds, e.g. "Sending request to backend… (1s)" → "Waiting for backend response… (5s)".

This round ships the widget + wires it into the login dialog. Other spinner sites (registration, email-verify, OAuth bind, etc.) follow the same shape and can adopt the widget incrementally.

New: PhasedStatusIndicator

frontend/notechondria_shared/lib/src/components/phased_status.dart — a StatefulWidget that:

  • Takes a ValueListenable<String> for the phase label.
  • Resets its elapsed-seconds counter every time the phase string changes, so each step has its own countdown instead of a never-ending "total wait" clock (the latter tells you nothing about which step is stuck).
  • Repaints every 200 ms so the seconds tick smoothly — cheap even on a debug build.
  • Pins the counter to 1 s for the first 200 ms so users don't see a "0s" read that looks broken.
  • Empty label hides the widget completely, so the same instance can represent idle + active states.

The widget is re-exported at frontend/notechondria_shared/lib/notechondria_shared.dart:49 so all three apps pick it up automatically.

Login dialog rewired

frontend/notechondria_shared/lib/src/components/auth_dialogs.dart:

  • _EmailPasswordDialogState gains a ValueNotifier<String> _phase and a _phaseFallback Timer.
  • On tap, the phase is seeded synchronously to "Sending request to backend" and a 2-second fallback timer flips it to "Waiting for backend response" if the network call hasn't resolved yet.
  • After the await returns we set "Applying response" long enough for any setState(_submitting = false) to land, then clear the phase.
  • The central CircularProgressIndicator() is replaced by Center(child: PhasedStatusIndicator(phase: _phase)). The small 16×16 spinner inside the indicator stays as a glyph so the widget still reads as "work in progress" at a glance — but the seconds suffix is the real information.

So a login that hangs for 7 seconds now shows:

[0–2s]  Sending request to backend… (1s) → (2s)
[2–7s]  Waiting for backend response… (1s) → (5s)

and the user can immediately tell the backend is the slow part, not client-side validation or JSON decoding.

Why only login this round

The user explicitly flagged the login prompt as the canonical example. Generalizing the widget (phased_status.dart) means registration, email-verify, OAuth callback, and the three app_shell _splashStatus call sites can all adopt the same pattern in follow-up commits without rewriting the pattern each time. The main app bootstrap splash already shows a ticker-style status via _LoadingStatusText — similar shape, different UI placement — so it doesn't need the indicator, but an OAuth bind-flow adoption is a natural next round.

Files Changed

New

Modified

Verification

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

Notes / follow-ups

  • Remaining spinner sites. EmailCodeDialog, InvitationCodeDialog, EmailRegistrationDialog, PasswordResetDialog (request + confirm) still use the "Working..." button label without a phased line. The widget is now available — pick each one up when touched for another reason.
  • OAuth bind callback. Each app_shell's _handleOAuthCallback already has a _splashStatus.value = 'Linking $provider account' line. Consider switching that to a phased cadence too once we need a second iteration.
  • Backend-side phases. The soft 2-second fallback is a client heuristic; the backend doesn't push progress events. If a later round needs true server-side phases, route them through a streaming endpoint or a polling handshake — not this widget's concern.