feat(server,contracts,web): dashboard library counts and engine runs triggeredBy/multi-status filters

- GET /dashboard: automation.library {entities, movies, series, seasons,
  episodes, assets} and automation.slotSatisfiedRate; home media overview
  shows the counts instead of placeholders.
- GET /engine/runs: `status` accepts comma-separated values; new
  `triggeredBy` filter (comma-separated) evaluated server-side by scanning
  batches and paging with the last inspected row's cursor (scan cap 2000).
- Task stream: source filter and the "active" tab (running,pending) are now
  one paginated server query.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 16:49:11 +08:00
1 parent 615917bb45
commit 363f85b1a1
13 files changed
+165 -250

No files matched your search

@@ -445,6 +445,12 @@ describe.skipIf(!hasPg)("acquisition-organize 纵切(真实 PG + mock qB + 临
expect(dash.body.data.recentOutput.length).toBeGreaterThan(0);
expect(dash.body.data.cache.volumes[0]).toMatchObject({ hardThresholdPercent: 90, resumePercent: 85 });
expect(dash.body.data.needsAttention.total).toBeGreaterThan(0);
const auto = dash.body.data.automation;
expect(auto.library.entities).toBeGreaterThanOrEqual(1);
expect(auto.library.movies).toBeGreaterThanOrEqual(1);
expect(auto.library.assets).toBeGreaterThanOrEqual(1);
const slotTotal = Object.values(auto.slotCounts as Record<string, number>).reduce((a, b) => a + b, 0);
expect(auto.slotSatisfiedRate).toBe(slotTotal ? auto.slotCounts.satisfied / slotTotal : null);
const libList = await h.api("GET", "/library?q=沙丘");
expect(libList.body.data.items[0]).toMatchObject({ entity: { id: movieId }, assetCount: 1 });
const det = await h.api("GET", `/library/${movieId}`);
@@ -63,6 +63,26 @@ export function registerDashboardRoutes(app: ModuleApp, d: AoDeps) {
attention: 0,
};
for (const r of slotRows) slotCounts[r.status] = r.n;
const slotTotal = Object.values(slotCounts).reduce((a, b) => a + b, 0);
const library = (
await sql<{
entities: number;
movies: number;
series: number;
seasons: number;
episodes: number;
assets: number;
}>`
select
(select count(*) from media_entity where status <> 'merged')::int as entities,
(select count(*) from media_entity where status <> 'merged' and kind = 'movie')::int as movies,
(select count(*) from media_entity where status <> 'merged' and kind in ('series','variety'))::int as series,
(select count(*) from content_unit where kind = 'season' and status <> 'retired')::int as seasons,
(select count(*) from content_unit where kind = 'episode' and status <> 'retired')::int as episodes,
(select count(*) from media_asset where status = 'active' and kind in ('video','bluray_dir'))::int as assets`.execute(
db,
)
).rows[0]!;
const dlRows = await db
.selectFrom("download_task")
@@ -203,6 +223,8 @@ export function registerDashboardRoutes(app: ModuleApp, d: AoDeps) {
},
activeSubscriptions: today.subs,
slotCounts,
slotSatisfiedRate: slotTotal ? slotCounts.satisfied / slotTotal : null,
library,
},
currentWork: {
downloading: count("downloading"),