aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-16 16:07:18 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-16 16:07:18 +0200
commit2916aa376009283305a7acec4aafa3c96544499e (patch)
treebc19d010e8bebf39c175271564b3be01d1c06aa0 /packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js
parent25f62e169e049f0757ef611d5b409e7523958dee (diff)
downloadmeshbay-2916aa376009283305a7acec4aafa3c96544499e.tar.gz
playlists: make the writes actually leave the browser
Reported from a phone: signing in with the same account showed no playlists. syncWith was called from exactly one place in the interface, so creating a playlist, deleting one, removing a track and saving the queue all wrote to IndexedDB and stopped there. The store pushes itself now, coalesced, so a new mutation cannot forget to. Silence was the real defect. The node audited only successes, so a refusal left no trace and user_blob_list none at all; the background push swallowed its reason; the interface said nothing. All three report now, and "Sync now" says what happened either way. An unreadable blob on a node was treated as a fetch failure and returned before the push — permanent, once the node held anything. It is an absence: the client is the authority, and it gets overwritten. A sign-in reconciles whatever this browser already holds, a pending push is flushed when the page goes away, and a push that did not land is retried once. no_key is spelled out: a client that signs in with its remembered device key only ever has a bundle key persisted before the playlist subkey existed, and an AES handle is non-extractable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js20
1 files changed, 19 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js b/packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js
index 8d85b68..e2fb3f9 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js
@@ -183,7 +183,25 @@ function PlaylistMenuButton({ userId, lists, reload, onPlayQueue, onSync, cached
onSelect: async () => {
const r = await onSync();
await reload();
- say(r && r.ok ? t('playlists.synced') : t('playlists.sync_failed'));
+ // The reason, not just "it failed": a sync that quietly does nothing
+ // is the one failure mode nobody can act on, and the reason is what
+ // separates an offline node from a wedged one.
+ // On success, say how much moved; on failure, say why. Either way
+ // the reader learns something they can act on — the one thing this
+ // never did.
+ const last = P.lastSync();
+ const why = (r && r.reason) || last.reason || '?';
+ // `no_key` is not a fault the reader can do anything about unless it
+ // is spelled out. This browser signed in with its remembered device
+ // key, so the only bundle key it has is one persisted before the
+ // playlist subkey existed — and an AES handle is non-extractable, so
+ // there is nothing to derive it from. One sign-in with the passphrase
+ // fixes it for good; a code on screen does not say that.
+ say(r && r.ok
+ ? `${t('playlists.synced')} (${r.pushed || 0}↑ ${r.pulled || 0}↓)`
+ : (why === 'no_key'
+ ? t('playlists.err_no_key')
+ : `${t('playlists.sync_failed')}: ${why}`));
},
},
]);