0.1.107 — sidebar pin diagnostic dedupe + richer payload + Restore-Inbox refreshState fix

Follow-up to a user report that 0.1.105's defensive Inbox pinning still wasn't enough: the user's pin_diagnostics log showed a sustained total=1 pinned=0 warning across ~120 rebuilds, AND tapping "Restore default Inbox" appeared to do nothing. Two diagnostic upgrades + one real-fix land here.

1. Diagnostic now identifies the unpinned row

The 0.1.105 emit was honest but useless on its own: total=1 pinned=0 told us the row failed both the is_default == true and title.toLowerCase() == "inbox" checks, but it did not tell us what the row actually was. So the user's only debugging move was "tap Restore" and re-paste a wall of warnings.

In editor_app/lib/core/build_helpers.dart, emitSidebarPinDiagnostics now describes up to the first 5 rows inline:

total=1 pinned=0 signedIn=true. Rows: #42 "Notes" plain/cloud/drag.

Each row tuple has shape:

#<id> "<title>" <default|plain>/<local|cloud>/<pin|drag>

so a single line tells the operator the row's id, exact title (quoted to surface stray whitespace or casing), default flag, which list it came from (_localCourses vs _courses), and whether isCategoryPinned matched it. Plus signedIn= so we know whether we're in the local-only or signed-in _allCategories branch.

2. Diagnostic deduped per composition change

Before: emitted on every sidebar rebuild — a busy editor logs ~120 lines/second of identical warnings while the user is just typing in the editor. The Debug Log card became unusable.

Constraint: extension methods can't declare instance fields, so the cache had to live on _AppShellState. New private field _lastSidebarPinDiagnosticKey parks the previous emit's (total/pinned/signedIn/rows-summary) key. The extension method reads + writes it directly, which is legal because Dart extensions can access existing fields on the underlying type.

Net effect: one log line per composition change (add/remove a category, flag flip, cloud sync replacing a row, sign-in/out). Editing notes no longer drowns the log.

3. Restore Default Inbox now triggers refreshState

Symptom (user-reported): tapping "Restore default Inbox" produced the success snackbar but the sidebar did not update.

Root cause in editor_app/lib/core/maintenance_actions.dart, _restoreLocalStarterTemplate: it called _seedStarterInboxAlongsideExisting() which mutates _localCourses / _selectedCourse directly (extension methods cannot call setState). The seeder relies on its caller to trigger a rebuild — _loadLocalState does, but _restoreLocalStarterTemplate did not. Without it, the new local Inbox sat in _localCourses until any unrelated rebuild flushed it through.

Fix: explicit if (mounted) refreshState(); immediately after the await _seedStarterInboxAlongsideExisting(); call.

Verification

  • flutter analyze clean across editor_app for the three modified files (app_shell.dart, core/build_helpers.dart, core/maintenance_actions.dart). Only pre-existing info-level lints (prefer_single_quotes, use_string_in_part_of_directives) remain — none new.
  • Diagnostic dedupe is purely additive: the warning still fires the first time pinned == 0 && total > 0 is observed; it re-fires whenever the row tuple changes; it does not fire between identical rebuilds. So we lose nothing diagnostic, just spam.
  • The refreshState() fix is identical in shape to the other if (mounted) refreshState(); calls already sprinkled through maintenance_actions.dart (see _clearLocalData, _emptyDeletedNotes, _pullCloudNotesToLocal).

Operator runbook

  1. Reproduce the original report:
    • Open the editor, open Settings → Debug log, filter source by sidebar.pin_diagnostics.
    • Confirm exactly one debug-level breadcrumb on first paint (no more spam).
    • If you see a pinned=0 total>0 warning, paste the line — the Rows: ... segment now tells us exactly what category is blocking the pin.
  2. Tap Settings → Restore default Inbox → confirm the sidebar immediately gains a pinned "Inbox" row and the diagnostic re-emits with pinned≥1 total≥1.