Option A chosen. Noting explicitly that Option B should not ship as an interim
step -- the two are not stackable, and a bounded max-age shipped now is the
thing that never gets revisited.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
/css/* and /js/* have no header rule at all, so every stylesheet and script
revalidates on every page load. The obvious fix is a trap: asset URLs are not
content-hashed, so a long max-age with immutable would pin returning users to
stale CSS/JS after each deploy with no way to bust it. Records both real
options and the measured live headers.
Also corrects an earlier hand-off note claiming ~10 failing tests here; all
three suites pass on unstable as of today.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMr9Yt8SYmZcfgrg3CV2G1
The 2026-06-25 audit blamed "logout logs out everyone" / broken profile
switching / unknown-user-after-refresh on the single shared nip46Client and
single-slot session storage. Re-auditing the current code shows every named
suspect has since been fixed piecemeal: per-account v2 session map with v1
last-connected default, keepStored logout, scoped no-arg clear (v1 slot only),
reuseStored+expectPubkey switch wired end-to-end into
useStoredRemoteSigner({pubkey}), and a validator-enforced boot restore.
None of that was pinned at the manager level, so any layer could regress
silently. tests/nip46-signer-lifecycle.test.mjs now exercises the real
SignerManager against the real signer registry and real (polyfilled) storage:
- logging out the active NIP-46 account destroys its live client exactly once
while BOTH accounts' stored sessions and the other account's registry signer
survive
- a failed targeted restore (passphrase-locked or missing session) fails typed
and deletes nothing
- disconnectRemoteSigner with and without keepStored never touches the
per-account map
Mutation-checked: reinjecting the historical bugs (logout without keepStored;
no-arg clear wiping the v2 map) fails 2/6; real code passes 6/6.
TODO #33 updated: remaining work is the on-phone Amber verification the item
already gates on — and if that still misbehaves, the failure is now known to
live ABOVE this contract (UI/state reconciliation on restore failure), which
narrows the search considerably.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LHH3wPBsoruwCaJo8Rpf6a
### Motivation
- Prevent a browser-policy muted-autoplay retry from overwriting a viewer's explicit mute/unmute preference and ensure fallback mutes are transient.
- Ensure that when a user logs in while a video comment modal is open, the open thread is reloaded (not just refreshed) so comments continue to display correctly after relay-backed services are reconfigured.
### Description
- Add `js/utils/autoplayPreference.js` with `muteForAutoplayFallback` and `consumeAutoplayFallbackMute` helpers to mark and consume transient fallback mutes.
- Update `js/services/playbackService.js` to treat sound as default unless the viewer explicitly chose mute, persist explicit choices via `localStorage`, ignore fallback-generated volume changes using `consumeAutoplayFallbackMute`, and expose `muteForAutoplayFallback` delegating to the new util.
- Replace direct `videoElement.muted = true` calls with `playbackService.muteForAutoplayFallback` in `js/app/playbackCoordinator.js` and `js/webtorrent.js` so fallback mutes are marked as transient.
- Change `js/app/authSessionCoordinator.js` to reload the currently open video comment thread with `commentController.load(activeModalVideo)` after login if a modal video is active, instead of only calling `refreshAuthState`.
- Update tests in `tests/services/playbackService.test.mjs` and add a test in `tests/app/login-defers-dms.test.mjs` to cover the login behavior that reloads an open comment thread.
### Testing
- Ran the `PlaybackService` unit tests (`tests/services/playbackService.test.mjs`) which assert `prepareVideoElement` behavior, explicit mute persistence, and the transient nature of fallback mute, and they passed.
- Ran the auth login unit tests including `tests/app/login-defers-dms.test.mjs` which verifies DM deferral and reload of an open comment thread, and they passed.
Writes docs/blossom-plan.md (house format) from the July 2026 research: use the
maintained blossom-client-sdk v5 (only dep @noble/hashes, already vendored); the
spec's current modular BUD map (BUD-01 retrieval, 02 /upload, 11 auth kind-24242,
12 list/delete, 03 kind-10063 discovery, 04 mirror, 05 /media, 06 preflight);
the existing service.uploadFile seam as the integration point; and 10 decisions
(SDK vs hand-roll, multi-server+mirror, discovery, /upload vs /media, signer
reuse, torrent parity, management, keyless config UI, FEATURE_BLOSSOM_STORAGE,
public-only privacy). Notes Blossom removes the encrypted-cred-sync surface
(no secret — server URLs + signer) and the lift is medium (config UI is the main
cost). Expands TODO #30 with the plan pointer + decisions.
Local commit (kept with the unpushed Bitcoin Connect batch); not pushed.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Requested: integrate getAlby/bitcoin-connect as a smooth NWC "connect wallet"
front-end, keeping manual URI entry as backup. Plan (docs/bitcoin-connect-plan.md)
grounds it in bitvid's existing NWC stack: BC's NWC connector exposes the raw
pairing URI (provider.client.nostrWalletConnectUrl) which feeds the existing
validateWalletUri → per-npub store → zap-split/pay_invoice pipeline unchanged.
BC is a connect adapter, not a payment layer. Covers delivery (vendor+lazy import
vs esm.sh; CSP already allows esm.sh), persistence ownership, NWC-only connector
scope, least-privilege requestMethods, UI placement, FEATURE_BITCOIN_CONNECT flag,
and the security constraints (bearer spending secret, Alpha lib pin/vendor).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
beacon.html "just shows a spinner": the `data-ds="new"` redesign's
`.ds-overlay-backdrop { display: flex }` (an author rule) overrides the UA
`[hidden] { display: none }`, so the processing overlay — `hidden` in the markup
— rendered on load as a full-screen backdrop over the whole page. (Not the #28
watchdog, which is compiled into the bundle correctly.)
Add `.ds-overlay-backdrop[hidden] { display: none }` (specificity 0,2,0 beats the
base rule) so the `hidden` attribute actually hides it — the same guard the video
modal already uses. No JS change; the JS inline-style toggle still works. The
overlay now only shows while a torrent is being added. Rebuilt CSS + dist.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Pulls the audio thread together into docs/audio-integration-plan.md, matching the
house format of the shorts/live plans: kind/NIP landscape (NIP-A0 voice 1222/1244,
NIP-73 podcast GUIDs, NIP-53 live audio, NIP-71 audio variants, native 30078
m audio/*, non-standard music), 10 open decisions (D1–D10) with recommendations,
config flags (FEATURE_AUDIO_INGEST, off = no trace), the shared media-category
resolver (video|audio|live|short) used by sidebar + channel + My Content tabs,
audio player model (modal → persistent mini-player), UX best practices, flag-gated
phases, moderation, V4V/zaps, risks. Links it from TODO #60.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Surveyed the local NIP mirror for audio/podcast kinds. Fixes the earlier wrong
"NIP-54-style podcast events" ref (NIP-54 is Wiki) and records the real targets:
NIP-A0 Voice Messages (kind 1222/1244), NIP-73 External Content IDs (podcast:guid
/ item / publisher — the Podcasting 2.0 bridge), NIP-53 live audio (30311/30312,
overlaps #16), NIP-71 audio variants (imeta m audio/* + waveform), and notes that
the podcasts we filter today are bespoke native kind-30078 with imeta m audio/*,
while music apps (Wavlake/Stemstr) use non-standard custom kinds.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds the creator-side mirror of the channel tabs: rename the profile-modal
My Videos pane (MyVideosController) to "My Content" with Videos/Audio/Live/Shorts
tabs, each scoping the management rows (edit/delete/mirror/storage) to one
category. Reuses the shared category resolver so items bucket identically on the
owner view and the public channel.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Expands the Audio-tab item with a channel-profile tab strip carrying the same
media categories (Videos / Audio / Live-Streams / Shorts), each its own lazily-
loaded per-tab playlist, empty categories hidden, mirrored on the sidebar. Adds a
shared media-category resolver (video|audio|live|short from kind + imeta m + t)
used by both the sidebar feeds and the channel tabs — the natural home for the
#17 unified media grid. Cross-refs #16 (NIP-53 live + kind-22 shorts) / #17.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A podcast/music note published as a native kind-30078 (content url = an .ogg/
.mp3 audio file, imeta `m audio/*`) was accepted by convertEventToVideo (it has
a url) and rendered as a broken <video> — black frame + audio on Chrome/FF,
MEDIA_ERR_SRC_NOT_SUPPORTED on Safari/iOS — since bitvid has no <audio> player.
The existing audio guard only covered the NIP-71 ingest path, not native events.
convertEventToVideo now marks such notes invalid ("audio-only source (no video)")
so every ingestion path that already drops invalid filters them: main feed buffer,
author/channel batch fetch, and local ingest. Audio-only = no magnet AND no
`imeta m video/*` AND (an `imeta m audio/*` variant OR an unambiguous audio URL
.mp3/.m4a/.aac/.wav/.flac/.opus/.oga/.weba). `.ogg` alone is NOT filtered
(ambiguous → video/ogg); only when imeta says audio. A video imeta variant wins.
Direct open-by-nevent is intentionally unaffected — an explicit link still loads.
Near-term half of TODO #60 (the future dedicated Audio tab remains open).
Tests: tests/video-schema-and-conversion.test.mjs (+5, 8/8). Lint + build green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>