Turns on the admin Submissions tab now that the loop is complete (schema +
facade + admin approve/deny + forms). The forms already publish kind-30083
unconditionally, so enabling the tab is what makes the new intake coherent —
submissions are now visible + actionable instead of landing with no admin UI.
Rollback: set false to hide the tab (forms keep publishing 30083).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
First slice of structured submissions (replacing the kind-4 encrypted DM forms).
Pure, hermetic core — no UI or network yet.
- nostrEventSchemas.js: SUBMISSION_KIND=30083 (+ NOTE_TYPES.SUBMISSION, BASE_SCHEMAS
entry) and buildSubmissionEvent(). PUBLIC + addressable; data in tags: d (id),
k (type), applicant (npub), p (admin), t=bitvid-submission; free-text in content.
- js/submissions/submissionService.js: SUBMISSION_TYPES + labels, parseSubmissionEvent,
buildSubmission, generateSubmissionId. Pure — unit-testable with no relays/signer.
- constants.js: FEATURE_SUBMISSIONS (default OFF until the admin UI + forms ship).
- tests/submission-service.test.mjs: 8 scenarios (build tags/type/default, d-tag
required, parse structure/reject/unknown-type→other, build→parse round-trip of
every type, id generation).
- check-file-size.mjs: bump nostrEventSchemas grandfather 3250→3541 (adding a new
event type to the schema registry is the prescribed pattern).
- docs/nostr-event-schemas.md: catalogue row.
Schema validation ✨, lint, build all pass; tests 8/8.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The create path — makes playlists actually usable end-to-end.
- js/ui/playlistPicker.js: an "Add to playlist" dialog (modeled on confirmDialog)
listing the signed-in user's playlists as checkboxes (checked when the video is
already in them) + a "Create new playlist" input. On Save it diffs the
checkboxes and publishes only changed playlists (add/remove the video) plus any
new one, via the facade (active signer). Reads the current pubkey from
nostrClient, so callers pass only the video coordinate.
- videoMenuRenderers.js: "Add to playlist" item in the video ⋯ menu, gated by a
new canManagePlaylists param.
- moreMenuController.js: computes canManagePlaylists (FEATURE_PLAYLISTS && logged
in), threads it into the panel context, and adds the add-to-playlist handler
(resolves the video → coordinate via buildVideoAddressPointer, closes the
popover, opens the picker; success/error routed to the app's toasts).
- constants.js: FEATURE_PLAYLISTS flipped ON now that the core loop (create /
add / remove / channel section / playlist view) works. Rollback: set false.
Owner controls on the playlist view (rename / drag-reorder / hard-delete) are a
follow-up; removing every video empties a playlist (drops from the listing).
Syntax + lint + build clean; more-menu-controller tests 9/9.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
New route #view=playlist&pubkey=<hex>&id=<playlist id>:
- views/playlist.html: title + meta + the shared #videoList grid.
- js/playlistView.js: initPlaylistView() reads pubkey+id from the hash, fetches
the playlist (facade), resolves its ordered `a` refs against the app's active
videos (buildVideoAddressPointer keys, same 30078 contract), points the back
link at the creator's channel, and renders via app.renderVideoList with
preserveOrder:true so the playlist's order is kept (not re-sorted by date —
#21). Unresolved refs are skipped; the meta line shows "N of M available".
Gated by FEATURE_PLAYLISTS.
- FEED_TYPES.PLAYLIST + viewManager registration (getApp: getApplication).
The channel-section cards now resolve to a working view. node --check + lint +
build clean. End-to-end browser verification comes with the create/edit slice
(needs a way to actually make a playlist).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- constants.js: FEATURE_PLAYLISTS (default OFF) so nothing is user-visible until
the whole playlist UI ships; flip to preview on unstable.
- js/channelPlaylists.js: wireChannelPlaylists(pubkey) fetches the creator's
playlists (facade) and renders a responsive row of cards (thumbnail + video
count + title) linking to `#view=playlist&pubkey=…&id=…`. Hidden when the flag
is off, no playlists, or the container is missing; a nav-token guards against a
stale fetch resolving after a channel switch. DOM built with textContent (no
innerHTML). Kept out of the 5729-line-capped channelProfile.js.
- channelProfile.js: one-line wireChannelPlaylists(currentChannelHex) hook next
to wireChannelZapTotal (no-op while the flag is off).
- views/channel-profile.html: #channelPlaylistsSection above the video grid.
- tailwind.source.css: .playlist-grid / .playlist-card* (token-only; responsive
via @apply like .bv-grid-video).
Lint + build clean. The playlist view the cards link to lands next.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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)