feat(rss): add connection status tracking and display

Track RSS feed reachability during refresh cycles. Each feed now stores
connection_status (healthy/error), last_checked_at, and last_error.
The RSS management page shows a green "Connected" tag for healthy feeds
and a red "Error" tag with tooltip for failed feeds.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Estrella Pan
2026-01-24 21:45:04 +01:00
parent cba4988e52
commit c5f4919e15
8 changed files with 80 additions and 5 deletions

View File

@@ -2,6 +2,11 @@
## Backend
### Features
- RSS 订阅源新增连接状态追踪:每次刷新后记录 `connection_status`healthy/error`last_checked_at``last_error`
- 新增数据库迁移 v2`rssitem` 表添加连接状态相关字段
### Performance
- 新增共享 HTTP 客户端连接池,复用 TCP/SSL 连接,减少每次请求的握手开销
@@ -24,6 +29,10 @@
## Frontend
### Features
- RSS 管理页面新增连接状态标签:健康时显示绿色「已连接」,错误时显示红色「错误」并通过 tooltip 显示错误详情
### Performance
- 下载器 store 使用 `shallowRef` 替代 `ref`,避免大数组的深层响应式代理

View File

@@ -15,7 +15,7 @@ from .user import UserDatabase
logger = logging.getLogger(__name__)
# Increment this when adding new migrations to MIGRATIONS list.
CURRENT_SCHEMA_VERSION = 1
CURRENT_SCHEMA_VERSION = 2
# Each migration is a tuple of (version, description, list of SQL statements).
# Migrations are applied in order. A migration at index i brings the schema
@@ -26,6 +26,15 @@ MIGRATIONS = [
"add air_weekday column to bangumi",
["ALTER TABLE bangumi ADD COLUMN air_weekday INTEGER"],
),
(
2,
"add connection status columns to rssitem",
[
"ALTER TABLE rssitem ADD COLUMN connection_status TEXT",
"ALTER TABLE rssitem ADD COLUMN last_checked_at TEXT",
"ALTER TABLE rssitem ADD COLUMN last_error TEXT",
],
),
]
@@ -88,6 +97,10 @@ class Database(Session):
columns = [col["name"] for col in inspector.get_columns("bangumi")]
if "air_weekday" in columns:
needs_run = False
if "rssitem" in tables and version == 2:
columns = [col["name"] for col in inspector.get_columns("rssitem")]
if "connection_status" in columns:
needs_run = False
if needs_run:
with self.engine.connect() as conn:
for stmt in statements:

View File

@@ -10,6 +10,9 @@ class RSSItem(SQLModel, table=True):
aggregate: bool = Field(False, alias="aggregate")
parser: str = Field("mikan", alias="parser")
enabled: bool = Field(True, alias="enabled")
connection_status: Optional[str] = Field(None, alias="connection_status")
last_checked_at: Optional[str] = Field(None, alias="last_checked_at")
last_error: Optional[str] = Field(None, alias="last_error")
class RSSUpdate(SQLModel):

View File

@@ -1,6 +1,7 @@
import asyncio
import logging
import re
from datetime import datetime, timezone
from typing import Optional
from module.database import Database, engine
@@ -98,6 +99,16 @@ class RSSEngine(Database):
new_torrents = self.torrent.check_new(torrents)
return new_torrents
async def _pull_rss_with_status(
self, rss_item: RSSItem
) -> tuple[list[Torrent], Optional[str]]:
try:
torrents = await self.pull_rss(rss_item)
return torrents, None
except Exception as e:
logger.warning(f"[Engine] Failed to fetch RSS {rss_item.name}: {e}")
return [], str(e)
_filter_cache: dict[str, re.Pattern] = {}
def _get_filter_pattern(self, filter_str: str) -> re.Pattern:
@@ -127,11 +138,17 @@ class RSSEngine(Database):
rss_items = [rss_item] if rss_item else []
# From RSS Items, fetch all torrents concurrently
logger.debug(f"[Engine] Get {len(rss_items)} RSS items")
all_torrents = await asyncio.gather(
*[self.pull_rss(rss_item) for rss_item in rss_items]
results = await asyncio.gather(
*[self._pull_rss_with_status(rss_item) for rss_item in rss_items]
)
now = datetime.now(timezone.utc).isoformat()
# Process results sequentially (DB operations)
for rss_item, new_torrents in zip(rss_items, all_torrents):
for rss_item, (new_torrents, error) in zip(rss_items, results):
# Update connection status
rss_item.connection_status = "error" if error else "healthy"
rss_item.last_checked_at = now
rss_item.last_error = error
self.add(rss_item)
for torrent in new_torrents:
matched_data = self.match_torrent(torrent)
if matched_data:
@@ -140,6 +157,7 @@ class RSSEngine(Database):
torrent.downloaded = True
# Add all torrents to database
self.torrent.add_all(new_torrents)
self.commit()
async def download_bangumi(self, bangumi: Bangumi):
async with RequestContent() as req:

View File

@@ -200,9 +200,11 @@
}
},
"rss": {
"connected": "Connected",
"delete": "Delete",
"disable": "Disable",
"enable": "Enable",
"error": "Error",
"name": "Name",
"selectbox": "Select",
"status": "Status",

View File

@@ -200,9 +200,11 @@
}
},
"rss": {
"connected": "已连接",
"delete": "删除",
"disable": "禁用",
"enable": "启用",
"error": "错误",
"name": "名称",
"selectbox": "选择",
"status": "状态",

View File

@@ -1,5 +1,5 @@
<script lang="tsx" setup>
import { NDataTable, type DataTableColumns } from 'naive-ui';
import { NDataTable, NTooltip, type DataTableColumns } from 'naive-ui';
import type { RSS } from '#/rss';
definePage({
@@ -49,6 +49,17 @@ const rssColumns = computed<DataTableColumns<RSS>>(() => [
<div flex="~ justify-end gap-x-8">
{rss.parser && <ab-tag type="primary" title={rss.parser} />}
{rss.aggregate && <ab-tag type="primary" title="aggregate" />}
{rss.connection_status === 'healthy' && (
<ab-tag type="active" title={t('rss.connected')} />
)}
{rss.connection_status === 'error' && (
<NTooltip>
{{
trigger: () => <ab-tag type="warn" title={t('rss.error')} />,
default: () => rss.last_error || 'Unknown error',
}}
</NTooltip>
)}
{rss.enabled ? (
<ab-tag type="active" title="active" />
) : (
@@ -85,6 +96,17 @@ const rssRowKey = (row: RSS) => row.id;
<div class="rss-card-tags">
<ab-tag v-if="item.parser" type="primary" :title="item.parser" />
<ab-tag v-if="item.aggregate" type="primary" title="aggregate" />
<ab-tag
v-if="item.connection_status === 'healthy'"
type="active"
:title="$t('rss.connected')"
/>
<NTooltip v-if="item.connection_status === 'error'">
<template #trigger>
<ab-tag type="warn" :title="$t('rss.error')" />
</template>
{{ item.last_error || 'Unknown error' }}
</NTooltip>
<ab-tag
:type="item.enabled ? 'active' : 'inactive'"
:title="item.enabled ? 'active' : 'inactive'"

View File

@@ -5,6 +5,9 @@ export interface RSS {
aggregate: boolean;
parser: string;
enabled: boolean;
connection_status: string | null;
last_checked_at: string | null;
last_error: string | null;
}
export const rssTemplate: RSS = {
@@ -14,4 +17,7 @@ export const rssTemplate: RSS = {
aggregate: false,
parser: '',
enabled: false,
connection_status: null,
last_checked_at: null,
last_error: null,
};