First slice of docs/zap-tally-plan.md (decisions D1-D5 resolved in the doc):
- ZAP_TALLY_KIND = 30081 (addressable, d=payment_hash → one canonical event
per real payment) + NOTE_TYPES.ZAP_TALLY schema + buildZapTallyEvent
(nostrEventSchemas.js). Video + profile pointers.
- FEATURE_ZAP_TALLY flag (constants.js), default ON per D4.
- zapReceiptValidator.js: extractBolt11Fields (payment_hash + description_hash
from the existing bech32 walker), verifyPaymentPreimage
(sha256(preimage)==payment_hash), verifyBitvidZapTally — the security core:
proves the invoice was paid AND bound to this zap request (description-hash
binding blocks reusing a leaked preimage for another video). Amount is
bolt11-derived, never the tag; pointers come from the embedded zap request.
- Schema-doc row for kind 30081.
Tests: tests/zap-tally-verify.test.mjs (8) — preimage check, bech32 field
round-trip, valid/bad-preimage/replay/zero-sats decisions, builder tag shape
(video + profile). Lint + build green.
No publishing or counting wired yet — next slices per the plan checklist.
Co-Authored-By: Claude Fable 5 <[email protected]>
Trending-pattern clone ranked by sats from kind-9735 receipts:
- js/zapTotals.js: per-pointer receipt aggregation. The feed's metric getter
returns the cached total and schedules a batched fetch (#a/#e chunks via
the L1 SubscriptionManager) for unknown/stale pointers; amounts parse via
nip57 bolt11 with the zap request's amount tag as fallback; receipts are
deduped by id so re-fetches never double-count; zero-receipt answers are
cached (2-min TTL).
- sorters.js: shared count-ranked sorter (metric desc, recency tie-break,
author spread, trusted-muted sink); Trending delegates unchanged, Most
Zapped ranks on runtime.getZapTotal.
- Full wiring: feed definition, view + info popover, change-signal re-rank
(order settles as receipts stream in), sidebar link, viewManager registry,
feedCoordinator case/loader, FEED_TYPES.MOST_ZAPPED, and a
FEATURE_MOST_ZAPPED_FEED flag (default on; off hides the tab).
Co-Authored-By: Claude Fable 5 <[email protected]>
Audit found the search pipeline largely real (token parser, local + NIP-50 relay
search, enforced author/tag/date/has/nsfw/kind filters, wired modal) with dead
spots, now fixed:
- sort: offered in the modal but never applied. New pure sortSearchResults —
recent (default), views (shared view-count cache), trending (views weighted by
recency), longest — applied in searchView.
- duration filter only read nip71.duration, so bitvid's OWN kind-30078 videos
(top-level duration) never matched. Both video types now satisfy duration
filters; same shapes cover the planned live (30311) / shorts kinds.
- textScope now enforced in the local title/description match (all|title|description).
- relay: now scopes the video relay-search to that relay (was parsed-but-unused).
- FEATURE_SEARCH_FILTERS removed: dev-only flag consumed by NOTHING while the
filter UI ships and works everywhere (plumbing + setter deleted, no consumers).
Tests (tests/search-filters.test.mjs, 4 — surface previously had zero): parser
tokens; matcher enforces tags/date/has:magnet/duration for BOTH duration shapes;
views/longest/recent sorts (no input mutation); trending ranks fresh-riser above
stale-hit. Lint + build green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Makes the card liveness-visibility behavior switchable so it can be A/B
feel-tested, and probes ahead of the scroll for a snappier result.
- CARD_LIVENESS_POLICY (config/instance-config.js): "show-pending" (default,
unchanged) | "hide-foreign" (foreign/ingested cards hidden until a source
verifies; native cards stay show-pending) | "hide-all". Owner's own cards are
always visible. Flip live for testing without a rebuild:
window.__BITVID_CARD_LIVENESS_POLICY__ = "hide-foreign" then refresh/scroll.
Implemented via a data-foreign provenance flag on the card +
getCardLivenessPolicy() in cardSourceVisibility.js.
- LIVENESS_PROBE_PREFETCH_MARGIN (default 600px): both liveness probes were
already viewport-gated (IntersectionObserver, on-screen-first, WebTorrent
priority-queued + concurrency-capped); this expands their rootMargin so cards
just below the fold verify before the user reaches them.
- Tests: three policy scenarios in tests/video-card-source-visibility.test.mjs;
also un-broke 3 pre-existing VideoCard tests by adding the missing
IntersectionObserver JSDOM stub.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
New FEED_TYPES.TRENDING tab: the active (recently-added) source ranked by view
count (recency tiebreak, trusted-muted last), gated by FEATURE_TRENDING_FEED
(default on). Builds on the now-trustworthy view counts (#4).
- createTrendingSorter reads counts from runtime.getViewCount, which resolves
each video's pointer via the app's canonical deriveVideoPointerInfo against the
shared viewCounter cache (new getVideoViewCountSnapshot export) — the same
counts the grid cards already load.
- Live re-rank: viewCounter now emits a coalesced onViewCountsChanged signal;
while the Trending view is active it debounce-re-runs the feed (cheap re-rank
over the in-memory active set, no relay re-fetch), so the order settles into
true trending as counts stream in. Cold cache reads as recency, then improves.
- Pipeline extracted to js/feedEngine/trendingFeed.js (feedCoordinator is at its
size cap; bumped the cap for the thin load/refresh delegations).
- Wiring: FEED_TYPES + flag, feedCoordinator cases, app.js wrappers,
applicationBootstrap registration (hides the sidebar link when flag off),
viewManager init + js/trendingView.js, views/trending.html, sidebar link.
Tests: tests/trending-sorter.test.mjs.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Addresses "some NIP-71 videos don't play / he has multiple links" + "speed
the liveness check + don't render unplayable".
- Phase 1 — multi-source capture: nip71IngestAdapter collectVideoSources()
exposes a `sources` list (all video imeta urls, in order; audio/image
excluded; deduped) so mirrors are preserved (the adapter previously kept
only one url).
- Phase 2 — multi-source liveness: urlHealthController.probeUrlList() probes
every candidate (primary + sources) via the existing video-element
playability probe — healthy as soon as one source plays, offline only if
ALL fail. So a dead primary host no longer hides a video with a live
mirror. (Card hide policy in cardSourceVisibility was already correct.)
- Phase 3 — speed: URL_PROBE_TIMEOUT_MS 8s -> 4s for a fast verdict so dead
cards hide quickly; WebTorrent swarm probe stays generous.
Remaining (TODO #5): player-side fail-over at play time; hide-until-verified
for foreign ingested videos. Tests + mutation-verified (collectVideoSources
video-only filter, probeUrlList first-ok/offline/timeout). 1271/1272 unit
(known UploadModal flake).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Foundation for inbound NIP-71 ingest. Adds a single admin switch in the
instance config (config/instance-config.js, default true), re-exported
through config.js and surfaced as a runtime-overridable flag in
constants.js — same pattern as FEATURE_NIP71_MIRROR / ALLOW_NSFW_CONTENT.
A deployer flips one constant to disable all inbound ingest if it ever
causes problems (spam, low-quality content, performance). The ingest
adapter + service (gated on this flag) land in follow-up commits; its
behavioral test will assert the service is inert when the flag is off.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Makes the NIP-71 mirror user-facing via the My Videos tab — the natural
manage-my-videos surface — without touching the single-writer publish path.
- js/constants.js: new FEATURE_NIP71_MIRROR flag (default on). Independent of
FEATURE_PUBLISH_NIP71, which is already wired to the LEGACY 21/22 auto-publish in
videoPublisher.js — flipping that would activate the wrong (non-opt-in) model, so
the opt-in mirror gets its own flag and that legacy path stays dormant.
- js/services/nip71MirrorFlags.js: per-video opt-in persistence
(pubkey+videoRootId, off by default) + pure resolveMirrorToggle decision.
- MyVideosController: per-row "Share to apps" / "Shared ✓" toggle (gated by the
flag), wired to nip71MirrorService.publish/remove. Ineligible videos show a
disabled "Share off" with the reason (private / NSFW-not-allowed-here / needs
hosted URL) — the ALLOW_NSFW_CONTENT moderation gate surfaces here too.
tests/nip71-mirror-flags.test.mjs: persistence scoping + toggle decision
(enabled→remove, eligible→publish, ineligible→blocked). Mutation-verified.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Follow-up to c79120ba (which only captured the file deletion): removes the
sidebar nav link and the unused DEV_DASHBOARD view constant. No dangling refs.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- Removes `wss://tracker.btorrent.xyz` from `js/constants.js` due to SSL cert error.
- Updates `scripts/agent/debug_frontend.py` to filter out unavoidable external content errors (CORS, 404, invalid certs) to ensure the debug agent focuses on regression in the codebase.
- Adds completion log for the weekly agent run.
Co-authored-by: PR0M3TH3AN <[email protected]>
- `handleVisibility` only scheduled intervals but didn't trigger an immediate refresh when returning to the tab.
- This caused `tests/unit/services/exploreDataService.test.mjs` to fail (assertion `0 !== 1`).
- Added explicit calls to `refreshWatchHistoryTagCounts` and `refreshTagIdf` when `document.hidden` becomes false.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Add "Dev Tools" button to the sidebar footer menu.
- Configure `js/constants.js` with `DEV_DASHBOARD` view type.
- Refactor `views/dev/agent-dashboard.html` to be compatible with `loadView` (moved styles to body, updated links).
- Add cleanup logic to dashboard script to prevent memory leaks when navigating away.
- Fix duplicate import in `js/nostr/reactionEvents.js`.
Co-authored-by: PR0M3TH3AN <[email protected]>
- Introduce `js/constants.js` with semantic time constants (e.g., `ONE_MINUTE_MS`, `STANDARD_TIMEOUT_MS`) and feature-specific TTLs.
- Export `KIND_MUTE_LIST` from `js/nostrEventSchemas.js` to standardize NIP-51 mute list kind usage.
- Refactor `js/state/cache.js`, `js/nostr/cachePolicies.js`, `js/channelProfile.js`, and various services to use centralized constants instead of hardcoded magic numbers.
- Update `js/nostr/relayConstants.js` to derive values from the shared time constants.
- Replace duplicate definitions of timeouts and retry delays across the codebase.
Co-authored-by: PR0M3TH3AN <[email protected]>
Introduced FEED_TYPES constants in js/constants.js and updated js/app/authSessionCoordinator.js, js/hashChangeHandler.js, js/viewManager.js, and js/app/feedCoordinator.js to use them. This improves maintainability and reduces the risk of typos in feed view names.
Co-authored-by: PR0M3TH3AN <[email protected]>
Introduces a race condition between the primary playback source (URL or Torrent) and a configurable timeout. If the primary source fails to reach the 'playing' state within the timeout window, the system automatically falls back to the alternative source.
- Adds `DEFAULT_PLAYBACK_START_TIMEOUT` to instance config (3000ms).
- Updates `PlaybackService` to implement timeout logic using `withTimeout` wrapper.
- Ensures proper cleanup of stalled attempts (resetting video element, destroying torrent client).
- Added `DEFAULT_PLAYBACK_SOURCE` to `config/instance-config.js` to allow choosing between "url" (default) and "torrent" priority.
- Updated `js/constants.js` to derive `URL_FIRST_ENABLED` flag from the configuration.
- Refactored `PlaybackService.execute()` in `js/services/playbackService.js` to robustly support "Torrent First" logic with fallback to URL, while maintaining "URL First" with fallback to Torrent.
- Added unit tests in `tests/services/playbackService_order.test.mjs` to verify playback order and fallback mechanics.
- Remove `wss://open.video.lukewarlow.uk:443/announce` from `DEFAULT_WSS_TRACKERS` in `js/constants.js`.
- This tracker was returning `net::ERR_SSL_PROTOCOL_ERROR`, causing console noise and connection failures.
- Added `scripts/agent/debug_frontend.py` to help diagnose frontend initialization errors.
Verified with `scripts/agent/debug_frontend.py` that the error is resolved.
Ran `npm run format`, `npm run lint`, and `npm run test:unit`.
- Increased tracker timeout to 6000ms and concurrency to 6 in `js/trackerConfig.js`
- Increased trackers per magnet check to 10 to ensure better peer discovery
- Added new reliable public WebSocket trackers to `DEFAULT_WSS_TRACKERS` in `js/constants.js`
- Verified configuration changes with `tests/verify_tracker_config.mjs`
- Refactored `js/userBlocks.js` to merge Kind 10000 and Kind 30002 events on load, preventing overwritten mutes.
- Updated `js/constants.js` to set `DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD` to 4, enabling soft-hide (blur) behavior for lower mute counts.
Removed the `ACCEPT_LEGACY_V1` flag and associated logic that supported parsing magnet links from unstructured text notes or tags, as well as support for video events with version < 2.
This simplifies `js/videoEventUtils.js` and `js/nostr/nip71.js`, and removes legacy regex scanning.
Deleted `tests/legacy-infohash.test.mjs` which tested this deprecated functionality.
- Introduce FEATURE_IMPROVED_COMMENT_FETCHING flag (default: true in unstable)
- Enhance createVideoCommentFilters to include comprehensive union of legacy (#e/#a) and uppercase (#E/#A/#I/#K/#P) tags for NIP-22 compliance, reducing misses in mixed-relay propagation
- Add localStorage caching (5min TTL) in CommentThreadService.fetchThread for fast loads post-reload; includes getCachedComments, cacheComments, and clearCommentCache
- Force full-history re-fetch in VideoModalCommentController.load with retry logic if low count, plus background refresh after 1s
- Bootstrap cache warm-up for recent videos on bitvidApp init (tracks via localStorage recentVideos list)
Fixes: Comments posted not reloading in video modal (root cause: incomplete filters + no persistence)
Closes: #N/A (internal fix)
QA: Verified post → reload → modal open shows comments via cache/query; no regressions in threading/UI
Rollout: Gated by flag; rollback by setting false + clear localStorage (^comments: keys)