Skip to content

Sync v1.4.0 - #5

Merged
Project516 merged 1 commit into
mainfrom
sync/v1.4.0
Aug 13, 2026
Merged

Sync v1.4.0#5
Project516 merged 1 commit into
mainfrom
sync/v1.4.0

Conversation

@SpectrumFRC3847

@SpectrumFRC3847 SpectrumFRC3847 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Driver match rotation, now inside the app. Enter a name list per role, set how many matches to cover, and generate a balanced rotation from the Schedule tab. Tapping a person highlights every match they are in, and the chart copies out as pasteable rows.

Also in this release: photos work on preview builds again. Uploads and existing photos were both failing on preview deploys because the photo Worker rejected the generated per-channel domain.

Summary by CodeRabbit

  • New Features
    • Added driver schedule creation with configurable modes, handoffs, conflict indicators, attendance views, and copyable schedule exports.
    • Added navigation to driver scheduling from schedule views.
  • Bug Fixes
    • Improved container-photo retry handling, replacement cleanup, and stable record identification.
    • Prevented failed updates from overwriting the last confirmed item state.
    • Cache clearing now waits for pending writes to finish.
    • Container photo locations cannot change during updates.
  • Accessibility
    • Added descriptive overdue-loan labels for screen readers.
  • Improvements
    • Limited optional contact entries to 256 characters.
    • Clarified checksum verification guidance.
    • Updated version to 1.4.0.

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds configurable driver schedule generation and a Flutter interface. It also updates container-photo handling, optimistic rollback, cache clearing, Firestore validation, accessibility labels, input limits, documentation, tests, and the application version.

Changes

Driver scheduling

Layer / File(s) Summary
Schedule models and derived views
lib/src/models/driver_schedule.dart
Adds schedule configurations, roster lookup, conflict and handoff highlights, and attendance aggregation.
Schedule generation and validation
lib/src/services/driver_schedule_generator.dart, test/driver_schedule_generator_test.dart
Adds parsing, validation, roster distribution, conflict resolution, handoff processing, export, and comprehensive tests.
Schedule screen and navigation
lib/src/ui/driver_schedule_screen.dart, lib/src/ui/schedule_tab.dart, test/driver_schedule_screen_test.dart
Adds schedule configuration inputs, generated tables, attendance views, highlighting, clipboard copying, navigation, and widget tests.

Operational reliability

Layer / File(s) Summary
Photo identity and cache integrity
lib/src/services/container_photo_sync_service.dart, firestore.rules, lib/src/services/photo_service.dart, test/container_photo_sync_service_test.dart, test/photo_service_test.dart
Adds hashed photo document IDs, immutable photo locations, queued-write handling during cache clearing, and coverage for these behaviors.
Packing photo and record flow
lib/src/ui/packing_tab.dart, test/packing_tab_test.dart, test/support/fake_container_photo_sync_service.dart
Adds photo retry and cleanup handling, advancement deduplication, inventory ID preservation, and related tests and fakes.
Confirmed optimistic state
lib/src/state/pit_controller_mixin.dart, test/inventory_controller_test.dart, test/support/fake_inventory_sync_service.dart
Tracks confirmed remote items and restores them after failed optimistic updates or deletes.

Release and interface polish

Layer / File(s) Summary
Guidance, accessibility, and release metadata
README.md, lib/src/ui/app_shell.dart, lib/src/ui/borrow_tab.dart, pubspec.yaml
Clarifies checksum verification, adds an overdue-count semantic label, limits contact input to 256 characters, and updates the application version.

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

Mergeability Score: 🔴 Critical · up to 2834b

The change adds schedule generation and photo persistence updates, but the current head still has a build-blocking type error, can strand or duplicate existing photo records, can race photo replacements, and can freeze the app for large schedules. These are concrete correctness and availability risks, so the PR is not merge-ready until the blocking issues are fixed; smaller validation and accessibility follow-ups also remain.

Possibly related PRs

Suggested reviewers: project516

Poem

I’m a rabbit reviewing rows,
Where drivers hop in ordered flows.
Hashes guard each photo’s place,
Confirmed states restore with grace.
Tables bloom and badges gleam—
A tidy, tested scheduling dream.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title identifies a versioned sync change but does not clearly describe the main driver schedule feature or the related fixes. Use a specific title such as "Add driver schedule rotation and fix photo synchronization".
✅ 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.4.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: 7

🤖 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/container_photo_sync_service.dart`:
- Around line 8-11: Update readKey to fall back to the legacy slug-only document
ID when the hash-suffixed ID is absent, and migrate the legacy document to the
new ID before returning it. Preserve existing behavior for current documents,
and add a regression test covering reads from a legacy document.
- Around line 8-11: Update containerPhotoDocId so unlabeledDocId is returned
only when trimmed is empty. For non-empty locations whose _readableSlug result
is empty, generate a document ID using a fixed readable fallback prefix plus
_hash8(trimmed), preserving the existing slug-hash format otherwise; add
coverage for a Unicode-only location.

In `@lib/src/services/driver_schedule_generator.dart`:
- Around line 75-80: Bound the repair work in the schedule-generation flow,
especially the loop around _resolveBackToBack and _resolveConflicts and their
internal pass loops. Replace fixed high iteration limits with a budget scaled to
slots or an overall work/time limit so large schedules cannot monopolize the UI
thread; preserve early termination when no moves occur. Reduce repeated per-slot
allocations in _namesIn and _othersIn where practical by reusing hoisted name
sets during each repair pass.

In `@lib/src/state/pit_controller_mixin.dart`:
- Around line 98-100: Convert the clamped indices passed to List.insert in both
affected sites of lib/src/state/pit_controller_mixin.dart (lines 98-100 and
129-135) to int with toInt(), including the calls in the restoration logic
around _pitConfirmed and its sibling insertion site.

In `@lib/src/ui/borrow_tab.dart`:
- Around line 774-775: Ensure _save or BorrowRecord.toJson validates _contact
before writing, enforcing the Firestore limit of 256 characters by rejecting or
truncating overlong values. Preserve valid contact values unchanged, and add a
regression test covering an overlong loaded contact during save.

In `@lib/src/ui/driver_schedule_screen.dart`:
- Around line 457-479: Update the flagged-cell build logic to render a small
non-color marker for non-null cellFlag values, while preserving the existing
background and semantics label; use the flag’s existing identity to choose the
marker. Add the same marker mapping and visual treatment to _LegendItem so the
legend communicates each flag without relying on color.

In `@lib/src/ui/packing_tab.dart`:
- Around line 240-244: Update _handleContainerPhoto to return immediately when
_containerPhotoChecks already contains location, preventing replacement actions
while the initial readKey request is pending. Preserve existing busy and
failed-state handling, and add a widget test covering an initial read completing
after a successful replacement without overwriting the new key.
🪄 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: 1dc3a54f-dfe8-4441-a059-7b2406f7957d

📥 Commits

Reviewing files that changed from the base of the PR and between 81dd6f9 and 2834bb6.

📒 Files selected for processing (21)
  • README.md
  • firestore.rules
  • lib/src/models/driver_schedule.dart
  • lib/src/services/container_photo_sync_service.dart
  • lib/src/services/driver_schedule_generator.dart
  • lib/src/services/photo_service.dart
  • lib/src/state/pit_controller_mixin.dart
  • lib/src/ui/app_shell.dart
  • lib/src/ui/borrow_tab.dart
  • lib/src/ui/driver_schedule_screen.dart
  • lib/src/ui/packing_tab.dart
  • lib/src/ui/schedule_tab.dart
  • pubspec.yaml
  • test/container_photo_sync_service_test.dart
  • test/driver_schedule_generator_test.dart
  • test/driver_schedule_screen_test.dart
  • test/inventory_controller_test.dart
  • test/packing_tab_test.dart
  • test/photo_service_test.dart
  • test/support/fake_container_photo_sync_service.dart
  • test/support/fake_inventory_sync_service.dart

Comment on lines 8 to +11
String containerPhotoDocId(String location) {
final slug = location
.trim()
.toLowerCase()
.replaceAll(RegExp('[^a-z0-9]+'), '-')
.replaceAll(RegExp('^-+|-+\$'), '');
return slug.isEmpty ? unlabeledDocId : slug;
final trimmed = location.trim();
final slug = _readableSlug(trimmed);
return slug.isEmpty ? unlabeledDocId : '$slug-${_hash8(trimmed)}';

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 | 🏗️ Heavy lift

Preserve access to existing slug-only photo documents.

readKey now reads only the hash-suffixed document ID. Photos saved before this change remain at the previous slug-only ID, so the UI treats them as missing and can create a second document for the same location.

Read the legacy ID as a fallback, then migrate it, or deploy a Firestore migration before this code. Add a regression test that reads a legacy document.

🤖 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/container_photo_sync_service.dart` around lines 8 - 11,
Update readKey to fall back to the legacy slug-only document ID when the
hash-suffixed ID is absent, and migrate the legacy document to the new ID before
returning it. Preserve existing behavior for current documents, and add a
regression test covering reads from a legacy document.

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

Do not map non-empty locations to unlabeled.

_readableSlug removes all non-[a-z0-9] characters. Locations such as "工具箱" then map to unlabeled, even though Firestore accepts them. Distinct locations can read the same photo. The first write also blocks later writes because location is immutable in firestore.rules.

Return unlabeledDocId only when trimmed.isEmpty. Use a fixed readable fallback prefix plus _hash8(trimmed) when the slug is empty. Add coverage for a Unicode-only location.

Proposed fix
 String containerPhotoDocId(String location) {
   final trimmed = location.trim();
+  if (trimmed.isEmpty) return unlabeledDocId;
   final slug = _readableSlug(trimmed);
-  return slug.isEmpty ? unlabeledDocId : '$slug-${_hash8(trimmed)}';
+  return '${slug.isEmpty ? 'location' : slug}-${_hash8(trimmed)}';
 }
📝 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
String containerPhotoDocId(String location) {
final slug = location
.trim()
.toLowerCase()
.replaceAll(RegExp('[^a-z0-9]+'), '-')
.replaceAll(RegExp('^-+|-+\$'), '');
return slug.isEmpty ? unlabeledDocId : slug;
final trimmed = location.trim();
final slug = _readableSlug(trimmed);
return slug.isEmpty ? unlabeledDocId : '$slug-${_hash8(trimmed)}';
String containerPhotoDocId(String location) {
final trimmed = location.trim();
if (trimmed.isEmpty) return unlabeledDocId;
final slug = _readableSlug(trimmed);
return '${slug.isEmpty ? 'location' : slug}-${_hash8(trimmed)}';
🤖 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/container_photo_sync_service.dart` around lines 8 - 11,
Update containerPhotoDocId so unlabeledDocId is returned only when trimmed is
empty. For non-empty locations whose _readableSlug result is empty, generate a
document ID using a fixed readable fallback prefix plus _hash8(trimmed),
preserving the existing slug-hash format otherwise; add coverage for a
Unicode-only location.

Comment on lines +75 to +80
_resolveConflicts(columns, slots, roleKeys);
for (var round = 0; round < 20; round++) {
final movedAdjacent = _resolveBackToBack(columns, slots, roleKeys);
final movedConflict = _resolveConflicts(columns, slots, roleKeys);
if (!movedAdjacent && !movedConflict) break;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Bound the repair work by slot count to protect the UI thread.

maxScheduleSlots is 500, and both repair passes are quadratic in slots. _resolveBackToBack runs slots × roleKeys × slots iterations per pass, and each inner iteration calls _namesIn and _othersIn, which allocate a new Set and call _at twice per role. One pass at 500 slots and 8 roles is about 16M inner steps. Each function allows up to 300 passes, and Line 76 repeats both up to 20 times.

DriverScheduleScreen._generate calls generate synchronously inside setState, so this work runs on the UI thread. A large match count can freeze the app for seconds.

Reduce the pass budget, scale it by slots, or add an overall work/time budget. Alternatively lower maxScheduleSlots to a realistic competition size.

⚡ Example: scale the pass budget and hoist the per-slot name set
-    _resolveConflicts(columns, slots, roleKeys);
-    for (var round = 0; round < 20; round++) {
+    final rounds = slots > 100 ? 4 : 20;
+    _resolveConflicts(columns, slots, roleKeys);
+    for (var round = 0; round < rounds; round++) {
       final movedAdjacent = _resolveBackToBack(columns, slots, roleKeys);
       final movedConflict = _resolveConflicts(columns, slots, roleKeys);
       if (!movedAdjacent && !movedConflict) break;
     }
   ) => {
     for (final key in roleKeys)
-      if (_at(columns, key, slot).isNotEmpty) _at(columns, key, slot),
+      if (_at(columns, key, slot) case final name when name.isNotEmpty) name,
   };

Also applies to: 183-275

🤖 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/driver_schedule_generator.dart` around lines 75 - 80, Bound
the repair work in the schedule-generation flow, especially the loop around
_resolveBackToBack and _resolveConflicts and their internal pass loops. Replace
fixed high iteration limits with a budget scaled to slots or an overall
work/time limit so large schedules cannot monopolize the UI thread; preserve
early termination when no moves occur. Reduce repeated per-slot allocations in
_namesIn and _othersIn where practical by reusing hoisted name sets during each
repair pass.

Comment on lines +98 to +100
final confirmed = _pitConfirmed[item.id];
if (confirmed != null) {
restored.insert(previousIndex.clamp(0, restored.length), confirmed);

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 | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 2 '\.insert\(\s*.*\.clamp\(' lib test

Repository: Spectrum3847/spectrum-pit

Length of output: 967


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- target source ---'
sed -n '80,145p' lib/src/state/pit_controller_mixin.dart

printf '%s\n' '--- Dart configuration ---'
fd -HI -t f '(pubspec\.yaml|analysis_options\.yaml|.*\.dart-version$)' . -x sh -c 'echo "--- $1"; sed -n "1,180p" "$1"' sh {}

Repository: Spectrum3847/spectrum-pit

Length of output: 7275


🏁 Script executed:

#!/bin/bash
set -euo pipefail

if command -v dart >/dev/null 2>&1; then
  cat >/tmp/clamp_insert_check.dart <<'DART'
void main() {
  final previousIndex = 1;
  final restored = <Object>[];
  restored.insert(previousIndex.clamp(0, restored.length), Object());
}
DART
  dart analyze /tmp/clamp_insert_check.dart 2>&1 || true
else
  echo "Dart SDK is not available"
fi

python3 - <<'PY'
from pathlib import Path
text = Path("lib/src/state/pit_controller_mixin.dart").read_text()
needles = [
    "restored.insert(previousIndex.clamp(0, restored.length), confirmed);",
]
print("matching call count:", text.count(needles[0]))
print("all matching calls use .toInt():", all(
    ".toInt()" in line for line in text.splitlines()
    if "restored.insert(previousIndex.clamp(0, restored.length), confirmed);" in line
))
PY

Repository: Spectrum3847/spectrum-pit

Length of output: 251


Convert each clamped index to int.

int.clamp returns num, but List.insert requires an int index. Convert both calls with .toInt().

📍 Affects 1 file
  • lib/src/state/pit_controller_mixin.dart#L98-L100 (this comment)
  • lib/src/state/pit_controller_mixin.dart#L129-L135
🤖 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 98 - 100, Convert the
clamped indices passed to List.insert in both affected sites of
lib/src/state/pit_controller_mixin.dart (lines 98-100 and 129-135) to int with
toInt(), including the calls in the restoration logic around _pitConfirmed and
its sibling insertion site.

Comment on lines +774 to +775
maxLength: 256,
keyboardType: TextInputType.text,

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 \
  'maxLength: 256|_contact|contact:|toJson|LengthLimitingTextInputFormatter' \
  lib/src/ui/borrow_tab.dart \
  lib/src/models/borrow_record.dart \
  test

Repository: Spectrum3847/spectrum-pit

Length of output: 33525


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- BorrowRecord model ---'
cat -n lib/src/models/borrow_record.dart

printf '%s\n' '--- Borrow editor save and contact field ---'
sed -n '560,635p' lib/src/ui/borrow_tab.dart
sed -n '750,785p' lib/src/ui/borrow_tab.dart

printf '%s\n' '--- BorrowRecord persistence and validation references ---'
rg -n -C 5 \
  'BorrowRecord|borrow_records|contact|maxLength|256|length|validate|validation|withConverter|toJson|fromJson' \
  lib test firestore.rules firebase.json README.md 2>/dev/null || true

Repository: Spectrum3847/spectrum-pit

Length of output: 50384


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Borrow controller write path ---'
ast-grep outline lib/src/state/borrow_controller.dart
rg -n -C 12 \
  'upsert|onSubmit|save|add|update|catch|error|isValidBorrowRecord' \
  lib/src/state/borrow_controller.dart \
  lib/src/ui/borrow_tab.dart \
  lib/src/services \
  test/support/fake_borrow_sync_service.dart

printf '%s\n' '--- Relevant Firestore rule block ---'
sed -n '221,246p' firestore.rules

printf '%s\n' '--- Existing editor save tests ---'
rg -n -C 10 \
  'open.*Borrow|edit.*borrow|Contact|contact|maxLength|Check out|Save|save' \
  test/borrow_tab_test.dart

Repository: Spectrum3847/spectrum-pit

Length of output: 50382


Validate the contact length before saving.

firestore.rules requires contact.size() <= 256, but TextField.maxLength does not constrain an overlong value loaded into _contact. Saving such a record sends the unchanged value through BorrowRecord.toJson(), and Firestore rejects the write. Validate or truncate the value in _save or the model, and add a regression 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/borrow_tab.dart` around lines 774 - 775, Ensure _save or
BorrowRecord.toJson validates _contact before writing, enforcing the Firestore
limit of 256 characters by rejecting or truncating overlong values. Preserve
valid contact values unchanged, and add a regression test covering an overlong
loaded contact during save.

Source: MCP tools

Comment on lines +457 to +479
@override
Widget build(BuildContext context) {
final accent = PitPalette.accentOf(context);

final cellFlag = flag;
final label = name.isEmpty ? 'Nobody' : name;

return Semantics(
label: cellFlag == null ? label : '$label, ${cellFlag.label}',
excludeSemantics: true,
child: Container(
decoration: BoxDecoration(
color: cellFlag?.colorOf(context).withValues(alpha: 0.20),
border: selected ? Border.all(color: accent, width: 2) : null,
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
child: Text(
name.isEmpty ? '-' : name,
style: Theme.of(context).textTheme.bodyMedium,
),
),
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a non-color marker for the cell flag.

The cell communicates conflict, backToBack, and handoff only through a background fill at 20% alpha. Users who cannot distinguish those hues get no visual cue, and the legend is also color-keyed. The Semantics label covers screen-reader users only.

Add a small icon or text marker inside the flagged cell, and repeat the same marker in _LegendItem.

♿ Example: add a per-flag icon
         padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
-        child: Text(
-          name.isEmpty ? '-' : name,
-          style: Theme.of(context).textTheme.bodyMedium,
+        child: Row(
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            if (cellFlag != null) ...[
+              Icon(cellFlag.icon, size: 14, color: cellFlag.colorOf(context)),
+              const SizedBox(width: 6),
+            ],
+            Text(
+              name.isEmpty ? '-' : name,
+              style: Theme.of(context).textTheme.bodyMedium,
+            ),
+          ],
         ),
🤖 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/driver_schedule_screen.dart` around lines 457 - 479, Update the
flagged-cell build logic to render a small non-color marker for non-null
cellFlag values, while preserving the existing background and semantics label;
use the flag’s existing identity to choose the marker. Add the same marker
mapping and visual treatment to _LegendItem so the legend communicates each flag
without relying on color.

Comment on lines 240 to +244
Future<void> _handleContainerPhoto(String location) {
if (_busyContainerPhotos.contains(location)) return Future<void>.value();
if (_containerPhotoLoadFailed.contains(location)) {
_containerPhotoChecks.add(location);
return _loadContainerPhoto(location);

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

Block photo actions while the initial read is pending.

A user can start a replacement before the automatic readKey call completes. If that read returns afterward, _loadContainerPhoto overwrites _containerPhotoKeys[location] with the stale key. The new key remains persisted, and the old key can remain undeleted.

Return early while _containerPhotoChecks contains location, or version reads and ignore obsolete results. Add a widget test that completes an initial read after a replacement succeeds.

Proposed fix
 Future<void> _handleContainerPhoto(String location) {
-  if (_busyContainerPhotos.contains(location)) return Future<void>.value();
+  if (_busyContainerPhotos.contains(location) ||
+      _containerPhotoChecks.contains(location)) {
+    return Future<void>.value();
+  }
   if (_containerPhotoLoadFailed.contains(location)) {
📝 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
Future<void> _handleContainerPhoto(String location) {
if (_busyContainerPhotos.contains(location)) return Future<void>.value();
if (_containerPhotoLoadFailed.contains(location)) {
_containerPhotoChecks.add(location);
return _loadContainerPhoto(location);
Future<void> _handleContainerPhoto(String location) {
if (_busyContainerPhotos.contains(location) ||
_containerPhotoChecks.contains(location)) {
return Future<void>.value();
}
if (_containerPhotoLoadFailed.contains(location)) {
_containerPhotoChecks.add(location);
return _loadContainerPhoto(location);
🤖 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 240 - 244, Update
_handleContainerPhoto to return immediately when _containerPhotoChecks already
contains location, preventing replacement actions while the initial readKey
request is pending. Preserve existing busy and failed-state handling, and add a
widget test covering an initial read completing after a successful replacement
without overwriting the new key.

@Project516
Project516 merged commit a4baccc into main Aug 13, 2026
2 checks passed
@Project516
Project516 deleted the sync/v1.4.0 branch August 13, 2026 02:12
@coderabbitai coderabbitai Bot mentioned this pull request Aug 14, 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