From 6fa42eb26725adefc52d2803ba307d4a78c3989d Mon Sep 17 00:00:00 2001
From: th
Date: Thu, 16 Apr 2026 18:20:46 +0200
Subject: [PATCH] fix(shopping): stale-row cleanup, cache-busting, view
persistence
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Root cause of the doubled lists: the relay DB had stale rows from
previous activations that were never deleted. The composite PK
prevented same-uid duplicates but not leftover rows from cards that
were deactivated without a successful DELETE.
Fixes:
- Client tick sends DELETE { keep: [active uids] } after every
push cycle. The relay drops all rows for this tenant that aren't
in the keep list — cleans up migration leftovers, failed deletes,
old single-list sentinels, everything in one shot.
- Relay DELETE endpoint accepts { keep: [...] } for bulk cleanup
alongside the existing { card_uid } single-delete.
- Relay layout.php adds cache-busting query strings on style.css
and app.js via filemtime() so phone browsers pick up new code
on deploy instead of serving stale cached versions.
- showView persists the active view name to localStorage; after
login the relay restores it instead of always landing on compose.
- Shopping-list-box border/background removed — items have their
own chrome, the box is just a flex grouper now.
- View title is always "Shopping"; each list box keeps its own
header with the card title. No more doubled name.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
config.php | 2 +-
electron/package.json | 2 +-
public/plugins/shopping/shopping.js | 9 ++++++++-
relay/inc/routes/shopping.php | 21 ++++++++++++++++++++-
relay/ui/app.js | 20 ++++++++++++--------
relay/ui/layout.php | 4 ++--
relay/ui/style.css | 8 ++------
7 files changed, 46 insertions(+), 20 deletions(-)
diff --git a/config.php b/config.php
index 1ac86c0..0cf3ce2 100644
--- a/config.php
+++ b/config.php
@@ -1,7 +1,7 @@
String(c.id));
// Push all active cards first.
for (const card of activeCards) {
try { await pushOne(card); } catch {}
}
+ // Tell the relay to drop any rows that aren't in our active set.
+ // This cleans up stale entries from previous activations that
+ // were never explicitly deleted (old single-list era, failed
+ // DELETEs, migration leftovers).
+ try {
+ await hyprcrd.api('api/shopping', { method: 'DELETE', body: { keep: activeUids } });
+ } catch {}
// Pull: fetch all lists from relay, match by card_uid.
try {
const res = await hyprcrd.api('api/shopping', { method: 'GET' });
diff --git a/relay/inc/routes/shopping.php b/relay/inc/routes/shopping.php
index 0974a7c..6904194 100644
--- a/relay/inc/routes/shopping.php
+++ b/relay/inc/routes/shopping.php
@@ -63,11 +63,30 @@ if ($route === '/api/shopping' && $method === 'POST') {
}
// ── DELETE /api/shopping ─────────────────────────────────────
+// Two modes:
+// { card_uid: "123" } → delete one specific list
+// { keep: ["123", "456"] } → delete everything EXCEPT these
if ($route === '/api/shopping' && $method === 'DELETE') {
if (!$clientTenant) jsonResponse(['error' => 'unauthorized'], 401);
$input = json_decode(file_get_contents('php://input'), true) ?: [];
+ $keep = is_array($input['keep'] ?? null) ? $input['keep'] : null;
$uid = trim((string)($input['card_uid'] ?? ''));
- if ($uid === '') jsonResponse(['error' => 'card_uid required'], 400);
+
+ if ($keep !== null) {
+ // Bulk cleanup: keep only the listed card_uids, drop the rest.
+ if (count($keep)) {
+ $placeholders = implode(',', array_fill(0, count($keep), '?'));
+ $params = array_merge([$clientTenant['id']], array_map('strval', $keep));
+ $db->prepare("DELETE FROM shopping_state WHERE tenant_id = ? AND card_uid NOT IN ($placeholders)")
+ ->execute($params);
+ } else {
+ $db->prepare('DELETE FROM shopping_state WHERE tenant_id = :t')
+ ->execute(['t' => $clientTenant['id']]);
+ }
+ jsonResponse(['ok' => true]);
+ }
+
+ if ($uid === '') jsonResponse(['error' => 'card_uid or keep[] required'], 400);
$db->prepare('DELETE FROM shopping_state WHERE tenant_id = :t AND card_uid = :u')
->execute(['t' => $clientTenant['id'], 'u' => $uid]);
jsonResponse(['ok' => true]);
diff --git a/relay/ui/app.js b/relay/ui/app.js
index 4be75a5..1c1e3fd 100644
--- a/relay/ui/app.js
+++ b/relay/ui/app.js
@@ -112,6 +112,11 @@ function showView(name) {
} else {
stopShoppingPoll();
}
+ // Remember the active view so a page reload lands back here
+ // instead of defaulting to compose every time.
+ if (['compose', 'account', 'admin', 'chat', 'shopping'].includes(name)) {
+ try { localStorage.setItem('relay-last-view', name); } catch {}
+ }
if (name === 'admin') {
loadUsers();
} else if (name === 'account') {
@@ -279,7 +284,8 @@ function routePostLogin(status) {
showView('account');
toast('set your client secret to start using the relay');
} else {
- showView('compose');
+ const last = localStorage.getItem('relay-last-view') || 'compose';
+ showView(['compose', 'account', 'admin', 'chat', 'shopping'].includes(last) ? last : 'compose');
}
}
@@ -786,7 +792,7 @@ async function loadShopping(quiet = false) {
list.innerHTML = '