Commit Graph
228 Commits
Author SHA1 Message Date
QuickandClaude Opus 4.8 6cebbb3cd9 feat(calendar): preset selector with responsive pills, persistence, empty-state nudge
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 14:13:51 -04:00
QuickandClaude Opus 4.8 f859cbfd7a feat(calendar): add watched field to CalendarEvent type
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 14:13:51 -04:00
QuickandClaude Opus 4.8 e9847a4424 feat(calendar): wire popular and trending sources into calendar handler
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 13:56:14 -04:00
QuickandClaude Opus 4.8 6560dfc470 feat(calendar): resolve presets to id-sets and overlay watched status
Also drops the now-unused Filter/UserID/ProfileID fields from the
blendUpcomingIntoDiscoverRows CalendarFilter literal in recommendations.go,
which only wants an unrestricted windowed query.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 13:55:39 -04:00
QuickandClaude Opus 4.8 134ebf399e feat(calendar): add per-profile followed/favorites/watchlist/watched resolvers
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 13:50:48 -04:00
QuickandClaude Opus 4.8 a119b60ca6 feat(calendar): generalize personal filter to an id-set restriction
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 13:49:34 -04:00
QuickandClaude Opus 4.8 1f729c4764 docs(calendar): add presets design spec and implementation plan
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 13:47:45 -04:00
QuickandClaude Opus 4.8 cade7f37aa fix(calendar): order events by viewer-local wall-clock time
The local-airtime change re-sorted calendar events in Go using air_at,
the absolute UTC instant, which is nil whenever air_timezone is unset.
Since air_timezone is only inferred for a few networks/countries, most
events fell through to the alphabetical title tiebreak while still
displaying their raw air_time, so each day appeared scrambled.

Sort each local day by the wall-clock time the viewer actually sees,
mirroring the client: zoned events convert air_at into the viewer
timezone, unzoned events use the raw air_time, and date-only entries
(no air_time) sort last. The timezone reasoning lives in the new
catalog.CalendarEventLocalTime helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 12:16:18 -04:00
QuickandGitHub 57bcae38e9 Merge pull request #24 from Silo-Server/claude/amazing-thompson-b2843d
fix(catalog): project air_timezone in episode catalog subquery
2026-05-29 11:55:56 -04:00
QuickandClaude Opus 4.8 aa1a0bd38b fix(catalog): project air_timezone in episode catalog subquery
episodeCatalogSelectBody is the derived "mi" relation that episode catalog
hydration and preview read qualifiedListItemColumns("mi") from. The local
episode airtimes feature (808f265f) added air_timezone to the shared column
lists but not to this hand-written subquery, so the outer projection
referenced mi.air_timezone, which the subquery never exposed.

Postgres returns SQLSTATE 42703 (undefined_column), which is not one of the
codes episodeCatalogEntriesUnavailable treats as "fast path unavailable" (it
only catches 42P01/42883), so episode catalog requests failed with HTTP 500
instead of degrading. movie and series scopes query media_items directly, so
the column is present there and only episode scope broke.

Add si.air_timezone to the subquery, and add a regression test asserting that
episodeCatalogSelectBody exposes every column qualifiedListItemColumns reads
off mi, so future additions to the shared column lists cannot silently drift
from the episode read model again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 11:53:32 -04:00
QuickandGitHub 3c338e855a Merge pull request #23 from Silo-Server/feat/trending-section
feat(sections): persist trending_discover via background-refreshed snapshot
2026-05-29 11:35:05 -04:00
QuickandClaude Opus 4.8 f105361658 fix(sections): harden trending refresher per PR review
- Interleave Trakt movies/shows by rank so the mixed row shows both types
  instead of burying all series past the display limit.
- Treat any Trakt sub-fetch failure as fatal (errors.Join) so a partial
  result never overwrites the last-good snapshot with a media type missing.
- Skip non-title entries (TMDB trending/all returns media_type "person") in
  both ID batching and ordering so they can't match an unrelated library title.
- Guard the refresh task against a nil refresher.
- Tests: person skip, Trakt interleave, Trakt partial-failure preserves
  last-good, snapshot read error propagation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 11:28:59 -04:00
QuickandClaude Opus 4.8 c98f828c25 fix(migrations): renumber trending_discover_snapshots 166 -> 167
The shared dev DB already recorded version 166 (166_trending_blend_collection_type
from another branch), so the integer-version migration runner silently skipped our
166 and the table was never created — the trending section errored out empty.
167 is the next free version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:57:57 -04:00
Quick 4455c2cd40 chore(sections): satisfy lint (wrap trakt errors, lift source/window constants) 2026-05-29 10:51:54 -04:00
Quick 9d03ad0b37 feat: wire trending refresh task and snapshot reader 2026-05-29 10:49:01 -04:00
Quick 38723c8b81 refactor(sections): read trending_discover from persisted snapshot 2026-05-29 10:46:01 -04:00
Quick 3c3ef82ae8 feat(tasks): add refresh_trending_discover task 2026-05-29 10:44:15 -04:00
Quick ff5f399e2c feat(sections): add trending refresher with persisted snapshots 2026-05-29 10:43:54 -04:00
Quick 7d70bf0cf4 feat(sections): list enabled trending_discover section configs 2026-05-29 10:42:09 -04:00
Quick 94a588c7a3 feat(sections): add trending snapshot model and repository 2026-05-29 10:41:47 -04:00
Quick ca4cd230a4 feat(sections): add trending_discover_snapshots table 2026-05-29 10:40:58 -04:00
QuickandClaude Opus 4.8 1a74bdbb6a refactor(sections): tidy trending_discover fetch and cache helpers
Extract newTrendingEntry, reuse orderMediaItems, and collapse concurrent
cache-miss loads with singleflight. Baseline for the persistent snapshot work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:40:13 -04:00
QuickandClaude Opus 4.8 cc5fee0956 docs: add trending_discover persistent snapshot implementation plan
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:37:27 -04:00
QuickandClaude Opus 4.8 7992470cc3 docs: add trending_discover persistent snapshot design
Replace the in-process 1h trending cache with a background-refreshed,
persisted snapshot for reliability under upstream failure and sync-run
observability.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:25:31 -04:00
QuickandClaude Opus 4.8 ba4fd9d9fd feat(sections): add trending_discover home section
A library-agnostic home section that surfaces external global trending
(TMDB or Trakt, admin-selectable) mixing movies + series, matched to
titles in the viewer's enabled libraries. TMDB uses /trending/all/{window}
(natively mixed); Trakt merges trending movies + shows. Fetched live with
a 1h in-process cache, so no background job or stored collection — and no
per-library duplication.

Appears in the admin section gallery via its recipe presets (TMDB Trending
Today/This Week, Trakt Trending); featured -> hero via the existing flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 09:25:17 -04:00
QuickandGitHub c216899c2f Merge pull request #22 from Silo-Server/feat/case-insensitive-usernames
feat(auth): make usernames and emails case-insensitive
2026-05-28 21:46:07 -04:00
QuickandClaude Opus 4.7 c61381dc79 feat(auth): make usernames and emails case-insensitive
Login identifiers were compared case-sensitively, so "John" and "john"
were distinct accounts and a user could not log in unless they matched the
exact casing used at registration.

Convert users.username and users.email to the citext type (migration 165).
citext compares case-insensitively while preserving the originally stored
casing for display, so the existing unique constraints become
case-insensitive and `WHERE username = $1` / `email = $1` lookups match
regardless of case with no change to the query code itself.

Also add auth.NormalizeUsername/NormalizeEmail (trim-only; case preserved),
applied at the repository chokepoints (Create, Update, GetByUsername,
GetByEmail) and before validation in the create paths, so surrounding
whitespace no longer defeats matching or creates lookalike accounts.

Verified non-destructively against the dev DB: mixed-case lookups resolve
to the same row, case-variant inserts are rejected by the unique
constraint, and the down migration cleanly reverts to text.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:44:51 -04:00
QuickandGitHub 834f3e5b84 Merge pull request #21 from Silo-Server/fix/web-a11y-visual-qa
fix(web): accessibility & UX fixes from visual QA pass
2026-05-28 21:09:43 -04:00
QuickandClaude Opus 4.8 9ac181dbf4 fix(startup): recover from panics in deferred background init
The deferred background-init steps run in a detached goroutine after the
HTTP listener is already accepting connections. An unrecovered panic in any
step (queue seeding, legacy cleanup, scrobble sweep) would crash the entire
live server. Wrap each step in a recover that logs the panic with a stack
and continues to the next step.

Found during PR #21 review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 20:39:08 -04:00
QuickandClaude Opus 4.8 d34e292e54 revert(startup): keep open-scrobble sweep deferred for fast startup
Reverts 62563f5c. Running the sweep synchronously before the listener could
add up to 30s to restart-before-playback when a watch provider is
unreachable, which regresses the deliberate startup-deferral from a1a6c6cf.
Prefer the fast-startup behavior and accept the small window where a resume
immediately after restart may create a duplicate scrobble; the sweep returns
to the deferred background-init list. (Panic-safety for that list is added in
a follow-up commit.)

Per PR #21 review decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 20:37:33 -04:00
QuickandClaude Opus 4.8 b10ae3eabc fix(catalog): scan air_timezone in paginated item queries
scanItemsWithTotal was not updated for the new air_timezone column, yet the
shared column lists it reads (itemColumns, qualifiedListItemColumns) include
it. Search and BrowseFavorites build their SELECTs from those lists with
COUNT(*) OVER (), so each row carried one more column than the scan had
destinations and every call failed at scan time with a pgx mismatch. Add the
missing &item.AirTimezone target between AirTime and ShowStatus.

Found during PR #21 review (critical: Search/Favorites runtime regression).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 20:37:18 -04:00
QuickandClaude Opus 4.8 62563f5c80 fix(startup): sweep open scrobbles before accepting playback
The open-scrobble sweep was queued in the deferred background-init list,
which runs concurrently with the HTTP listener; a resume immediately after
restart could start new scrobbles before the previous process's open
sessions were stopped, leaving overlapping/stale scrobbles on remote
providers. Run the sweep synchronously before the listener starts, bounded
by a 30s timeout so an unreachable provider can't hang startup (the heavier
non-critical init stays deferred).

Addresses PR #21 review (P2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 20:26:15 -04:00
QuickandClaude Opus 4.8 9fceae63fc fix(metadata): persist a cleared air_timezone instead of skipping it
Clearing a previously-set air timezone sent JSON null, which decodes to a
nil *string that UpdateMetadata treats as "skip this column", so the old
value remained. The dialog now sends "" (accepted by ValidateAirTimezone),
and UpdateMetadata maps air_timezone through NULLIF so an empty value
persists as SQL NULL (matching the nullable column) rather than "".

Addresses PR #21 review (P2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 20:26:15 -04:00
QuickandClaude Opus 4.8 a4c394b827 fix(catalog): re-check orphan status when deleting library items
Orphan detection moved outside the media_items delete in the batched
library-delete rewrite, opening a TOCTOU race: a concurrent scan/import
could attach one of the collected content IDs to another library between
collectOrphanBatch and the delete, after which the unconditional
`DELETE FROM media_items WHERE content_id = ANY($1)` would still remove the
shared row and cascade away the newly-added membership — dropping the item
from the other library. Re-check the orphan invariant inside the delete
(NOT EXISTS a membership in another folder) and count rows actually deleted.

Addresses PR #21 review (P1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 20:26:15 -04:00
QuickandClaude Opus 4.8 234869d41e fix(web): accessibility & UX fixes from visual QA pass
Accessibility (WCAG AA):
- Lighten the Standard-theme `--muted-foreground` (#6e6e78 -> #9696a0,
  ~3.4:1 -> >=5.3:1) and darken the light-theme equivalent so secondary
  text meets 1.4.3 contrast app-wide; the opt-in High Contrast mode is no
  longer the only conformant path.
- Give icon-only controls accessible names (4.1.2): the password show/hide
  toggle (also drop tabIndex={-1} so it's keyboard reachable), and the
  Edit/Delete/health/copy/refresh actions across the Users, Libraries,
  Nodes, API Keys, Catalog Maintenance and Job History admin tables.
- Fix the Switch off-state (invisible track -> visible border + fill) and
  the PlaybackSettings SettingRow label association (the <label htmlFor>
  pointed at a wrapping <div>; the id now lands on the Switch/SelectTrigger).
- Login: wrap the card in <main> and add an <h1>; Profiles: add an
  accessible PIN-protected label and a corner lock badge.
- Player + catalog: role="status" on the initial loading overlay; scope the
  catalog count ("0 in library" for search) and announce it via aria-live;
  trim the verbose poster-link name to the title.

UX / consistency:
- Emphasize overdue scheduled tasks (warning colour + icon + word, not
  colour alone).
- Per-source catalog subtitles instead of one shared string.
- Add a Reconnect affordance when the admin log stream drops (it does not
  auto-retry).
- Page titles for Watch Party + all admin sub-pages (incl. plugins); admin
  heading capitalisation normalised to Title Case.
- Show "dev build" instead of "unavailable" when no build revision is
  stamped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 18:47:11 -04:00
QuickandClaude Opus 4.8 c30b38d220 build: stamp git revision via Makefile ldflags
`make build` did not inject buildinfo's `revisionOverride`/`dirtyOverride`
ldflags (the Dockerfile already does), so binaries built via make report
their version as "unavailable" in the admin Build panel whenever Go's VCS
metadata isn't embedded. Mirror the Dockerfile by computing the git
revision + dirty state and passing them through `-ldflags -X`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 18:46:56 -04:00
QuickandClaude Opus 4.7 ee3a6d1776 fix(migrations): make air_timezone column add idempotent
Use ADD COLUMN IF NOT EXISTS so re-running 162 on a database that already has
the column is a no-op.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:11:11 -04:00
QuickandClaude Opus 4.7 a1a6c6cfac perf(startup): defer non-critical init off the HTTP listener path
Collect catalog-size-dependent seeding (metadata match queues, legacy
series-group cleanup) and the watch-provider scrobble sweep into a
backgroundInit slice that runs sequentially in a background goroutine after
the server is ready, instead of blocking startup before the listener accepts
connections. Steps log failures and stop early on shutdown.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:11:11 -04:00
QuickandClaude Opus 4.7 772569cefe fix(catalog): clamp still/poster/logo backdrops to largest cached variant
Episode stills used as backdrops only exist at w500/w300 in the cache, so
requesting a w1280/w1920 backdrop width 404s. Add catalog.BackdropVariantPath
+ imageTypeFromCachedPath and route featured (w1920) and Continue Watching /
Next Up (w1280) backdrops through it; still/poster/logo paths clamp to their
type's largest cached variant while real backdrops keep the requested width.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:11:11 -04:00
Quick 0d651cc947 refactor(catalog): wrap orphan-batch iteration error 2026-05-28 17:02:53 -04:00
Quick c1f12d1b1b fix(catalog): delete libraries in deadlock-retrying batches
Replaces the single multi-minute delete transaction with phased, batched
autocommit deletes (orphan items, media files, memberships, folder row),
each retried on deadlock. Holds only short locks, survives concurrent
writers, and is resumable on failure.
2026-05-28 17:02:53 -04:00
QuickandClaude Sonnet 4.6 d4520677cc refactor(catalog): make image-dir helpers querier-agnostic
Add rowQuerier interface satisfied by both *pgxpool.Pool and pgx.Tx.
Split collectImageDirs into collectRawImageDirs (raw collection) and a
thin wrapper that filters via filterUnreferencedImageDirs. Both helpers
now accept rowQuerier so a later task can call them from pool-level
batch deletes without an open transaction.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 17:02:53 -04:00
Quick 518cc7eac7 feat(catalog): add deleteInBatches loop helper 2026-05-28 17:02:53 -04:00
Quick fdfa1c16b8 test(catalog): clarify cancel-path expectation in retry test 2026-05-28 17:02:53 -04:00
Quick 5a366ec42b feat(catalog): add deadlock-retry helper for batched deletes 2026-05-28 17:02:53 -04:00
QuickandClaude Opus 4.7 be06b32c82 docs: add implementation plan for resilient library deletion
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:02:53 -04:00
QuickandClaude Opus 4.7 3f1254d374 docs: add resilient library deletion design spec
Batched, deadlock-retrying rewrite of delete_library to replace the
single multi-minute transaction that deadlocks on large libraries.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:02:53 -04:00
Quick c19f0d6c6f Merge branch 'main' of https://github.com/Silo-Server/silo-server 2026-05-28 13:11:49 -04:00
QuickandGitHub 481a577937 Merge pull request #19 from fluxis/mr/a-matcher-reliability
fix(metadata): matcher reliability — Sonarr token cleanup, bare-bracket IMDb IDs, year-corroboration, provider-priority tie resolution
2026-05-28 13:11:43 -04:00
Quick 9f62c6977c fix(metadata): tighten matcher and bare IMDb parsing 2026-05-28 13:09:56 -04:00