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
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
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
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
- 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
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
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
- 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
- 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
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
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
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
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
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
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
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
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
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
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
- 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
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
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
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
- 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
- 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
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
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
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
- 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
- CSRF: reset/all, enable/disable, log/clear, program start/stop/restart/shutdown, rss refresh are now POST; frontend api calls updated to match
- Batch delete/disable: type list[int], guard empty input, aggregate response, register /many before /{id}
- auto_tag_torrents: load bangumi once and match in memory (was N+1 per torrent)
- Type bangumi_id path params as int; correct 406->404/400; drop false SSE response_model; remove dead imports/handlers
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014w1Z6Nxy6XTRgkFXqPr9Zh
Prevent memory leaks by ensuring the search EventSource connection is
closed when the modal unmounts and setTimeout handles are cleared in
copy-to-clipboard flows across modal components.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Allow users to drag bangumi cards from the "Unknown" section into weekday
columns in the calendar view. Manual assignments are locked so calendar
refresh from Bangumi.tv doesn't overwrite them. A reset button lets users
unlock and send cards back to Unknown.
Backend:
- Add weekday_locked field to Bangumi model (migration v9)
- Add PATCH /api/v1/bangumi/{id}/weekday endpoint
- Skip locked items in refresh_calendar()
Frontend:
- Add vuedraggable for smooth drag-and-drop
- Pin indicator and unpin button on manually-assigned cards
- Drop zone highlighting during drag
- i18n strings for drag/pin/unpin
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- useAuth: replace watcher with explicit router.replace on login/logout
- useMyI18n: create single i18n instance at module level (avoid dupes)
- usePasskey: detect WebAuthn support synchronously (no onMounted)
- main.ts: import i18n from hook module instead of calling composable
- ab-add-rss: hoist useApi composables outside functions to avoid
recreating them on each call
- calendar: prevent duplicate refreshes when already refreshing
- downloader: guard interval polling against stale activation state
- router: only mark setupChecked on successful status check
- log store: stop SSE log updates on logout
- i18n: add missing "edit" translation key (en + zh-CN)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace non-existent ab-dialog with ab-popup for proper modal behavior,
use ab-button/ab-label components and consistent spacing/styles matching
other settings panels (search-provider, proxy).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add NotificationProvider base class with send() and test() methods
- Add NotificationManager for handling multiple providers simultaneously
- Add new providers: Discord, Gotify, Pushover, generic Webhook
- Migrate existing providers (Telegram, Bark, Server Chan, WeChat Work) to new architecture
- Add API endpoints for testing providers (/notification/test, /notification/test-config)
- Auto-migrate legacy single-provider configs to new multi-provider format
- Update WebUI with card-based multi-provider settings UI
- Add test button for each provider in settings
- Generic webhook supports template variables: {{title}}, {{season}}, {{episode}}, {{poster_url}}
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix qBittorrent rename verification (verify file actually renamed)
- Add pending rename cooldown to prevent spam when rename delayed
- Add torrent tagging API for accurate offset lookup
- Add auto calendar refresh every 24 hours
- Fix frontend error handling (don't logout on server errors)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add filter section with group, resolution, subtitle, season categories
- Implement multi-select filters with smart disabling for incompatible options
- Display result variant tags as non-clickable colored pills
- Unify tag styling (pill shape, 12px font) across filters and results
- Add expand/collapse for filter categories and variant overflow
- Normalize tag values (resolution: FHD/HD/4K, subtitle: 简/繁/双语)
- Calculate poster height to match 4 rows of variant chips (168px)
- Add click-outside-to-close for modal
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Standardize form element dimensions for visual consistency:
- Input height: 48px → 44px (matches button height)
- Input font: 16px → 14px (consistent with form elements)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The media library path is now taken directly from the downloader path
field, eliminating the unnecessary separate step.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
URLs entered without http:// or https:// were being treated as relative
paths by the browser. Added normalizeUrl() function that prepends http://
when no protocol is present.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add common.selectAll and common.items translations
- Add theme.light and theme.dark translations
- Update ab-mobile-nav to use i18n for theme toggle label
- Remove hardcoded fallback in ab-data-list
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use RSS type instead of non-existent RSSItem/RSSResponse
- Add expire field to mockLoginSuccess
- Replace offset with episode_offset/season_offset in mockBangumiAPI
- Add needs_review_reason field to mockBangumiAPI
- Add missing RSS fields (connection_status, last_checked_at, last_error)
- Fix generic types in test utilities
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Backend:
- Add API test files for auth, program, downloader, config, log, bangumi extended, search, and passkey endpoints
- Update conftest.py with new fixtures (app, authed_client, unauthed_client, mock_program, mock_webauthn, mock_download_client)
- Update factories.py with make_config and make_passkey functions
Frontend:
- Setup vitest testing infrastructure with happy-dom environment
- Add test setup file with mocks for axios, router, i18n, localStorage
- Add mock API data for testing
- Add tests for API logic, store logic, hooks, and basic components
- Add @vue/test-utils and happy-dom dev dependencies
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>