Commit Graph
15 Commits
Author SHA1 Message Date
bvandeusen 381e9cedb7 feat(net): trusted-proxy depth so real client IPs survive a proxy — #2453
test-go / test (push) Failing after 50s
test-web / test (push) Failing after 50s
test-go / integration (push) Failing after 2m19s
Fixes the defect the operator spotted in #370 immediately after it shipped:
auth.ClientIP ignored X-Forwarded-For whenever RemoteAddr was public, so a
proxy on a public address — a separate host, or a CDN, i.e. anyone running
this publicly, since public means TLS means a proxy — recorded the PROXY for
every session. created_ip and last_ip were then always equal and the
"Address changed" signal could never fire. The feature looked like it worked
and reported nothing.

Replaced with the standard trusted-hop model (Rails, Caddy, Traefik, nginx).
XFF grows left-to-right as each proxy appends the peer it received from, so
for client -> CDN -> own-proxy -> app the app sees [client, CDN] with
RemoteAddr = own-proxy, and the client sits at XFF[len - hops]:

  0  RemoteAddr, XFF ignored — no proxy
  1  the address your own proxy observed
  2  through a CDN in front of your proxy

Default 1, per the operator: publicly reachable means a TLS terminator in
front.

The cost is real and stated rather than hidden. hops >= 1 DECLARES that a
proxy exists; set it with no proxy, or deeper than the actual chain, and the
index reaches attacker-supplied entries, letting a visitor choose which
address their own session shows — defeating exactly the detection #370 is
for. That's inherent to the model, which is why 0 is a first-class value and
the admin card says "count your proxies, don't guess high" instead of just
exposing a number. Both mis-set shapes are pinned by tests so they stay known
consequences rather than surprises.

Migration 0053 + internal/netsettings, cached under an RWMutex. That's not an
optimisation: ClientIP runs in RequireUser for every authenticated request, so
a per-request query would put the database on the critical path of the whole
API. New() always returns a usable service so a boot-time DB hiccup degrades
to the default instead of breaking that path (rule #131), and Hops() is
nil-safe because test routers construct middleware without it.

RequireUser now takes a func() int rather than an int — the value is
operator-editable at runtime while the middleware is built once at boot, and
reading it per request is what makes a save take effect with no restart
(rule #25).

The admin card is verifiable, not just configurable: it reports the address
the CURRENT setting resolves THIS request to, the raw forwarded chain, and the
socket peer — so you set the number, save, and confirm the address matches the
machine you're on. It also counts the arriving chain and says how many proxies
that implies. GET/PUT both return that payload, PUT recomputed under the new
value, so the effect is visible without a reload.

Also fixes styling in the #370 card that CI could not catch: text-destructive
and bg-destructive don't exist in this Tailwind config — the palette is
colors.action.destructive — so the "Address changed" warning and the
sign-out-others button were rendering unstyled. Both now use
text-action-destructive / bg-action-destructive / text-action-fg.

Not done here: requestlog.go still logs raw RemoteAddr and will disagree with
the sessions UI about who connected. Left for its own change.
2026-08-05 10:07:43 -04:00
bvandeusenandClaude Opus 4.8 797ed1f5ad feat(web): tag enrichment sources admin card (#1490 Step 4 web)
test-go / test (push) Successful in 34s
test-web / test (push) Successful in 40s
test-go / integration (push) Successful in 4m45s
Add a "Tag enrichment sources" card to the admin integrations page,
mirroring the cover-art providers card: per-provider enable toggle +
API-key field + Save + Test connection, over /api/admin/tag-sources.
Enabling/keying a source (e.g. pasting a Last.fm key) re-opens settled
tracks for re-enrichment via the version bump.

- admin.ts: TagProvider types + get/update/test functions +
  createTagProvidersQuery; qk.tagProviders key.
- integrations/+page.svelte: the card + local edit state, with
  MusicBrainz (keyless baseline) and Last.fm (needs a free key) notes.
- Tests: admin.tag-sources API test + integrations component tests
  (render / enable+key save / test connection), plus the mock plumbing
  the existing suite needs for the new query.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 22:33:21 -04:00
bvandeusenandClaude Opus 4.7 bca8622640 feat(web/admin): Save runs Test first, dropdowns honor test response
Click sequence on a fresh Lidarr config: enter URL + API key → click
Save. The page now calls testLidarrConnection first, stashes the
returned profiles/folders into local state, lets the existing
auto-default $effects pick first-of-each, then sends the put-config
request with non-zero defaults. If the test fails, the save aborts
and the error surfaces in saveError.

The explicit Test connection button keeps working and additionally
populates the dropdowns from the same response so an operator can
inspect/adjust before saving.

Each dropdown reads from its test-state array when present, falling
back to the existing query data (which only works after the first
successful save). list_errors entries surface as inline notes under
their respective dropdowns.

Closes the chicken-and-egg where saving required defaults that could
only be fetched after saving.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:08:58 -04:00
bvandeusen 617477b702 refactor(web): pushToast store + ToastHost; 8 sites migrated (W3) 2026-05-08 05:45:34 -04:00
bvandeusen 970752a153 refactor(web): Modal component; admin + discover modals migrated (W2) 2026-05-08 05:37:58 -04:00
bvandeusen 9c91a342e2 refactor(web/api): errCode + errMessage helpers; 41 sites migrated (W1) 2026-05-07 22:17:45 -04:00
bvandeusen 9ed576a029 fix(ci): pgconn import path + reset-password token type + SMTP label
Three CI fixes from the U1+U2+U3 push:

- internal/api/admin_users.go imported the standalone
  github.com/jackc/pgconn — that module isn't in go.sum and
  go vet caught it. Switch to github.com/jackc/pgx/v5/pgconn
  (the path used elsewhere in the package, e.g. auth_register.go
  and me_profile.go).

- /reset-password/[token] dereferenced page.params.token without
  the undefined narrowing svelte-kit's typegen requires. Coalesce
  to '' on read and reject empty token at submit time with a
  clear error.

- /admin/integrations SMTP card had a label without an associated
  control as the TLS row's leading column header. Switched to a
  span — the actual checkbox lives in the wrapping label below it.
2026-05-07 17:48:44 -04:00
bvandeusenandClaude Sonnet 4.6 5da2c85f70 feat(web/m7-user-mgmt): /reset-password page + admin SMTP card + tests (U3-T5b)
Completes the U3 frontend that T5a's crashed dispatch left half-done.

- /reset-password/[token] — public; new password + confirm. Calls
  POST /api/auth/reset-password with the URL's token. invalid_token,
  password_too_short, and mismatched-passwords surface as inline
  errors. Success redirects to /login?reset=ok.
- /admin/integrations gains an SMTP card (alongside Lidarr) with
  enabled toggle + all fields + Save and Send-test-email buttons.
  Password input is a separate state from the loaded form, so empty
  submission preserves the stored server-side value (matches the
  backend's empty-password-preserves contract). Test button error
  codes (no_email_on_file / not_configured / send_failed) surface
  as actionable toasts.

Tests cover both new pages plus extensions to settings + integrations
test files for the cards landed in T5a.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 17:43:47 -04:00
bvandeusenandClaude Sonnet 4.6 8307cbdbf9 feat(web/m7-cover-sources): admin /integrations cover-provider cards
Adds a "Cover art providers" section to the existing /admin/integrations
page with one card per registered provider (currently MBCAA + TheAudioDB).

Each card has:
- Provider name + capability badges (album cover · artist thumb · artist fanart)
- Status dot (green if enabled; muted if disabled)
- Enabled toggle + API key text field (only when requires_api_key)
- Save button (explicit save matches existing Lidarr card pattern)
- Test connection button (when testable) with inline ok/fail indicator
- Footer help line for TheAudioDB linking to the free key application

Save invalidates qk.coverProviders() always and qk.coverage() when
the PATCH response indicates version_bumped: true (so the coverage
gauge re-queries to show settled→with_art transitions on the next
scan).

API key field uses type=password with a placeholder that clarifies
the test-key fallback behaviour — never echoes back the stored key
(api_key_set bool from the GET response controls the "✓ Set"
indicator).

Local edit state is keyed by provider_id and survives query refetches
so in-flight edits aren't clobbered.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 13:15:10 -04:00
bvandeusenandClaude Opus 4.7 9ad2e0343a feat(web/m7-363): per-route svelte:head titles for static routes
Add browser tab titles to all 18 static page routes using the pageTitle()
helper from $lib/branding. Titles follow the Minstrel · Section format.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 17:37:13 -04:00
bvandeusenandClaude Sonnet 4.6 f0bfac2555 refactor(web/m7-362): action-button labels use non-flipping text-action-fg
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 13:42:42 -04:00
bvandeusenandClaude Opus 4.7 c265b871c3 feat(admin): metadata-profile picker on integrations page
Persists the operator's metadata-profile choice alongside quality
profile + root folder. Defaults to the first profile Lidarr returns
(usually 'Standard' on a vanilla install) so the common case is
zero-click; operators with custom profiles like 'Singles only' can pick
explicitly.

Backend:
- Migration 0013: adds nullable default_metadata_profile_id to
  lidarr_config. Existing rows get NULL and the service falls back to
  fetch-and-pick-first until they save.
- Updated lidarr_config queries + sqlc + lidarrconfig.Config + admin
  view/put body to round-trip the new field.
- handlePutLidarrConfig requires it (along with QP and root folder)
  when enabled=true — matches the existing missing_defaults gate.
- New GET /api/admin/lidarr/metadata-profiles handler + lidarr.Client
  ListMetadataProfiles (GET /api/v1/metadataprofile, same shape as
  the quality-profile endpoint).
- lidarrrequests.Approve prefers cfg.DefaultMetadataProfileID; falls
  back to the fetch-list path only when 0 (back-compat for upgraders).

Frontend:
- LidarrConfig type + LidarrMetadataProfile type + qk.lidarrMetadataProfiles.
- listMetadataProfiles + createMetadataProfilesQuery client helpers.
- Integrations page: third <select> picker, auto-defaults to first
  profile when the saved value is 0, sends the new field on save and
  clears it on disconnect.
- Updated test fixtures + the duplicate 'Standard' option string in
  the dropdown-populates assertion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:09:13 -04:00
bvandeusenandClaude Opus 4.7 ab8235dd0b fix(admin): approve flow surfaces real errors; auto-default Lidarr picks
The "I can't approve a request, the toast just says unknown" bug was
four problems compounded:

1. Lidarr config let enabled=true save with default_quality_profile_id=0
   and default_root_folder_path=''. Approve then sent invalid POST bodies
   to Lidarr, which 5xx'd.
2. lidarr.client.post() discarded Lidarr's response body on error, so
   we couldn't tell why Lidarr 5xx'd from server logs.
3. handleApproveRequest's error switch didn't map ErrServerError or
   ErrLookupFailed — both fell through to a generic 500 server_error.
4. apiFetch only parsed {error: {code, message}} envelopes, but admin
   endpoints write {error: 'code_string'}. Every admin error toast
   rendered as 'unknown'.

Fixes:

- internal/lidarr/client.go: capture up to 512 bytes of Lidarr's response
  body when it returns 4xx/5xx; include in the wrapped error so server
  logs show what Lidarr actually said instead of just the status bucket.
- internal/lidarrrequests/service.go: new ErrDefaultsIncomplete fires
  before the Lidarr call when QP=0 or root_folder=''. Stops the bad
  POST entirely.
- internal/api/admin_requests.go: handleApproveRequest now maps
  ErrDefaultsIncomplete -> 'lidarr_defaults_incomplete' (400),
  ErrServerError -> 'lidarr_server_error' (502),
  ErrLookupFailed -> 'lidarr_rejected' (502).
- internal/api/admin_lidarr.go: handlePutLidarrConfig now requires
  QP + root folder to be set whenever enabled=true.
- web/src/lib/api/client.ts: apiFetch handles both error envelope shapes
  so admin error codes propagate to toasts.
- web/src/routes/admin/integrations/+page.svelte: auto-default to the
  first quality profile and first root folder Lidarr returns when the
  operator hasn't picked one yet — saves a click for typical
  one-profile/one-folder home setups.
- web/src/routes/admin/requests/+page.svelte: friendly toast copy for
  the new error codes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:37:17 -04:00
bvandeusen b1b50187b3 fix(web): integrations panel review fixes
- Trim whitespace before checking the typed-confirm value (paste-with-spaces
  was silently keeping the button disabled).
- Wrap onConfirmDisconnect in try/catch and surface disconnect_failed inline
  in the modal — operators on flaky networks now see why nothing happened.
- Invalidate quality-profile + root-folder caches alongside config on both
  save and disconnect, so a base_url change refetches the lists immediately.
2026-04-29 23:11:32 -04:00
bvandeusenandClaude Opus 4.7 c281c8b5dd feat(web): add /admin/integrations Lidarr connection panel
Connection management for Lidarr — base URL, API key, default quality
profile, default root folder. Empty api_key on save preserves the
masked-saved key per backend semantics. Disconnect requires typed
"DISCONNECT" confirmation. Includes a placeholder MusicBrainz overrides
section for future integrations.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 23:06:19 -04:00