Parent
What to build
Deleting a Board stops removing its row. It sets deleted_at instead, on the phone and on the Vescape server, so a Board's Ride History outlives the Board that produced it.
The decision, its rejected alternatives and its consequences are in docs/adr/0027-boards-are-tombstoned-never-deleted.md and the server's docs/adr/0008-boards-are-tombstoned-never-deleted.md. Read those first; this issue is the implementation.
App side:
boards gains a nullable deleted_at, stamped from the native clock like every other change column, with sync_seq and updated_at moving as they do on any other write. Room 29 -> 30 and the matching GRDB migration, additive, existing rows left null.
- Deleting a Board keeps its current behaviour for configuration — board settings, board warnings and Alert Rules are still hard-deleted — and keeps leaving telemetry untouched. The only change is that the Board row itself survives.
- Tune Profiles are deliberately not deleted. They survive their Board and need their own deletion to go away.
- The Rider-facing Board list filters tombstones. Lookup by id deliberately does not, because Ride History still has to name a deleted Board. Anything that acts on a Board rather than describing one — building a session config for a connect — refuses a tombstoned Board.
Server side, inside KacperKozak/vescape-server#12:
- Applying a Board deletion sets
deleted_at under the same WHERE updated_at <= deletedAt guard every action uses, and explicitly deletes the Board's configuration rows. The existing ON DELETE CASCADE behind those tables stops firing, since nothing is deleted any more.
- Tune Profiles come out of that cascade, reversing what the schema shipped with.
This closes the mismatch flagged in the server PR at #7 and #8: an orphaned Tune Profile re-uploaded after a Board deletion currently hits the composite foreign key and refuses the whole Sync Batch. With a surviving parent row it lands.
Likely files
modules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryEntities.kt - BoardEntity, where the column is declared
modules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryDao.kt - deleteBoardWithSettings, getBoards, getBoard
modules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryDatabase.kt - Room version and the migration
modules/vescape-core/ios/telemetry/TelemetryDatabase.swift - the GRDB peer migration
modules/vescape-core/ios/telemetry/TelemetryDao.swift - the GRDB peer of the delete and read paths
modules/vescape-core/android/src/main/java/expo/modules/vescapecore/connection/SessionConfigBuilder.kt - buildSessionConfig, which must refuse a tombstoned Board
modules/vescape-core/src/index.ts - Board / BoardInput, which gain the field and must keep it un-fabricatable from JS
src/modules/board/store/boardStore.ts - the JS side of the Board list
Implementation hints
upsertBoard in TelemetryDao.kt already stamps both sync columns from the native clock and never trusts a bridge value, using ratchetUpdatedAt(previous, now). Follow that shape rather than adding a second stamping path — a tombstone is an ordinary write.
BoardInput is Omit<Board, 'updatedAt'> today, which is what stops a call site fabricating a timestamp. deletedAt needs the same treatment: deletion goes through the delete path, never through an upsert from JS.
deleteBoardWithSettings is the single Rider-facing delete and already sequences its children (deleteBoardSettings, deleteBoardWarnings, deleteAlertRules, deleteBoard). Replace only the last call; leave the ordering alone.
getBoards() and getBoard(id) are the only two SELECT ... FROM boards reads in the DAO, so the filtering change is genuinely two queries — but check the iOS peer separately, its DAO does not mirror them one-for-one.
Acceptance criteria
Blocked by
Related
Parent
What to build
Deleting a Board stops removing its row. It sets
deleted_atinstead, on the phone and on the Vescape server, so a Board's Ride History outlives the Board that produced it.The decision, its rejected alternatives and its consequences are in
docs/adr/0027-boards-are-tombstoned-never-deleted.mdand the server'sdocs/adr/0008-boards-are-tombstoned-never-deleted.md. Read those first; this issue is the implementation.App side:
boardsgains a nullabledeleted_at, stamped from the native clock like every other change column, withsync_seqandupdated_atmoving as they do on any other write. Room 29 -> 30 and the matching GRDB migration, additive, existing rows left null.Server side, inside
KacperKozak/vescape-server#12:deleted_atunder the sameWHERE updated_at <= deletedAtguard every action uses, and explicitly deletes the Board's configuration rows. The existingON DELETE CASCADEbehind those tables stops firing, since nothing is deleted any more.This closes the mismatch flagged in the server PR at #7 and #8: an orphaned Tune Profile re-uploaded after a Board deletion currently hits the composite foreign key and refuses the whole Sync Batch. With a surviving parent row it lands.
Likely files
modules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryEntities.kt-BoardEntity, where the column is declaredmodules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryDao.kt-deleteBoardWithSettings,getBoards,getBoardmodules/vescape-core/android/src/main/java/expo/modules/vescapecore/telemetry/TelemetryDatabase.kt- Room version and the migrationmodules/vescape-core/ios/telemetry/TelemetryDatabase.swift- the GRDB peer migrationmodules/vescape-core/ios/telemetry/TelemetryDao.swift- the GRDB peer of the delete and read pathsmodules/vescape-core/android/src/main/java/expo/modules/vescapecore/connection/SessionConfigBuilder.kt-buildSessionConfig, which must refuse a tombstoned Boardmodules/vescape-core/src/index.ts-Board/BoardInput, which gain the field and must keep it un-fabricatable from JSsrc/modules/board/store/boardStore.ts- the JS side of the Board listImplementation hints
upsertBoardinTelemetryDao.ktalready stamps both sync columns from the native clock and never trusts a bridge value, usingratchetUpdatedAt(previous, now). Follow that shape rather than adding a second stamping path — a tombstone is an ordinary write.BoardInputisOmit<Board, 'updatedAt'>today, which is what stops a call site fabricating a timestamp.deletedAtneeds the same treatment: deletion goes through the delete path, never through an upsert from JS.deleteBoardWithSettingsis the single Rider-facing delete and already sequences its children (deleteBoardSettings,deleteBoardWarnings,deleteAlertRules,deleteBoard). Replace only the last call; leave the ordering alone.getBoards()andgetBoard(id)are the only twoSELECT ... FROM boardsreads in the DAO, so the filtering change is genuinely two queries — but check the iOS peer separately, its DAO does not mirror them one-for-one.Acceptance criteria
deleted_atset, on both platformsBlocked by
Related