From 2916aa376009283305a7acec4aafa3c96544499e Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 16 Sep 2026 16:07:18 +0200 Subject: playlists: make the writes actually leave the browser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/meshbay_hub/static/playlist-menu.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js') 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}`)); }, }, ]); -- cgit v1.2.3