Skip to content

Sync v1.2.0 - #3

Merged
Project516 merged 1 commit into
mainfrom
sync/v1.2.0
Aug 7, 2026
Merged

Sync v1.2.0#3
Project516 merged 1 commit into
mainfrom
sync/v1.2.0

Conversation

@SpectrumFRC3847

@SpectrumFRC3847 SpectrumFRC3847 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Desktop gets real offline support and a keyboard path.

Firestore reads on desktop now come from a per-account cache, so the app keeps showing event data when the venue wifi drops, including after a relaunch with no connection at all.

Every desktop screen is now reachable from the keyboard, without reaching for a mouse.

Sign-in handles being revoked mid-session instead of leaving the app looking signed in, and a photo upload no longer fails outright when the token refresh cannot reach the network.

Accessibility: the map diagram and its pins are announced to screen readers, and the tab bar stays usable at 200 percent text size.

Fixed a desktop launcher entry that broke when the install path contained a percent sign or a backslash.

Summary by CodeRabbit

  • New Features

    • Added Ctrl/Command+S saving and left/right keyboard navigation across editors.
    • Improved map accessibility with descriptive labels for diagrams and pins.
    • Added installation guidance for all supported platforms and sideloading options.
    • Added user-specific desktop data caching.
  • Bug Fixes

    • Improved handling of expired, revoked, offline, and interrupted sign-in sessions.
    • Prevented stale edits from overwriting newer changes.
    • Improved update checks when a download source is unavailable.
    • Improved desktop launching for paths containing special characters.
    • Reduced layout issues at large text sizes and narrow screen widths.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR configures automated reviews and installation documentation, adds desktop authentication cleanup with user-scoped Firestore caching, hardens service and mutation error handling, and adds keyboard shortcuts, accessibility semantics, and large-text layout coverage across Flutter editors.

Changes

Platform, service, and editor updates

Layer / File(s) Summary
Release and review configuration
.pr_agent.toml, README.md, pubspec.yaml
Adds PR-Agent settings, platform installation instructions, and release metadata updates.
Desktop authentication and scoped cache lifecycle
lib/main.dart, lib/src/services/desktop_auth_service.dart, lib/src/services/desktop_firestore_cache_*.dart, lib/src/services/user_scoped_firestore_cache.dart, test/desktop_auth_service_test.dart, test/user_scoped_firestore_cache_test.dart
Adds serialized session teardown, revoked-session handling, session callbacks, user-scoped desktop caching, and lifecycle tests.
Service error handling and mutation ordering
lib/src/services/desktop_launcher_service.dart, lib/src/services/desktop_update_service.dart, lib/src/services/spectrum_auth_service.dart, lib/src/services/photo_service.dart, lib/src/services/synced_map_image_store.dart, lib/src/state/pit_controller_mixin.dart, test/desktop_*_test.dart, test/synced_map_image_store_test.dart, test/support/*
Preserves launcher escaping, improves update failure handling, persists uploaded photos, reports remote clear failures, and prevents stale mutation rollback.
Editor shortcuts and accessible layouts
lib/src/widgets/keyboard_shortcuts.dart, lib/src/ui/*_tab.dart, lib/src/ui/location_code.dart, test/keyboard_shortcuts_test.dart, test/maps_tab_test.dart, test/large_text_layout_test.dart, test/borrow_tab_test.dart
Adds save and navigation shortcuts, rich-text labels, map semantics, flexible location text, and widget coverage for shortcuts, accessibility, and large text.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

  • Spectrum3847/SpectrumPit#202 — The launcher escaping changes add the percent and backslash handling described by this issue.

Possibly related PRs

  • Spectrum3847/spectrum-pit#1 — Earlier authentication, Firestore, desktop service, controller, and UI code is extended by this PR.
  • Spectrum3847/spectrum-pit#2 — The PRs overlap in desktop services, authentication, photo handling, diagram synchronization, controller state, and UI changes.

Suggested reviewers: project516

Poem

A rabbit taps Save with a light little hop,
While caches know which user’s burrow to stop.
Revoked sessions tidy each trail,
Stale mutations no longer prevail.
Maps speak clearly, and layouts grow bright—
The codebase thumps softly through day and night.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title identifies the Sync 1.2.0 release but does not describe the primary changes, such as desktop offline caching and keyboard navigation. Use a descriptive title that highlights the main change, for example: "Add desktop offline caching and keyboard navigation".
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sync/v1.2.0

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/src/state/pit_controller_mixin.dart (1)

56-71: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Order server writes per item ID.

_pitMutations only disables stale local rollback; upsert and delete still start remote writes concurrently and the sync services always complete deletes and last upserts. If an updated upsert for an item completes after a delete, the server can keep restoring the deleted item. Queue writes per item ID, or include/use a revision the server rejects when stale.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/state/pit_controller_mixin.dart` around lines 56 - 71, Serialize
remote writes per item ID in the mutation flow around _pitBeginMutation,
pitUpsertRemote, and the corresponding delete operation, ensuring each item’s
upsert/delete executes only after its prior write completes. Preserve existing
stale-mutation rollback behavior while preventing an older write from completing
after a newer operation; use the existing per-item mutation state or a dedicated
per-ID queue rather than global serialization.
🧹 Nitpick comments (9)
lib/src/services/desktop_firestore_cache_io.dart (1)

16-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The currentUid closure here is dead.

clearForUid(uid) calls _cacheFor(uid) directly and never reads currentUid. The currentUid: () => uid argument at line 21 has no effect. The code reads as if the current-user state matters for the clear, which it does not.

Consider exposing a static or top-level clear on UserScopedFirestoreCache that takes only the root and the uid, so the caller does not have to supply a meaningless closure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/services/desktop_firestore_cache_io.dart` around lines 16 - 23,
Remove the unused currentUid dependency from clearDesktopFirestoreCacheFor by
exposing and calling a static or top-level UserScopedFirestoreCache clear
operation that accepts only root and uid. Preserve the existing cache-root
lookup and clearForUid behavior without constructing the instance with a
meaningless closure.
lib/src/services/desktop_auth_service.dart (3)

181-193: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

_runTeardown does not chain onto a pending teardown.

_ensureAuthStateListener at lines 82-93 chains each setup onto the previous one. _runTeardown does not use the same pattern. It overwrites _teardown at line 191 without awaiting the previous value.

Two concurrent signOut() calls reach this path. signOut() does not await _teardown first, unlike signIn() at line 140. Both calls then run _forgetStoredSession() and _endSession(uid), so onSessionEnded fires twice for the same uid. The first teardown to finish also resets _endingSession to false at line 188 while the second is still running, which reopens the _handleSessionRevoked guard early.

clearDesktopFirestoreCacheFor is idempotent, so the current impact is a duplicate callback rather than data loss. Chaining makes the behavior match the listener-setup path.

♻️ Suggested change
   Future<void> _runTeardown(String? uid) {
     _endingSession = true;
+    final previous = _teardown;
     final done = () async {
+      await previous;
       try {
         await _forgetStoredSession();
         if (uid != null) await _endSession(uid);
       } finally {
         _endingSession = false;
       }
     }();
     _teardown = done;
     return done;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/services/desktop_auth_service.dart` around lines 181 - 193, Update
_runTeardown to await and chain after the existing _teardown promise before
running _forgetStoredSession and _endSession. Preserve the serialized teardown
ordering and ensure _endingSession remains true until the entire queued teardown
sequence completes, preventing duplicate session-ending callbacks.

96-136: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add diagnostics to the silent catch at line 135.

The validation and cleanup logic is correct and matches the tests. One gap: the outer catch (_) {} discards every failure from _prefsLoader(), _session.restore(), and _endSession(). A restore that fails for a non-network reason leaves the app signed out with no signal about the cause.

lib/main.dart line 70 already uses debugPrint for the equivalent Firebase bootstrap failure. Consider matching that.

♻️ Suggested change
-    } catch (_) {}
+    } catch (error) {
+      debugPrint('Desktop session restore failed: $error');
+    }

Note that line 130 re-reads payload['uid']. The local uid from line 110 is already a validated non-empty String at that point and can be used directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/services/desktop_auth_service.dart` around lines 96 - 136, Update
initialize() so the outer catch logs the caught failure with debugPrint,
matching the existing Firebase bootstrap diagnostics in main.dart, while
preserving the current silent-recovery behavior. In the restore-failure cleanup
branch, reuse the already validated local uid instead of reading payload['uid']
again when calling _endSession.

82-93: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider isolating errors retained by _listenerSetup.

_listenerSetup stores the setup future permanently. If cancel() or listen() throws, the stored future completes with an error. Every later await _listenerSetup then rethrows. Line 210 in dispose() is the important one: the rethrow skips _controller.close() and _session.close(), so the stream controller and the session are never released.

The chained await previous at line 85 has the same problem. One failed setup poisons all later initialize() calls.

♻️ Suggested guard
   Future<void> _ensureAuthStateListener() {
     final previous = _listenerSetup;
     final next = () async {
-      await previous;
-      await _authStateSub?.cancel();
-      _authStateSub = _session.authStateChanges.listen((user) {
-        if (user == null) unawaited(_handleSessionRevoked());
-      });
+      try {
+        await previous;
+        await _authStateSub?.cancel();
+        _authStateSub = _session.authStateChanges.listen((user) {
+          if (user == null) unawaited(_handleSessionRevoked());
+        });
+      } catch (_) {}
     }();
     _listenerSetup = next;
     return next;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/services/desktop_auth_service.dart` around lines 82 - 93, Update
_ensureAuthStateListener so failures from cancel() or listen() do not
permanently poison _listenerSetup or the await previous chain. Isolate or
recover setup errors before storing the future, while preserving serialized
listener setup; ensure later initialize() calls and dispose() can continue to
close _controller and _session even after a setup failure.
test/user_scoped_firestore_cache_test.dart (1)

51-58: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Consider extending the rejected-uid cases.

The test covers the POSIX traversal form '../elsewhere'. The allowlist regex in _safeUid also rejects the Windows form r'..\elsewhere', an absolute path such as '/etc/passwd', a drive-qualified path such as r'C:\temp', the empty string, and a uid longer than 64 characters.

Because this regex is the security boundary for the cache directory, a table of rejected inputs guards against a future relaxation of the pattern.

♻️ Suggested change
-  test('a uid that could escape the root gets no cache', () async {
-    final subject = cache();
-    uid = '../elsewhere';
-    await subject.write('doc/teams/1234', '{"a":1}');
-
-    expect(await subject.read('doc/teams/1234'), isNull);
-    expect(root.listSync(), isEmpty);
-  });
+  for (final bad in <String>[
+    '../elsewhere',
+    r'..\elsewhere',
+    '/etc/passwd',
+    r'C:\temp',
+    '',
+    'a' * 65,
+  ]) {
+    test('a uid that could escape the root gets no cache: "$bad"', () async {
+      final subject = cache();
+      uid = bad;
+      await subject.write('doc/teams/1234', '{"a":1}');
+
+      expect(await subject.read('doc/teams/1234'), isNull);
+      expect(root.listSync(), isEmpty);
+    });
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/user_scoped_firestore_cache_test.dart` around lines 51 - 58, Extend the
cache security test around the existing “uid that could escape the root” case
into a table-driven test covering Windows traversal, absolute and
drive-qualified paths, the empty uid, and a uid exceeding 64 characters. For
every rejected uid, assert that writing produces no readable cache entry and
leaves root.listSync() empty, preserving the existing '../elsewhere' coverage.
test/desktop_auth_service_test.dart (1)

287-295: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer pumpEventQueue() over a single zero-duration delay.

Lines 289 and 343 flush exactly one microtask turn before asserting. The teardown they wait for is a chain: the authStateChanges listener calls _handleSessionRevoked, which awaits _runTeardown, which awaits _forgetStoredSession (a SharedPreferences load plus a remove) and then _endSession.

A single Future<void>.delayed(Duration.zero) is not guaranteed to drain that chain. The risk cuts both ways. The assertion at line 346 checks that a duplicate teardown did not happen, so an under-drained queue makes that test pass even if the dedup guard is broken.

pumpEventQueue() from flutter_test drains the queue until it is empty.

♻️ Suggested change
-    await Future<void>.delayed(Duration.zero);
+    await pumpEventQueue();

The same applies to line 308.

Also applies to: 341-347

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/desktop_auth_service_test.dart` around lines 287 - 295, Replace the
single zero-duration delays in the affected auth teardown tests, including the
checks around the authStateChanges listener and duplicate teardown assertion,
with Flutter’s pumpEventQueue(). Ensure the queue is fully drained before
asserting session state, endedFor, or stored preferences, while preserving the
existing assertions and test behavior.
.pr_agent.toml (3)

58-58: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Keep pubspec.lock in automated review scope.

.pr_agent.toml ignores both the explicit pubspec.lock entry and the broad **/*.lock glob. For firestore_client, pubspec.yaml points to a Git source while pubspec.lock records the resolved Git commit under packages.firestore_client.description. Remove pubspec.lock from this ignore list or narrow the lockfile ignores so application Git dependency resolvers stay in the diff.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.pr_agent.toml at line 58, Update the ignore configuration in .pr_agent.toml
to stop excluding pubspec.lock, removing its explicit entry and adjusting the
broad **/*.lock rule as needed so application lockfiles containing Git
dependency resolutions remain in automated review scope.

9-20: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Use a distinct fallback model for review availability.

fallback_models points to the same openrouter/openrouter/free identifier used by model, so PR-Agent has no separate secondary model if the router fails. Pin a specific primary model and use a different supported fallback, or remove the duplicate fallback if free-router routing is intentional and document the failure mode.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.pr_agent.toml around lines 9 - 20, Update the PR-Agent model configuration
so fallback_models does not duplicate the primary model identifier. Either pin
model to a specific supported OpenRouter model and configure a distinct
supported fallback, or remove the duplicate fallback when retaining free-router
routing and document the expected failure behavior.

22-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move PR-Agent automation flags to supported sections.

Put GitHub Action automations under [github_action_config]: use auto_review, auto_describe, auto_improve, and pr_actions for ready/draft behavior. Put ignore_bot_pr = true under [github] instead of ignore_bot_prs in [config], and remove require_ready_for_review unless a PR-Agent version supports that exact key.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.pr_agent.toml around lines 22 - 30, Update the PR-Agent configuration by
moving the automation settings into [github_action_config] using auto_review,
auto_describe, auto_improve, and pr_actions for ready/draft behavior. Move the
bot-ignore setting to [github] as ignore_bot, and remove unsupported
require_ready_for_review unless the installed PR-Agent version explicitly
supports it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/src/services/desktop_auth_service.dart`:
- Around line 164-171: Update signOut() to guarantee teardown runs even when
_session.signOut() throws, using the existing _runTeardown(departingUid) flow so
_endingSession is reset, persisted session data is removed, and signedOut is
emitted. Add a test covering a SocketException from _session.signOut() that
verifies the signed-out state, onSessionEnded callback, and persisted-key
removal.

In `@lib/src/services/photo_service.dart`:
- Around line 112-113: Update the disk-cache write path in the photo service to
serialize writes with deletion: do not fire-and-forget PhotoDiskCache.write.
Await it and coordinate any subsequent remove/delete so a delayed write cannot
recreate a deleted key, using per-key operation queuing if needed. Add a
regression test that delays write, deletes the same key, and verifies no disk
entry remains.

In `@lib/src/widgets/keyboard_shortcuts.dart`:
- Around line 27-50: Integrate HorizontalStepShortcuts into the relevant desktop
screen widgets, wrapping their content and supplying onPrevious and onNext
callbacks for the screen’s horizontal navigation. Ensure the callbacks are wired
before relying on the existing arrow-key bindings, while preserving the current
screen content behavior.

In `@README.md`:
- Line 13: Update the release artifact documentation around the release links
and platform installation instructions to publish a release-generated SHA-256
manifest and direct users to verify downloaded files against it before bypassing
SmartScreen, Gatekeeper, or other unsigned-artifact warnings.

---

Outside diff comments:
In `@lib/src/state/pit_controller_mixin.dart`:
- Around line 56-71: Serialize remote writes per item ID in the mutation flow
around _pitBeginMutation, pitUpsertRemote, and the corresponding delete
operation, ensuring each item’s upsert/delete executes only after its prior
write completes. Preserve existing stale-mutation rollback behavior while
preventing an older write from completing after a newer operation; use the
existing per-item mutation state or a dedicated per-ID queue rather than global
serialization.

---

Nitpick comments:
In @.pr_agent.toml:
- Line 58: Update the ignore configuration in .pr_agent.toml to stop excluding
pubspec.lock, removing its explicit entry and adjusting the broad **/*.lock rule
as needed so application lockfiles containing Git dependency resolutions remain
in automated review scope.
- Around line 9-20: Update the PR-Agent model configuration so fallback_models
does not duplicate the primary model identifier. Either pin model to a specific
supported OpenRouter model and configure a distinct supported fallback, or
remove the duplicate fallback when retaining free-router routing and document
the expected failure behavior.
- Around line 22-30: Update the PR-Agent configuration by moving the automation
settings into [github_action_config] using auto_review, auto_describe,
auto_improve, and pr_actions for ready/draft behavior. Move the bot-ignore
setting to [github] as ignore_bot, and remove unsupported
require_ready_for_review unless the installed PR-Agent version explicitly
supports it.

In `@lib/src/services/desktop_auth_service.dart`:
- Around line 181-193: Update _runTeardown to await and chain after the existing
_teardown promise before running _forgetStoredSession and _endSession. Preserve
the serialized teardown ordering and ensure _endingSession remains true until
the entire queued teardown sequence completes, preventing duplicate
session-ending callbacks.
- Around line 96-136: Update initialize() so the outer catch logs the caught
failure with debugPrint, matching the existing Firebase bootstrap diagnostics in
main.dart, while preserving the current silent-recovery behavior. In the
restore-failure cleanup branch, reuse the already validated local uid instead of
reading payload['uid'] again when calling _endSession.
- Around line 82-93: Update _ensureAuthStateListener so failures from cancel()
or listen() do not permanently poison _listenerSetup or the await previous
chain. Isolate or recover setup errors before storing the future, while
preserving serialized listener setup; ensure later initialize() calls and
dispose() can continue to close _controller and _session even after a setup
failure.

In `@lib/src/services/desktop_firestore_cache_io.dart`:
- Around line 16-23: Remove the unused currentUid dependency from
clearDesktopFirestoreCacheFor by exposing and calling a static or top-level
UserScopedFirestoreCache clear operation that accepts only root and uid.
Preserve the existing cache-root lookup and clearForUid behavior without
constructing the instance with a meaningless closure.

In `@test/desktop_auth_service_test.dart`:
- Around line 287-295: Replace the single zero-duration delays in the affected
auth teardown tests, including the checks around the authStateChanges listener
and duplicate teardown assertion, with Flutter’s pumpEventQueue(). Ensure the
queue is fully drained before asserting session state, endedFor, or stored
preferences, while preserving the existing assertions and test behavior.

In `@test/user_scoped_firestore_cache_test.dart`:
- Around line 51-58: Extend the cache security test around the existing “uid
that could escape the root” case into a table-driven test covering Windows
traversal, absolute and drive-qualified paths, the empty uid, and a uid
exceeding 64 characters. For every rejected uid, assert that writing produces no
readable cache entry and leaves root.listSync() empty, preserving the existing
'../elsewhere' coverage.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9b447a28-4293-42bd-883b-02a37b026135

📥 Commits

Reviewing files that changed from the base of the PR and between 2d1a29f and 2e8eb75.

⛔ Files ignored due to path filters (1)
  • pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (32)
  • .pr_agent.toml
  • README.md
  • lib/main.dart
  • lib/src/services/desktop_auth_service.dart
  • lib/src/services/desktop_firestore_cache_io.dart
  • lib/src/services/desktop_firestore_cache_stub.dart
  • lib/src/services/desktop_launcher_service.dart
  • lib/src/services/desktop_update_service.dart
  • lib/src/services/photo_service.dart
  • lib/src/services/spectrum_auth_service.dart
  • lib/src/services/synced_map_image_store.dart
  • lib/src/services/user_scoped_firestore_cache.dart
  • lib/src/state/pit_controller_mixin.dart
  • lib/src/ui/borrow_tab.dart
  • lib/src/ui/inventory_tab.dart
  • lib/src/ui/location_code.dart
  • lib/src/ui/maps_tab.dart
  • lib/src/ui/packing_tab.dart
  • lib/src/ui/schedule_tab.dart
  • lib/src/widgets/keyboard_shortcuts.dart
  • pubspec.yaml
  • test/borrow_tab_test.dart
  • test/desktop_auth_service_test.dart
  • test/desktop_launcher_service_test.dart
  • test/desktop_update_service_test.dart
  • test/keyboard_shortcuts_test.dart
  • test/large_text_layout_test.dart
  • test/maps_tab_test.dart
  • test/support/fake_map_diagram_sync_service.dart
  • test/support/photo_test_support.dart
  • test/synced_map_image_store_test.dart
  • test/user_scoped_firestore_cache_test.dart

Comment on lines 164 to +171
@override
Future<void> signOut() async {
_endingSession = true;
final departingUid = currentUser?.uid;
await _session.signOut();
await _runTeardown(departingUid);
_emit(const SpectrumAuthSnapshot(state: SpectrumAuthState.signedOut));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

signOut() leaves the service permanently stuck if _session.signOut() throws.

Line 166 sets _endingSession = true. Line 168 then awaits _session.signOut(). _endingSession is only reset in the finally inside _runTeardown at line 188. If line 168 throws, line 169 never runs, so the flag is never reset.

Two consequences follow:

  1. _handleSessionRevoked returns early forever at line 174. Mid-session revocation is no longer detected for the life of the service.
  2. No signedOut snapshot is emitted, and the stored session is never removed. The UI still shows the user as signed in, and the departing user's cached documents stay on disk.

_session.signOut() performs a network call. A sign-out attempted with no network is a realistic trigger. This is the offline path the PR targets.

🐛 Proposed fix
   `@override`
   Future<void> signOut() async {
     _endingSession = true;
     final departingUid = currentUser?.uid;
-    await _session.signOut();
+    try {
+      await _session.signOut();
+    } catch (_) {
+      // The local session still has to be torn down when the revoke call
+      // cannot reach the network.
+    }
     await _runTeardown(departingUid);
     _emit(const SpectrumAuthSnapshot(state: SpectrumAuthState.signedOut));
   }

Add a test that makes _session.signOut() throw a SocketException and asserts that the state becomes signedOut, that onSessionEnded fires, and that the persisted key is removed.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@override
Future<void> signOut() async {
_endingSession = true;
final departingUid = currentUser?.uid;
await _session.signOut();
await _runTeardown(departingUid);
_emit(const SpectrumAuthSnapshot(state: SpectrumAuthState.signedOut));
}
`@override`
Future<void> signOut() async {
_endingSession = true;
final departingUid = currentUser?.uid;
try {
await _session.signOut();
} catch (_) {
// The local session still has to be torn down when the revoke call
// cannot reach the network.
}
await _runTeardown(departingUid);
_emit(const SpectrumAuthSnapshot(state: SpectrumAuthState.signedOut));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/services/desktop_auth_service.dart` around lines 164 - 171, Update
signOut() to guarantee teardown runs even when _session.signOut() throws, using
the existing _runTeardown(departingUid) flow so _endingSession is reset,
persisted session data is removed, and signedOut is emitted. Add a test covering
a SocketException from _session.signOut() that verifies the signed-out state,
onSessionEnded callback, and persisted-key removal.

Comment on lines +112 to +113
final disk = _diskCache;
if (disk != null) unawaited(disk.write(key, photo.bytes));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Repository files matching photo_service.dart:\n'
fd -a 'photo_service\.dart$' . || true

printf '\nRelevant file outline:\n'
for f in $(fd 'photo_service\.dart$' .); do
  echo "--- $f"
  wc -l "$f"
  ast-grep outline "$f" --view compact || true
done

printf '\nRelevant sections:\n'
for f in $(fd 'photo_service\.dart$' .); do
  echo "--- $f lines 1-220"
  sed -n '1,220p' "$f" | nl -ba
done

printf '\nSearch cache implementations and usages:\n'
rg -n "class PhotoDiskCache|write\(|remove\(|unawaited|delete\\(" . -S --glob '!build/**' --glob '!dist/**' || true

Repository: Spectrum3847/spectrum-pit

Length of output: 638


🏁 Script executed:

#!/bin/bash
set -euo pipefail

f="$(fd 'photo_service\.dart$' . | head -n 1)"

echo "--- $f outline"
ast-grep outline "$f" --view expanded || true

echo "\n--- $f lines 1-220"
awk '{print NR ": " $0}' "$f" | sed -n '1,220p'

echo "\n--- cache definitions/usages"
rg -n "class PhotoDiskCache|PhotoDiskCache|write\\(|remove\\(|unawaited|void upload|upload\\(|void delete|delete\\(" . -S --glob '!build/**' --glob '!dist/**' || true

Repository: Spectrum3847/spectrum-pit

Length of output: 21584


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- lib/src/services/photo_disk_cache.dart"
awk '{print NR ": " $0}' lib/src/services/photo_disk_cache.dart

echo "\n--- test/photo_disk_cache_test.dart"
awk '{print NR ": " $0}' test/photo_disk_cache_test.dart

echo "\n--- test/photo_service_test.dart relevant tests"
awk '{print NR ": " $0}' test/photo_service_test.dart | sed -n '1,110p'

echo "\n--- deterministic call-order probe: write/removes scheduled concurrently without queue"
python3 - <<'PY'
from dataclasses import dataclass

`@dataclass`
class DiskCache:
    files: set

    async def write(self, key, data):
        # async-ish path: enqueue write and complete after caller may have returned
        print(f"write scheduled {key}")
        print(f"remove ran before write {key}")
        self.files.add(key)
        print(f"write completed {key}")

    async def remove(self, key):
        print(f"remove scheduled {key}")
        self.files.discard(key)

async def upload_delete_sequence():
    files = set()
    disk = DiskCache(files)
    key = "abc"
    disk.files.add(key)
    files.add(key)

    # simulate unawaited write after upload returns
    write_task = disk.write(key, b"data")
    # user then deletes immediately
    await disk.remove(key)
    # pending cache write completes later
    await write_task
    print(f"final files={disk.files}")

async def upload_delete_seq_awaited_best_effort():
    files = set()
    disk = DiskCache(files)
    key = "abc"
    disk.files.add(key)
    files.add(key)

    try:
        await disk.write(key, b"data")
    except Exception:
        pass
    await disk.remove(key)
    print(f"final files={disk.files}")

import asyncio
print("unawaited:")
asyncio.run(upload_delete_sequence())
print("awaited:")
asyncio.run(upload_delete_seq_awaited_best_effort())
PY

Repository: Spectrum3847/spectrum-pit

Length of output: 13012


Serialize disk-cache writes with deletion.

PhotoDiskCache.write creates and deletes cache files independently. If upload returns before the scheduled write completes, a later delete can remove the file before _trim writes it back, leaving a deleted photo in disk cache. Await the write and retry after remove, or queue per-key writes/removes. Add a regression test that delays PhotoDiskCache.write, deletes the key, then verifies no disk entry remains.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/services/photo_service.dart` around lines 112 - 113, Update the
disk-cache write path in the photo service to serialize writes with deletion: do
not fire-and-forget PhotoDiskCache.write. Await it and coordinate any subsequent
remove/delete so a delayed write cannot recreate a deleted key, using per-key
operation queuing if needed. Add a regression test that delays write, deletes
the same key, and verifies no disk entry remains.

Comment on lines +27 to +50
class HorizontalStepShortcuts extends StatelessWidget {
const HorizontalStepShortcuts({
required this.onPrevious,
required this.onNext,
required this.child,
super.key,
});

final VoidCallback onPrevious;
final VoidCallback onNext;
final Widget child;

@override
Widget build(BuildContext context) {
return FocusTraversalGroup(
child: CallbackShortcuts(
bindings: <ShortcutActivator, VoidCallback>{
const SingleActivator(LogicalKeyboardKey.arrowLeft): onPrevious,
const SingleActivator(LogicalKeyboardKey.arrowRight): onNext,
},
child: child,
),
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Expect production UI call sites for HorizontalStepShortcuts.
rg -n -C 3 --glob '*.dart' \
  'HorizontalStepShortcuts\s*\(' lib test

Repository: Spectrum3847/spectrum-pit

Length of output: 1122


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- relevant keyboard_shortcuts.dart ---\n'
sed -n '1,120p' lib/src/widgets/keyboard_shortcuts.dart

printf '\n--- test imports and HorizontalStepShortcuts test ---\n'
sed -n '1,120p' test/keyboard_shortcuts_test.dart

printf '\n--- repository-wide HorizontalStepShortcuts references ---\n'
rg -n --glob '*.dart' 'HorizontalStepShortcuts' .

Repository: Spectrum3847/spectrum-pit

Length of output: 254


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- relevant keyboard_shortcuts.dart ---'
sed -n '1,120p' lib/src/widgets/keyboard_shortcuts.dart

printf '%s\n' ''
printf '%s\n' '--- test imports and HorizontalStepShortcuts test ---'
sed -n '1,120p' test/keyboard_shortcuts_test.dart

printf '%s\n' ''
printf '%s\n' '--- repository-wide HorizontalStepShortcuts references ---'
rg -n --glob '*.dart' 'HorizontalStepShortcuts' .

Repository: Spectrum3847/spectrum-pit

Length of output: 5081


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate UI files by common screen/form naming ---'
git ls-files lib | grep --regexp='lib/src/(screens|features|pages|widgets)/.*\.dart$' | sed -n '1,120p'

printf '%s\n' ''
printf '%s\n' '--- keyboard shortcut imports/usages ---'
rg -n --glob '*.dart' "import .*keyboard_shortcuts|SaveShortcut\(|HorizontalStepShortcuts\(" lib test

printf '%s\n' ''
printf '%s\n' '--- focused widgets that are likely step targets ---'
rg -n --glob '*.dart' "TextButton|ElevatedButton|ButtonBar|Steps|Stepper|FutureBuilder|TextFormField|TextField" lib/src | sed -n '1,240p'

Repository: Spectrum3847/spectrum-pit

Length of output: 219


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- lib files ---'
git ls-files lib | sort

printf '%s\n' ''
printf '%s\n' '--- all keyboard shortcut references ---'
rg -n --glob '*.dart' "keyboard_shortcuts|SaveShortcut\(|HorizontalStepShortcuts\(" lib test || true

printf '%s\n' ''
printf '%s\n' '--- focusable interactive widgets in lib ---'
rg -n --glob '*.dart' "TextButton|ElevatedButton|ButtonBar|Stepper|Steps|TextFormField|TextField|FocusNode|autofocus" lib test || true

Repository: Spectrum3847/spectrum-pit

Length of output: 8454


Wire HorizontalStepShortcuts into desktop UI screens.

HorizontalStepShortcuts is only referenced in the tests, so desktop arrow navigation is not integrated. Wrap the relevant screen content and wire onPrevious/onNext before relying on the shortcuts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/widgets/keyboard_shortcuts.dart` around lines 27 - 50, Integrate
HorizontalStepShortcuts into the relevant desktop screen widgets, wrapping their
content and supplying onPrevious and onNext callbacks for the screen’s
horizontal navigation. Ensure the callbacks are wired before relying on the
existing arrow-key bindings, while preserving the current screen content
behavior.

Comment thread README.md

## Install

Builds are attached to [this repo's releases](https://github.com/Spectrum3847/spectrum-pit/releases). They are unsigned, so each platform needs a step or two.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Publish integrity metadata for unsigned artifacts.

Lines 13, 28, and 32-36 tell users to install unsigned files or bypass SmartScreen and Gatekeeper. The installation flow provides no checksum or other artifact-integrity check.

Publish a release-generated SHA-256 manifest and add a verification step before asking users to override platform warnings.

Also applies to: 28-28, 32-36

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 13, Update the release artifact documentation around the
release links and platform installation instructions to publish a
release-generated SHA-256 manifest and direct users to verify downloaded files
against it before bypassing SmartScreen, Gatekeeper, or other unsigned-artifact
warnings.

@Project516
Project516 merged commit 4bf1992 into main Aug 7, 2026
2 checks passed
@Project516
Project516 deleted the sync/v1.2.0 branch August 7, 2026 14:59
@coderabbitai coderabbitai Bot mentioned this pull request Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants