commits
* feat: add track description field and RSS feed generation
Add nullable description column to tracks for liner notes/show notes,
and RSS feed endpoints for artist, album, and playlist collections.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move deferred import to top of feeds module
No circular dependency — feeds.py is a leaf module.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
reflects the default flip from #1060 — uploading now stores both
audio and metadata on the user's PDS with graceful fallback.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
PDS audio uploads were opt-in; flip the default so new uploads go to
the user's PDS automatically. Users who have never toggled the setting
get PDS uploads; users who explicitly disabled it stay opted out.
Falls back to R2-only if the PDS rejects the blob (too large).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
account deletion keeps ATProto records on the user's PDS by default,
but users can choose to delete them too — the docs previously implied
records unconditionally remain.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
use separate <p> tags instead of <br /> for actual visual spacing.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
settings: point developer token links to docs.plyr.fm/developers
instead of raw GitHub repo
portal: add "learn more" link on supporter gating hint to
docs.plyr.fm/artists/#supporter-gated-tracks
docs: add "leaving" sections to listeners and artists pages with
data portability reassurance and link to detailed offboarding docs
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
the audience grid cards already cover this — no need to duplicate
it in the hero.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
devlog: pencil → open book (reads as blog, not edit)
source: chain link → code brackets (universally recognized)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
add bar-chart icon linking to status.zzstoatzz.io/@plyr.fm (matching
the main app's header) and pencil icon linking to plyr.leaflet.pub
devlog alongside the existing source code link.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
document the experimental ATProtoFans integration for track gating,
note the binary support limitation, and link to the permissioned
data proposal as the path toward artist-controlled gated storage.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use monospace font in OG image to match docs site
regenerate og.png with SF Mono to match the --sl-font stack used
across docs.plyr.fm instead of Helvetica.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use real ATProto record on homepage, remove transcoding claim
replace the fake "late night drive" example with plyr.fm's actual
latest dev podcast record (including audioBlob). remove the
"files are transcoded for streaming automatically" line since
transcoding is feature-flagged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
regenerate og.png with SF Mono to match the --sl-font stack used
across docs.plyr.fm instead of Helvetica.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
generate a 1200x630 OG card with plyr.fm branding, add description
frontmatter to all pages missing it, and configure Starlight head
meta so shared links render properly on Discord, Slack, Twitter, etc.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
--sl-color-white was set to #fafafa in light mode, but Starlight uses
this variable for high-contrast foreground text. fix it to #171717
(matching --sl-color-gray-1) so text is dark on light backgrounds,
consistent with how Starlight's own defaults flip this variable.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
rate limits were tracked per Fly Machine instance, so 2 machines meant
2x the configured limit. use the existing docket Redis for global
counters. falls back to memory:// when DOCKET_URL is not set (local dev).
closes #1043
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* perf: fix slow homepage load times with SWR caching and pool warmup
follow graph: stale-while-revalidate pattern (TTL 60min, stale at 8min)
returns cached data immediately, schedules background re-warm when stale.
removes redundant login-time cache warming from auth paths.
track listing: cache anonymous first-page discovery feed in Redis (60s TTL)
with invalidation on upload, delete, and edit.
connection pool: warm one connection at startup to eliminate cold connect
penalty on first request after deploy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: remove stale schedule_follow_graph_warm mock from test
the import was removed from auth.py, so the test mock path no longer exists.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: narrow bare except Exception to specific types
- Redis operations: catch (RuntimeError, RedisError) instead of Exception
- DB pool warmup: catch (OSError, SQLAlchemyError) instead of Exception
- Move deferred imports in main.py to top level
- Update tests to use redis.exceptions.ConnectionError
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
replace docs/README.md link with docs.plyr.fm, move to top of links section.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- skill now points at existing resources (CLAUDE.md, .env.example,
docs-internal/setup.md) instead of restating their contents
- structured as a 5-step playbook: orient, fork, change, validate, PR
- add contributing link at bottom of developers page
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- fix GitHub vs tangled.org messaging (GitHub is primary, tangled is mirror)
- add "using a coding assistant?" section with copy-pasteable prompt
- document Redis requirement (just dev-services) and Postgres setup
- link to backend/.env.example for env var reference
- link to docs-internal/local-development/setup.md for detailed guide
- remove "never push to main" (branch protection handles this)
- add fork-based workflow instructions
- create skills/contribute/SKILL.md for agent-based contributors
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- remove max-width: 32rem from trending and search sections so they
fill the landing-section width naturally (48rem, centered)
- switch stats from grid to flexbox so all 4 cards stay in one row
on desktop (flex: 1, no wrapping). wraps to 2x2 on mobile only.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- center trending tracks and search sections on desktop (margin: 0 auto)
- fix stats grid: use repeat(4, 1fr) so tracks card isn't taller than others
- add contact email (plyrdotfm@proton.me) at bottom of landing page
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- listeners: link Bluesky + BlackSky, use "Atmosphere account" with
atproto.com link, remove vibe search (feature flagged), remove
unimplemented volume shortcuts
- artists: link export to portal, remove verify claim
- developers: fix MCP server section (link to repo, correct package
name plyrfm-mcp), fix SDK examples to match actual API
- lexicons: remove codegen + adding new lexicons sections, replace
misleading "never use bsky lexicons" with teal.fm integration note,
add login scopes table
- homepage: replace single embed with trending tracks (/tracks/top),
add hero waveform visual, improve mobile responsiveness
- sidebar: fix contributing double nesting
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
move internal/contributor docs to docs-internal/, create audience pages
(listeners, artists, developers), rewrite contributing.md to be concise.
landing page now has audience cards, live track embed, clickable search
results with thumbnails, and updated sidebar.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
the production regex only allowed `plyr.fm` and `www.plyr.fm` — any
other subdomain (e.g. `docs.plyr.fm`) got blocked. widen the pattern
to `([a-z0-9-]+\.)?plyr\.fm` so all first-party subdomains work.
same fix applied to the staging regex.
adds regression tests for CORS origin matching across all environments.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* docs: favicon, tangled link, interactive landing page
- replace emoji favicon with actual plyr.fm logo
- swap GitHub social icon for tangled.org repo link
- add live stats cards, track search, ATProto record example to landing page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: use api.plyr.fm instead of relay-api.fly.dev
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
move 38 internal pages to docs-internal/, simplify sidebar to 3 sections,
add mermaid rendering via starlight-client-mermaid, fix broken cross-references.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* docs: restructure information architecture (#1030)
move internal-only content (research notes, meeting minutes, AI agent
index) to docs-internal/ and consolidate sidebar from 12 flat sections
to 5 audience-oriented groups: getting started, development, operations,
platform, legal.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: convert all internal doc links to absolute paths
relative .md links (e.g. ./overview.md, ../backend/config.md) don't
resolve correctly in Starlight — they render as literal hrefs causing
404s. convert all 25 internal links across 13 files to absolute paths
without .md extensions (e.g. /moderation/overview/).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* docs: publish docs with Starlight at docs.plyr.fm
adds a Starlight (Astro) docs site that symlinks to the existing docs/
directory, keeping it as the single source of truth. includes:
- frontmatter injection across all ~65 markdown files
- docs-site/ scaffold with astro config, sidebar, and justfile
- contributing.md synthesized from setup, README, STATUS, and CLAUDE.md
- root justfile mod for `just docs run/build`
- loq limit bumps for 4 docs files (+4 lines from frontmatter)
Cloudflare Pages project setup is manual (see PR description).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* add GHA workflow for docs deployment via wrangler pages deploy
- path-filtered: triggers on docs/** and docs-site/** changes
- uses cloudflare/wrangler-action for deployment
- documents docs site in deployment/environments.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add landing page and match plyr.fm visual style
- add docs/index.md with splash template hero page
- custom CSS: plyr.fm colors (#0a0a0a bg, #6a9fff accent),
monospace font stack, light mode overrides
- homepage no longer 404s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: remove duplicate h1 headings and normalize title casing
Starlight renders the frontmatter title as an h1, so the body # heading
was doubling every page title. stripped all body h1s and normalized
casing to lowercase aesthetic (respecting proper nouns).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: drop "decentralized" from docs tagline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* ops: bsky DM on deploy failure
prod deploy v284 silently failed and went unnoticed for ~5h. add a
self-contained notification script that sends a bsky DM when the deploy
workflow fails, using the same atproto DM pattern as the backend.
- scripts/notify_deploy_failure.py: standalone script (no backend deps)
- deploy-prod.yml: if: failure() step runs via uvx --with atproto
requires 3 GHA secrets: BSKY_NOTIFY_HANDLE, BSKY_NOTIFY_PASSWORD,
BSKY_NOTIFY_RECIPIENT
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add setup-uv step before uvx notify script
uvx isn't available on GHA runners by default — need
astral-sh/setup-uv first, matching other workflows.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: reuse existing NOTIFY_BOT_* / NOTIFY_RECIPIENT_HANDLE secrets
these are already set in GHA — no need for new BSKY_NOTIFY_* secrets.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
_playCountLocked was a plain field, not $state(). The $effect calling
incrementPlayCount() never re-ran when the lock was released, so play
counts stopped firing after the first track transition.
Also moves loadeddata listener before .load() to prevent race with
cached audio where the event fires before the listener is attached.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: deduplicate teal.fm scrobbles during track transitions
backend: add redis SET NX dedup (60s TTL) in schedule_teal_scrobble so
concurrent requests from multiple fly machines only schedule one task.
frontend: lock play counting during track transitions via _playCountLocked
flag — set on resetPlayCount(), cleared on loadeddata — preventing spurious
fires from stale currentTime/duration before new audio loads.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: move redis import to top level in sync.py
deferred imports are only for circular import avoidance, not here.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
the fork's main branch was slimmed down for upstream PR #636. pin to
oauth-full which retains the full scope (scopes library, etc.) needed
by plyr.fm.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resilient PDS record fetching with DID resolution fallback
when a user migrates PDS servers, the cached pds_url in the artist table
goes stale, causing 500s on playlist/album GETs. add
get_record_public_resilient() that retries with DID resolution when the
cached PDS URL fails, and heals the DB cache on success.
fixes 500 on GET /lists/playlists/ for @pixeline.be (migrated from
shiitake.us-east.host.bsky.network to eurosky.social).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update test mocks for get_record_public_resilient
existing tests mocked get_record_public at the old import path — update
to mock get_record_public_resilient with the (data, resolved_url) return
type.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: correct mock target for albums test
deferred import means mock must target the records module, not albums.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
restore homepage to pre-activity state (cd2e7b7) and restore the
standalone activity page to its polished version (3515b21). the
activity page remains functional at /activity but is unlisted —
no navigation links point to it.
removes activity-feed module, ActivityRow, and Sparkline components
added in #1020.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
the dedicated /activity page created persistent navigation friction
(4 PRs worth of icon/placement churn). root cause: a separate page
feels orphaned when the primary surface is a track feed.
the pulse icon becomes a toggle that interleaves compact activity
rows (likes, comments, joins) chronologically between track cards.
upload events are excluded since they already appear as track cards.
- activity toggle in section header with accent highlight when active
- compact single-line activity rows with type-colored left borders
- 7-day sparkline histogram shown when activity is toggled on
- dual-cursor pagination: tracks and activity load independently
- /activity page now redirects to / (preserves bookmarks)
- extracted ActivityRow, Sparkline components and activity-feed module
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
pulse icon now exclusively means activity; status page gets ascending bars
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
client-side navigation preserves audio playback
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
wrap each action SVG in a 20px-wide flex container (icon-slot) that
centers the 14px icon, matching the header avatar width. the geometric
centers of the avatar and the icon below it now sit on the same
vertical axis.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
the header avatar is 20px and the action icon is 14px, so the text
on each line started at different horizontal positions. 3px horizontal
margin on the icon centers it in the same 20px space as the avatar,
aligning the text columns.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
line-height: 0 on avatar link and action icon removes extra inline
space from SVGs. explicit line-height: 1.2 on handle-link and
event-action keeps text midpoints consistent with icon/avatar centers.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- moved actor avatar from artwork corner badge to inline next to the
display name in the header (where it identifies who did the action)
- "shared" → "uploaded" for track upload events
- hide "by {artist}" when the actor IS the artist (self-uploads)
- track uploads never show "by" since they're always self-actions
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The :start::date syntax is ambiguous for SQLAlchemy's text() parser.
It interprets :start as a bind parameter, then the ::date cast causes
a syntax error. Every request to /activity/histogram has been 500ing
in production. Switch to CAST(:start AS date) which is unambiguous.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Promise.all rejects if either fetch fails at the network level,
causing the entire activity feed to show "no activity yet" when
only the histogram fetch fails. Switch to Promise.allSettled so
each fetch is handled independently.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
sparkline: moved histogram fetch into the page loader so it arrives
with the initial data instead of a separate onMount fetch that was
silently failing in production.
pagination: added staggered fly-in transitions for newly loaded items
(batch-index-based delay via svelte/transition). loading spinner now
only appears after 400ms so fast responses don't flash it.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- revert sparkline to 7-day window (was accidentally changed to 30)
- wrap verb text in accent-tinted spans (smaller, subtly colored)
- add artist_avatar_url to ActivityTrack model + inline avatar rendering
- fix pagination by encoding cursor (+ in UTC offset decoded as space)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- sparkline: fetch 30 days instead of 7, skip render when all buckets zero
- neon glow: use inset overlay with border-radius: inherit so accent
follows card corners instead of floating as a clipped bar
- timestamp: add title attr for native hover tooltip with full date/time
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- add GET /activity/histogram endpoint (daily counts over last 7 days)
- SVG sparkline at top of activity page with accent gradient fill
- replace solid border-left with fuzzy neon glow (::before + box-shadow)
- smaller, less blurred lava blobs (5 blobs, 50px blur, ~180-280px max)
- fix TrackCard avatar fallback: container becomes circular when showing
artist avatar so no ugly square outline around circle
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- animated lava lamp with accent-tinted gradient orbs (3 blobs, 24-34s cycles)
- artwork moved to left column with avatar badge overlay (fixes alignment)
- glass cards with backdrop-filter and type-tinted hover glow via --type-color
- respects prefers-reduced-motion
- mobile-responsive, compact CSS (331 lines)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- show track thumbnail/image on right side of event cards
- use person silhouette SVG for avatar placeholders (matches TrackItem/TrackCard)
- stats-informed header showing track/artist/duration counts
- play button placeholder for tracks without artwork
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
drop filled icons for outline-style strokes matching app conventions.
upload arrow for shares (not music note — could be any audio), heart
outline for likes, person+ for joins (not star). add per-type colored
left border accent for visual rhythm. larger avatars, two-line layout
with name/time header + action line below.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- filter out artists with empty handle/display_name from join events
(corrupted row: did:plc:dqbj7k7dierhqdowuyv5ll2z in staging)
- redesign activity page: card containers with glass bg + hover states,
event type icons (heart/note/chat/star), accent-styled comment
previews, right-aligned timestamps, consistent design token usage
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add platform-wide activity feed (#971)
chronological feed of likes, track uploads, comments, and profile
joins. unlisted page at /activity — no nav link, accessible by URL.
reconstructed from existing DB tables via raw SQL UNION ALL query
with cursor-based pagination. includes 9 backend tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add db_session to audio tests missing database setup
test_stream_audio_track_not_found and test_get_audio_url_not_found
were missing db_session fixture, causing "relation tracks does not
exist" under xdist when no prior test on the worker triggered table
creation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: handle docket worker teardown on stale event loop
under xdist, session-scoped TestClient teardown can run on a
different event loop than the one the docket worker task was created
on, causing RuntimeError. catch and log it during shutdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: skip docket worker in test lifespan to fix xdist teardown
the production lifespan starts a docket Worker that creates
asyncio.Task objects bound to the TestClient's portal event loop.
under xdist, session teardown runs on a different loop, causing
"attached to a different loop" in Worker.__aexit__.
no test needs a live docket worker (all docket usage is mocked),
so swap in a lightweight test lifespan that skips it entirely.
also reverts the _is_stale_loop_error hack in background.py.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* perf: add per-branch LIMIT and created_at indexes for activity feed
each UNION ALL branch now sorts+limits independently so postgres can
index-scan the top N per table instead of materializing all rows.
adds standalone created_at DESC indexes on artists, track_likes, and
track_comments to support the global feed query pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Research on preserving deep links through auth flows, covering
approaches from GitHub, Discord, Slack, and others. Recommends
combining query parameter + cookie + OAuth state for ATProto.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1px accent bar glows on playback (matching main Player style), dims on pause.
Inline share button copies the plyr.fm page URL with "copied!" feedback.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add track description field and RSS feed generation
Add nullable description column to tracks for liner notes/show notes,
and RSS feed endpoints for artist, album, and playlist collections.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move deferred import to top of feeds module
No circular dependency — feeds.py is a leaf module.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
PDS audio uploads were opt-in; flip the default so new uploads go to
the user's PDS automatically. Users who have never toggled the setting
get PDS uploads; users who explicitly disabled it stay opted out.
Falls back to R2-only if the PDS rejects the blob (too large).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
settings: point developer token links to docs.plyr.fm/developers
instead of raw GitHub repo
portal: add "learn more" link on supporter gating hint to
docs.plyr.fm/artists/#supporter-gated-tracks
docs: add "leaving" sections to listeners and artists pages with
data portability reassurance and link to detailed offboarding docs
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use monospace font in OG image to match docs site
regenerate og.png with SF Mono to match the --sl-font stack used
across docs.plyr.fm instead of Helvetica.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use real ATProto record on homepage, remove transcoding claim
replace the fake "late night drive" example with plyr.fm's actual
latest dev podcast record (including audioBlob). remove the
"files are transcoded for streaming automatically" line since
transcoding is feature-flagged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
--sl-color-white was set to #fafafa in light mode, but Starlight uses
this variable for high-contrast foreground text. fix it to #171717
(matching --sl-color-gray-1) so text is dark on light backgrounds,
consistent with how Starlight's own defaults flip this variable.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
rate limits were tracked per Fly Machine instance, so 2 machines meant
2x the configured limit. use the existing docket Redis for global
counters. falls back to memory:// when DOCKET_URL is not set (local dev).
closes #1043
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* perf: fix slow homepage load times with SWR caching and pool warmup
follow graph: stale-while-revalidate pattern (TTL 60min, stale at 8min)
returns cached data immediately, schedules background re-warm when stale.
removes redundant login-time cache warming from auth paths.
track listing: cache anonymous first-page discovery feed in Redis (60s TTL)
with invalidation on upload, delete, and edit.
connection pool: warm one connection at startup to eliminate cold connect
penalty on first request after deploy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: remove stale schedule_follow_graph_warm mock from test
the import was removed from auth.py, so the test mock path no longer exists.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: narrow bare except Exception to specific types
- Redis operations: catch (RuntimeError, RedisError) instead of Exception
- DB pool warmup: catch (OSError, SQLAlchemyError) instead of Exception
- Move deferred imports in main.py to top level
- Update tests to use redis.exceptions.ConnectionError
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- skill now points at existing resources (CLAUDE.md, .env.example,
docs-internal/setup.md) instead of restating their contents
- structured as a 5-step playbook: orient, fork, change, validate, PR
- add contributing link at bottom of developers page
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- fix GitHub vs tangled.org messaging (GitHub is primary, tangled is mirror)
- add "using a coding assistant?" section with copy-pasteable prompt
- document Redis requirement (just dev-services) and Postgres setup
- link to backend/.env.example for env var reference
- link to docs-internal/local-development/setup.md for detailed guide
- remove "never push to main" (branch protection handles this)
- add fork-based workflow instructions
- create skills/contribute/SKILL.md for agent-based contributors
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- remove max-width: 32rem from trending and search sections so they
fill the landing-section width naturally (48rem, centered)
- switch stats from grid to flexbox so all 4 cards stay in one row
on desktop (flex: 1, no wrapping). wraps to 2x2 on mobile only.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- listeners: link Bluesky + BlackSky, use "Atmosphere account" with
atproto.com link, remove vibe search (feature flagged), remove
unimplemented volume shortcuts
- artists: link export to portal, remove verify claim
- developers: fix MCP server section (link to repo, correct package
name plyrfm-mcp), fix SDK examples to match actual API
- lexicons: remove codegen + adding new lexicons sections, replace
misleading "never use bsky lexicons" with teal.fm integration note,
add login scopes table
- homepage: replace single embed with trending tracks (/tracks/top),
add hero waveform visual, improve mobile responsiveness
- sidebar: fix contributing double nesting
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
move internal/contributor docs to docs-internal/, create audience pages
(listeners, artists, developers), rewrite contributing.md to be concise.
landing page now has audience cards, live track embed, clickable search
results with thumbnails, and updated sidebar.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
the production regex only allowed `plyr.fm` and `www.plyr.fm` — any
other subdomain (e.g. `docs.plyr.fm`) got blocked. widen the pattern
to `([a-z0-9-]+\.)?plyr\.fm` so all first-party subdomains work.
same fix applied to the staging regex.
adds regression tests for CORS origin matching across all environments.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* docs: favicon, tangled link, interactive landing page
- replace emoji favicon with actual plyr.fm logo
- swap GitHub social icon for tangled.org repo link
- add live stats cards, track search, ATProto record example to landing page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: use api.plyr.fm instead of relay-api.fly.dev
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* docs: restructure information architecture (#1030)
move internal-only content (research notes, meeting minutes, AI agent
index) to docs-internal/ and consolidate sidebar from 12 flat sections
to 5 audience-oriented groups: getting started, development, operations,
platform, legal.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: convert all internal doc links to absolute paths
relative .md links (e.g. ./overview.md, ../backend/config.md) don't
resolve correctly in Starlight — they render as literal hrefs causing
404s. convert all 25 internal links across 13 files to absolute paths
without .md extensions (e.g. /moderation/overview/).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* docs: publish docs with Starlight at docs.plyr.fm
adds a Starlight (Astro) docs site that symlinks to the existing docs/
directory, keeping it as the single source of truth. includes:
- frontmatter injection across all ~65 markdown files
- docs-site/ scaffold with astro config, sidebar, and justfile
- contributing.md synthesized from setup, README, STATUS, and CLAUDE.md
- root justfile mod for `just docs run/build`
- loq limit bumps for 4 docs files (+4 lines from frontmatter)
Cloudflare Pages project setup is manual (see PR description).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* add GHA workflow for docs deployment via wrangler pages deploy
- path-filtered: triggers on docs/** and docs-site/** changes
- uses cloudflare/wrangler-action for deployment
- documents docs site in deployment/environments.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add landing page and match plyr.fm visual style
- add docs/index.md with splash template hero page
- custom CSS: plyr.fm colors (#0a0a0a bg, #6a9fff accent),
monospace font stack, light mode overrides
- homepage no longer 404s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: remove duplicate h1 headings and normalize title casing
Starlight renders the frontmatter title as an h1, so the body # heading
was doubling every page title. stripped all body h1s and normalized
casing to lowercase aesthetic (respecting proper nouns).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: drop "decentralized" from docs tagline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* ops: bsky DM on deploy failure
prod deploy v284 silently failed and went unnoticed for ~5h. add a
self-contained notification script that sends a bsky DM when the deploy
workflow fails, using the same atproto DM pattern as the backend.
- scripts/notify_deploy_failure.py: standalone script (no backend deps)
- deploy-prod.yml: if: failure() step runs via uvx --with atproto
requires 3 GHA secrets: BSKY_NOTIFY_HANDLE, BSKY_NOTIFY_PASSWORD,
BSKY_NOTIFY_RECIPIENT
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add setup-uv step before uvx notify script
uvx isn't available on GHA runners by default — need
astral-sh/setup-uv first, matching other workflows.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: reuse existing NOTIFY_BOT_* / NOTIFY_RECIPIENT_HANDLE secrets
these are already set in GHA — no need for new BSKY_NOTIFY_* secrets.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
_playCountLocked was a plain field, not $state(). The $effect calling
incrementPlayCount() never re-ran when the lock was released, so play
counts stopped firing after the first track transition.
Also moves loadeddata listener before .load() to prevent race with
cached audio where the event fires before the listener is attached.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: deduplicate teal.fm scrobbles during track transitions
backend: add redis SET NX dedup (60s TTL) in schedule_teal_scrobble so
concurrent requests from multiple fly machines only schedule one task.
frontend: lock play counting during track transitions via _playCountLocked
flag — set on resetPlayCount(), cleared on loadeddata — preventing spurious
fires from stale currentTime/duration before new audio loads.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: move redis import to top level in sync.py
deferred imports are only for circular import avoidance, not here.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resilient PDS record fetching with DID resolution fallback
when a user migrates PDS servers, the cached pds_url in the artist table
goes stale, causing 500s on playlist/album GETs. add
get_record_public_resilient() that retries with DID resolution when the
cached PDS URL fails, and heals the DB cache on success.
fixes 500 on GET /lists/playlists/ for @pixeline.be (migrated from
shiitake.us-east.host.bsky.network to eurosky.social).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update test mocks for get_record_public_resilient
existing tests mocked get_record_public at the old import path — update
to mock get_record_public_resilient with the (data, resolved_url) return
type.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: correct mock target for albums test
deferred import means mock must target the records module, not albums.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
restore homepage to pre-activity state (cd2e7b7) and restore the
standalone activity page to its polished version (3515b21). the
activity page remains functional at /activity but is unlisted —
no navigation links point to it.
removes activity-feed module, ActivityRow, and Sparkline components
added in #1020.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
the dedicated /activity page created persistent navigation friction
(4 PRs worth of icon/placement churn). root cause: a separate page
feels orphaned when the primary surface is a track feed.
the pulse icon becomes a toggle that interleaves compact activity
rows (likes, comments, joins) chronologically between track cards.
upload events are excluded since they already appear as track cards.
- activity toggle in section header with accent highlight when active
- compact single-line activity rows with type-colored left borders
- 7-day sparkline histogram shown when activity is toggled on
- dual-cursor pagination: tracks and activity load independently
- /activity page now redirects to / (preserves bookmarks)
- extracted ActivityRow, Sparkline components and activity-feed module
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- moved actor avatar from artwork corner badge to inline next to the
display name in the header (where it identifies who did the action)
- "shared" → "uploaded" for track upload events
- hide "by {artist}" when the actor IS the artist (self-uploads)
- track uploads never show "by" since they're always self-actions
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The :start::date syntax is ambiguous for SQLAlchemy's text() parser.
It interprets :start as a bind parameter, then the ::date cast causes
a syntax error. Every request to /activity/histogram has been 500ing
in production. Switch to CAST(:start AS date) which is unambiguous.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
sparkline: moved histogram fetch into the page loader so it arrives
with the initial data instead of a separate onMount fetch that was
silently failing in production.
pagination: added staggered fly-in transitions for newly loaded items
(batch-index-based delay via svelte/transition). loading spinner now
only appears after 400ms so fast responses don't flash it.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- revert sparkline to 7-day window (was accidentally changed to 30)
- wrap verb text in accent-tinted spans (smaller, subtly colored)
- add artist_avatar_url to ActivityTrack model + inline avatar rendering
- fix pagination by encoding cursor (+ in UTC offset decoded as space)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- sparkline: fetch 30 days instead of 7, skip render when all buckets zero
- neon glow: use inset overlay with border-radius: inherit so accent
follows card corners instead of floating as a clipped bar
- timestamp: add title attr for native hover tooltip with full date/time
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- add GET /activity/histogram endpoint (daily counts over last 7 days)
- SVG sparkline at top of activity page with accent gradient fill
- replace solid border-left with fuzzy neon glow (::before + box-shadow)
- smaller, less blurred lava blobs (5 blobs, 50px blur, ~180-280px max)
- fix TrackCard avatar fallback: container becomes circular when showing
artist avatar so no ugly square outline around circle
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- animated lava lamp with accent-tinted gradient orbs (3 blobs, 24-34s cycles)
- artwork moved to left column with avatar badge overlay (fixes alignment)
- glass cards with backdrop-filter and type-tinted hover glow via --type-color
- respects prefers-reduced-motion
- mobile-responsive, compact CSS (331 lines)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- show track thumbnail/image on right side of event cards
- use person silhouette SVG for avatar placeholders (matches TrackItem/TrackCard)
- stats-informed header showing track/artist/duration counts
- play button placeholder for tracks without artwork
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
drop filled icons for outline-style strokes matching app conventions.
upload arrow for shares (not music note — could be any audio), heart
outline for likes, person+ for joins (not star). add per-type colored
left border accent for visual rhythm. larger avatars, two-line layout
with name/time header + action line below.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- filter out artists with empty handle/display_name from join events
(corrupted row: did:plc:dqbj7k7dierhqdowuyv5ll2z in staging)
- redesign activity page: card containers with glass bg + hover states,
event type icons (heart/note/chat/star), accent-styled comment
previews, right-aligned timestamps, consistent design token usage
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add platform-wide activity feed (#971)
chronological feed of likes, track uploads, comments, and profile
joins. unlisted page at /activity — no nav link, accessible by URL.
reconstructed from existing DB tables via raw SQL UNION ALL query
with cursor-based pagination. includes 9 backend tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add db_session to audio tests missing database setup
test_stream_audio_track_not_found and test_get_audio_url_not_found
were missing db_session fixture, causing "relation tracks does not
exist" under xdist when no prior test on the worker triggered table
creation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: handle docket worker teardown on stale event loop
under xdist, session-scoped TestClient teardown can run on a
different event loop than the one the docket worker task was created
on, causing RuntimeError. catch and log it during shutdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: skip docket worker in test lifespan to fix xdist teardown
the production lifespan starts a docket Worker that creates
asyncio.Task objects bound to the TestClient's portal event loop.
under xdist, session teardown runs on a different loop, causing
"attached to a different loop" in Worker.__aexit__.
no test needs a live docket worker (all docket usage is mocked),
so swap in a lightweight test lifespan that skips it entirely.
also reverts the _is_stale_loop_error hack in background.py.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* perf: add per-branch LIMIT and created_at indexes for activity feed
each UNION ALL branch now sorts+limits independently so postgres can
index-scan the top N per table instead of materializing all rows.
adds standalone created_at DESC indexes on artists, track_likes, and
track_comments to support the global feed query pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>