Skip to content

Sync v1.1.0 - #2

Merged
Project516 merged 1 commit into
mainfrom
sync/v1.1.0
Aug 5, 2026
Merged

Sync v1.1.0#2
Project516 merged 1 commit into
mainfrom
sync/v1.1.0

Conversation

@SpectrumFRC3847

@SpectrumFRC3847 SpectrumFRC3847 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Pit photos now survive a relaunch. They are cached on the device, so opening the
packing list on venue wifi no longer refetches every image, and signing out wipes
them so a shared tablet does not keep the previous user's photos.

On desktop you can move between tabs from the keyboard, with Ctrl or Cmd and the
bracket keys.

Usage data is now on by default, matching Spectrum Strategy. It stays anonymous
(app version, platform, and which tabs get opened) and Settings has a switch to
turn it off.

Fixes: the startup "Try again" button can now actually recover from a failed
launch, the security headers that were reporting into a dead endpoint are gone,
and a map image no longer leaks memory while its size is read.

Downloads now live on the public spectrum-pit repository. If you install through
AltStore your existing source keeps working; it picks up the new location on its
next refresh.

Summary by CodeRabbit

  • New Features

    • Added offline photo caching to improve image loading and reduce repeated downloads.
    • Added keyboard shortcuts for navigating between application sections.
    • Added safer, checksum-verified desktop updates.
    • Added support for camera-less devices and improved platform networking access.
    • Added clearer status colors, tooltips, and updated Spectrum Pit branding.
  • Bug Fixes

    • Improved photo cleanup, date handling, role loading, telemetry preferences, and sync recovery.
    • Strengthened profile and telemetry data validation.
    • Prevented invalid schedule ranges and unsafe map or image operations.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This release updates platform settings, Firestore rules, desktop synchronization and updates, photo caching, state recovery, UI navigation, data validation, and test coverage across Android, macOS, Linux, Windows, and Flutter.

Changes

Application hardening and release updates

Layer / File(s) Summary
Platform and Firestore security
android/..., firestore.rules, macos/...
Release signing, network access, optional camera hardware, profile validation, and telemetry document validation are updated.
Models and services
lib/src/models/*, lib/src/services/*
Parsing, role interfaces, desktop polling, update verification, photo caching, image handling, and telemetry persistence are updated.
State and UI behavior
lib/src/state/*, lib/src/theme/*, lib/src/ui/*
Bootstrap retries, optimistic rollback, keyboard navigation, UUID identifiers, photo cleanup, date validation, status colors, and telemetry controls are updated.
Packaging and tests
pubspec.yaml, test/*, tool/*, platform metadata
Dependencies, fonts, application metadata, resource cleanup, and regression coverage are updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

  • Spectrum3847/SpectrumPit#169 — The PR includes the issue’s UUID, teardown, palette, picker lifecycle, telemetry, and documentation asset changes.

Possibly related PRs

  • Spectrum3847/spectrum-pit#1 — The PR extends the same release, Firestore, synchronization, photo, authentication, model, and UI areas.

Suggested reviewers: project516

Poem

A rabbit checks the cache at night,
And signs releases with care.
It hops through tabs by keyboard light,
While UUIDs fill the air.
Failed polls slow, photos clear,
Safe updates now draw near.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title identifies the v1.1.0 synchronization release and is related to the pull request's broad release changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sync/v1.1.0

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: 15

Caution

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

⚠️ Outside diff range comments (2)
lib/src/services/photo_service.dart (1)

121-156: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Sensitive Data Exposure (CWE-200): Exposure of Sensitive Information to an Unauthorized Actor

Reachability: External · Exploitability: Moderate

Reachability path
● Entry
  lib/src/services/desktop_auth_service.dart:11
  DesktopAuthService
│
▼
● Hop
  lib/src/services/synced_map_image_store.dart
│
▼
● Sink
  lib/src/services/photo_service.dart

Scope photo caches to the active session.

When sign-out starts clearCache(), an in-flight fetch can finish afterward and repopulate both caches. A later session can then receive those bytes from _cache before _send runs. Add a session generation or identity check to discard stale reads and writes, and add an interleaving test.

🤖 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 121 - 156, The photo cache
must be scoped to the active session so an in-flight fetch cannot repopulate
caches after clearCache(). Update fetch, clearCache, and the related cache
read/write flow to track a session generation or identity, discard stale
in-flight results, and prevent stale writes to both _cache and _diskCache;
ensure later sessions do not receive prior-session bytes before _send runs. Add
a test covering clearCache interleaved with an in-flight fetch.
lib/src/services/desktop_borrow_sync_service.dart (1)

44-78: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Stop desktop borrow and packing pollers on disposal. Both async* loops use while (true). During repeated listDocuments failures, they reach no yield, so stream cancellation does not stop the generator or its polling. Add _disposed and dispose(), use while (!_disposed), and call dispose() from the application lifecycle. lib/main.dart stores these services behind interfaces, while lib/src/app.dart disposes only their controllers; make service disposal reachable there. Add sustained-failure teardown tests for both services.

🤖 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_borrow_sync_service.dart` around lines 44 - 78,
Update DesktopBorrowSyncService in
lib/src/services/desktop_borrow_sync_service.dart:44-78 and
DesktopPackingSyncService in
lib/src/services/desktop_packing_sync_service.dart:53-83 to add an _disposed
state, expose dispose(), and replace each unconditional async* while (true) loop
with a condition that exits after disposal, including during sustained
listDocuments failures and polling delays. Make service disposal reachable
through the interfaces stored by lib/main.dart, invoke it from the application
lifecycle in lib/src/app.dart alongside controller disposal, and add teardown
tests covering repeated failures for both services.
🧹 Nitpick comments (5)
lib/src/models/pit_shift.dart (1)

87-92: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Apply _dateTime to updatedAt for consistent parsing.

startsAt and endsAt now tolerate both DateTime and String inputs. updatedAt at Line 91 still uses data['updatedAt'] as String?. That cast throws a TypeError if the backend supplies a non-string value, which would make the whole fromJson call fail instead of falling back to the epoch default. Route updatedAt through the same helper to keep one parsing rule for all time fields.

♻️ Proposed refactor
       notes: data['notes'] as String?,
-      updatedAt:
-          DateTime.tryParse(data['updatedAt'] as String? ?? '') ??
-          DateTime.fromMillisecondsSinceEpoch(0, isUtc: true),
+      updatedAt:
+          _dateTime(data['updatedAt']) ??
+          DateTime.fromMillisecondsSinceEpoch(0, isUtc: true),
     );
🤖 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/models/pit_shift.dart` around lines 87 - 92, Update the updatedAt
field in the model’s fromJson construction to use the existing _dateTime helper,
matching startsAt and endsAt, while preserving the epoch fallback for invalid or
absent values.
lib/src/services/desktop_update_service.dart (2)

146-156: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider excluding prerelease tags from the update offer.

Moving to pub_semver is the right change; it replaces hand-rolled comparison logic with the standard implementation.

One behavior note. pub_semver applies SemVer precedence, so Version.parse('1.1.1-beta.1').compareTo(Version.parse('1.1.0')) > 0. checkForUpdate therefore offers a prerelease tag to every user as an installable update. If you publish prerelease tags on this repository, filter them out.

♻️ Proposed refactor
       final version = _parseVersion(tagName);
+      if (version.isPreRelease) {
+        return null;
+      }

Apply this in _loadLatestRelease after the null check on version.

🤖 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_update_service.dart` around lines 146 - 156, Update
_loadLatestRelease after its null check on version to exclude prerelease
versions from update offers, using the parsed Version’s prerelease indicator
before comparing or returning the release. Keep stable-version update behavior
unchanged.

108-122: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Select the AppImage explicitly.

The GitHub REST API provides the asset digest field with a sha256: prefix, so the checksum path is valid. If a release contains multiple AppImage architectures, this helper returns the first one and can install an incompatible binary. Match the target architecture or reject ambiguous releases.

🤖 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_update_service.dart` around lines 108 - 122, The
_appImageAsset helper currently selects the first .AppImage without verifying
architecture. Update its asset selection to match the target architecture, using
the project’s existing architecture-detection or naming convention, and reject
ambiguous releases when no unique compatible asset can be identified; preserve
the existing URL and sha256 digest extraction for the selected asset.
test/widget_test.dart (1)

255-260: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the macOS Command shortcut.

The helper sends only LogicalKeyboardKey.controlLeft. The Ctrl/Cmd navigation contract has no LogicalKeyboardKey.metaLeft assertion. A Command-key regression can pass these tests.

Add equivalent forward and backward assertions with metaLeft.

🤖 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/widget_test.dart` around lines 255 - 260, Add macOS Command-key coverage
alongside the existing press helper in the widget tests: create equivalent
forward and backward navigation assertions that invoke press with
LogicalKeyboardKey.metaLeft, while preserving the current controlLeft assertions
and helper behavior.
test/photo_disk_cache_test.dart (1)

80-89: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise PhotoDiskCache.read in the eviction test.

Lines 80-89 set file times directly. They do not verify that read refreshes the timestamp that _trim uses. If PhotoDiskCache.read stops calling setLastModified, this test still passes.

Proposed test update
-      // Times set explicitly rather than by sleeping between writes: wall-clock
-      // gaps made this flaky, and the eviction order is the only thing under test.
-      // 'old' is the most recently read, so it must survive.
+      // Set deterministic stale times, then use the public read path to mark
+      // `old` as recently used.
       final dir = Directory('${base.path}/photos');
-      await File('${dir.path}/old').setLastModified(DateTime.now());
+      final now = DateTime.now();
+      await File(
+        '${dir.path}/old',
+      ).setLastModified(now.subtract(const Duration(hours: 2)));
       await File(
         '${dir.path}/newer',
-      ).setLastModified(DateTime.now().subtract(const Duration(hours: 1)));
+      ).setLastModified(now.subtract(const Duration(hours: 1)));
+
+      await cache.read('old');
 
       await cache.write('pushes-over', bytes(400));
🤖 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/photo_disk_cache_test.dart` around lines 80 - 89, Update the eviction
test to make the “old” file’s timestamp result from calling PhotoDiskCache.read
rather than setting it directly, while retaining the explicit older timestamp
for “newer.” Ensure the test still verifies that the read-refreshed file
survives _trim when cache.write('pushes-over', bytes(400)) triggers eviction.
🤖 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_launcher_service.dart`:
- Around line 49-52: Update the Exec-path escaping logic in the desktop launcher
service to encode each literal percent in appImagePath as %% before adding the
%U field code, while preserving escaping for $ and backslashes. Add or update
tests covering paths containing percent signs, dollar signs, and backslashes.

In `@lib/src/services/desktop_update_service.dart`:
- Around line 103-105: Update checkForUpdate to distinguish a repository with no
newer release from transport failures: log caught errors, do not convert network
or TimeoutException failures into null, and propagate the failure so
_DesktopUpdateTileState._check can display its existing error message. If
iterating multiple repositories, only rethrow after all repositories fail;
preserve null for a successful check with no release.

In `@lib/src/services/photo_disk_cache.dart`:
- Around line 1-4: Make PhotoDiskCache platform-conditional so Web builds never
resolve the dart:io-dependent implementation: split the native and Web
implementations behind conditional imports or exports, preserving the existing
PhotoDiskCache API while providing a Web-safe implementation. Update the
PhotoService integration to reference this conditional abstraction rather than
importing native cache code directly.

In `@lib/src/services/photo_service.dart`:
- Around line 134-136: Update the upload flow to persist the newly uploaded
photo bytes in the disk cache after the server returns its key. In the upload
method, alongside the existing _remember call, invoke the _diskCache write using
the returned key and photo.bytes, preserving the existing behavior when no disk
cache is configured.

In `@lib/src/services/spectrum_auth_service.dart`:
- Around line 75-78: Make the initialization flow around
_StrategyAppState._retryBootstrap and _authStateSubscription idempotent: prevent
repeated initialize() calls from registering duplicate authStateChanges
listeners, either by caching and reusing the initialization future or by
cancelling the existing subscription before replacing it. Ensure dispose() can
cleanly terminate the active listener and failed bootstrap retries do not leave
earlier listeners active.

In `@lib/src/services/synced_map_image_store.dart`:
- Around line 89-98: Update the cleanup flow around diagramSync.clearKey so it
records any clearKey failure, still performs the local photoService.delete
cleanup when applicable, then rethrows the original pointer-clear error after
cleanup completes. Preserve successful completion when clearing the synchronized
pointer succeeds.

In `@lib/src/services/telemetry_service.dart`:
- Around line 46-62: Update the telemetry consent/settings text and
corresponding privacy documentation to accurately describe the persistent
pseudonymous device identifier generated by _deviceId and stored by
_createDeviceId in SharedPreferences, including its purpose and retention
period; do not refer to this telemetry as only “anonymous usage data.”

In `@lib/src/state/pit_controller_mixin.dart`:
- Around line 47-101: Guard the rollback logic in upsert and delete against
overlapping mutations for the same ID by tracking a per-item revision or
serializing operations per ID. Only restore the previous state when the failed
mutation still owns the current item; otherwise preserve the newer mutation and
its cache state. Add an interleaving test covering two updates to the same ID
where the earlier update fails.

In `@lib/src/ui/borrow_tab.dart`:
- Around line 578-579: Update the date picker bounds in the borrow-tab flows
around firstDate and lastDate (including the corresponding second block) to
derive both endpoints from now, allowing selection through the same calendar
date one year later rather than January 1. Add boundary tests covering the
rolling one-year limits.

In `@lib/src/ui/packing_tab.dart`:
- Around line 197-207: Update the deletion flow around widget.controller.delete
so a failed deletion returns immediately when deleted remains false, keeping the
editor open. Only execute the subsequent photo cleanup and editor-closing logic
after successful deletion.
- Around line 841-861: The save completion flow around widget.onSubmit must
clean up captured photos even when the sheet unmounts or the save is not
committed. Delete all _capturedKeys when committed is false; on success, retain
the claimed _photoRef while deleting unclaimed captures and the replaced
_originalPhotoRef. Remove the broad mounted/committed early return, and guard
only Navigator.of(context).pop() with mounted; add deferred-submit tests
covering both outcomes.

In `@lib/src/ui/settings_tab.dart`:
- Around line 510-529: Prevent the asynchronous isEnabled read in the settings
initialization flow from overwriting a value changed by _toggle: either disable
the switch until the read completes or ignore its callback once a toggle begins.
Preserve the existing toggle persistence and rollback behavior, and add a
deferred isEnabled/setEnabled test covering the race.

In `@test/docs_assets_exist_test.dart`:
- Around line 50-55: Update the unbundled asset filter to match file assets only
by exact equality, while allowing prefix matching exclusively for declared
assets whose path ends with “/”. Adjust the predicate in the referenced/declared
comparison so file names such as docs/guide.md-extra.md do not match
docs/guide.md.

In `@test/support/fake_map_diagram_sync_service.dart`:
- Line 22: Update the fake service’s key storage and the read/write/clear
methods in FakeMapDiagramSyncService to maintain an independent key for each
MapType, ensuring readKey, successful writes, and clears only access the
requested map type and cannot affect other maps.

In `@test/support/photo_test_support.dart`:
- Around line 45-50: Update the authentication guard in the request fake so it
immediately returns a 401 response when token is null, before computing or
comparing the expected Authorization header. Preserve the existing Bearer-token
comparison for non-null tokens.

---

Outside diff comments:
In `@lib/src/services/desktop_borrow_sync_service.dart`:
- Around line 44-78: Update DesktopBorrowSyncService in
lib/src/services/desktop_borrow_sync_service.dart:44-78 and
DesktopPackingSyncService in
lib/src/services/desktop_packing_sync_service.dart:53-83 to add an _disposed
state, expose dispose(), and replace each unconditional async* while (true) loop
with a condition that exits after disposal, including during sustained
listDocuments failures and polling delays. Make service disposal reachable
through the interfaces stored by lib/main.dart, invoke it from the application
lifecycle in lib/src/app.dart alongside controller disposal, and add teardown
tests covering repeated failures for both services.

In `@lib/src/services/photo_service.dart`:
- Around line 121-156: The photo cache must be scoped to the active session so
an in-flight fetch cannot repopulate caches after clearCache(). Update fetch,
clearCache, and the related cache read/write flow to track a session generation
or identity, discard stale in-flight results, and prevent stale writes to both
_cache and _diskCache; ensure later sessions do not receive prior-session bytes
before _send runs. Add a test covering clearCache interleaved with an in-flight
fetch.

---

Nitpick comments:
In `@lib/src/models/pit_shift.dart`:
- Around line 87-92: Update the updatedAt field in the model’s fromJson
construction to use the existing _dateTime helper, matching startsAt and endsAt,
while preserving the epoch fallback for invalid or absent values.

In `@lib/src/services/desktop_update_service.dart`:
- Around line 146-156: Update _loadLatestRelease after its null check on version
to exclude prerelease versions from update offers, using the parsed Version’s
prerelease indicator before comparing or returning the release. Keep
stable-version update behavior unchanged.
- Around line 108-122: The _appImageAsset helper currently selects the first
.AppImage without verifying architecture. Update its asset selection to match
the target architecture, using the project’s existing architecture-detection or
naming convention, and reject ambiguous releases when no unique compatible asset
can be identified; preserve the existing URL and sha256 digest extraction for
the selected asset.

In `@test/photo_disk_cache_test.dart`:
- Around line 80-89: Update the eviction test to make the “old” file’s timestamp
result from calling PhotoDiskCache.read rather than setting it directly, while
retaining the explicit older timestamp for “newer.” Ensure the test still
verifies that the read-refreshed file survives _trim when
cache.write('pushes-over', bytes(400)) triggers eviction.

In `@test/widget_test.dart`:
- Around line 255-260: Add macOS Command-key coverage alongside the existing
press helper in the widget tests: create equivalent forward and backward
navigation assertions that invoke press with LogicalKeyboardKey.metaLeft, while
preserving the current controlLeft assertions and helper behavior.
🪄 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: 7a27c93b-9aef-4811-9d5d-bb0e6e6233bd

📥 Commits

Reviewing files that changed from the base of the PR and between 59cfaa1 and a8aeb75.

⛔ Files ignored due to path filters (5)
  • assets/fonts/IBMPlexMono-Medium.ttf is excluded by !**/*.ttf
  • assets/fonts/IBMPlexSans-Bold.ttf is excluded by !**/*.ttf
  • assets/fonts/IBMPlexSans-Regular.ttf is excluded by !**/*.ttf
  • assets/fonts/IBMPlexSans-SemiBold.ttf is excluded by !**/*.ttf
  • pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (87)
  • android/app/build.gradle.kts
  • android/app/src/main/AndroidManifest.xml
  • firestore.rules
  • ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
  • lib/main.dart
  • lib/src/app.dart
  • lib/src/models/inventory_item.dart
  • lib/src/models/packing_record.dart
  • lib/src/models/pit_shift.dart
  • lib/src/models/user_profile.dart
  • lib/src/models/user_role.dart
  • lib/src/services/desktop_auth_service.dart
  • lib/src/services/desktop_borrow_sync_service.dart
  • lib/src/services/desktop_inventory_sync_service.dart
  • lib/src/services/desktop_launcher_service.dart
  • lib/src/services/desktop_map_location_sync_service.dart
  • lib/src/services/desktop_packing_sync_service.dart
  • lib/src/services/desktop_pit_shift_sync_service.dart
  • lib/src/services/desktop_polling.dart
  • lib/src/services/desktop_self_update_service.dart
  • lib/src/services/desktop_update_service.dart
  • lib/src/services/desktop_user_role_service.dart
  • lib/src/services/local_only_services.dart
  • lib/src/services/map_image_store.dart
  • lib/src/services/photo_disk_cache.dart
  • lib/src/services/photo_service.dart
  • lib/src/services/pit_shift_sync_service.dart
  • lib/src/services/spectrum_auth_service.dart
  • lib/src/services/synced_map_image_store.dart
  • lib/src/services/telemetry_service.dart
  • lib/src/services/user_role_service.dart
  • lib/src/services/user_role_service_interface.dart
  • lib/src/state/pit_controller_mixin.dart
  • lib/src/state/theme_controller.dart
  • lib/src/state/user_role_controller.dart
  • lib/src/theme/app_theme.dart
  • lib/src/theme/pit_palette.dart
  • lib/src/ui/app_shell.dart
  • lib/src/ui/borrow_tab.dart
  • lib/src/ui/inventory_tab.dart
  • lib/src/ui/maps_tab.dart
  • lib/src/ui/packing_photo.dart
  • lib/src/ui/packing_tab.dart
  • lib/src/ui/schedule_tab.dart
  • lib/src/ui/settings_tab.dart
  • lib/src/ui/user_management_screen.dart
  • linux/runner/my_application.cc
  • macos/Runner/DebugProfile.entitlements
  • macos/Runner/Info.plist
  • macos/Runner/Release.entitlements
  • pubspec.yaml
  • test/borrow_tab_test.dart
  • test/dark_theme_tokens_test.dart
  • test/desktop_auth_service_test.dart
  • test/desktop_launcher_service_test.dart
  • test/desktop_pit_shift_sync_service_test.dart
  • test/desktop_self_update_service_test.dart
  • test/desktop_update_service_test.dart
  • test/docs_assets_exist_test.dart
  • test/docs_viewer_test.dart
  • test/inventory_tab_test.dart
  • test/issue_report_service_test.dart
  • test/local_only_services_test.dart
  • test/map_location_controller_test.dart
  • test/map_location_test.dart
  • test/maps_tab_test.dart
  • test/packing_controller_test.dart
  • test/packing_tab_test.dart
  • test/photo_disk_cache_test.dart
  • test/photo_service_test.dart
  • test/pit_shift_test.dart
  • test/schedule_tab_test.dart
  • test/support/fake_map_diagram_sync_service.dart
  • test/support/fake_map_image_store.dart
  • test/support/fake_map_location_sync_service.dart
  • test/support/fake_pit_shift_sync_service.dart
  • test/support/fake_user_role_service.dart
  • test/support/photo_test_support.dart
  • test/synced_map_image_store_test.dart
  • test/telemetry_service_test.dart
  • test/user_management_screen_test.dart
  • test/user_profile_test.dart
  • test/user_role_controller_test.dart
  • test/widget_test.dart
  • tool/generate_icons.py
  • windows/runner/Runner.rc
  • windows/runner/flutter_window.cpp

Comment on lines +49 to +52
final exec = appImagePath.replaceAllMapped(
RegExp(r'["`$\\]'),
(m) => '\\${m[0]}',
);

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 -eu

file=$(git ls-files | rg '(^|/)lib/src/services/desktop_launcher_service\.dart$' | head -n 1)
printf '%s\n' "FILE=$file"
wc -l "$file"
sed -n '1,130p' "$file"

printf '%s\n' '--- related symbols and tests ---'
rg -n -C 3 'desktopEntry|appImagePath|APPIMAGE|%U|Exec' . --glob '!build/**' --glob '!dist/**' --glob '!node_modules/**'

Repository: Spectrum3847/spectrum-pit

Length of output: 23229


🌐 Web query:

Freedesktop Desktop Entry Specification latest Exec key literal percent sign %% quoted argument field codes

💡 Result:

According to the Freedesktop Desktop Entry Specification, the Exec key handles literal percent signs by using the escape sequence %% [1][2]. Regarding the interaction between field codes and quoted arguments, the specification explicitly states that field codes (such as %f, %u, %F, or %U) must not be used inside a quoted argument [2]. The results of field code expansion when placed inside a quoted argument are defined as undefined behavior [2][3]. To handle complex command lines that might otherwise require field codes within quotes, developers typically use shell wrappers or pass the field codes as separate arguments to a shell command (e.g., sh -c "..." sh %u) [3]. Implementations are required to perform quote removal before expanding field codes and before passing the arguments to the executable [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
# Model the exact character transformations in the Dart source and in the
# proposed replacement. String.replaceAll uses literal replacement strings.
def current(value):
    return ''.join(('\\' + c) if c in '"`$\\' else c for c in value)

def proposed(value):
    return (value
            .replace('\\', '\\\\\\\\')  # Dart raw r'\\\\' => four backslashes
            .replace('$', '\\\\$')      # Dart raw r'\\$' => two backslashes + $
            .replace('"', '\\"')
            .replace('`', '\\`')
            .replace('%', '%%'))

for value in [
    '/tmp/App%20.AppImage',
    r'/tmp/App$foo\App"Image`',
    r'/tmp/App%U.AppImage',
]:
    print(f'input:    {value!r}')
    print(f'current:  {current(value)!r}')
    print(f'proposed: {proposed(value)!r}')
PY

printf '%s\n' '--- available desktop-entry validators/parsers ---'
command -v desktop-file-validate || true
command -v gio || true
command -v gdbus || true

Repository: Spectrum3847/spectrum-pit

Length of output: 544


🏁 Script executed:

#!/bin/bash
set -eu

url='https://specifications.freedesktop.org/desktop-entry/latest-single/'
if command -v curl >/dev/null 2>&1; then
  curl -L --fail --silent --show-error "$url" |
    rg -n -C 4 'literal percent|field code|quoted argument|Exec key|%%'
else
  printf '%s\n' 'curl is unavailable'
fi

Repository: Spectrum3847/spectrum-pit

Length of output: 8578


Escape literal percent signs in Exec.

When appImagePath contains %, encode it as %% before appending the %U field code. Also preserve the required Desktop Entry escaping for $ and backslashes. Add tests for paths containing %, $, and backslashes.

🤖 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_launcher_service.dart` around lines 49 - 52, Update
the Exec-path escaping logic in the desktop launcher service to encode each
literal percent in appImagePath as %% before adding the %U field code, while
preserving escaping for $ and backslashes. Add or update tests covering paths
containing percent signs, dollar signs, and backslashes.

Comment on lines +103 to 105
} catch (_) {
return null;
}

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 | 🟠 Major | ⚡ Quick win

The blanket catch reports a failed update check as "up to date".

catch (_) { return null; } swallows every error, including the TimeoutException that TimeoutHttpClient raises and any network failure. checkForUpdate then treats the repository as having no release, skips it, and returns null. The consumer in lib/src/ui/settings_tab.dart maps a null result to "You are on the latest version."

The user therefore sees a success message after a failed check. That branch also makes the existing "Could not check for updates right now." handler in _check unreachable for network errors, because no exception escapes this method. For an update mechanism that carries security fixes, a false "up to date" state is the wrong default.

Distinguish a transport failure from "no newer release", and log the error so the failure is diagnosable.

🐛 Proposed fix
-    } catch (_) {
-      return null;
-    }
+    } on FormatException catch (error) {
+      debugPrint('release metadata for $repository was malformed: $error');
+      return null;
+    } catch (error) {
+      debugPrint('release check for $repository failed: $error');
+      rethrow;
+    }

checkForUpdate then propagates the failure, and the existing catch in _DesktopUpdateTileState._check shows "Could not check for updates right now." If you keep more than one entry in _repositories, rethrow only after every repository fails. debugPrint comes from package:flutter/foundation.dart.

📝 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
} catch (_) {
return null;
}
} on FormatException catch (error) {
debugPrint('release metadata for $repository was malformed: $error');
return null;
} catch (error) {
debugPrint('release check for $repository failed: $error');
rethrow;
}
🤖 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_update_service.dart` around lines 103 - 105, Update
checkForUpdate to distinguish a repository with no newer release from transport
failures: log caught errors, do not convert network or TimeoutException failures
into null, and propagate the failure so _DesktopUpdateTileState._check can
display its existing error message. If iterating multiple repositories, only
rethrow after all repositories fail; preserve null for a successful check with
no release.

Comment on lines +1 to +4
import 'dart:io';

import 'package:flutter/foundation.dart' show Uint8List, debugPrint, kIsWeb;
import 'package:path_provider/path_provider.dart';

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 2 "import 'dart:io'|class PhotoDiskCache|PhotoDiskCache" lib
rg -n -i -C 2 "flutter build web|run -d chrome|web" README.md pubspec.yaml 2>/dev/null || true

Repository: Spectrum3847/spectrum-pit

Length of output: 4852


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- tracked platform/config files ---'
git ls-files | rg -i '(^|/)(readme|pubspec|analysis_options|web|android|ios|macos|windows|linux)(\.|/|$)' | head -200

printf '%s\n' '--- pubspec.yaml ---'
cat -n pubspec.yaml | sed -n '1,180p'

printf '%s\n' '--- photo_disk_cache.dart ---'
cat -n lib/src/services/photo_disk_cache.dart

printf '%s\n' '--- photo_service.dart imports and constructor ---'
cat -n lib/src/services/photo_service.dart | sed -n '1,90p'

printf '%s\n' '--- main.dart context ---'
cat -n lib/main.dart | sed -n '145,175p'

printf '%s\n' '--- platform guards and conditional imports ---'
rg -n -C 3 "kIsWeb|Platform\.|if \(dart\.library|if \(dart\.|conditional import|conditional export|dart:io" lib test pubspec.yaml README.md 2>/dev/null | head -300

Repository: Spectrum3847/spectrum-pit

Length of output: 39706


Hide the native cache behind a conditional implementation for Web builds.

Web is an application target, but PhotoDiskCache imports dart:io through PhotoService. A runtime kIsWeb check cannot prevent browser compilation from resolving dart:io.

🤖 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_disk_cache.dart` around lines 1 - 4, Make
PhotoDiskCache platform-conditional so Web builds never resolve the
dart:io-dependent implementation: split the native and Web implementations
behind conditional imports or exports, preserving the existing PhotoDiskCache
API while providing a Web-safe implementation. Update the PhotoService
integration to reference this conditional abstraction rather than importing
native cache code directly.

Comment on lines +134 to 136
final disk = _diskCache;
if (disk != null) unawaited(disk.write(key, bytes));
return 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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Persist newly uploaded photo bytes.

The disk tier only stores GET responses. upload only calls _remember, so a newly captured photo is unavailable after an app restart until another fetch succeeds. Store photo.bytes in the disk cache after the upload returns its key.

Proposed fix
     _remember(key, photo.bytes);
+    final disk = _diskCache;
+    if (disk != null) unawaited(disk.write(key, photo.bytes));
     return 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 `@lib/src/services/photo_service.dart` around lines 134 - 136, Update the
upload flow to persist the newly uploaded photo bytes in the disk cache after
the server returns its key. In the upload method, alongside the existing
_remember call, invoke the _diskCache write using the returned key and
photo.bytes, preserving the existing behavior when no disk cache is configured.

Comment on lines +75 to 78
if (!kIsWeb) {
await _googleSignIn.initialize();
}
_authStateSubscription = _appAuth.authStateChanges().listen((user) {

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

Make initialize() idempotent before startup retry.

_StrategyAppState._retryBootstrap() calls initialize() again after any bootstrap failure. If the first call already subscribed, Line 78 overwrites _authStateSubscription without cancelling the old listener. Auth events then emit twice, and dispose() leaves the old listener active.

Cache the initialization future, or cancel and replace the existing subscription before registering a new listener.

🤖 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/spectrum_auth_service.dart` around lines 75 - 78, Make the
initialization flow around _StrategyAppState._retryBootstrap and
_authStateSubscription idempotent: prevent repeated initialize() calls from
registering duplicate authStateChanges listeners, either by caching and reusing
the initialization future or by cancelling the existing subscription before
replacing it. Ensure dispose() can cleanly terminate the active listener and
failed bootstrap retries do not leave earlier listeners active.

Comment on lines +841 to +861
} finally {
_saving = false;
}
if (!mounted || !committed) return;
_saved = true;

final claimed = _photoRef;
for (final key in _capturedKeys) {
if (key != claimed) {
try {
await widget.photoService.delete(key);
} catch (_) {}
}
}
final original = _originalPhotoRef;
if (original != null && original != claimed) {
try {
await widget.photoService.delete(original);
} catch (_) {}
}
if (mounted) Navigator.of(context).pop();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- candidate symbols ---'
ast-grep outline lib/src/ui/packing_tab.dart --match 'PackingTab' --view expanded || true
printf '%s\n' '--- lifecycle/save/cleanup references ---'
rg -n -C 5 '_saving|_capturedKeys|_photoRef|_originalPhotoRef|onSubmit|dispose|committed|delete\\(' lib/src/ui/packing_tab.dart
printf '%s\n' '--- related tests ---'
rg -n -C 3 'PackingTab|capturedKeys|photoRef|onSubmit|photoService|delete\\(' test lib 2>/dev/null | head -n 500

Repository: Spectrum3847/spectrum-pit

Length of output: 419


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- target file symbols ---'
ast-grep outline lib/src/ui/packing_tab.dart --match 'PackingTab' --view expanded || true
printf '%s\n' '--- lifecycle, save, and cleanup references ---'
rg -n -C 5 '_saving|_capturedKeys|_photoRef|_originalPhotoRef|onSubmit|dispose|committed|delete\\(' lib/src/ui/packing_tab.dart
printf '%s\n' '--- related tests and usages ---'
rg -n -C 3 'PackingTab|capturedKeys|photoRef|onSubmit|photoService|delete\\(' test lib 2>/dev/null | head -n 500

Repository: Spectrum3847/spectrum-pit

Length of output: 427


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- target file size ---'
wc -l lib/src/ui/packing_tab.dart
printf '%s\n' '--- relevant identifiers ---'
for term in _saving _capturedKeys _photoRef _originalPhotoRef onSubmit dispose committed delete; do
  printf '\n[%s]\n' "$term"
  rg -n -F -C 4 "$term" lib/src/ui/packing_tab.dart || true
done
printf '%s\n' '--- target section ---'
sed -n '650,900p' lib/src/ui/packing_tab.dart
printf '%s\n' '--- test files ---'
git ls-files | rg '(^|/)(test|tests)/|packing_tab'

Repository: Spectrum3847/spectrum-pit

Length of output: 18993


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- sheet-opening and submit callback ---'
sed -n '130,225p' lib/src/ui/packing_tab.dart
printf '%s\n' '--- packing tab test outline and relevant matches ---'
ast-grep outline test/packing_tab_test.dart --view compact || true
rg -n -F -C 6 'onSubmit' test/packing_tab_test.dart
rg -n -F -C 6 'PhotoService' test/packing_tab_test.dart
rg -n -F -C 6 'delete' test/packing_tab_test.dart
printf '%s\n' '--- photo service contract ---'
rg -n -F -C 8 'abstract class PhotoService' lib test
rg -n -F -C 8 'class PhotoService' lib/src test

Repository: Spectrum3847/spectrum-pit

Length of output: 3400


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- packing tab test matches ---'
rg -n -F -C 8 'onSubmit' test/packing_tab_test.dart || true
rg -n -F -C 8 'PhotoService' test/packing_tab_test.dart || true
rg -n -F -C 8 'delete' test/packing_tab_test.dart || true
rg -n -F -C 5 'showModalBottomSheet' test/packing_tab_test.dart || true
printf '%s\n' '--- photo service definitions ---'
rg -n -F -C 10 'abstract class PhotoService' lib test || true
rg -n -F -C 10 'class PhotoService' lib/src test || true
printf '%s\n' '--- photo support helpers ---'
rg -n -F -C 8 'FakePhoto' test lib || true
rg -n -F -C 8 'delete(' test/support/photo_test_support.dart lib/src/services lib/src 2>/dev/null | head -n 400 || true

Repository: Spectrum3847/spectrum-pit

Length of output: 35263


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path

source = Path("lib/src/ui/packing_tab.dart").read_text()
required = [
    "if (!_saved && !_saving) {",
    "committed = await widget.onSubmit(",
    "if (!mounted || !committed) return;",
    "for (final key in _capturedKeys) {",
    "if (key != claimed) {",
]
for fragment in required:
    assert fragment in source, fragment

def current_cleanup(mounted, committed, captured, claimed, original):
    # This mirrors the control flow in _save after onSubmit resolves.
    if not mounted or not committed:
        return []
    deleted = [key for key in captured if key != claimed]
    if original is not None and original != claimed:
        deleted.append(original)
    return deleted

captured = ["new-1", "new-2"]
assert current_cleanup(True, False, captured, "new-2", "old") == []
assert current_cleanup(False, False, captured, "new-2", "old") == []
assert current_cleanup(False, True, captured, "new-2", "old") == []
assert current_cleanup(True, True, captured, "new-2", "old") == ["new-1", "old"]

print("current _save flow:")
print("  mounted + failure: no cleanup")
print("  unmounted + failure: no cleanup")
print("  unmounted + success: no cleanup")
print("  mounted + success: delete unclaimed capture and replaced original")
PY
printf '%s\n' '--- exact PhotoService.delete behavior ---'
sed -n '133,154p' lib/src/services/photo_service.dart
printf '%s\n' '--- remaining packing tab tests ---'
sed -n '290,390p' test/packing_tab_test.dart

Repository: Spectrum3847/spectrum-pit

Length of output: 2950


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path

source = Path("lib/src/ui/packing_tab.dart").read_text()
required = [
    "if (!_saved && !_saving) {",
    "committed = await widget.onSubmit(",
    "if (!mounted || !committed) return;",
    "for (final key in _capturedKeys) {",
    "if (key != claimed) {",
]
for fragment in required:
    assert fragment in source, fragment

def current_cleanup(mounted, committed, captured, claimed, original):
    if not mounted or not committed:
        return []
    deleted = [key for key in captured if key != claimed]
    if original is not None and original != claimed:
        deleted.append(original)
    return deleted

captured = ["new-1", "new-2"]
assert current_cleanup(True, False, captured, "new-2", "old") == []
assert current_cleanup(False, False, captured, "new-2", "old") == []
assert current_cleanup(False, True, captured, "new-2", "old") == []
assert current_cleanup(True, True, captured, "new-2", "old") == ["new-1", "old"]

print("current _save flow:")
print("  mounted + failure: no cleanup")
print("  unmounted + failure: no cleanup")
print("  unmounted + success: no cleanup")
print("  mounted + success: delete unclaimed capture and replaced original")
PY
printf '%s\n' '--- exact PhotoService.delete behavior ---'
sed -n '133,154p' lib/src/services/photo_service.dart
printf '%s\n' '--- remaining packing tab tests ---'
sed -n '290,390p' test/packing_tab_test.dart

Repository: Spectrum3847/spectrum-pit

Length of output: 2950


Run photo cleanup after an in-flight save completes.

When the sheet unmounts during widget.onSubmit, _saving prevents dispose() from cleaning _capturedKeys, and the mounted guard skips post-save cleanup. Delete all captured keys when committed is false. On success, delete unclaimed captures and the replaced _originalPhotoRef. Guard only Navigator.of(context).pop() with mounted. Add deferred-submit tests for both outcomes.

🤖 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/ui/packing_tab.dart` around lines 841 - 861, The save completion flow
around widget.onSubmit must clean up captured photos even when the sheet
unmounts or the save is not committed. Delete all _capturedKeys when committed
is false; on success, retain the claimed _photoRef while deleting unclaimed
captures and the replaced _originalPhotoRef. Remove the broad mounted/committed
early return, and guard only Navigator.of(context).pop() with mounted; add
deferred-submit tests covering both outcomes.

Comment on lines +510 to +529
_service
.isEnabled()
.then((value) {
if (mounted) setState(() => _enabled = value);
})
.catchError((Object error) {
debugPrint('Telemetry preference read failed: $error');
});
}

Future<void> _toggle(bool value) async {
if (_busy) return;
_busy = true;
final previous = _enabled;
setState(() => _enabled = value);
await _service.setEnabled(value);
try {
await _service.setEnabled(value);
} catch (error) {
debugPrint('Telemetry preference write failed: $error');
if (mounted) setState(() => _enabled = previous);

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 -eu
printf '%s\n' '--- file outline ---'
ast-grep outline lib/src/ui/settings_tab.dart --view compact
printf '%s\n' '--- target section ---'
sed -n '450,560p' lib/src/ui/settings_tab.dart
printf '%s\n' '--- related symbols and tests ---'
rg -n --glob '*.dart' '_enabled|_toggle|isEnabled\\(|setEnabled\\(' lib test . 2>/dev/null | head -240

Repository: Spectrum3847/spectrum-pit

Length of output: 335


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- file outline ---'
ast-grep outline lib/src/ui/settings_tab.dart --view signatures
printf '%s\n' '--- target section ---'
sed -n '450,560p' lib/src/ui/settings_tab.dart
printf '%s\n' '--- related symbols and tests ---'
rg -n --glob '*.dart' '_enabled|_toggle|isEnabled\\(|setEnabled\\(' lib test . 2>/dev/null | head -240

Repository: Spectrum3847/spectrum-pit

Length of output: 3915


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- telemetry declarations and implementations ---'
rg -n -C 8 --glob '*.dart' 'class TelemetryService|abstract class TelemetryService|TelemetryService|isEnabled|setEnabled' .
printf '%s\n' '--- settings widget tests ---'
find . -type f \( -name '*test.dart' -o -name '*widget_test.dart' \) -print | sort | head -200
printf '%s\n' '--- project test configuration ---'
find . -maxdepth 3 -type f \( -name 'pubspec.yaml' -o -name 'analysis_options.yaml' \) -print -exec sed -n '1,180p' {} \;

Repository: Spectrum3847/spectrum-pit

Length of output: 32055


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
from collections import deque

source = Path("lib/src/ui/settings_tab.dart").read_text()
required = [
    ".isEnabled()",
    "if (mounted) setState(() => _enabled = value);",
    "Future<void> _toggle(bool value) async",
    "setState(() => _enabled = value);",
    "await _service.setEnabled(value);",
    "onChanged: _toggle,",
]
missing = [item for item in required if item not in source]
if missing:
    raise SystemExit(f"missing expected source fragments: {missing}")

# Model the two independent completions. The read captures the persisted
# value before the write completes, then its UI callback runs after the toggle.
enabled = True
persisted = False
events = deque([
    ("toggle", True),
    ("read_complete", persisted),
    ("write_complete", True),
])
for event, value in events:
    if event == "toggle":
        enabled = value
    elif event == "read_complete":
        enabled = value
    elif event == "write_complete":
        persisted = value

print({
    "source_fragments_present": True,
    "persisted_after_write": persisted,
    "ui_after_late_read": enabled,
    "ui_differs_from_persisted": enabled != persisted,
})
assert persisted is True
assert enabled is False
assert enabled != persisted
PY

printf '%s\n' '--- settings references in tests ---'
rg -n --glob '*.dart' 'SettingsTab|_TelemetryTile|Share anonymous usage data' test lib || true

Repository: Spectrum3847/spectrum-pit

Length of output: 1335


Prevent the initial read from overwriting a user toggle.

If isEnabled() completes after _toggle() starts, its callback can restore the old value after setEnabled(value) persists the new value. Disable the switch until the initial read completes, or ignore the read after a toggle starts. Add a deferred isEnabled()/setEnabled() test.

🤖 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/ui/settings_tab.dart` around lines 510 - 529, Prevent the
asynchronous isEnabled read in the settings initialization flow from overwriting
a value changed by _toggle: either disable the switch until the read completes
or ignore its callback once a toggle begins. Preserve the existing toggle
persistence and rollback behavior, and add a deferred isEnabled/setEnabled test
covering the race.

Comment on lines +50 to +55
final unbundled =
referenced
.where(
(p) =>
!declared.any((asset) => p == asset || p.startsWith(asset)),
)

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 -eu
file="test/docs_assets_exist_test.dart"
printf '%s\n' '--- target file outline ---'
ast-grep outline "$file" --lang dart 2>/dev/null || true
printf '%s\n' '--- target lines ---'
sed -n '1,120p' "$file"
printf '%s\n' '--- related symbols and fixtures ---'
rg -n -S "docs_assets_exist|declared|referenced|unbundled|asset.endsWith|startsWith" . \
  -g '*.dart' -g '*.md' -g '*.yaml' -g '*.yml' | head -200

Repository: Spectrum3847/spectrum-pit

Length of output: 4561


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- asset configuration ---'
rg -n -A25 -B5 '^[[:space:]]*assets:' pubspec.yaml
printf '%s\n' '--- referenced and declared documentation paths ---'
python3 - <<'PY'
from pathlib import Path
import re

pattern = re.compile(r"""['"](docs/[\w./-]+\.md)['"]""")
referenced = set()
for path in Path("lib").rglob("*.dart"):
    referenced.update(m.group(1) for m in pattern.finditer(path.read_text()))

declared = []
in_assets = False
for line in Path("pubspec.yaml").read_text().splitlines():
    trimmed = line.strip()
    if trimmed == "assets:":
        in_assets = True
    elif in_assets:
        if trimmed.startswith("- "):
            declared.append(trimmed[2:].strip())
        elif trimmed and not trimmed.startswith("#"):
            in_assets = False

print("referenced:", sorted(referenced))
print("declared:", declared)
print("current unbundled:", sorted(
    p for p in referenced
    if not any(p == asset or p.startswith(asset) for asset in declared)
))
print("false-positive candidates:", sorted(
    (p, asset) for p in referenced for asset in declared
    if p != asset and p.startswith(asset) and not asset.endswith("/")
))
PY
printf '%s\n' '--- documentation files ---'
find docs -type f -name '*.md' -print | sort

Repository: Spectrum3847/spectrum-pit

Length of output: 1292


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
def covered_current(path, assets):
    return any(path == asset or path.startswith(asset) for asset in assets)

def covered_proposed(path, assets):
    return any(
        path == asset or (asset.endswith("/") and path.startswith(asset))
        for asset in assets
    )

cases = [
    ("docs/guide.md", ["docs/guide.md"]),
    ("docs/guide.md-extra.md", ["docs/guide.md"]),
    ("docs/guide.md", ["docs/"]),
    ("docs/guide.md-extra.md", ["docs/"]),
]
for path, assets in cases:
    print(path, assets, covered_current(path, assets), covered_proposed(path, assets))
PY

Repository: Spectrum3847/spectrum-pit

Length of output: 334


Restrict prefix matching to directory assets. Use exact equality for file assets, and use startsWith only when asset.endsWith('/'). Otherwise, docs/guide.md-extra.md can match the declared file docs/guide.md.

🤖 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/docs_assets_exist_test.dart` around lines 50 - 55, Update the unbundled
asset filter to match file assets only by exact equality, while allowing prefix
matching exclusively for declared assets whose path ends with “/”. Adjust the
predicate in the referenced/declared comparison so file names such as
docs/guide.md-extra.md do not match docs/guide.md.

_clearFailure = clearFailure;

final String? _readKeyValue;
String? _readKeyValue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Keep fake diagram keys isolated by MapType.

readKey ignores mapType, and successful writes and clears mutate one shared value. A lab write can change a pit read. A lab clear can remove the pit key. This fake can hide cross-map pointer isolation defects in SyncedMapImageStore.

Store a separate key for each MapType.

Also applies to: 49-62

🤖 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/support/fake_map_diagram_sync_service.dart` at line 22, Update the fake
service’s key storage and the read/write/clear methods in
FakeMapDiagramSyncService to maintain an independent key for each MapType,
ensuring readKey, successful writes, and clears only access the requested map
type and cannot affect other maps.

Comment on lines +45 to +50
// The Worker authenticates every request with the caller's Bearer token;
// simulate that gate so an unauthenticated request cannot hit the
// storage logic.
final expected = token == null ? null : 'Bearer $token';
if (request.headers['Authorization'] != expected) {
return http.Response('{"error":"Unauthorized"}', 401);

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

Reject requests when the fake has no token.

When token is null, expected is also null. A missing Authorization header then passes the comparison and reaches the fake storage logic. This contradicts the stated authentication contract and can hide a missing-header regression.

Return 401 when token == null before comparing the header.

Proposed fix
-      final expected = token == null ? null : 'Bearer $token';
-      if (request.headers['Authorization'] != expected) {
+      if (
+          token == null ||
+          request.headers['Authorization'] != 'Bearer $token'
+      ) {
         return http.Response('{"error":"Unauthorized"}', 401);
       }
📝 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
// The Worker authenticates every request with the caller's Bearer token;
// simulate that gate so an unauthenticated request cannot hit the
// storage logic.
final expected = token == null ? null : 'Bearer $token';
if (request.headers['Authorization'] != expected) {
return http.Response('{"error":"Unauthorized"}', 401);
// The Worker authenticates every request with the caller's Bearer token;
// simulate that gate so an unauthenticated request cannot hit the
// storage logic.
if (
token == null ||
request.headers['Authorization'] != 'Bearer $token'
) {
return http.Response('{"error":"Unauthorized"}', 401);
🤖 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/support/photo_test_support.dart` around lines 45 - 50, Update the
authentication guard in the request fake so it immediately returns a 401
response when token is null, before computing or comparing the expected
Authorization header. Preserve the existing Bearer-token comparison for non-null
tokens.

@Project516
Project516 merged commit 2d1a29f into main Aug 5, 2026
2 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Aug 7, 2026
@Project516
Project516 deleted the sync/v1.1.0 branch August 7, 2026 15:16
This was referenced 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