Commit Graph

1940 Commits

Author SHA1 Message Date
Estrella Pan
90e2c55ffa Merge pull request #1057 from EstrellaXD/fix/issues-1052-1053
fix: torrent-download 429 throttling and stale RSS entries after deletion (#1052, #1053)
llm-plugins
2026-07-07 14:05:26 +02:00
Estrella Pan
5fdc2e723f fix: address code-review findings on #1052/#1053 fixes
- Disable (not delete) orphaned per-sub RSS feeds on bangumi deletion:
  a hard delete could not distinguish search-created subscriptions from
  user-added non-aggregate feeds, and RSSDatabase.delete's cascade also
  wiped torrent dedupe records that other bangumi may rely on. Disabled
  feeds keep their history and can be re-enabled; batch-disabled in one
  commit via disable_batch.
- Never remap known parser types (mikan/tmdb/parser) through the
  search-provider config in /rss/subscribe; only genuine site names
  (nyaa/dmhy) are mapped. Prevents parser='mikan' from being rewritten
  when the user customized the mikan provider's parser.
- RSSDatabase.search_url tolerates duplicate-URL rows (first() instead
  of scalar_one_or_none) - url has no DB unique constraint.
- _retry_after_delay rejects NaN Retry-After values, which would
  propagate through min/max into asyncio.sleep(nan) and never wake.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JhTduCYBGeHETU7dSWoh37
2026-07-07 13:02:32 +02:00
Estrella Pan
63821917a5 fix(downloader): throttle batch torrent fetches and back off on HTTP 429 (#1052)
Batch-collecting a season fired all .torrent file downloads at once via
an unbounded asyncio.gather, tripping nyaa's rate limiter; get_url then
gave up on the first 429 with no retry, so the whole batch silently
failed.

- add_torrent now fetches torrent files serially per host with a 1s
  delay between requests (parallel across hosts), mirroring the
  RSS_PER_HOST_DELAY pattern from #1026
- get_url retries 429 responses within the existing retry budget,
  honoring a numeric Retry-After header (clamped to 60s) and falling
  back to a 10s delay otherwise

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JhTduCYBGeHETU7dSWoh37
2026-07-07 12:42:35 +02:00
Estrella Pan
8f91899670 fix(manager): remove orphaned per-sub RSS feeds when deleting a bangumi (#1053)
Deleting a bangumi left its individually-subscribed (aggregate=False)
RSSItem enabled, so the WebUI kept showing the feed as active and the
RSS loop kept polling it. delete_rule now removes per-sub feeds that no
remaining bangumi references, splitting comma-joined rss_link values and
matching URLs exactly. Aggregate feeds and feeds still referenced by any
bangumi (including soft-disabled ones) are never touched.

Adds RSSDatabase.search_url for exact-URL lookup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JhTduCYBGeHETU7dSWoh37
2026-07-07 12:42:35 +02:00
Estrella Pan
b659795288 fix(rss): map search-site name to its configured parser on subscribe (#1053)
Search-subscribe sent the provider site name (nyaa/dmhy) as the RSSItem
parser, but the analyser only understands mikan/tmdb, so those
subscriptions silently skipped TMDB title/poster enrichment. Resolve the
site name through the search-provider config at the subscribe boundary;
values that are already parser types pass through unchanged.

Note: the WebUI RSS tag for search-subscribed nyaa/dmhy feeds now reads
"tmdb" (the parser type) instead of the site name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JhTduCYBGeHETU7dSWoh37
2026-07-07 12:42:35 +02:00
Estrella Pan
1b928ee326 feat(rss): setting to disable orphan-torrent tracking
New bangumi_manage.track_orphans toggle (default on = current
behavior). When off, refresh_rss no longer persists unmatched torrents
(bangumi_id NULL orphan records): they are re-matched in memory each
tick (cheap, no network — the analyser dedups via bangumi rules, not
torrent records), and a rule added later can retroactively pick up
episodes still in the feed, which persisted orphans used to block
forever. Season-collect flows still persist their NULL records for
feed dedup and are not gated.

WebUI: "Track Unmatched Torrents" switch in Settings -> Manage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 12:15:06 +02:00
Estrella Pan
f2875d9870 chore: release 3.3.0-beta.8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
3.3.0-beta.8
2026-07-07 11:38:00 +02:00
Estrella Pan
3caefb5705 Merge feat/torrent-management: bangumi/orphan torrent management (#1051)
3.3 port of #1020 (orphan-torrent DB methods, 7 API endpoints, torrent
management pages), rebased onto 3.3-dev and reworked to the Soft Ink
ab-* component system per code review. Merged locally because the
rebased branch could not be force-pushed to the PR head.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 11:34:47 +02:00
Estrella Pan
55105d4841 chore(webui): regenerate auto-imports after #1056 merge
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 11:34:47 +02:00
Estrella Pan
b887f1833c fix(log): keep log history across restarts and rotation
The log page intermittently showed nothing because the file lifecycle
destroyed history at both ends:

- setup_logger(reset=True) deleted log.txt on every process start
  (container restart, online update), wiping history exactly when the
  user wants to inspect what happened before the restart. Boot now
  rotates the previous run into log.txt.1-3 via the stdlib
  RotatingFileHandler.doRollover (delay=True, rename-only), matching
  runtime rotation naming; failures warn instead of aborting boot.
- At rotation, the UI collapsed to a near-empty log.txt: both readers
  (GET /log and the SSE log event) only read log.txt. The tail read
  now stitches log.txt.1 in front when the current file fits inside
  the 512 KB budget — and never when the read was truncated, which
  would fake a time-jump above a silent gap. A file renamed mid-read
  (rollover race) reads as empty instead of raising, which previously
  could kill the whole SSE stream.
- Rotation cap tightened to 2 MB x 3 backups (~8 MB total); the UI
  renders at most 512 KB so nothing visible is lost.
- POST /log/clear also removes rotated backups (stitching would
  resurrect them) and runs its file I/O in a thread like get_log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 11:32:53 +02:00
Estrella Pan
d91686486b refactor(webui): rework torrent-management UI to Soft Ink ab-* layer
Post-rebase rework addressing review findings on #1051 (the branch
predated the component-system migration):

- Replace retired ab-popup confirm with the useConfirm() promise hook
- Replace NButton and bare <button> with ab-button / ab-icon-button;
  badge pills with ab-tag / ab-badge marks; empty state with ab-empty;
  drop hardcoded colors for theme variables
- Make the per-bangumi page's route param reactive with a NaN guard,
  and remount the list on param change
- Add a "View torrents" entry point in the rule edit dialog
- Reload the torrent list in finally so partial batch-delete failures
  don't leave stale rows
- Align the orphan card title across locales; regenerate dts files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 10:58:58 +02:00
Estrella Pan
7c1a7938b1 fix(types): annotate CursorResult.rowcount for mypy in delete_orphans
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-07 10:48:07 +02:00
Estrella Pan
679bd2621c feat: manage bangumi torrents and orphan torrents (port of #1020)
Port zhushanwen321's torrent-management feature (#1020) from the 3.2 sync
architecture to the 3.3 async design.

Backend:
- TorrentDatabase: search_orphans / count_orphans / delete_orphans / delete_obj
- bangumi API: list/delete endpoints for per-bangumi torrents and orphan
  torrents (bangumi_id IS NULL); literal /torrents/orphans routes registered
  before /{bangumi_id}/torrents

WebUI:
- Others card on the bangumi list with orphan count badge
- Torrent list pages (per-bangumi and orphans) with multi-select,
  batch delete, and clear-all confirm dialog

The PR's mikan_parser and rss/analyser fixes are not ported: 3.3-dev
already handles failed poster downloads and missing elements.

Co-authored-by: zhushanwen321 <zhushanwen321@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-07 10:48:07 +02:00
Estrella Pan
a9839e0725 Merge pull request #1056 from EstrellaXD/feat/nexusphp-pt-provider
feat(webui): NexusPHP PT-site preset for search providers
2026-07-07 10:45:52 +02:00
Estrella Pan
fd2dccac4d Merge pull request #1055 from EstrellaXD/perf/slim-docker-image
perf(docker): shrink image 208MB -> 160MB
2026-07-07 10:44:51 +02:00
Estrella Pan
77e50484f3 fix(webui): correct NexusPHP category format and validate input
Review findings on #1056, verified against upstream NexusPHP source:

- torrentrss.php only recognizes per-category flags (cat402=1); the
  bare cat=402 form was silently ignored. Emit cat<ID>=1 and accept
  comma-separated IDs.
- Validate that category IDs are numeric, with a warning and Apply
  gating in the dialog.
- Stock NexusPHP hardcodes $searchstr = null (search unsupported), so
  the template only filters on sites/forks that kept search support —
  the hint now tells users to verify this on their site instead of
  implying it always works.
- Mask the passkey in the dialog's URL preview (stored URL unchanged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 10:42:53 +02:00
Estrella Pan
afcfc0d605 fix(webui): submit login on Enter and auto-prompt passkey
- The login button was type="button", so the form had no submit button
  and Enter in the password field did nothing (no implicit submission
  with two inputs). The button is now the form's submit button and the
  handler guards re-entry.
- Auto-trigger the passkey prompt on the login page for browsers that
  have successfully used a passkey before (hasPasskey localStorage
  flag, set on passkey login/registration). A cancelled or unavailable
  silent ceremony (DOMException) disarms the flag so a stale entry
  can't doom every visit; API errors keep it (interceptor already
  toasts). An explicit logout suppresses the next auto-prompt via
  sessionStorage so a reflexive Touch ID confirm can't log straight
  back in.
- Extract the shared withSetup test helper instead of a third copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 10:22:37 +02:00
Estrella Pan
028a0420d5 fix(downloader): qBittorrent 5.x WebAPI compatibility
Verified against qB source at release tags 4.6.7/5.0.0/5.1.0/5.2.0:

- torrents/add: accept qB 5.2 JSON response (200/202) — partial success
  and pending URL fetches count as ADDED, matching legacy "Ok."
  semantics; 409 Conflict is DUPLICATE for synchronous file uploads and
  hash-confirmed magnets, otherwise raised for retry. Send both paused
  and stopped form params (renamed in 5.0, each era ignores the other).
- torrents/pause|resume: renamed to stop|start in qB 5.0 with no
  aliases; try the modern name first, fall back on 404, remember which
  era worked.
- torrents/info: filter=paused was renamed and unknown filter values
  silently mean All — filter paused/stopped client-side by state.
- torrents/delete, torrents/renameFile: treat any 2xx as success
  (qB 5.2 returns 204 for empty-body responses).
- webui: map qB 5.0+ stoppedDL/stoppedUP states to the paused label.
- .gitignore: exclude local runtime data (backend/data), agent
  workspace (.claude), and task notes (tasks).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pz1EmcTPzjgEpkGQWzb8AQ
2026-07-07 10:22:27 +02:00
Estrella Pan
378acc90cb feat(webui): NexusPHP PT-site preset for search providers
Add PT (private-tracker) support via the existing RSS-based search
pipeline. The Add Search Provider dialog gains a "PT Site (NexusPHP)"
mode that builds a torrentrss.php search template from base URL +
passkey + optional category ID (e.g. Audiences.me anime = 402), so
search -> subscribe -> auto-download -> rename works end-to-end on
NexusPHP trackers with no backend changes.

- webui/src/utils/nexusphp.ts: URL-template builder (vitest-covered)
- config-search-provider.vue: segmented custom/NexusPHP add dialog
  with live URL preview; en + zh-CN strings
- backend test_nexusphp_rss.py: regression tests proving NexusPHP
  torrentrss output (enclosure and linktype=dl variants) parses
  through the generic rss_parser path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JhTduCYBGeHETU7dSWoh37
2026-07-07 10:19:26 +02:00
Estrella Pan
c1a0cf8559 perf(docker): shrink image 208MB -> 160MB
Two changes, verified with local builds:

- Drop UV_COMPILE_BYTECODE=1: shipped .pyc files were 51MB. The venv is
  root-owned and the app runs long-lived as 'ab', so bytecode only saved
  a few seconds of import time once per container start. This also
  matches the boot_overlay uv-sync path, which never compiled bytecode.
- mcp[cli] -> mcp: the app only imports mcp.server; the cli extra pulled
  typer/rich/pygments/markdown-it (~14MB) for an unused console script.
  python-dotenv, the extra's other member, is already a direct dep.

Compressed pull size drops 64.2MB -> 48.8MB. Image verified: packages
absent, no __pycache__, 'import main' succeeds; pytest and mypy green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEnmRvBJL93fAURbRf1koK
2026-07-07 10:13:51 +02:00
Estrella Pan
c19da52497 chore: release 3.3.0-beta.7
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
3.3.0-beta.7
2026-07-06 16:38:20 +02:00
Estrella Pan
f14ab65574 fix: address stable-review findings across config, update, downloader
Ship-readiness review of main...3.3-dev (multi-agent + codex verify pass)
surfaced 10 confirmed defects; all fixed with red-first tests (+24):

- config: masked list secrets restored by identity match, not index —
  deleting/reordering a notification provider no longer corrupts the
  survivor's credentials; unresolvable masks return 400 instead of guessing
- statics: mount dist/fonts — self-hosted Inter 404'd in every production
  build and the whole UI fell back to OS fonts
- boot_overlay: chown replaced trees to the app user (crash-loop under
  UMASK=077); snapshot-and-restore around the EXDEV in-place fallback so a
  mid-copy failure can't gut /app/module
- updater: rollback refuses to swap without the retained signed backup
  bundle — reverts to image version honestly instead of half-swapping
- qbittorrent: credential-failure latch (cleared on settings save) plus
  single-flighted auth sharing the negative result with queued waiters —
  failed logins no longer accumulate into a WebUI IP ban
- download_client: holder refcount registered before auth (no mid-login
  close), stale clients accumulated in a list (no pool leak on rapid
  settings saves), holder released on cancellation during __aenter__
- entrypoint: ownership marker written only after chown -R succeeds
- aria2: torrents_info provides num_seeds/num_leechs/eta/added_on and maps
  states to qB vocabulary (UI showed undefined / NaNhNaNm / raw states)
- program API: deprecated GET aliases for /restart /start /stop /shutdown
  (3.2 automation compatibility)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-06 16:37:28 +02:00
Estrella Pan
f751d1c6e7 chore: release 3.3.0-beta.6
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
3.3.0-beta.6
2026-07-06 12:43:25 +02:00
Estrella Pan
1589186c14 Merge feat/webui-soft-ink: Soft Ink component-system redesign
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-06 12:43:02 +02:00
Estrella Pan
6c6425d033 fix(webui): address code-review findings before beta
- Nest ab-edit-rule's delete confirmation inside the main ab-modal so
  headlessui suspends the outer dialog's Escape/outside-click while the
  inner one is open (clicking the delete-files checkbox no longer
  closes both layers); regression-tested with a nested-modal vitest.
- Restore autocomplete passthrough (ab-input prop + ab-setting wiring)
  so downloader/proxy password fields keep autocomplete=off.
- Guarantee 44px touch targets for ab-button sm on touch; repoint or
  remove the dead .n-button deep selectors in wizard/config/downloader/
  log left over from the sweep.
- a11y labels: modal X and search closes announce 'Close', tag remove
  announces 'Remove' (new common.close/common.remove i18n pairs);
  ab-field renders label[for] only when the control adopts the id,
  span + aria-labelledby otherwise.
- Convert the last NPopconfirms (passkey/search-provider/update-card)
  to useConfirm with danger icon buttons; drop legacy
  .notification-action CSS that fought ab-button variants.
- Fix missing homepage.rule.offset key (→ episode_offset), normalize
  test names to the should-convention, run prettier over the branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-06 12:38:23 +02:00
Estrella Pan
3756dc7e8a fix(webui): live-review UX round — status ring, select widths, modal footers
- ab-status: back to the ring form (thin border circle, lamp inside)
  per review; keeps the 4-state API and pulse.
- Select widths: drop the competing 140px floors in ab-setting and
  ab-select; ab-field's 200px desktop floor now applies, so config
  dropdowns stop collapsing to arrow-width.
- Modal actions standardized into ab-modal's footer slot (right-
  aligned, consistent padding): search-provider, notification,
  passkey dialogs; cancel buttons demoted from danger to secondary.
- ab-add-rss and ab-edit-rule converted from hand-rolled dialogs to
  ab-modal (focus trap, unified header/footer; stray divider above
  the add button gone); edit-rule's inner delete dialog is a nested
  ab-modal, enable-rule dialog gains a proper footer.
- ab-modal: showClose prop + bare mode — bodyless confirms render
  compact (no empty body strip, no doubled hairlines, no X); the
  notification provider row uses ab-icon-button with a red remove.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-06 11:57:26 +02:00
Estrella Pan
e309301dd6 refactor(webui): complete ab-* migration, retire legacy components
Migrate the remaining surfaces: all seven ab-popup call sites to
ab-modal (footer slots, sm sizing), every ab-label to ab-field, all
raw ab-input-shortcut inputs to the ab-input component (textareas get
local Soft Ink styles), rss mobile list to ab-list with loading
skeleton, and de-tint the hand-rolled warning callouts (hairline +
ink, per the no-tinted-boxes rule). Wire aria-live into pull-refresh.

Delete dead code: ab-popup, ab-adaptive-modal, ab-label, ab-data-list,
ab-add, ab-button-multi, ab-image, ab-rule, ab-swipe-container (+
orphaned stories); drop the ab-input/input-error UnoCSS shortcuts.
Add stories for the full universal set and document the final
22-component catalog in DESIGN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-06 11:38:59 +02:00
Estrella Pan
ba41059a6b refactor(webui): migrate pages and composites to the ab-* layer
Sweep every NButton and styled bare button to ab-button /
ab-icon-button (variant map: primary→primary, secondary→secondary,
error→danger with hover escalation); route the five NPopconfirm sites
and the config leave-guard through useConfirm(); migrate ab-tag call
sites to semantic types (primary→info, active→success, warn→danger,
inactive→neutral); wire ab-badge into the notification bell and
ab-progress into the downloader table; first two ab-popup call sites
moved to ab-modal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-06 11:15:29 +02:00
Estrella Pan
6e5cb2ca07 feat(webui): Soft Ink universal component set
Overlays: ab-modal (adaptive dialog/sheet on headlessui, focus trap,
footer slot), promise-based useConfirm + host, ab-tooltip. Forms:
ab-field (label/description/error/aria wiring), ab-input component
(affixes, clearable, password reveal), ab-select/ab-switch wrappers,
ab-segmented underline tabs; ab-setting rebuilt on them with new
description/error/required props. Display: ab-tag v2 (semantic marks,
ink text, no truncation), ab-status v2 (4 states + label/detail),
ab-badge, ab-progress (scaleX fill), ab-container footer slot,
accessible animated ab-fold-panel. Feedback: ab-alert (hairline, bold
lead word), inverted-ink toast theme, ab-empty, ab-skeleton, ab-list
(selection, loading, density), ab-toolbar. All with vitest coverage
and Storybook stories; bottom sheet gains footer slot and drops
hard-coded overlay/z-index.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-06 11:15:16 +02:00
Estrella Pan
4051bea609 feat(webui): add ab-button, ab-icon-button, ab-menu, ab-split-button
Soft Ink action components with tests and stories: single button
vocabulary (primary/secondary/ghost/danger with hover-escalating
danger), 44px touch targets, focus-visible rings, keyboard-navigable
menu on headlessui, controlled split button replacing ab-button-multi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-06 10:41:33 +02:00
Estrella Pan
1a3bb4558d feat(webui): Soft Ink foundation — tokens, naive-ui theme, input shortcut
Add --color-surface-2 (filled controls), --color-primary-alpha (focus
rings), --font-mono (data text); shift control radius to 4px. Theme
naive-ui Input/Select as borderless filled controls with purple focus.
Restyle the ab-input shortcut to Soft Ink (left-aligned, filled) so
un-migrated call sites shift immediately. Document the Soft Ink rules
and anti-patterns in DESIGN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-06 10:36:44 +02:00
Estrella Pan
c96a5dbbeb fix(types): satisfy mypy on the new notification/LLM code
CI runs `mypy src` (not part of the local pytest/ruff/black gate); the
feature commit tripped 22 type errors. No behaviour change:

- events.py: rename the SSE notification payload local so it no longer
  collides with the str-typed `payload` earlier in the generator.
- inbox.py: type: ignore on the `update().where(read == False)` DML
  predicate and the CursorResult.rowcount reads.
- tests: narrow Optional returns (a typed `_upsert` helper for
  InboxDatabase, `assert ... is not None` for credential loads) and
  annotate a bare dict.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-06 09:55:31 +02:00
Estrella Pan
e6d39e5624 feat: in-app notification center and LLM provider plugin framework
Two 3.3 platform features sharing core infra (notification events, DB
migrations/wiring, config model, API registration), committed together
because they cannot be split at file granularity without broken
intermediate builds.

Notification center:
- New `inboxmessage` table (migration 16) + repo with coalescing
  (unread rows with the same dedup_key increment a count; `once` kinds
  like update_available notify per-version) and 500-row pruning.
- SystemEvent contract (kind/severity/dedup_key/payload) with new
  DownloaderUnavailable / UpdateAvailable / UpdateApplied events;
  NotificationManager.send_event persists to the inbox first, then
  broadcasts to external providers only when notification.enable.
- SSE `notification` frames driven by an in-process revision counter;
  REST endpoints for list/unread-count/read/read-all/delete/clear.
- Wire-ups: qBittorrent auth now reports last_auth_error
  (credentials|banned|unreachable) and stops retrying rejected
  credentials to avoid a WebUI IP ban; the startup wait loop aborts
  early on credential errors and emits a notification; a 24h
  update-check tick surfaces new versions; apply/rollback emit results.
- WebUI bell + unread badge + dropdown inbox (severity, coalesce count,
  relative time, deep links), SSE-driven with a 30s poll fallback.

LLM provider plugin framework + subscription auth:
- LLMParser refactored to a thin facade over a provider adapter
  contract + registry; the three built-ins (openai/anthropic/gemini)
  move to adapters with zero behaviour change (existing tests unchanged).
- base_url presets for zai/minimax/deepseek/kimi (mainland + overseas)
  reusing the OpenAI-compatible adapter, with a json_object structured-
  output fallback for providers without json_schema.
- Downloadable signed plugins: catalog + installer clone the update
  system's ed25519 verify / zip-slip-guarded extract pipeline (unsigned
  refused, catalog signed against downgrade); versioned-module-name
  loader hot-loads without restart.
- Subscription auth: `llmcredential` table (migration 17) stores OAuth
  tokens server-side only (never sent to the frontend); device-flow
  auth runs a server-side polling task keyed by an opaque handle so the
  device_code stays off the wire; AuthExpiredError trips the breaker and
  notifies. Two grey-area device-flow plugins (github-copilot,
  codex-chatgpt) under plugins/, flagged at-your-own-risk.
- WebUI: grouped provider select (built-in/preset/subscription/
  downloadable), install with risk confirmation, device-flow connect
  dialog, connected-account chip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-06 09:43:45 +02:00
Estrella Pan
2cdf146f91 fix(bangumi): inherit year for same-title entries across subtitle groups
When a new bangumi entry is created for an already-known show (same
official_title, different group/quality so no semantic merge), it now
inherits the existing entry's year. The save path is built as
`title (year)/Season N`, so a Mikan-parsed entry (which never fills
year) previously diverged into a separate library folder from a
TMDB-parsed sibling that had one. `_inherit_year` in add()/add_all()
copies the year from any non-deleted same-title row when the incoming
entry has none; a parsed year is never overwritten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
2026-07-06 09:42:58 +02:00
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