[feature] Persist and synchronize favorites #6
- @github-anthodev opened this issue · Aug 29, 2026edited
🔢 Impact Version (SemVer)
MINOR
🌿 Suggested Branch
feature/sync-favorites/companion/6
📝 Description
Persist one complete versioned phone configuration—favorites plus the personal PRIM key—in PebbleKit JS, expose only non-secret state to the page/watch, cache last normalized results, and synchronize favorite metadata atomically. This ticket owns localStorage schema/migration/removal,
ApiKeyUpdate, credential status, event plumbing, compact AppMessage sequencing, restart recovery, and secret-safe logs.
✅ Expected Behavior
One atomic phone-local configuration survives restarts. Options can keep/replace/remove the key without receiving its stored value. The watch receives favorites plus non-secret key status, never the key. Interrupted/repeated synchronization preserves the prior complete list, while phone-local last results support immediate event-driven display without background activity.
📋 Acceptance Criteria
- Store one versioned JSON object under exact UUID-scoped localStorage key
lapinFuteConfig, containing favorites,primApiKey, and non-secretkeyStatus; document plaintext storage and survival across uninstall/reinstall. - Open options via a non-server-visible fragment containing favorites plus
hasKeyonly. ApplyApiKeyUpdateatomically:KEEPretains key/status,REPLACEoverwrites key and setsCONFIGURED,REMOVEerases key and setsMISSING; invalid/cancelled responses preserve prior bytes. - Maintain non-secret phone/watch
keyStatus(MISSING | CONFIGURED | INVALID). Missing maps toAPI_KEY_REQUIRED; 401/403 marksINVALIDuntil replace/remove.MISSING/INVALIDsuppress every departure XHR, and the raw key never reaches watch. - Centralize all backend departure XHR in one helper that reads the key only at send time, sets the Authorization header, strips it from errors, and records only credential-free request/cache timing counters.
- Store one compact last complete result per current favorite under
lapinFuteResults, never the key; prune deleted/unknown favorites and enforce the frozen favorite/result size bounds. On launch/selection, send cached data immediately with original age before an optional event fetch. - Encode only favorites,
keyStatus, and normalized result data within the frozen AppMessage budgets. Chain multi-message updates on callbacks and atomically commit only the complete matching sequence. - Resume/restart interrupted synchronization without mixing versions; migrate explicitly supported schemas and reject unknown versions without modifying valid local/watch data.
- Derive/document maximum favorite count from the tightest measured AppMessage, watch heap/storage, localStorage, and encoded configuration-fragment round-trip limit; keep that many labels browsable offline.
- Favorites/key configuration survive companion restart; favorites/key status survive watch restart; add/reorder/rename/delete yield the same ordered list on both sides.
- Interruption after any message preserves the previous watch configuration; repeated delivery is idempotent; invalid/unsupported input never replaces valid data.
- Prove app exit stops PebbleKit JS and produces no XHR/AppMessage/background activity for a 10-minute observed idle window.
- Record storage/migration/remove/invalid-key proof, secret scan/log audit, result-cache launch behavior, restart/edit parity, every-position interruption, AppMessage bytes/counts, and zero-background evidence in a resolution comment.
🔗 Affected Endpoints/Routes
- No backend route is added; implement the foundation-frozen departure Authorization/error client against recorded HTTP fixtures. The composed live route remains gated by both this issue and
[feature] Normalize live PRIM departures(https://github.com/Anthodev/lapin-fute/issues/4) in the watch ticket. - Own the options open/save fragment,
lapinFuteConfig/lapinFuteResults, centralized XHR helper, and secret-free PebbleKit JS ↔ watch AppMessage plane.
🧪 Tests to Add
Type Coverage PebbleKit JS unit tests Atomic config storage, `KEEP Secret boundary tests Exact lifecycle: new page memory → close fragment → PKJS handoff/request memory + lapinFuteConfig→ Authorization → relay request/upstream memory → exact-host PRIMapikey; absent elsewherePebbleKit JS/Alloy AppMessage tests keyStatus, atomic commit, incomplete/mismatched rejection, callback sequencing, idempotent redelivery, measured bytes/count proven on both emulators (emery and gabbro)Lifecycle/both-emulator smoke Restart/edit parity, every-position interruption, cached-result launch, disconnect browse, 10-minute exited-app zero activity on emery and gabbro
⚙️ Constraints
- Reuse the frozen contracts/budgets from
[feature] Establish the application foundation and contracts(https://github.com/Anthodev/lapin-fute/issues/2); amend them rather than creating a second protocol. - Accept the explicit product trade-off: personal key is plaintext in UUID-scoped PebbleKit JS localStorage; minimize exposure rather than adding ineffective client-side encryption.
- The one-time configuration-close fragment may contain a replacement key; never GET/query/log it. The existing key never re-enters the hosted page.
- Whole-list replacement is atomic;
serviceIdstays opaque; user/backend labels remain verbatim. - No key in favorites/results/AppMessage/watch storage, no server-side credential/user preference storage, and no background scheduler.
🔀 Dependencies
- Blocked by
[feature] Build the companion configuration page(https://github.com/Anthodev/lapin-fute/issues/5). - Reuse the foundation contracts from
[feature] Establish the application foundation and contracts(https://github.com/Anthodev/lapin-fute/issues/2).
⚠️ Impact on Existing Behavior
Replaces the one-favorite round-trip fixture from
[feature] Establish the application foundation and contracts(https://github.com/Anthodev/lapin-fute/issues/2) with durable whole-list persistence and synchronization, and gives[feature] Build the companion configuration page(https://github.com/Anthodev/lapin-fute/issues/5) its save semantics.
🔍 Technical Context
Required references
- Store one versioned JSON object under exact UUID-scoped localStorage key
Sign in to comment.