Skip to content

Sync: Fixing Teleop submission issue & local storage improvements & DB debug function#42

Merged
1834423612 merged 12 commits intomasterfrom
features-master
Apr 6, 2026
Merged

Sync: Fixing Teleop submission issue & local storage improvements & DB debug function#42
1834423612 merged 12 commits intomasterfrom
features-master

Conversation

@1834423612
Copy link
Copy Markdown
Owner

@1834423612 1834423612 commented Apr 4, 2026

Summary by CodeRabbit

  • New Features

    • Local database inspection added to Settings (schema snapshot, table/row browsing, search, pagination) — visible in debug builds.
    • New settings screen wiring and conditional Settings nav.
  • Improvements

    • Teleop state restoration, live snapshot flushing, and more reliable timing persistence.
    • Immediate-trigger network sync for faster push/fetch.
    • Improved upload tracking and safer serialized local database writes.
  • Chores

    • App version bumped to 1.4.8.

@1834423612 1834423612 self-assigned this Apr 4, 2026
Copilot AI review requested due to automatic review settings April 4, 2026 16:18
@1834423612 1834423612 added bug Something isn't working enhancement New feature or request 1st Priority 1st/top Priority Sync Sync labels Apr 4, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 4, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 38a2b7f0-9bb7-4c0b-b700-7762562c07d7

📥 Commits

Reviewing files that changed from the base of the PR and between 6e52529 and c31ee7e.

📒 Files selected for processing (3)
  • app/build.gradle.kts
  • app/src/main/java/com/team695/scoutifyapp/data/repository/LocalDatabaseDebugRepository.kt
  • app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt

Walkthrough

Bumps app version to 1.4.8 (code 42); introduces LocalDatabaseWriteCoordinator to serialize DB writes; extends GameDetails/Comments schema with local-teleop and upload-tracking fields plus migrations; adds LocalDatabaseDebugRepository and settings UI; converts NetworkMonitor sync loop to event-driven signalling with requestImmediateSync().

Changes

Cohort / File(s) Summary
Version
app/build.gradle.kts
Bumped versionCode 1 → 42 and versionName "1.0.3" → "1.4.8" (affects emitted BuildConfig.VERSION_NAME).
DB Schema & Migrations
app/src/main/sqldelight/.../GameDetails.sq, .../Comments.sq, app/src/main/sqldelight/migrations/3.sqm, 4.sqm, 5.sqm
Added local-teleop columns and pending_upload to gameDetailsEntity; added uploaded to commentsEntity; updated insert/select/update queries and added migrations/backfill.
Model & Adapters
app/src/main/java/.../data/api/model/GameDetails.kt, app/src/main/java/.../MainActivity.kt
Added transient local-teleop fields to GameDetails; added integer adapters in MainActivity for new GameDetailsEntity columns.
Global DB Write Coordination
app/src/main/java/.../data/repository/LocalDatabaseWriteCoordinator.kt
Added LocalDatabaseWriteCoordinator with suspend fun <T> withWriteLock(block: suspend () -> T) to serialize in-app DB writes.
Repository Changes & Upload Tracking
app/src/main/java/.../data/repository/*Repository.kt
Wrapped write paths in withWriteLock; GameDetailRepository computes/persists pending_upload and compares upload-relevant fields before clearing; CommentRepository tracks uploaded and marks rows after push; many repos updated to use the coordinator.
Local DB Debugging
app/src/main/java/.../data/repository/LocalDatabaseDebugRepository.kt, app/src/main/java/.../ui/viewModels/SettingsViewModel.kt
Added LocalDatabaseDebugRepository (snapshot/row loaders, redaction) and SettingsViewModel (snapshot/row loading, UI state).
Settings UI & Wiring
app/src/main/java/.../ui/screens/FormScreen.kt, app/src/main/java/.../ui/components/NavRail.kt, app/src/main/java/.../navigation/AppNav.kt, app/src/main/java/.../Root.kt, app/src/main/java/.../MainActivity.kt
Threaded LocalDatabaseDebugRepository MainActivity→Root→AppNav; NavRail gets showSettings flag; FormScreen becomes settings/debug DB viewer backed by SettingsViewModel.
Network Sync Flow
app/src/main/java/.../data/api/NetworkMonitor.kt
Reworked sync loop to use syncRequests: MutableSharedFlow<Unit> with requestImmediateSync(); replaced unconditional delays with waitForNextSyncWindow() and reordered push-before-fetch retries.
Teleop & Persistence
app/src/main/java/.../ui/viewModels/DataViewModel.kt, app/src/main/java/.../ui/screens/data/{DataScreen,EndgameDetails,PostgameDetails}.kt
Added teleop restore state, explicit flush APIs (flushNow, flushNowAsync), live snapshot flushing, refined teleop ticking/completion, and flush error-handling in UI transitions.
SQL Convenience
app/src/main/sqldelight/.../PitScoutingTab.sq, app/src/main/sqldelight/.../Pitscout.sq
Added clearAllTabs, deletePitscoutByEventId, and clearAllPitscout statements.
Config
app/src/main/java/.../config/DebugConfig.kt
Added ENABLE_LOCAL_DATABASE_DEBUGGING = BuildConfig.DEBUG to gate debug repo/UI.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant NM as NetworkMonitor
    participant SYNC as SyncLoop
    participant SRV as RemoteServer
    participant DB as LocalDatabase
    User->>NM: network capability events
    NM->>SYNC: requestImmediateSync() (emit)
    Note right of SYNC: waitForNextSyncWindow() suspends until event or timeout
    SYNC->>SRV: retryPushUntilSuccess()
    SRV-->>SYNC: push result
    SYNC->>SRV: retryFetchUntilSuccess()
    SRV-->>SYNC: fetched data
    SYNC->>DB: apply fetch results
    DB->>DB: LocalDatabaseWriteCoordinator.withWriteLock { transaction }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 Mutex hums, I hop in rows and cells,
Timers tucked in fields where teleop dwells.
Debug tables peep, secrets softly masked,
Sync wakes on a ping — no more idle task.
A bunny cheers: the DB sleeps unharassed.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the three main changes: teleop submission fix, local storage improvements, and DB debug functionality. It is concise, specific, and directly reflects the primary objectives of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 features-master

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 and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses offline sync reliability (especially Teleop completion/submission), adds local-state persistence for Teleop timing/section, introduces upload-tracking flags for game details + comments, and adds a local database debug/inspection utility in Settings.

Changes:

  • Add pending_upload (game details) and uploaded (comments) flags to track what still needs to be pushed, plus associated migrations/queries.
  • Persist and restore local Teleop UI state (section + timers) to improve resilience across app restarts/navigation.
  • Add a Settings “Local Database Preview” tool backed by a new debug repository, plus a write-coordinator to serialize DB writes.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
app/src/main/sqldelight/migrations/3.sqm Adds local Teleop persistence columns to gameDetailsEntity.
app/src/main/sqldelight/migrations/4.sqm Adds upload-tracking columns and backfills flags for existing rows.
app/src/main/sqldelight/com/team695/scoutifyapp/db/GameDetails.sq Adds local Teleop columns + pending_upload, filters “completed tasks” by pending upload, adds update query.
app/src/main/sqldelight/com/team695/scoutifyapp/db/Comments.sq Adds uploaded flag and new queries to fetch/mark pending submitted comments.
app/src/main/java/com/team695/scoutifyapp/ui/viewModels/SettingsViewModel.kt New VM for loading/refreshing DB debug snapshots and UI selection state.
app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt Persists local Teleop state + adds explicit flush APIs and more frequent persistence during Teleop.
app/src/main/java/com/team695/scoutifyapp/ui/screens/FormScreen.kt Replaces placeholder with Settings UI + local database inspection UI.
app/src/main/java/com/team695/scoutifyapp/ui/screens/data/PostgameDetails.kt Flushes form state before returning home when completed.
app/src/main/java/com/team695/scoutifyapp/ui/screens/data/EndgameDetails.kt Ensures Teleop is completed + flushed before transitioning to postgame.
app/src/main/java/com/team695/scoutifyapp/ui/screens/data/DataScreen.kt Flush-on-dispose and updated Teleop timer loop using latest state.
app/src/main/java/com/team695/scoutifyapp/Root.kt Wires LocalDatabaseDebugRepository into navigation root.
app/src/main/java/com/team695/scoutifyapp/navigation/AppNav.kt Injects SettingsViewModel and passes it to the Settings screen.
app/src/main/java/com/team695/scoutifyapp/MainActivity.kt Adds adapters for new local Teleop timing fields and instantiates debug repository.
app/src/main/java/com/team695/scoutifyapp/data/repository/UserRepository.kt Serializes DB writes via LocalDatabaseWriteCoordinator.
app/src/main/java/com/team695/scoutifyapp/data/repository/TeamNameRepository.kt Serializes DB writes via LocalDatabaseWriteCoordinator.
app/src/main/java/com/team695/scoutifyapp/data/repository/TaskRepository.kt Serializes DB writes via LocalDatabaseWriteCoordinator.
app/src/main/java/com/team695/scoutifyapp/data/repository/PitScoutingRepository.kt Serializes DB writes; makes persistTab suspend + write-locked.
app/src/main/java/com/team695/scoutifyapp/data/repository/MatchRepository.kt Serializes match writes via LocalDatabaseWriteCoordinator.
app/src/main/java/com/team695/scoutifyapp/data/repository/LocalDatabaseWriteCoordinator.kt New global mutex to serialize local DB writes.
app/src/main/java/com/team695/scoutifyapp/data/repository/LocalDatabaseDebugRepository.kt New debug repository to snapshot tables/rows/columns via raw SQL queries.
app/src/main/java/com/team695/scoutifyapp/data/repository/GameDetailRepository.kt Uses pending_upload gating for pushes and computes pending state on writes.
app/src/main/java/com/team695/scoutifyapp/data/repository/CommentRepository.kt Pushes only pending-submitted comments and marks them uploaded.
app/src/main/java/com/team695/scoutifyapp/data/api/NetworkMonitor.kt Adds immediate-sync requests and replaces fixed delay with “wait for sync window.”
app/src/main/java/com/team695/scoutifyapp/data/api/model/GameDetails.kt Adds transient local Teleop fields when mapping from DB.
app/build.gradle.kts Bumps app version name.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/src/main/java/com/team695/scoutifyapp/data/repository/GameDetailRepository.kt Outdated
Comment thread app/src/main/sqldelight/com/team695/scoutifyapp/db/Comments.sq
Comment thread app/src/main/java/com/team695/scoutifyapp/ui/screens/FormScreen.kt
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: be342a975e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/src/main/java/com/team695/scoutifyapp/data/repository/GameDetailRepository.kt Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🧹 Nitpick comments (4)
app/src/main/java/com/team695/scoutifyapp/navigation/AppNav.kt (1)

170-186: Consider gating DB-debug UI in non-debug builds.

If SettingsViewModel exposes raw local DB inspection, restrict this route behind a debug/build flag to reduce accidental data exposure in release.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/team695/scoutifyapp/navigation/AppNav.kt` around lines
170 - 186, The settings route currently mounts a SettingsViewModel and
FormScreen that expose local DB debugging; restrict this to debug builds by
gating the composable registration (the composable(route = "settings") block)
behind a build or feature flag check (e.g., BuildConfig.DEBUG or a runtime
debugFeature flag) so that SettingsViewModel (constructed via ViewModelFactory
with localDatabaseDebugRepository) and FormScreen are only created in debug/dev
builds; in non-debug builds either register a safe placeholder route or omit the
route entirely and ensure AuthGuard and navController logic remain unchanged.
app/src/main/sqldelight/migrations/4.sqm (1)

15-17: Redundant backfill can be removed for faster migration.

uploaded is added as NOT NULL DEFAULT 0, so existing rows already get 0; this update is a no-op.

Optional cleanup
-UPDATE commentsEntity
-SET uploaded = 0
-WHERE submitted = 1;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/sqldelight/migrations/4.sqm` around lines 15 - 17, The UPDATE in
migration 4.sqm that sets uploaded = 0 on commentsEntity where submitted = 1 is
redundant because the uploaded column was added as NOT NULL DEFAULT 0 and
existing rows already receive 0; remove that UPDATE statement from the migration
to speed it up and avoid a no-op update, leaving only the ALTER/ADD column
statements that add uploaded with NOT NULL DEFAULT 0 (reference: commentsEntity
table and uploaded column in migration 4.sqm).
app/src/main/java/com/team695/scoutifyapp/data/repository/LocalDatabaseDebugRepository.kt (1)

51-69: Load row data on demand instead of snapshotting the entire DB.

Lines 53-56 materialize every row of every table before the screen can render. That makes each refresh O(total database size) in time and memory even if the user inspects only one table. Consider loading table names/columns first and fetching or paging rows for the selected table.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/team695/scoutifyapp/data/repository/LocalDatabaseDebugRepository.kt`
around lines 51 - 69, The current loadSnapshot implementation eagerly
materializes every row by calling loadRows for each table; change it to only
collect table metadata (use loadTableNames and loadColumns) and avoid calling
loadRows inside loadSnapshot, instead populate LocalDatabaseDebugTable with
name, columns and a rowCount obtained via a lightweight count query
(implement/getTableRowCount if needed), and add a separate paged loader method
(e.g., loadRowsForTable(tableName, limit, offset) or loadRowsOnDemand) that UI
can call when a specific table is inspected; update LocalDatabaseDebugSnapshot
construction to include only schemaVersion, loadedAtMillis and the lightweight
tables list.
app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt (1)

89-107: Avoid double-writing the same snapshot.

Most mutations now flush immediately and then get written again by the debounced collector 2 seconds later. Consider funnelling both paths through one write queue, or skipping the debounced save when the snapshot already matches the last persisted state.

Also applies to: 146-154

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt`
around lines 89 - 107, The debounced collector on _formState is causing
duplicate writes because immediate mutations are also persisted; to fix,
centralize writes by introducing a single persistence gate: add a private var
lastPersistedState: GameFormState? in the ViewModel and update it inside
persistFormState (or its completion callback), then in the .onEach block compare
currentFormState to lastPersistedState and return early if they are equal;
alternatively replace direct mutation-path calls to persistFormState with a
single enqueuePersist(currentFormState) helper that updates lastPersistedState
and performs the actual write so both immediate mutations and the debounced
collector funnel through the same function (referencing _formState,
persistFormState, GameFormState, viewModelScope).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/build.gradle.kts`:
- Around line 30-31: The APK/Bundle metadata was updated to versionName =
"1.4.1" but versionCode remains 1; update the integer assigned to versionCode to
a higher value than the current (e.g., bump versionCode to the next sequential
integer) so Android stores treat this as a new release; modify the versionCode
entry in the same build.gradle.kts where versionName is set (look for the
versionCode and versionName declarations) to the appropriate incremented value.

In
`@app/src/main/java/com/team695/scoutifyapp/data/repository/GameDetailRepository.kt`:
- Around line 127-130: The new-record branch in the pendingUpload calculation
wrongly uses hasMeaningfulUploadData(details) so brand-new rows can be created
with pending_upload=false; update the when in GameDetailRepository.kt (the
pendingUpload assignment that checks existingEntity, hasMeaningfulUploadData,
and areUploadRelevantFieldsEqual) so that existingEntity == null returns true
(new rows default to pending) and only clear pending_upload after a successful
push or when the other branches determine it; leave the other branches
(areUploadRelevantFieldsEqual -> existingEntity.pending_upload, else -> true)
unchanged.
- Around line 51-71: The code currently snapshots pendingEntities via
selectCompletedTasks(), sends them with
gameDetailsService.updateGameDetails(...), then unconditionally clears
pending_upload for each task_id inside
LocalDatabaseWriteCoordinator.withWriteLock, which can race with
updateDbFromGameDetails() and drop newer unsent edits; to fix, inside the lock
re-query the current row for each task_id (using
db.gameDetailsQueries.selectByTaskId or equivalent), compare the current row's
fields (the ones included in the payload produced by createGameDetailsFromDb())
to the payload you just sent, and only call
db.gameDetailsQueries.updatePendingUploadState(task_id=...,
pending_upload=false) when the stored row still matches that payload (otherwise
skip clearing); keep the snapshot-to-send flow using createGameDetailsFromDb and
perform comparisons by unique identifiers/fields to ensure you don't clear a
newer pending_upload.

In
`@app/src/main/java/com/team695/scoutifyapp/data/repository/LocalDatabaseDebugRepository.kt`:
- Around line 74-139: The repository is currently exposing all non-system tables
and their rows via loadTableNames, loadColumns and loadRows which makes any
tokens/PII visible; add a runtime gate (e.g., a boolean like
enableLocalDatabaseDebugging read from a secure BuildConfig/debug flag or an
internal settings toggle) and enforce it in loadTableNames/loadRows/loadColumns
so no data is returned unless the flag is true, or implement an explicit
allowlist of table names and a column-redaction policy (e.g., redact or omit
values for column names matching /token|secret|password|api_key|email/i) before
constructing LocalDatabaseDebugCell/LocalDatabaseDebugRow; ensure FormScreen’s
rendering will receive only filtered results by changing these functions rather
than the UI.

In
`@app/src/main/java/com/team695/scoutifyapp/data/repository/PitScoutingRepository.kt`:
- Around line 158-160: Multiple pit-scouting mutations are split across separate
LocalDatabaseWriteCoordinator.withWriteLock calls (e.g., calls that delete via
db.pitscoutQueries.deletePitscoutByFormId and persistTab()), which allows
interleaving coroutines and inconsistent state; refactor by extracting
non-locking helper variants (e.g., persistTabInternal or persistTabNoLock) that
do the DB mutations without acquiring the mutex, and then wrap each logical
mutation sequence in a single LocalDatabaseWriteCoordinator.withWriteLock {
db.transaction { ... } } block so all pitscoutQueries and pitScoutingTabQueries
updates (and queued-submission row changes) happen atomically; apply the same
pattern to the other mentioned sites (lines around 184-190, 199-205, 318-320,
377-389, 562-579) to avoid split locks.

In
`@app/src/main/java/com/team695/scoutifyapp/data/repository/TeamNameRepository.kt`:
- Around line 21-29: The refresh currently calls
LocalDatabaseWriteCoordinator.withWriteLock and performs queries.deleteAll()
followed by multiple queries.insertTeam() calls, which can leave readers seeing
an empty/partial cache; fix this by wrapping the deleteAll + all insertTeam
operations in a single database transaction inside the withWriteLock block
(e.g., use your queries' transaction/withTransaction API or the DB's transaction
method) so that getTeam()/getAllTeams() cannot observe intermediate state—ensure
the transaction scope includes queries.deleteAll() and the entire teams.forEach
{ queries.insertTeam(...) } sequence.

In `@app/src/main/java/com/team695/scoutifyapp/data/repository/UserRepository.kt`:
- Around line 101-110: The logout path currently removes the user and clears
some tables inside LocalDatabaseWriteCoordinator.withWriteLock but can race with
an in-flight sync job that will reinsert old-session rows and it also omits
pit-scouting tables; before acquiring the write lock, cancel and await the
active sync job (the sync coroutine/job managed by your sync service/controller)
to ensure no network worker can write after logout, then inside
LocalDatabaseWriteCoordinator.withWriteLock perform db.userQueries.deleteUser()
and extend the db.transaction to also clear pit-scouting tables (e.g.,
db.pitScoutingQueries.clearAllPitScouting() or the equivalent table methods) in
addition to db.matchQueries.clearAllMatches(), db.taskQueries.clearAllTasks(),
and db.commentsQueries.clearAllComments(), so teardown is atomic and cannot be
repopulated by a concurrent sync.

In `@app/src/main/java/com/team695/scoutifyapp/ui/screens/data/DataScreen.kt`:
- Around line 176-191: The code clamps teleopTotalMilliseconds to switchTime
then calls dataViewModel.updateTime with the full deltaTime, which double-counts
the portion of the frame that was already applied before the boundary; modify
the logic in the frame handler around
currentFormState.teleopTotalMilliseconds/switchTime so that after detecting a
boundary you compute the overflow (overflow = deltaTime - (switchTime -
priorTeleopTotal)) and call dataViewModel.setTeleopSection(...) with
teleopTotalMilliseconds = switchTime, then if nextSection == TeleopSection.ENDED
call dataViewModel.completeTeleop() and set startTime appropriately; otherwise
call dataViewModel.updateTime only with the overflow and loop/iterate to handle
the case where a single frame spans multiple switchTime boundaries until
overflow is consumed. Ensure references to
currentFormState.teleopTotalMilliseconds, switchTime, nextSection,
TeleopSection, dataViewModel.setTeleopSection, dataViewModel.updateTime and
dataViewModel.completeTeleop are updated accordingly.

In `@app/src/main/java/com/team695/scoutifyapp/ui/screens/data/EndgameDetails.kt`:
- Around line 153-155: The submit coroutine calls
dataViewModel.completeTeleop(), dataViewModel.flushNow(), then
switchToPostgame() but does not handle exceptions from flushNow(), so failures
abort navigation silently; wrap the persistence call in try/catch inside the
same coroutine, catch exceptions from dataViewModel.flushNow(), show
user-visible feedback (e.g., a Snackbar/Toast or dialog) and allow retry or
cancel, and only call switchToPostgame() on success; ensure you reference the
same coroutine scope used for submission and keep completeTeleop() as-is before
attempting flushNow().

In
`@app/src/main/java/com/team695/scoutifyapp/ui/screens/data/PostgameDetails.kt`:
- Around line 75-78: Wrap the persistence call in coroutineScope.launch with a
try/catch around dataViewModel.flushNow() so a thrown exception is handled: on
success call returnToHome(), on failure log the exception and surface user
feedback (e.g., Snackbar/Toast via scaffoldState.snackbarHostState.showSnackbar
or a UI error helper) so the user knows the save failed and navigation is
prevented; ensure you reference dataViewModel.flushNow() and returnToHome() in
the catch handling and include a logged error message for diagnostics.

In `@app/src/main/java/com/team695/scoutifyapp/ui/screens/FormScreen.kt`:
- Around line 672-695: The Columns card (the Column containing the "Columns"
Text and the SelectionContainer that displays columnText with
modifier.verticalScroll(scrollState)) currently has no height cap and can grow
to fill the pane; constrain it by adding a height limit to the card or to the
scrollable Text (e.g., add Modifier.heightIn(max = <sensible dp>) to the outer
Column or to the SelectionContainer/Text modifier) or implement a simple
collapse/expand toggle around that block so long lists don’t consume the entire
view; refer to the Column, SelectionContainer, columnText and scrollState
identifiers when making the change.

In `@app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt`:
- Around line 34-38: The snapshot type RestoredTeleopState currently omits the
teleopRunning flag so restores cannot distinguish paused vs active timers;
modify RestoredTeleopState to include a Boolean teleopRunning, update the code
that builds/saves the snapshot (where you serialize teleop state) to store the
current teleopRunning, and update the restore logic (the constructor/mapper that
consumes RestoredTeleopState) to rehydrate teleopRunning so updateTime() and
endTeleop() behavior is correct after restart; ensure references to
teleopRunning in updateTime() and endTeleop() remain consistent with the
restored value.
- Around line 358-364: The five MutableStateFlow update blocks (calls to
_formState.update) mix the retry-safe lambda parameter with live reads of
_formState.value; fix each block (the ones updating paths/currentStroke/undoTree
and similar logic) to only reference the lambda parameter (e.g., use it.paths,
it.currentStroke, it.justUndid, it.undoTree) inside the update lambda, compute
any derived values from that parameter (for example newUndoTree = if
(it.justUndid) emptyList() else it.undoTree) and return it.copy(...) using those
values rather than reading _formState.value directly; do this consistently in
the blocks that modify paths/currentStroke/undoTree and the other four update
usages referenced in the comment.

---

Nitpick comments:
In
`@app/src/main/java/com/team695/scoutifyapp/data/repository/LocalDatabaseDebugRepository.kt`:
- Around line 51-69: The current loadSnapshot implementation eagerly
materializes every row by calling loadRows for each table; change it to only
collect table metadata (use loadTableNames and loadColumns) and avoid calling
loadRows inside loadSnapshot, instead populate LocalDatabaseDebugTable with
name, columns and a rowCount obtained via a lightweight count query
(implement/getTableRowCount if needed), and add a separate paged loader method
(e.g., loadRowsForTable(tableName, limit, offset) or loadRowsOnDemand) that UI
can call when a specific table is inspected; update LocalDatabaseDebugSnapshot
construction to include only schemaVersion, loadedAtMillis and the lightweight
tables list.

In `@app/src/main/java/com/team695/scoutifyapp/navigation/AppNav.kt`:
- Around line 170-186: The settings route currently mounts a SettingsViewModel
and FormScreen that expose local DB debugging; restrict this to debug builds by
gating the composable registration (the composable(route = "settings") block)
behind a build or feature flag check (e.g., BuildConfig.DEBUG or a runtime
debugFeature flag) so that SettingsViewModel (constructed via ViewModelFactory
with localDatabaseDebugRepository) and FormScreen are only created in debug/dev
builds; in non-debug builds either register a safe placeholder route or omit the
route entirely and ensure AuthGuard and navController logic remain unchanged.

In `@app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt`:
- Around line 89-107: The debounced collector on _formState is causing duplicate
writes because immediate mutations are also persisted; to fix, centralize writes
by introducing a single persistence gate: add a private var lastPersistedState:
GameFormState? in the ViewModel and update it inside persistFormState (or its
completion callback), then in the .onEach block compare currentFormState to
lastPersistedState and return early if they are equal; alternatively replace
direct mutation-path calls to persistFormState with a single
enqueuePersist(currentFormState) helper that updates lastPersistedState and
performs the actual write so both immediate mutations and the debounced
collector funnel through the same function (referencing _formState,
persistFormState, GameFormState, viewModelScope).

In `@app/src/main/sqldelight/migrations/4.sqm`:
- Around line 15-17: The UPDATE in migration 4.sqm that sets uploaded = 0 on
commentsEntity where submitted = 1 is redundant because the uploaded column was
added as NOT NULL DEFAULT 0 and existing rows already receive 0; remove that
UPDATE statement from the migration to speed it up and avoid a no-op update,
leaving only the ALTER/ADD column statements that add uploaded with NOT NULL
DEFAULT 0 (reference: commentsEntity table and uploaded column in migration
4.sqm).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • [ x ] 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

Run ID: 0d141b9d-05a4-45bc-bdc2-10428fdec3e0

📥 Commits

Reviewing files that changed from the base of the PR and between c394756 and be342a9.

📒 Files selected for processing (25)
  • app/build.gradle.kts
  • app/src/main/java/com/team695/scoutifyapp/MainActivity.kt
  • app/src/main/java/com/team695/scoutifyapp/Root.kt
  • app/src/main/java/com/team695/scoutifyapp/data/api/NetworkMonitor.kt
  • app/src/main/java/com/team695/scoutifyapp/data/api/model/GameDetails.kt
  • app/src/main/java/com/team695/scoutifyapp/data/repository/CommentRepository.kt
  • app/src/main/java/com/team695/scoutifyapp/data/repository/GameDetailRepository.kt
  • app/src/main/java/com/team695/scoutifyapp/data/repository/LocalDatabaseDebugRepository.kt
  • app/src/main/java/com/team695/scoutifyapp/data/repository/LocalDatabaseWriteCoordinator.kt
  • app/src/main/java/com/team695/scoutifyapp/data/repository/MatchRepository.kt
  • app/src/main/java/com/team695/scoutifyapp/data/repository/PitScoutingRepository.kt
  • app/src/main/java/com/team695/scoutifyapp/data/repository/TaskRepository.kt
  • app/src/main/java/com/team695/scoutifyapp/data/repository/TeamNameRepository.kt
  • app/src/main/java/com/team695/scoutifyapp/data/repository/UserRepository.kt
  • app/src/main/java/com/team695/scoutifyapp/navigation/AppNav.kt
  • app/src/main/java/com/team695/scoutifyapp/ui/screens/FormScreen.kt
  • app/src/main/java/com/team695/scoutifyapp/ui/screens/data/DataScreen.kt
  • app/src/main/java/com/team695/scoutifyapp/ui/screens/data/EndgameDetails.kt
  • app/src/main/java/com/team695/scoutifyapp/ui/screens/data/PostgameDetails.kt
  • app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt
  • app/src/main/java/com/team695/scoutifyapp/ui/viewModels/SettingsViewModel.kt
  • app/src/main/sqldelight/com/team695/scoutifyapp/db/Comments.sq
  • app/src/main/sqldelight/com/team695/scoutifyapp/db/GameDetails.sq
  • app/src/main/sqldelight/migrations/3.sqm
  • app/src/main/sqldelight/migrations/4.sqm

Comment thread app/build.gradle.kts Outdated
Comment thread app/src/main/java/com/team695/scoutifyapp/ui/screens/data/EndgameDetails.kt Outdated
Comment thread app/src/main/java/com/team695/scoutifyapp/ui/screens/FormScreen.kt
Comment thread app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt Outdated
Copy link
Copy Markdown
Owner Author

Implemented a follow-up fix pass for the AI review findings and pushed the changes in commit 0f7a748.

This pass focused on the issues that were most likely to affect release safety, data integrity, and debug exposure:

  • Fixed versionCode so the 1.4.1 release is publishable as a new Android build.
  • Fixed pending_upload handling for new game detail rows.
  • Fixed the race where a successful push could incorrectly clear pending_upload after newer local edits had already been written.
  • Restricted local database inspection to debug builds and added redaction for sensitive columns.
  • Changed DB debug snapshot loading to metadata + row counts first, with rows loaded on demand.
  • Wrapped multi-step pit scouting mutations in single write-lock + transaction scopes to avoid interleaving writes.
  • Wrapped team name refresh in a transaction so readers do not observe an empty or partially rebuilt cache.
  • Hardened logout by cancelling and awaiting the active sync job before clearing local data, and extended logout cleanup to pit scouting tables.
  • Fixed teleop section boundary accounting so frame overflow is not double-counted across phase transitions.
  • Added failure handling for flushNow() in endgame and postgame flows so navigation does not silently continue after a failed save.
  • Persisted and restored teleopRunning explicitly via a new migration (local_teleop_running) so resumed sessions restore timer state correctly.
  • Removed duplicate persistence of identical form state and cleaned up StateFlow.update blocks to avoid mixing retry-safe lambda state with live reads.
  • Removed the redundant uploaded = 0 migration update and capped the settings Columns panel height.

Local verification completed successfully:

  • ./gradlew.bat :app:compileDebugKotlin
  • ./gradlew.bat :app:testDebugUnitTest

I did not address the docstring coverage warning in this pass, since it is not release-blocking and does not affect runtime behavior or safety.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt (1)

92-113: Keep the debounced autosave on Dispatchers.IO.

launchIn(viewModelScope) collects on the ViewModel's main context, so this save path no longer shares the IO confinement used by the initial load and flushNowAsync(). If either repository call is synchronous, autosave will run DB work on the UI thread.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt`
around lines 92 - 113, The debounced autosave is currently being collected on
the ViewModel main dispatcher via launchIn(viewModelScope), which can run
repository/DB work on the UI thread; change the collector to run on
Dispatchers.IO so persistFormState (and the same IO path as flushNowAsync/load)
executes off the main thread—e.g., collect the debounced _formState on IO by
launching the flow in viewModelScope + Dispatchers.IO or by using
viewModelScope.launch(Dispatchers.IO) {
_formState.debounce(...).distinctUntilChanged()....collect { ...
persistFormState(...) } } so that persistFormState runs on IO.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/team695/scoutifyapp/data/repository/PitScoutingRepository.kt`:
- Around line 202-217: The current code deletes tabs using a stale snapshot
(tabs fetched before acquiring the write lock) which can leave orphaned pitscout
rows; inside the LocalDatabaseWriteCoordinator.withWriteLock { db.transaction {
... } } block, either re-query the current tabs for eventKey (e.g., call
db.pitScoutingTabQueries.getAllTabsForEvent(eventKey).executeAsList() and map to
formId) and then call db.pitscoutQueries.deletePitscoutByFormId(formId) for each
up-to-date tab and deleteTabsForEvent(eventKey), or replace the per-form delete
with a new SQLDelight query like
db.pitscoutQueries.deletePitscoutsByEventId(eventKey) (and add the matching
DELETE query to Pitscout.sq) so deletion is performed atomically and covers any
tabs created after the initial prefetch; ensure deleteLocalFiles(...) still runs
for files of the tabs you decide to remove.
- Around line 330-338: The code currently calls
deleteLocalFiles(preparedTab.images) before the DB transaction so if the
transaction fails the saved fallback preparedTab will reference deleted local
files; move the deletion to occur only after the DB reset and persist
succeed—i.e., remove the call to deleteLocalFiles from before buildClearedTab
and instead invoke deleteLocalFiles(preparedTab.images) after
LocalDatabaseWriteCoordinator.withWriteLock { db.transaction { ...
persistTabInternal(clearedTab) } } completes successfully (inside the successful
runCatching path), referencing preparedTab and clearedTab and leaving the
rollback/fallback path intact.

In `@app/src/main/java/com/team695/scoutifyapp/ui/screens/data/DataScreen.kt`:
- Around line 132-199: The loop reads a stale snapshot from latestFormState (via
rememberUpdatedState) after calling dataViewModel.updateTime,
dataViewModel.setTeleopSection, or dataViewModel.completeTeleop, causing wrong
behavior across multiple section boundaries; fix by introducing local mutable
vars (e.g., localSection and localTotalMilliseconds) initialized from
latestFormState.teleopSection and latestFormState.teleopTotalMilliseconds, use
those local vars for the when/switchTime/timeUntilBoundary calculations, and
update them immediately whenever you call dataViewModel.updateTime(deltaTime),
dataViewModel.setTeleopSection(...), or dataViewModel.completeTeleop() so
subsequent inner-loop iterations operate on the current values rather than the
stale latestFormState snapshot.

In `@app/src/main/java/com/team695/scoutifyapp/ui/screens/FormScreen.kt`:
- Around line 74-86: The remember block computing filteredRows currently depends
on selectedTable and uiState.rowSearchQuery but not on uiState.loadedRows;
update the remember key list for filteredRows to include uiState.loadedRows so
the computation reruns when rows finish loading (i.e., change
remember(selectedTable, uiState.rowSearchQuery) to remember(selectedTable,
uiState.rowSearchQuery, uiState.loadedRows) or equivalent), keeping the existing
filtering logic using uiState.rowSearchQuery, selectedTable, and
it.searchableText.

In `@app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt`:
- Around line 322-334: completeTeleop() currently zeroes
teleopCachedMilliseconds which discards any unassigned teleop time; update the
_formState.update call in completeTeleop (and only that method) to stop setting
teleopCachedMilliseconds to 0 so the cached bucket is preserved when
teleopTotalMilliseconds is set to ENDGAME_END_TIME and teleopSection is set to
TeleopSection.ENDED; keep teleopRunning = false and set
gameDetails.teleopCompleted = true, then call flushAfterStateMutation() as
before.

---

Nitpick comments:
In `@app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt`:
- Around line 92-113: The debounced autosave is currently being collected on the
ViewModel main dispatcher via launchIn(viewModelScope), which can run
repository/DB work on the UI thread; change the collector to run on
Dispatchers.IO so persistFormState (and the same IO path as flushNowAsync/load)
executes off the main thread—e.g., collect the debounced _formState on IO by
launching the flow in viewModelScope + Dispatchers.IO or by using
viewModelScope.launch(Dispatchers.IO) {
_formState.debounce(...).distinctUntilChanged()....collect { ...
persistFormState(...) } } so that persistFormState runs on IO.
🪄 Autofix (Beta)

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

Run ID: eb1c49f8-84fa-4d08-bccb-411d7716b642

📥 Commits

Reviewing files that changed from the base of the PR and between be342a9 and 0f7a748.

📒 Files selected for processing (22)
  • app/build.gradle.kts
  • app/src/main/java/com/team695/scoutifyapp/Root.kt
  • app/src/main/java/com/team695/scoutifyapp/config/DebugConfig.kt
  • app/src/main/java/com/team695/scoutifyapp/data/api/model/GameDetails.kt
  • app/src/main/java/com/team695/scoutifyapp/data/repository/GameDetailRepository.kt
  • app/src/main/java/com/team695/scoutifyapp/data/repository/LocalDatabaseDebugRepository.kt
  • app/src/main/java/com/team695/scoutifyapp/data/repository/PitScoutingRepository.kt
  • app/src/main/java/com/team695/scoutifyapp/data/repository/TeamNameRepository.kt
  • app/src/main/java/com/team695/scoutifyapp/data/repository/UserRepository.kt
  • app/src/main/java/com/team695/scoutifyapp/navigation/AppNav.kt
  • app/src/main/java/com/team695/scoutifyapp/ui/components/NavRail.kt
  • app/src/main/java/com/team695/scoutifyapp/ui/screens/FormScreen.kt
  • app/src/main/java/com/team695/scoutifyapp/ui/screens/data/DataScreen.kt
  • app/src/main/java/com/team695/scoutifyapp/ui/screens/data/EndgameDetails.kt
  • app/src/main/java/com/team695/scoutifyapp/ui/screens/data/PostgameDetails.kt
  • app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt
  • app/src/main/java/com/team695/scoutifyapp/ui/viewModels/SettingsViewModel.kt
  • app/src/main/sqldelight/com/team695/scoutifyapp/db/GameDetails.sq
  • app/src/main/sqldelight/com/team695/scoutifyapp/db/PitScoutingTab.sq
  • app/src/main/sqldelight/com/team695/scoutifyapp/db/Pitscout.sq
  • app/src/main/sqldelight/migrations/4.sqm
  • app/src/main/sqldelight/migrations/5.sqm
✅ Files skipped from review due to trivial changes (4)
  • app/src/main/java/com/team695/scoutifyapp/config/DebugConfig.kt
  • app/src/main/sqldelight/migrations/5.sqm
  • app/src/main/sqldelight/com/team695/scoutifyapp/db/PitScoutingTab.sq
  • app/src/main/sqldelight/migrations/4.sqm
🚧 Files skipped from review as they are similar to previous changes (8)
  • app/src/main/java/com/team695/scoutifyapp/data/repository/TeamNameRepository.kt
  • app/src/main/java/com/team695/scoutifyapp/ui/screens/data/PostgameDetails.kt
  • app/src/main/java/com/team695/scoutifyapp/ui/screens/data/EndgameDetails.kt
  • app/build.gradle.kts
  • app/src/main/java/com/team695/scoutifyapp/navigation/AppNav.kt
  • app/src/main/java/com/team695/scoutifyapp/data/api/model/GameDetails.kt
  • app/src/main/java/com/team695/scoutifyapp/ui/viewModels/SettingsViewModel.kt
  • app/src/main/sqldelight/com/team695/scoutifyapp/db/GameDetails.sq

Comment thread app/src/main/java/com/team695/scoutifyapp/ui/screens/FormScreen.kt
Copilot AI review requested due to automatic review settings April 6, 2026 17:08
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt Outdated
Comment thread app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
app/src/main/java/com/team695/scoutifyapp/data/repository/PitScoutingRepository.kt (1)

440-460: ⚠️ Potential issue | 🟠 Major

Add ID requirement for upload success or implement URL-based cleanup fallback.

When uploadImageIfNeeded() marks an image as uploaded at line 459 using remoteId.isNotBlank() || remoteUrl.isNotBlank(), it allows URL-only uploads. However, removeImage() only attempts remote deletion when image.id is non-blank (line 239), passing only the ID to deletePitImage(). No URL-based delete path exists in the API. After a failed survey submission, a URL-only asset can remain on the server with no cleanup mechanism. Either require both ID and URL before setting uploaded = true, or add a URL-fallback delete endpoint if the API supports it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/team695/scoutifyapp/data/repository/PitScoutingRepository.kt`
around lines 440 - 460, The uploadImageIfNeeded() currently sets uploaded = true
when either remoteId or remoteUrl is present, which can leave URL-only assets
undeletable because removeImage() only deletes when image.id is set; change
uploadImageIfNeeded() (the block that returns image.copy(... uploaded = ...)) to
require both remoteId.isNotBlank() && remoteUrl.isNotBlank() before marking
uploaded, and keep id/url fields populated as before; alternatively, if the
server supports URL-based deletes, implement a deletePitImageByUrl(url: String)
and update removeImage() (which calls deletePitImage()) to fall back to calling
deletePitImageByUrl(image.url) when image.id.isBlank() but
image.url.isNotBlank().
🧹 Nitpick comments (1)
app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt (1)

216-235: Minor: dtEndgame calculation reads state outside the update block.

Lines 217-218 read _formState.value.teleopRunning and _formState.value.teleopTotalMilliseconds outside the update, then line 223 uses it.teleopCachedMilliseconds inside. If state mutates between the reads and the update, dtEndgame could be based on stale teleopTotalMilliseconds.

In practice this is called rapidly on the UI thread during teleop frames, so the window is small. Consider moving the calculation inside the update for consistency:

♻️ Optional refactor
 fun updateTime(deltaTime: Int) {
-    if (!_formState.value.teleopRunning) {
-        val dtEndgame = ENDGAME_END_TIME - _formState.value.teleopTotalMilliseconds
-        _formState.update {
+    _formState.update {
+        if (!it.teleopRunning) {
+            val dtEndgame = ENDGAME_END_TIME - it.teleopTotalMilliseconds
             it.copy(
                 teleopTotalMilliseconds = ENDGAME_END_TIME,
                 teleopCachedMilliseconds = it.teleopCachedMilliseconds + dtEndgame
             )
+        } else {
+            it.copy(
+                teleopTotalMilliseconds = it.teleopTotalMilliseconds + deltaTime,
+                teleopCachedMilliseconds = it.teleopCachedMilliseconds + deltaTime,
+            )
         }
-    } else {
-        _formState.update {
-            it.copy(
-                teleopTotalMilliseconds = it.teleopTotalMilliseconds + deltaTime,
-                teleopCachedMilliseconds = it.teleopCachedMilliseconds + deltaTime,
-            )
-        }
     }
     maybeFlushLiveTeleopSnapshot()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt`
around lines 216 - 235, The dtEndgame computation currently reads
_formState.value.teleopTotalMilliseconds and teleopRunning outside the atomic
_formState.update, which can produce stale values; move the dtEndgame
calculation into the false branch inside _formState.update so you compute
dtEndgame from the updater parameter (it.teleopTotalMilliseconds) and then set
teleopTotalMilliseconds = ENDGAME_END_TIME and teleopCachedMilliseconds =
it.teleopCachedMilliseconds + dtEndgame, leaving the true branch unchanged
(referencing updateTime, _formState.update, teleopTotalMilliseconds,
teleopCachedMilliseconds, teleopRunning, and ENDGAME_END_TIME).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/team695/scoutifyapp/data/repository/PitScoutingRepository.kt`:
- Around line 329-337: preparedTab is captured before the upload/submit
round-trip so clearing using clearedTab (from buildClearedTab) can overwrite
newer edits or resurrect a deleted tab; modify the
LocalDatabaseWriteCoordinator.withWriteLock/db.transaction block to re-query the
current pitscout row by preparedTab.formId inside the mutex and compare its
formId/updatedAt (or equivalent stamp) to the preparedTab values, and only call
db.pitscoutQueries.deletePitscoutByFormId and persistTabInternal(clearedTab)
when they still match; keep deleteLocalFiles(preparedTab.images) outside or
gated appropriately after confirming the DB change to avoid deleting files for a
newer entry.
- Around line 157-163: The code reads `existing`/`tab` before acquiring
LocalDatabaseWriteCoordinator.withWriteLock and then uses those stale snapshots
inside db.transaction (e.g. calling db.pitscoutQueries.deletePitscoutByFormId
and persistTabInternal) and later deleteLocalFiles(existing.images), which can
reintroduce deleted/updated data; fix by re-loading the current row inside the
lock/transaction (e.g. query the tab/form by id from db within the
withWriteLock/db.transaction block) or implement a locked helper like
mutateTab(tabId) that fetches the up-to-date snapshot and performs all mutations
and subsequent actions (including deciding which files to delete) based on that
fresh in-transaction snapshot instead of the pre-lock `existing`/`tab`; apply
the same change to the other occurrences you noted (around lines indicated:
185-195, 392-416, 586-589).

In `@app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt`:
- Around line 431-455: The undo/redo methods check collection emptiness outside
the _formState.update lambda causing a race where state.paths.last() or
state.undoTree.last() can throw if the collections change concurrently; fix by
performing the emptiness check and mutation atomically inside the update lambda
in undo() and redo() (e.g., inspect state.paths/undoTree inside the update,
compute newPaths/newUndoTree and a didChange flag there, return state or
modified copy accordingly), and then call flushAfterStateMutation() only when
didChange is true so you don't flush when no mutation occurred; refer to the
functions undo(), redo(), the _formState.update lambda, and fields paths,
undoTree, and justUndid when making the change.

---

Outside diff comments:
In
`@app/src/main/java/com/team695/scoutifyapp/data/repository/PitScoutingRepository.kt`:
- Around line 440-460: The uploadImageIfNeeded() currently sets uploaded = true
when either remoteId or remoteUrl is present, which can leave URL-only assets
undeletable because removeImage() only deletes when image.id is set; change
uploadImageIfNeeded() (the block that returns image.copy(... uploaded = ...)) to
require both remoteId.isNotBlank() && remoteUrl.isNotBlank() before marking
uploaded, and keep id/url fields populated as before; alternatively, if the
server supports URL-based deletes, implement a deletePitImageByUrl(url: String)
and update removeImage() (which calls deletePitImage()) to fall back to calling
deletePitImageByUrl(image.url) when image.id.isBlank() but
image.url.isNotBlank().

---

Nitpick comments:
In `@app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt`:
- Around line 216-235: The dtEndgame computation currently reads
_formState.value.teleopTotalMilliseconds and teleopRunning outside the atomic
_formState.update, which can produce stale values; move the dtEndgame
calculation into the false branch inside _formState.update so you compute
dtEndgame from the updater parameter (it.teleopTotalMilliseconds) and then set
teleopTotalMilliseconds = ENDGAME_END_TIME and teleopCachedMilliseconds =
it.teleopCachedMilliseconds + dtEndgame, leaving the true branch unchanged
(referencing updateTime, _formState.update, teleopTotalMilliseconds,
teleopCachedMilliseconds, teleopRunning, and ENDGAME_END_TIME).
🪄 Autofix (Beta)

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

Run ID: e734e747-f63b-4d35-b799-d31fab81b612

📥 Commits

Reviewing files that changed from the base of the PR and between 0f7a748 and 6e52529.

📒 Files selected for processing (6)
  • app/build.gradle.kts
  • app/src/main/java/com/team695/scoutifyapp/data/repository/PitScoutingRepository.kt
  • app/src/main/java/com/team695/scoutifyapp/ui/screens/FormScreen.kt
  • app/src/main/java/com/team695/scoutifyapp/ui/screens/data/DataScreen.kt
  • app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt
  • app/src/main/sqldelight/com/team695/scoutifyapp/db/Pitscout.sq
✅ Files skipped from review due to trivial changes (2)
  • app/build.gradle.kts
  • app/src/main/sqldelight/com/team695/scoutifyapp/db/Pitscout.sq
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/com/team695/scoutifyapp/ui/screens/FormScreen.kt

Comment on lines +157 to 163
LocalDatabaseWriteCoordinator.withWriteLock {
db.transaction {
db.pitscoutQueries.deletePitscoutByFormId(existing.formId)
persistTabInternal(cleared)
}
}
deleteLocalFiles(existing.images)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

These write-locked mutations still run from stale tab snapshots.

existing/tab are resolved before withWriteLock, and the new persistTab() wrapper only locks the final insert. A concurrent edit or delete can still land first and then get overwritten here, which can recreate a deleted tab, drop newer form/image changes, or queue stale submission data. Please re-read the current row inside the locked transaction (or add a locked mutateTab(tabId) helper) and use that snapshot for any later file cleanup.

Also applies to: 185-195, 392-416, 586-589

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/team695/scoutifyapp/data/repository/PitScoutingRepository.kt`
around lines 157 - 163, The code reads `existing`/`tab` before acquiring
LocalDatabaseWriteCoordinator.withWriteLock and then uses those stale snapshots
inside db.transaction (e.g. calling db.pitscoutQueries.deletePitscoutByFormId
and persistTabInternal) and later deleteLocalFiles(existing.images), which can
reintroduce deleted/updated data; fix by re-loading the current row inside the
lock/transaction (e.g. query the tab/form by id from db within the
withWriteLock/db.transaction block) or implement a locked helper like
mutateTab(tabId) that fetches the up-to-date snapshot and performs all mutations
and subsequent actions (including deciding which files to delete) based on that
fresh in-transaction snapshot instead of the pre-lock `existing`/`tab`; apply
the same change to the other occurrences you noted (around lines indicated:
185-195, 392-416, 586-589).

Comment on lines +329 to +337
val clearedTab = buildClearedTab(preparedTab, regenerateFormId = true)
runCatching {
clearTab(preparedTab.tabId, regenerateFormId = true)
LocalDatabaseWriteCoordinator.withWriteLock {
db.transaction {
db.pitscoutQueries.deletePitscoutByFormId(preparedTab.formId)
persistTabInternal(clearedTab)
}
}
deleteLocalFiles(preparedTab.images)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't clear the tab from a stale pre-submit snapshot.

preparedTab comes from Line 313, before the upload + submit round-trip. If the tab is edited or deleted while those calls are in flight, Lines 331-335 will still write clearedTab, which can discard newer local changes or recreate a tab the user already removed. Re-read under the mutex and only reset when the current row still matches the submitted formId/updatedAt.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/team695/scoutifyapp/data/repository/PitScoutingRepository.kt`
around lines 329 - 337, preparedTab is captured before the upload/submit
round-trip so clearing using clearedTab (from buildClearedTab) can overwrite
newer edits or resurrect a deleted tab; modify the
LocalDatabaseWriteCoordinator.withWriteLock/db.transaction block to re-query the
current pitscout row by preparedTab.formId inside the mutex and compare its
formId/updatedAt (or equivalent stamp) to the preparedTab values, and only call
db.pitscoutQueries.deletePitscoutByFormId and persistTabInternal(clearedTab)
when they still match; keep deleteLocalFiles(preparedTab.images) outside or
gated appropriately after confirming the DB change to avoid deleting files for a
newer entry.

Comment on lines 431 to 455
fun undo() {
if (_formState.value.paths.isNotEmpty()) {
_formState.update {
it.copy(
paths = _formState.value.paths.dropLast(1),
undoTree = _formState.value.undoTree + listOf(_formState.value.paths.last()),
justUndid=true
_formState.update { state ->
state.copy(
paths = state.paths.dropLast(1),
undoTree = state.undoTree + listOf(state.paths.last()),
justUndid = true
)
}
flushAfterStateMutation()
}
}

// Redo last undone stroke
fun redo() {
if (_formState.value.undoTree.isNotEmpty()) {
_formState.update {
it.copy(
paths = _formState.value.paths + listOf(_formState.value.undoTree.last()),
undoTree = _formState.value.undoTree.dropLast(1),
justUndid=false
_formState.update { state ->
state.copy(
paths = state.paths + listOf(state.undoTree.last()),
undoTree = state.undoTree.dropLast(1),
justUndid = false
)
}
flushAfterStateMutation()
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Race condition: condition checked outside update block can become stale.

undo() checks _formState.value.paths.isNotEmpty() outside the update lambda, then calls state.paths.last() inside. If another coroutine empties paths between the check and the lambda execution (or during a retry), last() throws NoSuchElementException. Same issue in redo() with undoTree.

🛠️ Proposed fix
 fun undo() {
-    if (_formState.value.paths.isNotEmpty()) {
-        _formState.update { state ->
+    _formState.update { state ->
+        if (state.paths.isEmpty()) return@update state
+        state.copy(
+            paths = state.paths.dropLast(1),
+            undoTree = state.undoTree + state.paths.takeLast(1),
+            justUndid = true
+        )
+    }
+    flushAfterStateMutation()
+}
+
+fun redo() {
+    _formState.update { state ->
+        if (state.undoTree.isEmpty()) return@update state
+        state.copy(
+            paths = state.paths + state.undoTree.takeLast(1),
+            undoTree = state.undoTree.dropLast(1),
+            justUndid = false
+        )
+    }
+    flushAfterStateMutation()
+}

Note: This will flush even when no change occurs. If that's undesirable, track a didChange flag inside the update and conditionally flush.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/team695/scoutifyapp/ui/viewModels/DataViewModel.kt`
around lines 431 - 455, The undo/redo methods check collection emptiness outside
the _formState.update lambda causing a race where state.paths.last() or
state.undoTree.last() can throw if the collections change concurrently; fix by
performing the emptiness check and mutation atomically inside the update lambda
in undo() and redo() (e.g., inspect state.paths/undoTree inside the update,
compute newPaths/newUndoTree and a didChange flag there, return state or
modified copy accordingly), and then call flushAfterStateMutation() only when
didChange is true so you don't flush when no mutation occurred; refer to the
functions undo(), redo(), the _formState.update lambda, and fields paths,
undoTree, and justUndid when making the change.

@1834423612 1834423612 merged commit 7c36701 into master Apr 6, 2026
0 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1st Priority 1st/top Priority bug Something isn't working enhancement New feature or request Sync Sync

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants