3.2.4#989
Conversation
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>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This pull request implements two major features for release 3.2.4: configurable security controls and calendar drag-and-drop functionality for manual bangumi scheduling.
Changes:
- Adds a comprehensive
Securityconfiguration model supporting IP whitelists (login and MCP) and bearer token authentication for both login and MCP endpoints - Implements calendar drag-and-drop to manually assign bangumi to weekdays with a locking mechanism to prevent calendar refresh from overwriting manual assignments
- Refactors authentication flow to support three authentication methods: DEV bypass, bearer tokens, and JWT cookies
Reviewed changes
Copilot reviewed 36 out of 40 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| backend/src/module/models/config.py | Adds Security model with login/MCP whitelists and tokens; refactors env var expansion |
| backend/src/module/mcp/security.py | Renames LocalNetworkMiddleware to McpAccessMiddleware; adds configurable CIDR whitelist and bearer token auth |
| backend/src/module/security/api.py | Adds check_login_ip dependency and bearer token bypass in get_current_user |
| backend/src/module/api/auth.py | Refactors JWT issuance into _issue_token helper; adds login IP check dependency |
| backend/src/module/api/config.py | Fixes sanitization to only mask string values, preventing integer masking |
| backend/src/module/api/bangumi.py | Adds PATCH /bangumi/{id}/weekday endpoint for manual weekday assignment |
| backend/src/module/database/bangumi.py | Adds set_weekday method to handle weekday locking |
| backend/src/module/database/combine.py | Adds migration v9 for weekday_locked column |
| backend/src/module/models/bangumi.py | Adds weekday_locked field to Bangumi and BangumiUpdate models |
| backend/src/module/manager/torrent.py | Skips locked bangumi in calendar refresh |
| backend/src/module/conf/const.py | Adds security defaults with private network MCP whitelist |
| backend/src/module/conf/config.py | Adds security section migration in _migrate_old_config |
| backend/src/test/* | Adds 101 new tests covering security, auth, config, and downloader modules |
| webui/types/config.ts | Adds Security interface; removes deprecated RssParser fields |
| webui/types/bangumi.ts | Adds weekday_locked to BangumiRule type |
| webui/src/pages/index/calendar.vue | Implements drag-and-drop with vuedraggable; adds pin/unpin UI |
| webui/src/components/setting/config-security.vue | New component for security settings configuration |
| webui/src/store/bangumi.ts | Adds setWeekday action |
| webui/src/api/bangumi.ts | Adds setWeekday API client method |
| webui/src/i18n/*.json | Adds translations for drag-and-drop and security features |
| webui/package.json | Adds vuedraggable dependency |
| backend/pyproject.toml | Bumps version to 3.2.4 |
| CHANGELOG.md | Documents all changes |
Files not reviewed (1)
- webui/pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @router.patch( | ||
| path="/{bangumi_id}/weekday", | ||
| response_model=APIResponse, | ||
| dependencies=[Depends(get_current_user)], | ||
| ) | ||
| async def set_weekday(bangumi_id: int, request: SetWeekdayRequest): | ||
| """Manually set the broadcast weekday for a bangumi.""" | ||
| if request.weekday is not None and not (0 <= request.weekday <= 6): | ||
| return JSONResponse( | ||
| status_code=400, | ||
| content={ | ||
| "status": False, | ||
| "msg_en": "Weekday must be 0-6 (Mon-Sun) or null.", | ||
| "msg_zh": "星期必须是 0-6(周一至周日)或空。", | ||
| }, | ||
| ) | ||
| with Database() as db: | ||
| success = db.bangumi.set_weekday(bangumi_id, request.weekday) | ||
| if success: | ||
| action = f"weekday {request.weekday}" if request.weekday is not None else "unknown" | ||
| return JSONResponse( | ||
| status_code=200, | ||
| content={ | ||
| "status": True, | ||
| "msg_en": f"Set bangumi to {action}.", | ||
| "msg_zh": f"已设置放送日为 {action}。", | ||
| }, | ||
| ) | ||
| return JSONResponse( | ||
| status_code=404, | ||
| content={ | ||
| "status": False, | ||
| "msg_en": f"Bangumi {bangumi_id} not found.", | ||
| "msg_zh": f"未找到番剧 {bangumi_id}。", | ||
| }, | ||
| ) |
There was a problem hiding this comment.
The new set_weekday API endpoint (PATCH /bangumi/{id}/weekday) lacks automated test coverage. While 101 new tests were added to this PR, none specifically test this new endpoint's behavior including validation of weekday range (0-6), null handling, success/failure responses, and database state updates. Adding test coverage would help ensure this critical user-facing feature works correctly.
| if isinstance(v, dict): | ||
| result[k] = _sanitize_dict(v) | ||
| elif any(s in k.lower() for s in _SENSITIVE_KEYS): | ||
| elif isinstance(v, str) and any(s in k.lower() for s in _SENSITIVE_KEYS): |
There was a problem hiding this comment.
The sanitization function now only masks string values, which is correct. However, there's a potential issue: the function checks if the value v is a string, but if a sensitive key has a non-string value (like a list of tokens), those values won't be masked. For the Security model, login_tokens and mcp_tokens are lists of strings, not strings themselves, so they will not be sanitized and will be exposed in the API response.
| async function onDropToDay(dayIndex: number, evt: any) { | ||
| if (evt.added) { | ||
| const group: BangumiGroup = evt.added.element; | ||
| for (const rule of group.rules) { | ||
| await setWeekday(rule.id, dayIndex); | ||
| } | ||
| } |
There was a problem hiding this comment.
The onDropToDay function uses sequential await calls in a for loop, which can be slow when a group contains multiple rules. Consider using Promise.all() to set the weekday for all rules concurrently, improving performance.
| <button | ||
| v-if="group.primary.weekday_locked" | ||
| class="calendar-unpin-btn" | ||
| :title="$t('calendar.unpin')" | ||
| @click="onUnpin(group, $event)" | ||
| > | ||
| × | ||
| </button> |
There was a problem hiding this comment.
The unpin button lacks keyboard accessibility. It only has a click handler but no keyboard event handlers (like @keydown.enter or @keydown.space). Users relying on keyboard navigation won't be able to trigger the unpin action. Add keyboard event handlers to make this button accessible.
| <button | ||
| v-if="group.primary.weekday_locked" | ||
| class="calendar-unpin-btn" | ||
| :title="$t('calendar.unpin')" | ||
| @click="onUnpin(group, $event)" | ||
| > | ||
| × | ||
| </button> |
There was a problem hiding this comment.
The unpin button lacks an accessible label (aria-label). While it has a title attribute for tooltips, screen readers need an explicit aria-label to announce the button's purpose to users. Add aria-label with a descriptive label like "Unpin bangumi from this day".
| async function onUnpin(group: BangumiGroup, event: Event) { | ||
| event.stopPropagation(); | ||
| for (const rule of group.rules) { | ||
| await setWeekday(rule.id, null); | ||
| } | ||
| } |
There was a problem hiding this comment.
The onUnpin function also uses sequential await calls in a for loop. Similar to onDropToDay, consider using Promise.all() to reset the weekday for all rules concurrently.
| async function setWeekday(bangumiId: number, weekday: number | null) { | ||
| await apiBangumi.setWeekday(bangumiId, weekday); | ||
| const item = bangumi.value.find((b) => b.id === bangumiId); | ||
| if (item) { | ||
| item.air_weekday = weekday; | ||
| item.weekday_locked = weekday !== null; | ||
| } | ||
| } |
There was a problem hiding this comment.
The setWeekday function doesn't handle errors from the API call. If apiBangumi.setWeekday fails (e.g., due to network error or invalid bangumi_id), the local state will not be updated but there's no error notification to the user. The drag-and-drop UI will appear to have succeeded even though the backend update failed. Consider wrapping the API call in a try-catch block and showing an error notification on failure.
| def clear_network_cache(): | ||
| """Clear the parsed network cache (call after config reload).""" | ||
| _parse_network.cache_clear() |
There was a problem hiding this comment.
The clear_network_cache function is defined but never called. After config reload (e.g., in config.py's update_config), the LRU cache should be cleared to ensure stale CIDR parsing results don't persist. Without this, changes to mcp_whitelist won't take effect until the server restarts.
Titles like "29 岁单身冒险家的日常" cause the regex to match the leading number as episode, leaving title_raw as None. This cascades into storing null aliases and crashing match_torrent with TypeError. - Fall back to title_jp when title_en and title_zh are both None - Return None from raw_parser when no title can be extracted - Reject None/empty aliases in add_title_alias - Filter null values from parsed title_aliases JSON - Skip None title_raw in get_all_title_patterns Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
10 tests covering the full bug chain: - raw_parser misparses leading number as episode - TitleParser.raw_parser returns None for unparseable titles - add_title_alias rejects None and empty string - _get_aliases_list filters null values from JSON - get_all_title_patterns skips None title_raw - match_torrent and match_list handle corrupted data Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…#910, #773) Add _fallback_parse() tried when TITLE_RE.match() returns None, using two regex patterns to extract episode numbers from formats the main regex misses: - digits before [ bracket (issues #876, #910) - compound [02(57)] format (issue #773) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
…on (#923) - Decouple HTTPS scheme selection from TLS certificate verification: `verify=False` always, since self-signed certs are the norm for home-server/NAS/Docker qBittorrent setups - Bump connect timeout from 3.1s to 5.0s for slow TLS handshakes - Add actionable error messages when HTTPS connection fails - Fix `continue` → `break` bug in torrents_rename_file verification loop - Consolidate json imports to top-level - Add 31 unit tests for QbDownloader constructor, auth, and error handling Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
torrents_rename_file重命名验证循环逻辑错误 (continue→break)Test plan
Generated with /ship