Commit Graph

1906 Commits

Author SHA1 Message Date
Estrella Pan
1dc1eb01b4 chore: release 3.3.0-beta.5
Bump version, uv lock, and CHANGELOG for 3.3.0-beta.5 — ships the
overlayfs EXDEV fix for in-app updates (needs a fresh image to take
effect) and moves the Software Update card into Settings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
3.3.0-beta.5
2026-07-05 12:31:12 +02:00
Estrella Pan
a8740cb211 refactor(webui): move the Software Update card to Settings
The update card lived in the Log page sidebar; it belongs with the rest of
configuration. Add it as the last section of the Settings section-rail
(self-persisting, so groups: [] — outside the global save/dirty tracking)
and drop it from the Log sidebar, which keeps its About links.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-05 12:28:29 +02:00
Estrella Pan
17a3805974 fix(update): survive EXDEV when overlaying the module tree on overlayfs
In-app updates verified and staged fine but silently stayed on the old
version: boot_overlay swaps the code with os.rename('/app/module',
'/app/module.ab_old'), and /app/module comes from the image's overlayfs
lower layer. Some overlay/kernel combos refuse to rename a lower-layer
directory across the mount boundary and raise EXDEV (Errno 18), so the
swap failed on every boot ("Applying overlay ... / Failed to overlay
module tree: Cross-device link") while apply reported success — the user
just saw the frontend restart watch time out.

_replace_tree now catches EXDEV (dst is untouched, rename is atomic) and
falls back to replacing the directory's contents in place, which triggers
overlayfs copy-up per file and works regardless of mount/overlay topology.
Same fix covers the dist tree. Reproduced with a test that forces EXDEV on
the module/dist rename and asserts the overlay still lands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-05 12:28:29 +02:00
Estrella Pan
685711d927 chore: release 3.3.0-beta.4
Bump version, uv lock, and CHANGELOG for 3.3.0-beta.4 — bundles the login
redirect fixes, the container-startup optimizations, and the update-check
feedback improvement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
3.3.0-beta.4
2026-07-05 09:42:42 +02:00
Estrella Pan
40f83c476d feat(update): make "check for updates" give real feedback
Clicking "check for updates" felt like a no-op: the only signal was a
static one-line "up to date", already shown from the on-mount auto-check,
and the /check endpoint served a 15-minute cache so repeated clicks did
nothing server-side.

- Expose `force` on GET /update/check; the button passes force=true to
  bypass the cache and re-fetch, while the on-mount auto-check stays cached
  (no extra GitHub calls). Updater-level force was already implemented and
  is now locked by a test.
- Active feedback on a user-initiated check: a success toast when already
  current, an info toast naming the new version when one is found, and a
  "Last checked HH:MM:SS" line that updates every click. The check-failed
  toast now includes the backend's reason (rate limit / no release) instead
  of a generic message.

Verified end-to-end: the endpoint reaches GitHub and finds the latest
release (beta.3, whose signed update bundle is attached), so in-app update
is functional; the timestamp advances and the toast fires on each click.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-05 09:39:09 +02:00
Estrella Pan
6f72cfe1df perf(startup): defer LLM SDK imports and gate the volume chown
Two things dominated container start:

- The LLM SDKs (anthropic + openai + google-genai) imported at module load
  via parser.analyser/__init__ → llm.py, costing ~0.7s on every boot even
  though the LLM parser is off by default. Move those three heavy imports
  into the provider branch of __init__ and each _parse_* method, so they
  load only when an LLMParser is actually built. Measured: importing
  parser.analyser drops 782ms → 203ms; none of the SDKs are in sys.modules
  after the startup import chain. httpx/httpx_socks stay module-level (cheap,
  and the proxy tests patch them by module path).

- entrypoint.sh ran `chown -R /app/data /app/config` unconditionally every
  start — O(files), which a large poster cache on a NAS turns into the
  dominant startup cost. Walk the tree only when ownership needs fixing
  (first run, changed PUID/PGID, or a wrong-owned volume root), tracked by a
  marker; the legacy bangumi.json migration chowns its one file explicitly so
  the gate can't miss it. /home/ab (image-internal, tiny) is still chowned
  every time.

Also regenerates the auto-imports dts for onResponseError (exported for the
401 test in the previous commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-04 19:16:49 +02:00
Estrella Pan
b4e1113298 fix(webui): redirect to /login on 401, and silence the startup refresh
A 401 dropped isLoggedIn but never navigated, so an expired session left
the user stranded on an unauthorized page showing stale data (the logout
analog of the passkey "state changed but never navigated" bug). Route back
to /login on any 401, unless already there (a bad-credentials 401 on the
login form must not fight the login flow). Applies to silent background
polls too — an expired session is an expired session.

Also mark apiAuth.refresh() silent so the App.vue startup refresh of an
expired cookie doesn't flash a scary error toast; the 401 handler still
logs out and routes to /login.

Extracts the interceptor's error handler into an exported onResponseError
so the auth-redirect behavior is unit-tested (401 from app page redirects,
401 on /login does not, silent 401 redirects, non-auth error does not).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-04 18:40:20 +02:00
Estrella Pan
0d3fffdef4 fix(webui): login redirect gets stuck (passkey never navigates, guard wedge)
Three overlapping defects made login "sometimes stick without redirecting"
across all three methods:

- Passkey login set isLoggedIn=true but never navigated, so a successful
  passkey login stayed on the login page every time. Add the missing
  router.replace({ name: 'Index' }).
- useAuth.login() didn't return its promise chain, so login.vue's
  `await login()` resolved before the redirect ran (and the loading state
  cleared early). Return the promise.
- The router guard awaited apiSetup.getStatus() on every navigation while
  setupChecked was false; when the initial check failed, this blocking
  network call wedged the post-login redirect. Skip the setup gate while
  logged in — a session can only exist after setup completed.

Adds login-redirect.test.ts reproducing all three (navigate on success for
password + passkey, no navigate on failure).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-04 18:32:01 +02:00
Estrella Pan
9a0347cb95 chore: release 3.3.0-beta.3
Bump version, uv lock, and CHANGELOG for the 3.3.0-beta.3 beta.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
3.3.0-beta.3
2026-07-04 07:42:10 +02:00
Estrella Pan
620b4f5ecc feat(webui): config/log redesign, log dedup, 3.3 UI alignment, LLM model list
Config page redesigned to a section-rail layout (search + per-section
dirty dots, single column), a save bar that names the restart and
double-confirms save/discard, and a navigate-away guard. Log page
redesigned with a health strip (+ jump-to-first-problem), a unified
toolbar (level chips with counts + text search), severity-only row
colour (fixes the dark-mode 3.53:1 contrast failure), an extracted
module column, and overflow-wrap word handling.

Log deduplication: fix the queue log handler using Python's default
formatter (source of the "INFO::module.x:" duplicate prefix on every
line), strip 245 hand-written [Tag] prefixes, drop double-logged
offset-scan/calendar completion lines, demote "Config loaded" to DEBUG.

3.3 feature/UI alignment: aria2 selectable in Config + wizard (with an
aria2 JSON-RPC connection test), preferred group/resolution and
episode_type surfaced in the rule editor (migrations v10/v12), update
auto_check switch with immediate persistence (no restart).

LLM: model input is now a searchable/free-text select that fetches the
provider's model list on demand (new POST /api/v1/config/llm/models);
timeout/cache/concurrency/circuit-breaker tuning fields; default model
gpt-4o-mini -> gpt-5-mini (discontinued).

Accessibility: real <label for> on config controls, aria-labels on
switches/selects, aria-pressed on log chips, 44px touch targets.
Unify delete confirms to NPopconfirm; password inputs; copy fixes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-04 07:42:10 +02:00
Estrella Pan
c8f402fd68 chore: bump version to 3.3.0-beta.2
CHANGELOG: fold the unreleased in-app updater + multi-provider LLM
entries into 3.3.0-beta.2 and add the WebUI UX/a11y/perf pass and
post-review backend fixes. docs/changelog/3.3.md (the release body)
gains the beta.2 highlights.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
3.3.0-beta.2
2026-07-03 16:23:38 +02:00
Estrella Pan
39564a8a54 polish(webui): UX/a11y/perf pass from live review + parallel audits
Destructive-action safety:
- Rule-delete dialog no longer deletes on "No": clear message, "also
  delete downloaded files" checkbox, real Cancel + single red Delete
- RSS bulk delete, torrent bulk delete, clear-log and notification
  provider removal now behind NPopconfirm

Honest feedback and data states:
- axios interceptor gains per-request `silent` flag; background fetches
  (status/log/downloader polls, passkey list) stop toasting errors
- failed downloader poll keeps last data instead of faking "no torrents";
  failed first bangumi load shows error+retry instead of onboarding;
  search SSE failure distinct from "no results"; RSS table loading state;
  calendar refresh + provider save success toasts; wizard submit failures
  show the backend reason

Setup wizard dead ends:
- downloader step no longer hard-gated on a passing connection test
  (passwordless qB testable, editing fields re-arms untested state)
- inline username validation; progress persists to sessionStorage

Accessibility and touch:
- keyboard: logout button, search variant/filter chips as real buttons,
  Space on card buttons, Escape closes add/edit modals, rule-list rows
  focusable; visible close button on the search modal (was backdrop-only)
- air-weekday select in the rule editor — weekday assignment now works on
  mobile/keyboard, not just desktop drag
- mobile nav actually fixed to the bottom (was rendering in-flow at top)
- 44px touch targets: topbar, chips, modal close, offsets, fold headers,
  wizard controls (with 16px inputs to stop iOS zoom)

Performance:
- self-hosted Inter variable font replaces render-blocking Google Fonts
- lazy poster loading on the landing grid; log page bounded to newest
  1000 lines; KeepAlive capped at 3 pages

Design tokens: new warning bg/border/text tokens (light+dark) replace
hard-coded ambers; add-RSS and notification config use NSwitch/NSelect
instead of hand-rolled controls; ab-data-list selection is controllable
(mobile RSS checkboxes no longer desync after bulk actions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-03 16:18:27 +02:00
Estrella Pan
d28f5d5e1d fix(qb): rename verify must see new_path before reporting success (#754 #749)
The verify loop treated "neither old nor new name in the file list" as a
successful rename. A qBittorrent 200 that never materializes (target name
taken by a duplicate from another source, file list anomaly) was reported
as success, so the renamer re-renamed and re-notified every rename cycle.
Now only an observed new_path returns True; everything else returns False
and backs off via _PENDING_RENAME_COOLDOWN.

Regression tests adapted from PR #1037 by cyber-ayi, which diagnosed the
false-positive contract.

Co-authored-by: cyber-ayi <cyber-ayi@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-03 14:39:21 +02:00
Estrella Pan
d7e3f38732 fix(types): unbreak mypy on 3.3-dev after ed25519 signing commit
- boot_overlay/signing: narrow load_pem_public_key result to
  Ed25519PublicKey before verify; a non-ed25519 PEM is now rejected
  explicitly instead of via TypeError caught by the broad handler
- test_context: drive the scheduler mock through a local variable so
  mypy does not see an assignment to the read-only Scheduler.running
- test_migrations_module: _make_versioned_engine returns Engine

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-03 14:31:39 +02:00
Estrella Pan
27c04f5f5d feat(update): sign update bundles (ed25519) and stop root uv sync (P1)
Closes the update-chain trust gap: the sha256 came from the same GitHub
release as the bundle (integrity, not authenticity), and the boot overlay
re-applied ab-writable content as root — including uv sync on an
attacker-controllable lockfile.

- signing: new module/update/signing.py verifies an ed25519 signature;
  public key embedded in the image at /app/ab_update_pubkey.pem (outside
  the overlay-replaceable tree, so it is the trust root). Private key lives
  only in CI (UPDATE_SIGNING_KEY secret).
- updater: discover the .zip.sig asset, verify the signature after the
  sha256 check, reject any release without a signature, retain the verified
  bundle.zip+.sig for boot re-verification, and swap them on rollback.
- boot_overlay: re-verify the signed bundle at every boot and extract the
  module tree / webui dist FROM the verified zip — never from the
  ab-writable current/ tree; read the version from the zip manifest, not
  the ab-writable applied.json; drop to the ab user before any fallback
  uv sync so lockfile build hooks can't run as root; zip-slip guard uses
  is_relative_to (also fixed in updater._safe_extract).
- CI: build.yml signs the bundle with UPDATE_SIGNING_KEY and publishes the
  .zip.sig; the build fails if the secret is unset.
- deps: cryptography made an explicit dependency.

Tests cover bad-signature reject, unsigned-release reject, legacy overlay
without a signed bundle reject, verified-bundle retention, and boot-overlay
signature verification. Full backend suite: 1079 passed.

Ops: run `gh secret set UPDATE_SIGNING_KEY < ~/.autobangumi/update-signing-key.pem`
before the 3.3 release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-03 11:15:03 +02:00
Estrella Pan
98e016ce2e fix: apply architecture-review fixes P2-P10 across update, downloader, core, parser, webui
- update: snapshot DB before promote; rollback restores it only when the
  schema actually advanced, never from a stale on-disk fallback; snapshots
  are standalone (no WAL sidecars); dependency sync moved to apply time
  with a staged venv, plus boot-time venv reconcile after rollback;
  entrypoint restart loop forwards signals so docker stop stays graceful
- downloader: AddResult carried through the client protocol; qB "Fails."
  only classified duplicate when the magnet hash is confirmed present;
  aria2 partial-success batches report ADDED, magnet followedBy gids are
  migrated and metadata stubs skipped in torrents_info; renamed paths
  persisted per gid so the rename loop converges
- core: AppContext lifecycle serialized under one lock with running state
  derived from the scheduler; setup wizard starts background loops on
  completion
- parser: LLM path gains episode_type classification (LLM results only),
  timeout/cache/concurrency/breaker controls, proxy-aware SDK clients
  closed after a grace period on config reload
- webui: useApi returns an {ok} discriminant; config save and RSS refresh
  surface failures instead of false success
- migrations: per-statement guards so a crash mid-migration recovers

Implemented via Codex (P2-P10 from tasks/todo.md), then reviewed with a
multi-agent verify pass; 10 confirmed review findings fixed on top.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-03 10:52:09 +02:00
Estrella Pan
b34fa62403 feat(update): in-app auto-update via volume-backed overlay
Adds one-click update from the settings page so routine updates no
longer require pulling a new Docker image.

- CI attaches update-bundle-<version>.zip (+ .sha256) to each release:
  backend module tree, built webui, and a manifest; the image bakes
  /app/IMAGE_VERSION as the baseline
- boot_overlay.py (shipped in the image, outside /app/module so it can
  never overwrite its own deciding logic) runs from entrypoint before
  the app starts: if a persisted overlay in /app/config/updates is
  newer than the image it tree-replaces /app/module and /app/dist and
  runs `uv sync` when the lockfile changed; if the image is newer it
  clears the stale overlay. Applied via the config volume, so updates
  survive container recreate; a newer pulled image always wins
- GET/POST /api/v1/update/{check,apply,rollback} (auth-gated, repo
  pinned to EstrellaXD/Auto_Bangumi): check queries GitHub releases
  with a stable/beta channel; apply downloads, verifies sha256, stages,
  promotes current->backup, then exits the process so Docker restarts
  into the overlay; rollback swaps the backup back. Progress streams
  over the existing SSE channel
- Settings card (Log page) shows current/latest, release notes, channel
  toggle, update + rollback, with a restart:unless-stopped requirement
  note; compose examples updated

Requires restart: unless-stopped for self-restart (documented; no
Docker-socket fallback). 1043 backend tests, mypy 0, ruff clean; 135
frontend tests, vue-tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-03 09:22:34 +02:00
Estrella Pan
93d3f1a947 feat(parser): multi-provider LLM parser with selectable fallback/primary mode
Generalizes the OpenAI-only title parser into a provider-dispatching
LLMParser (openai-compatible / Anthropic / Gemini) with a parse-mode
switch:

- fallback (default): regex parser runs first, LLM only rescues titles
  the regex can't handle — minimal LLM spend
- primary: LLM parses every title first, regex is the safety net so no
  title is dropped on LLM failure

The openai provider takes a base_url, covering any OpenAI-compatible
endpoint (DeepSeek, Ollama, LM Studio, OpenRouter, OneAPI). Anthropic
uses structured JSON output with refusal handling; Gemini uses
response_mime_type=application/json.

Config auto-migrates the legacy experimental_openai section into a new
llm section on load (migrated users get mode=primary, preserving their
original LLM-first behavior); the old section is kept for downgrade
safety and llm.api_key is redacted by the config API. Settings page
swaps the OpenAI panel for an LLM panel with provider + mode selects.

1018 backend tests, mypy 0, ruff clean; 131 frontend tests, vue-tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-03 08:51:01 +02:00
Estrella Pan
e67dff8f6b chore: bump version to 3.3.0-beta.1, full release notes for the 3.3 beta
docs/changelog/3.3.md rewritten to cover the complete 3.3 scope (it is
the release body the tag build publishes): architecture, aria2
first-class, movies/specials, preference dedup, notifications, security
stack, PyJWT migration, performance pass, WebUI/SSE work, upgrade notes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
3.3.0-beta.1
2026-07-03 08:10:47 +02:00
Estrella Pan
a6d0ac8718 perf: precompiled parser regex, batched renamer fallback, restored lost optimizations
Audit of the Jan perf pass (cba4988) against the 3.3 rewrite found two
regressions and applied new profile-guided fixes:

- raw_parser: all static regex precompiled — 16% faster wall, re._compile
  calls down from 2.06M to 89k per 100k parses
- Renamer untagged-fallback offset lookup batched (same idiom as
  engine/auto-tag): 215ms/201 queries -> 19ms/2 queries at 200 torrents
- Notification dispatch across items re-parallelized (lost in the
  rewrite), after fixing the underlying RequestURL re-entrancy race
  that made naive gather unsafe
- Migrations 13/14: index backfill for upgrading databases — index=True
  only materializes via create_all on fresh DBs, so every upgraded
  install was full-scanning title_raw/url/rss_id lookups; new
  ix_torrent_bangumi_id flips offset-scanner and preference-dedup
  queries from SCAN to SEARCH

990 tests green (+6 regression tests), mypy 0 errors, ruff clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-03 08:03:09 +02:00
Estrella Pan
e5e8c91699 chore(webui): clear baseline lint errors now gating CI
The dev-branch CI added in this cycle runs pnpm lint, so the
pre-existing eslint errors (import sorting, unused imports, a
message-less Error in a test) started failing the pipeline. Auto-fixed
plus one manual message; remaining items are warnings only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-03 07:11:20 +02:00
Estrella Pan
edbc734d08 fix(security,sse): restore 72-byte password semantics, bound SSE downloader fetch
- bcrypt 5.x raises on >72-byte passwords where passlib silently
  truncated; hash and verify now truncate to 72 bytes so pre-upgrade
  users with long (e.g. CJK) passwords can still log in instead of
  hitting HTTP 500
- The SSE downloader payload is fetched under a 3s asyncio.wait_for
  (task cancelled cleanly on timeout) and emits an explicit null on
  failure, so a dead qBittorrent can no longer freeze the entire
  event stream while the UI shows connected

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 22:07:12 +02:00
Estrella Pan
d422e615b1 fix(renamer): distinct names for multi-file movies, revert group-tag prefix, season floor
- Multi-file movie torrents: the largest file keeps "Title (Year).ext",
  extras get "Title (Year) - <original-stem>.ext" (idempotent across
  rename cycles) — no more per-cycle qB conflicts or aria2 collisions
- Reverted the "[group] " filename prefix: group_tag historically only
  named the qB RSS rule; prefixing renames would mass-rename existing
  seeding libraries on upgrade
- Season floor restored for regular episodes (season+offset<1 reverts
  to the original season); only specials may land in Season 0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 22:07:12 +02:00
Estrella Pan
1f5a6195d8 fix(aria2): reconcile stale dedup rows, guard zero-length progress, never overwrite on rename
- On a dedup-key hit the gid is verified via tellStatus; rows for
  downloads removed outside AutoBangumi are deleted so the torrent can
  be re-added (unreachable aria2 conservatively keeps the row)
- JSON-RPC numeric strings are parsed before math; totalLength "0" no
  longer raises ZeroDivisionError in torrents_info
- Filesystem renames refuse to overwrite an existing destination and
  fail loudly instead of destroying data via os.replace

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 22:07:12 +02:00
Estrella Pan
eada4e7210 fix(rss): retry failed adds, stop spurious failure alerts, unpollute offset scan
- Failed torrent adds are no longer persisted, so the feed item
  re-enters the pipeline on the next tick — the previous
  downloaded=False row blocked retry forever via check_new's URL dedup
- add_torrent returns AddResult (ADDED/DUPLICATE/FAILED); healthy
  duplicates no longer emit download-failure notifications
- bangumi_id is set only on torrents that pass the exclusion filter,
  so OffsetScanner no longer computes suggestions from excluded
  episodes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 22:07:12 +02:00
Estrella Pan
8597e3d64f polish(webui): declutter RSS table, log page copy, nav labels, title clamps
- RSS: parser/aggregate chips move next to the feed name where they
  describe the source; Status keeps only Connected/Active state; URL
  column left-aligned with correct "URL" header; chip labels localized
- Log: "Contact Infomation" typo, "Update Now"→"Refresh" and
  "Reset"→"Clear log" (the buttons refresh/clear the log view, not the
  app), empty state message instead of a bare box
- Sidebar/mobile nav: "HomePage"→"Home", "RSS Manager"→"RSS" — labels
  no longer truncate in the mobile bottom nav
- Bangumi cards: titles clamp to two lines instead of one and carry a
  full-title tooltip
- Calendar: drop the redundant "Schedule" heading under the page h1

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 21:46:09 +02:00
Estrella Pan
ddd618b733 polish(webui): calendar card titles, table/header fixes, wizard and contrast
- Calendar cards without a poster now show the series title on the
  placeholder (was an anonymous gray box until hover)
- Downloader: Seeds/Peers column no longer wraps mid-word; progress
  percentage moved outside the bar where it is readable at 0%
- RSS: Refresh All icon uses NButton's icon slot (was glued to label)
- Setup wizard: progress bar counts the current step (was empty on
  step 1), animates via transform with reduced-motion fallback,
  description text raised to AA contrast
- Card titles use the primary text color — headers were lighter than
  body text (inverted hierarchy) across every card

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 21:33:02 +02:00
Estrella Pan
d80bf89cfa fix(webui): restore config page — naive-ui theme crash and missing controls
Two regressions from the component-system unification left the Config
page blank or control-less:

- App.vue passed var(--color-danger) as common.errorColor*; naive-ui
  runs JS color math (seemly rgba) on these tokens and threw
  "Invalid color value var(...)" while rendering NSelect, killing the
  whole page subtree. Error colors are now per-theme literals.
- ab-setting.vue still referenced the deleted AbSwitch/AbSelect
  primitives, which auto-resolution silently rendered as nothing —
  every switch/select in settings vanished. Replaced with themed
  NSwitch/NSelect plus an items→options adapter for the old API.
- Bumped naive-ui 2.39→2.44: 2.39 predates Vue 3.5 and crashed NTag
  rendering inside NDynamicTags ("theme.self undefined").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 21:33:02 +02:00
Estrella Pan
a630d10cde feat(dev): one-command dev environment via scripts/dev.sh
up/down/seed/status/reset/shot subcommands: reuses the e2e docker
services (qBittorrent + mock RSS), starts backend with AB_DEV_NO_AUTH=1
and the webui dev server, pins a stable qB password, completes the
setup wizard through the API, and subscribes the mock feed — a seeded,
login-free dev stack in one command. shot provides a headless-Chrome
screenshot fallback (the bundled Chrome-for-Testing hangs on
Page.captureScreenshot on macOS).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 21:33:02 +02:00
Estrella Pan
ddc3717121 test(e2e): update stale expectations for POST conversions and 404 semantics
The e2e suite never runs in CI and drifted from three intentional changes:
- destructive endpoints (start/stop/restart, log/clear, logout, bangumi
  reset) were converted from GET to POST for CSRF hardening
- disabling a nonexistent RSS now returns 404 instead of 406
- newer qBittorrent images return 204 with no body on login

Full e2e run now green: 67 passed, 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 20:02:35 +02:00
Estrella Pan
76c4f72a95 docs(changelog): summarize big-rocks phase (aria2, movies/specials, release preference, webui refactor)
Add Unreleased entries covering the aria2 first-class downloader, movie/
special episode recognition, per-bangumi release preference dedup, and
the webui component-split/naive-ui/SSE work landed this phase.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 19:49:51 +02:00
Estrella Pan
3baf1c72b3 refactor(webui): split oversized components, adopt naive-ui primitives, add SSE
Extract useBangumiRuleForm + shared preview/tag/rss-link/filter/offset
components out of ab-add-rss.vue and ab-edit-rule.vue, and split
calendar.vue into calendar-board/calendar-day-column/calendar-card/
calendar-mobile-list/calendar-rule-list-popup. Replace the bespoke
ab-button/ab-select/ab-switch/ab-checkbox primitives with themed
NButton/NSelect/NSwitch (naive-ui) across the app and delete the now-
unused wrappers. Add a backend SSE endpoint (GET /api/v1/events/stream)
streaming status/downloader/log ticks, and a shared useEventStream
composable that the status/downloader/log polling loops prefer over
their interval fetch, falling back to polling when SSE is unavailable
or the tab is hidden.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 19:48:46 +02:00
Estrella Pan
590220abfe feat(parser): recognize movies and specials as non-episodic bangumi
Teach raw_parser to detect 剧场版/劇場版/电影版/Movie tokens (episode_type
"movie") and OVA/OAD/SP/Special tokens (episode_type "special", season
forced to 0) instead of returning None. Add episode_type to Episode/
Bangumi/BangumiUpdate/EpisodeFile/SubtitleFile (migration v12), fall
back to TMDB's search/movie endpoint when search/tv finds no animation
match, and rename/organize movies with a flat "Title (Year)" layout
(no Season subfolder) while specials use Season 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 19:48:31 +02:00
Estrella Pan
27049dedc5 feat(downloader): make aria2 a first-class downloader backend
Replace the aria2 client's stub RPC layer with a typed, per-call-timeout
JSON-RPC implementation and real add/query/rename/move/delete support:
honest add-result tracking with local dedup (url/file-hash) plus
RPC duplicate-error detection, torrents_info/torrents_files mapped into
the qB-shaped dict the rest of the app expects, and a new aria2_gid
table (migration v11) persisting the "ab:<bangumi_id>" tag association
so Renamer's tag-based offset lookup works unmodified. Capability flags
(can_query/can_rename/can_manage) now reflect what is actually
implemented; can_rss_rules stays False (no qB-native RSS-rule surface
to honestly back it with).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 19:48:16 +02:00
Estrella Pan
cbab57cd05 feat(rss): add per-bangumi release group/resolution preference with dedup skip
Add preferred_group/preferred_resolution fields on Bangumi (migration v10)
and wire RSSEngine.refresh_rss to skip strictly-worse candidate releases
within a batch, or against an already-downloaded episode, when a
preference is set. Bangumi without a preference are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 19:48:06 +02:00
Estrella Pan
66dea1af0f chore(types): add mypy to CI and fix ~20 bugs it surfaced
Add mypy>=2.1.0 as a dev dependency with a check_untyped_defs baseline
config and per-module ignore overrides (module.__version__, httpx_socks,
aiohttp, test.* method-assign), and run it in CI right after ruff.

Convert positional Field(value, ...) defaults to Field(default=value, ...)
across models/config.py so pydantic v2 stops treating aliased fields as
required constructor args. Fix real bugs the sweep surfaced: unguarded
None bangumi_id in TorrentManager.refind_poster, RSSAnalyser.torrent_to_data
silently returning None past its declared type, missing __aenter__ guards
on RequestURL/downloader clients, database/engine.py's sync sessionmaker
on an AsyncEngine, and RequestContent.post_json shadowing RequestURL.post_json
(renamed to post_form_json). Remove a dead, type-unsafe str_to_list() helper
in api/bangumi.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 19:00:37 +02:00
Estrella Pan
72c4a040e4 feat(parser): per-provider parser choice, OpenAI as raw_parser fallback, honor group_tag
Store {url, parser} per search provider (backward-compatible with
legacy url-only configs) and use it in search_url() instead of the
hardcoded mikan-vs-tmdb rule.

TitleParser.raw_parser() now always tries the regex parser first and
only falls back to OpenAI when regex parsing fails and
experimental_openai.enable is set.

Renamer.gen_path() prefixes filenames with "[Group] " when
bangumi_manage.group_tag is enabled and a group tag was parsed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 19:00:18 +02:00
Estrella Pan
ca58fdc50e feat(offset): compute offset suggestions from real parsed episodes and expose apply endpoints
Replace the offset scanner's hardcoded parsed_episode=1 placeholder with
_get_latest_parsed_episode(), which parses a bangumi's actual torrent
names via raw_parser() and skips the suggestion when nothing parses.

Add BangumiDatabase.apply_offset() to copy suggested offsets into the
live season/episode offsets and clear the review flag, plus
POST /api/v1/bangumi/apply-offset/{bangumi_id} and .../apply-offset/many,
each triggering a rename pass afterward.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 19:00:11 +02:00
Estrella Pan
bf84d91926 feat(notification): typed system events, per-provider templates, composition-based base class
Introduce SystemEvent (RssFailureEvent/DownloadFailureEvent/OffsetReviewEvent)
raised by refresh_rss() on healthy->error transitions and add_torrent()
failures, and by the offset scanner; wire them through NotificationManager.send_event()
and the rss/offset-scan loops (gated by settings.notification.enable).

Extend the {{title}}/{{season}}/{{episode}}/{{poster_url}} template mechanism
from webhook-only to all providers via NotificationProvider._format_message(),
falling back to each provider's original default message when unset.

Replace NotificationProvider's multiple inheritance from RequestContent with
composition (self._http), removing the post_json self-shadowing workaround.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 19:00:00 +02:00
Estrella Pan
820ff29537 refactor(security): migrate JWT/password hashing from jose+passlib to pyjwt+bcrypt
Drop python-jose and passlib in favor of direct pyjwt (with pinned
decode algorithm) and bcrypt 4.x hashpw/checkpw calls. Existing
passlib-generated $2b$ hashes keep validating unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 18:58:22 +02:00
Estrella Pan
92eb224011 feat(webui): add RSS refresh buttons, fix vitest API contract tests going stale
Add per-row and 'refresh all' buttons to the RSS page, wired through
new store actions refreshRSS/refreshAllRSS backed by apiRSS.refresh(All).
Fix vitest.config.ts's auto-import dirs being out of sync with
vite.config.ts, which silently made every src/api/*.ts contract test a
no-op (bare axios/apiXxx identifiers threw under vitest, so the specs
never called real code). Add a shared axios mock helper and give every
src/api/*.ts module a real contract test asserting path + method against
the mocked axios instance and cross-checked against the FastAPI routers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 17:39:59 +02:00
Estrella Pan
c2261ece0a docs(ci): make release changelog lookup version-aware, add 3.3 changelog, fix docker-compose PGID/PUID
Derive the release changelog path from the release version instead of
hardcoding 3.2.md, so the workflow keeps working after 3.3 ships. Add
docs/changelog/3.3.md summarizing the 3.3.0 section of CHANGELOG.md.
Fix docker-compose.yml's PGID/PUID falling back to shell substitution
that never runs in a compose file, using numeric defaults instead, and
drop the obsolete top-level 'version' key. Fix README's Python badge to
3.13 to match backend/pyproject.toml's requires-python.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 17:39:49 +02:00
Estrella Pan
9fafaef494 feat(backend): add unauthenticated /health liveness endpoint
Add GET /health mounted outside the /api prefix so it bypasses
per-route auth and is reachable as a plain liveness/readiness probe.
Returns {status, version, db_ok}, degrading to db_ok=false with a 200
(not a raised exception) on database failure so the probe stays useful
even when the DB is unreachable. Add a Docker HEALTHCHECK that curls it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 17:39:39 +02:00
Estrella Pan
d9f41f4b11 refactor(backend): route config reload through AppContext, fix blocking I/O, dedupe N+1 query
Setup wizard and config reload now funnel through a single
AppContext.reload_settings() entry point instead of mutating settings
directly. Wrap remaining sync file I/O (image cache, settings load/save,
log tail read) in asyncio.to_thread so async request handlers stop
blocking the event loop. Add reset_cache() to each cache-owning parser/
searcher module (tmdb, mikan, title parser, poster cache) and wire them
into reload_settings() so a base-URL/API-key config change invalidates
stale cached results immediately. Fix BangumiDatabase.add_all's N+1 query
by batching semantic-duplicate candidate lookups into one SELECT for the
whole batch instead of one query per candidate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTnuEgR78cBk32bLNKar3z
2026-07-02 17:39:29 +02:00
Estrella Pan
42418d5b18 ci: restore release build fix, gate dev-branch PRs, mock the live TMDB test
- Apply the docker-actions/cache fix from 4a513df so 3.3 releases build; fix buildx outputs typo
- Run CI on PRs targeting 3.*-dev; add ruff + frontend lint/vitest steps
- .dockerignore excludes tests and __pycache__ from the image
- test_tmdb mocks the HTTP layer (was hitting the live TMDB API in the default suite)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 17:05:50 +02:00
Estrella Pan
7733bd18f7 fix(webui): setup loop, reactive selects, escape handler, axios/i18n and dep bumps
- Clear needSetup after completion so the wizard stops bouncing back to itself
- ab-select watches modelValue (config selects showed stale defaults); notification-type dropdown renders options
- Search modal Escape handler no longer opens the modal from anywhere; ab-button defaults type=button (no double login)
- axios surfaces 400/404/422; background polls suppress the network-error toast storm; calendar re-reads locally on activate
- Search subscribe uses the active provider + i18n; provider settings go through axios; dead search-store code removed
- Bump axios ^1.7 (CVE-2023-45857) and vue-i18n ^9.14.2 (XSS); add missing i18n keys

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 17:05:50 +02:00
Estrella Pan
04c913fdf7 fix(notification): send JSON bodies as JSON, gate poster URLs on a public base_url
- Discord/Webhook/Gotify/Bark POST real JSON via RequestURL.post_json (was form-encoded and always rejected)
- Route through a distinct _post_json to avoid the form-encoding post_json shadowed on RequestContent
- Poster fields use settings.notification.base_url when set, otherwise omitted (local paths were never reachable)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 17:05:50 +02:00
Estrella Pan
dc786a29e3 fix(downloader): real rename verification, pipe-join move hashes, stop closing shared client
- qB rename verification uses for/else so it actually polls (was returning True after one check)
- move_torrent pipe-joins hashes so qB applies the move (was silently dropped)
- request_url no longer aclose()s the shared client from the request path (killed concurrent in-flight requests); add post_json
- Single-flight qB 403 re-login behind a lock; ref-guard the cached client on teardown
- ab_decorator re-raises after retries and caps delay; collector add/update fixed; renamer offset-lookup failures skip instead of defaulting to 0; rule delete/disable no longer requires a live downloader

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 17:05:50 +02:00
Estrella Pan
f00d39b990 fix(rss/parser): repair OpenAI parser, retry failed adds, stop wiping titles
- OpenAI Episode model uses int season/episode (was str -> silently swallowed TypeError on every parse)
- Mark torrents downloaded only on a successful add so transient failures retry
- Mikan no-poster branch keeps the parsed official_title; set bangumi_id for empty-filter matches; set rss_link on aggregate discovery
- TMDB: guard None results, include test flag in cache key, TTL negative results, skip poster on failed fetch
- Empty filter means no filtering; provider config read at call time; OpenAIParser built once; bounded search enrichment/cache

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 17:05:50 +02:00
Estrella Pan
4bab9d00c8 fix(core): best-effort poster cache, abort boot on migration failure, non-blocking start
- cache_image/save_image skip None bytes; cross_version caches posters per-item best-effort (one bad URL no longer aborts boot)
- run_migrations re-raises after rollback so boot aborts on a half-migrated schema; run migrations before legacy data_migration
- AppContext.start_tasks() is non-blocking; startup task is cancelled+awaited on shutdown
- Settings.save/init create config/ dir; robust version.info parse; AB_SOCKS writes proxy type "socks5"
- checker delegates downloader health to the client (aria2 can pass); add webauthn/notification settings fields

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
2026-07-02 17:05:34 +02:00