-
Notifications
You must be signed in to change notification settings - Fork 0
[History] Favorites #292
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
[History] Favorites #292
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
0b6a05b
Prepare favorites work
KacperKozak da34ba2
Define Favorites domain
KacperKozak 13482b8
Decouple Favorites from sync
KacperKozak 2cc6b17
Keep Favorites scope local
KacperKozak 5f24d4a
Add Favorites table, bridge API and Favorites tab #287
KacperKozak 091c74d
Key favorites by board_id instead of ble device id #287
KacperKozak 8a36f96
Guard favorite star against double taps and surface failures #287
KacperKozak 8a5abd3
Trim range selection for Favorites #288
KacperKozak 4eda8a4
Merge remote-tracking branch 'origin/dev' into favorites
KacperKozak c562d14
Merge remote-tracking branch 'origin/dev' into favorites
KacperKozak b0e4792
Pin favorited telemetry during deletion #289
KacperKozak a422bce
Avoid bucket rebuilds for favorite deletion #289
KacperKozak 37e12de
Add favorite detail and rename #290
KacperKozak 3fae055
Store Favorite Media natively #291
KacperKozak 68f544b
Fix favorite back
KacperKozak ccd94c1
Fix favorites navigation
KacperKozak 8a5ddd2
Polish favorites controls
KacperKozak 094317b
Improve favorite trim
KacperKozak 6668bb9
Improve favorites
KacperKozak 4dba8ea
Suggest ride names
KacperKozak 4998f00
Fix history chart time
KacperKozak 5771ac6
Fix history drawer
KacperKozak 0b12793
Fix review feedback
KacperKozak 9c91dba
Polish favorites
KacperKozak e964cec
Mark favorite routes
KacperKozak 7318a2e
Mark favorite chart ranges
KacperKozak File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # Favorites pin telemetry ranges | ||
|
|
||
| A Favorite is a durable, optionally named time range `[startMs, endMs]` over telemetry history, stored in a native table in the telemetry DB on both platforms. It is not a pointer to a ride: history sessions are derived on read (ADR 0004/0005) and have no stable identity, while a time range survives regrouping and allows multiple Favorites per ride, including trimmed sub-ranges selected on the ride timeline. | ||
|
|
||
| ## Contract | ||
|
|
||
| - Favorites live in a native table (`@parity` iOS/Android) so telemetry deletion paths can see them. | ||
| - A Favorite has a native-minted stable UUID plus native-owned `created_at` and `updated_at`; JS cannot supply them. | ||
| - Re-trimming or renaming updates the existing Favorite row in place. Its UUID, `created_at`, Board ownership, and Favorite Media remain stable; native mints a new `updated_at`. | ||
| - `deleteTelemetryRange` and `clearTelemetryHistory` protect every minute bucket touched by a favorited range. Both the precomputed bucket and all its raw samples stay together; only buckets and telemetry wholly outside those bucket-aligned protected ranges are deleted. Deleting a ride around a Favorite leaves the protected buckets as a short standalone ride. | ||
| - Rides containing a favorited range are marked in history as not fully deletable. | ||
| - Removing a Favorite only unpins: its telemetry stays and becomes deletable like any ride. Its Favorite Media is deleted with it. | ||
| - Summary stats (mirroring history session summary fields) are computed from raw samples whenever the range is created or updated and denormalized onto the row (ADR 0005 style); the route preview is derived on read from pinned samples. | ||
|
|
||
| ## Considered Options | ||
|
|
||
| - **Favorite references a session id.** Rejected: session ids are synthesized by grouping and unstable. | ||
| - **Cascade delete favorites with their ride.** Rejected: starring means "keep this"; deletion silently destroying favorites betrays that intent. | ||
| - **JS-side favorite store passing protected ranges into native deletes.** Rejected: native truth would depend on JS remembering to send it. | ||
| - **Delete pinned telemetry when its Favorite is removed.** Rejected: unfavoriting silently destroying telemetry is surprising; unpin-only keeps one rule. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - Delete paths subtract bucket-aligned protected ranges. This deliberately keeps up to 59 seconds of telemetry beyond each exact Favorite edge so bucket summaries stay truthful without rebuilding the full telemetry database; the Favorite's own range and denormalized summary remain exact. | ||
| - Favorited telemetry is exempt from any future retention pruning. | ||
| - Orphan favorite islands appear in History after surrounding-ride deletion; this is accepted as honest. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # Favorite Media is curated, copied, favorite-owned storage | ||
|
|
||
| Supersedes ADR 0014 (Media History is a local derived view). The derived-on-read photo-library model was already abandoned in practice — Google Play policy blocked broad gallery reads, so media moved to an explicit picker flow that copies files into app storage (`rideMediaFiles.ts`). This ADR makes the current model official and re-keys it to Favorites. | ||
|
|
||
| ## Contract | ||
|
|
||
| - Media attaches only to Favorites. The rider explicitly picks assets; there is no automatic photo-library matching. | ||
| - Native owns a `favorite_media` manifest on both platforms. Each immutable row has a native-minted stable UUID primary key, `favorite_id`, capture time, MIME/media kind, byte count, SHA-256 content hash, and creation time. | ||
| - Picked files are imported into a canonical per-Favorite/per-media path in app storage. The manifest is durable metadata truth; filenames do not encode metadata. | ||
| - Map placement uses the nearest recording-backed GPS fix to capture time, as before. Asset GPS metadata is ignored. | ||
| - Deleting a Favorite raw-deletes its manifest rows as a parent-covered cascade and best-effort deletes its media directory. Reconciliation removes incomplete imports and orphaned files. | ||
| - Legacy `rideMedia/<sessionId>` folders are left untouched; no migration (PoC). | ||
|
|
||
| ## Considered Options | ||
|
|
||
| - **Keep session-id keying and gate UI to favorites.** Rejected: session ids are derived and unstable; the Favorite is the durable owner. | ||
| - **Keep the filesystem as the only record.** Rejected: metadata encoded only in filenames is fragile to enumerate, validate, and reconcile after interrupted operations. | ||
| - **Derived photo-library matching scoped to favorites.** Rejected: Play policy already forced the picker model; broad gallery reads are not coming back. | ||
| - **Migrate existing per-ride media into intersecting favorites.** Rejected as not worth it for a PoC. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - The native manifest owns durable Favorite Media metadata; app storage owns the local bytes. Assets survive photo-library changes but cost disk space. | ||
| - Import and deletion cross SQLite and the filesystem, so reconciliation is required to repair interrupted operations. | ||
| - Photos on non-favorited rides are no longer possible; favoriting is the gateway to attaching media. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| appId: ${APP_ID} | ||
| --- | ||
| - runFlow: _launch.yaml | ||
| - openLink: 'vescape://settings/components/modals' | ||
| - scrollUntilVisible: | ||
| element: | ||
| id: edge-drawer-focus-open | ||
| direction: DOWN | ||
| - tapOn: | ||
| id: edge-drawer-focus-open | ||
| - extendedWaitUntil: | ||
| visible: Focused list expanded | ||
| timeout: 5000 | ||
| - assertVisible: | ||
| id: edge-drawer-focused-row |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
73 changes: 73 additions & 0 deletions
73
...e-core/android/src/androidTest/java/expo/modules/vescapecore/telemetry/FavoriteDaoTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| package expo.modules.vescapecore.telemetry | ||
|
|
||
| import android.content.Context | ||
| import androidx.room.Room | ||
| import androidx.test.core.app.ApplicationProvider | ||
| import androidx.test.ext.junit.runners.AndroidJUnit4 | ||
| import kotlinx.coroutines.runBlocking | ||
| import org.junit.After | ||
| import org.junit.Assert.assertEquals | ||
| import org.junit.Assert.assertNull | ||
| import org.junit.Before | ||
| import org.junit.Test | ||
| import org.junit.runner.RunWith | ||
|
|
||
| @RunWith(AndroidJUnit4::class) | ||
| class FavoriteDaoTest { | ||
| private lateinit var database: TelemetryDatabase | ||
| private lateinit var dao: TelemetryDao | ||
|
|
||
| @Before | ||
| fun setUp() { | ||
| val context = ApplicationProvider.getApplicationContext<Context>() | ||
| database = Room.inMemoryDatabaseBuilder(context, TelemetryDatabase::class.java) | ||
| .allowMainThreadQueries() | ||
| .build() | ||
| dao = database.telemetryDao() | ||
| } | ||
|
|
||
| @After | ||
| fun tearDown() { | ||
| database.close() | ||
| } | ||
|
|
||
| @Test | ||
| fun favoriteCrudRoundTrip() = runBlocking { | ||
| val older = favorite(id = "older", name = null, startMs = 1_000, updatedAt = 1_000) | ||
| val newer = favorite(id = "newer", name = "Evening ride", startMs = 3_000, updatedAt = 3_000) | ||
|
|
||
| dao.insertFavorite(older) | ||
| dao.insertFavorite(newer) | ||
| assertEquals(listOf(newer, older), dao.getFavorites()) | ||
|
|
||
| val renamed = newer.copy(name = "Night ride", endMs = 4_500, updatedAt = 4_500) | ||
| assertEquals(1, dao.updateFavorite(renamed)) | ||
| assertEquals(renamed, dao.getFavorite(newer.id)) | ||
|
|
||
| assertEquals(1, dao.deleteFavorite(newer.id)) | ||
| assertNull(dao.getFavorite(newer.id)) | ||
| assertEquals(listOf(older), dao.getFavorites()) | ||
| } | ||
|
|
||
| private fun favorite( | ||
| id: String, | ||
| name: String?, | ||
| startMs: Long, | ||
| updatedAt: Long, | ||
| ) = FavoriteEntity( | ||
| id = id, | ||
| boardId = null, | ||
| name = name, | ||
| startMs = startMs, | ||
| endMs = startMs + 1_000, | ||
| createdAt = startMs, | ||
| updatedAt = updatedAt, | ||
| sampleCount = 10, | ||
| gpsPointCount = 5, | ||
| distanceCm = 120_000, | ||
| movingDurationMs = 60_000, | ||
| avgSpeedCentiKmh = 2_000, | ||
| maxSpeedCentiKmh = 3_000, | ||
| batteryUsedWhMilli = 1_500, | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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:
Repository: KacperKozak/vescape
Length of output: 41092
🏁 Script executed:
Repository: KacperKozak/vescape
Length of output: 9709
🏁 Script executed:
Repository: KacperKozak/vescape
Length of output: 1817
Handle invalid favorite ranges as
ERR_UPDATE_FAVORITEbefore the null guard.TelemetryRepository.updateFavoritealready callsrequire(endMs >= start_ms)before returning anullfavorite, soendMs < startMsthrowsIllegalArgumentExceptioninstead of returningnull. Align Android with iOS by either validating the range and returningnull, or catching and re-throwing it as the controlledERR_UPDATE_FAVORITErejection.🤖 Prompt for AI Agents
Source: Coding guidelines