diff --git a/src/renderer/views/statistics/StatisticsView.tsx b/src/renderer/views/statistics/StatisticsView.tsx index 50524f6..140f263 100644 --- a/src/renderer/views/statistics/StatisticsView.tsx +++ b/src/renderer/views/statistics/StatisticsView.tsx @@ -22,8 +22,9 @@ export interface StatisticsViewProps { } const rangeItems: Array<{ id: StatisticsRange; label: string }> = [ - { id: "session", label: "Sitzung" }, - { id: "today", label: "Heute" }, + { id: "session", label: "Sitzung" }, + { id: "today", label: "Heute" }, + { id: "last24", label: "Letzte 24 Stunden" }, { id: "week", label: "Sieben Tage" }, { id: "month", label: "30 Tage" }, { id: "all", label: "Gesamt" } @@ -69,6 +70,9 @@ function providerScopeLabel(scope: StatisticsProviderScope | null): string { if (scope === "today") { return "Heute"; } + if (scope === "last24") { + return "Letzte 24 Stunden"; + } if (scope === "week") { return "Sieben Tage"; } @@ -88,6 +92,9 @@ function emptyProviderMessage(model: StatisticsViewModel): string { if (model.providerScope === "today") { return "Heute wurden noch keine Providerbytes erfasst."; } + if (model.providerScope === "last24") { + return "In den vergangenen 24 Stunden wurde noch kein Account-Traffic erfasst."; + } if (model.providerScope === "week" || model.providerScope === "month") { return "In diesem Zeitraum wurden noch keine Providerwerte erfasst."; } @@ -144,7 +151,7 @@ export function StatisticsSidebarStatus({ model }: Pick value !== null); if (rows.length === 0) { return null; @@ -156,8 +163,10 @@ export function StatisticsSidebarStatus({ model }: Pick
@@ -193,14 +202,14 @@ export function StatisticsContent({ model, actions, chart }: StatisticsViewProps
{chart}
-
-
-

Provider

- {providerScopeLabel(model.providerScope)} -
-
-
- Provider +
+
+

{usageHeading}

+ {providerScopeLabel(model.providerScope)} +
+
+
+ {usageColumnHeading} Daten Ergebnisse
diff --git a/src/renderer/views/statistics/statistics-model.ts b/src/renderer/views/statistics/statistics-model.ts index 1185b06..07ade4e 100644 --- a/src/renderer/views/statistics/statistics-model.ts +++ b/src/renderer/views/statistics/statistics-model.ts @@ -1,10 +1,11 @@ import { aggregateStatisticsRange, type StatisticsAggregate } from "../../../shared/statistics-aggregation"; import type { DebridProvider, DownloadItem, DownloadSummary, StatisticsProviderBucket, UiSnapshot } from "../../../shared/types"; -export type StatisticsRange = "session" | "today" | "week" | "month" | "all"; +export type StatisticsRange = "session" | "today" | "last24" | "week" | "month" | "all"; export type StatisticsCoverage = "partial" | "unavailable"; export type StatisticsSessionState = "empty" | "idle" | "active" | "paused"; -export type StatisticsProviderScope = "current-queue" | "today" | "week" | "month" | "all"; +export type StatisticsProviderScope = "current-queue" | "today" | "last24" | "week" | "month" | "all"; +export type StatisticsUsageKind = "providers" | "accounts"; export type StatisticsMetricTone = "danger"; export interface StatisticsMetric { @@ -15,7 +16,7 @@ export interface StatisticsMetric { } export interface StatisticsProviderRow { - id: DebridProvider; + id: string; label: string; bytes: number; completed: number | null; @@ -37,6 +38,7 @@ export interface StatisticsViewModel { sessionState: StatisticsSessionState; metrics: StatisticsMetrics; providerScope: StatisticsProviderScope | null; + usageKind: StatisticsUsageKind; providers: StatisticsProviderRow[]; errorResetAvailable: boolean; } @@ -179,6 +181,16 @@ function deriveUsageProviders( return sortProviderRows(rows); } +function deriveRollingAccounts(snapshot: UiSnapshot): StatisticsProviderRow[] { + return (snapshot.stats.rolling24Hours?.accounts ?? []).map((account) => ({ + id: account.id, + label: `${providerLabels[account.provider]} · ${account.label}`, + bytes: normalizeNonNegative(account.bytes), + completed: null, + failed: null + })).sort((left, right) => right.bytes - left.bytes || left.label.localeCompare(right.label, "de-DE", { sensitivity: "base" }) || left.id.localeCompare(right.id)); +} + function aggregateMetrics(aggregate: StatisticsAggregate, sourceLabel: string): StatisticsMetrics { return { downloadedBytes: availableMetric(aggregate.downloadedBytes, sourceLabel), @@ -207,6 +219,30 @@ export function buildStatisticsViewModel( ): StatisticsViewModel { const sessionState = deriveSessionState(snapshot); + if (range === "last24") { + const rolling = snapshot.stats.rolling24Hours; + const hasFullCoverage = nowMs - Math.max(0, snapshot.stats.statistics?.startedAt ?? nowMs) >= 24 * 60 * 60 * 1_000; + return { + range, + coverage: "partial", + message: hasFullCoverage + ? "Account-Traffic der vergangenen 24 Stunden." + : "Letzte 24 Stunden: Werte seit Beginn der Aufzeichnung.", + sessionState, + metrics: { + downloadedBytes: availableMetric(rolling?.downloadedBytes ?? 0, "Letzte 24 Stunden"), + files: unavailableMetric("Dateien werden nicht minutengenau nach Account erfasst"), + successRate: unavailableMetric("Ergebnisse werden nicht minutengenau nach Account erfasst"), + averageSpeedBps: unavailableMetric("Aktive Downloadzeit wird nur tagesweise erfasst"), + errors: unavailableMetric("Fehler werden nicht minutengenau nach Account erfasst") + }, + providerScope: "last24", + usageKind: "accounts", + providers: deriveRollingAccounts(snapshot), + errorResetAvailable: false + }; + } + if (range === "today" || range === "week" || range === "month") { const days = range === "today" ? 1 : range === "week" ? 7 : 30; const aggregate = aggregateStatisticsRange(snapshot.stats.statistics, days, nowMs); @@ -220,6 +256,7 @@ export function buildStatisticsViewModel( sessionState, metrics: aggregateMetrics(aggregate, label), providerScope: range, + usageKind: "providers", providers: deriveUsageProviders( Object.fromEntries(Object.entries(aggregate.providers).map(([provider, bucket]) => [provider, bucket?.bytes ?? 0])), aggregate.providers @@ -245,6 +282,7 @@ export function buildStatisticsViewModel( errors: recordedMetrics.errors }, providerScope: "all", + usageKind: "providers", providers, errorResetAvailable: false }; @@ -278,6 +316,7 @@ export function buildStatisticsViewModel( errors: availableMetric(failed, resultSource, failed > 0 ? "danger" : undefined) }, providerScope: "current-queue", + usageKind: "providers", providers: deriveQueueProviders(items), errorResetAvailable: queueResults.failed > 0 }; diff --git a/tests/statistics-view.test.tsx b/tests/statistics-view.test.tsx index 33c51cf..60546c5 100644 --- a/tests/statistics-view.test.tsx +++ b/tests/statistics-view.test.tsx @@ -9,8 +9,9 @@ import { type StatisticsMetric } from "../src/renderer/views/statistics/statistics-model"; import { - StatisticsContent, - StatisticsSidebar, + StatisticsContent, + StatisticsSidebar, + StatisticsSidebarStatus, StatisticsView, type StatisticsViewActions } from "../src/renderer/views/statistics/StatisticsView"; @@ -210,7 +211,63 @@ describe("statistics model", () => { ["realdebrid", 500] ]); expect(model.providers.map((row) => [row.completed, row.failed])).toEqual([[0, 1], [1, 0]]); - }); + }); + + it("shows rolling account traffic with unavailable day-scoped metrics", () => { + const snapshot = createSnapshot(); + snapshot.stats.statistics = { + version: 2, + startedAt: now - (2 * 60 * 60 * 1_000), + days: [], + minutes: [] + }; + snapshot.stats.rolling24Hours = { + from: now - (24 * 60 * 60 * 1_000), + to: now, + downloadedBytes: 125, + accounts: [ + { id: "rdw_two", provider: "realdebrid", label: "Secondary", bytes: 75 }, + { id: "rdw_one", provider: "realdebrid", label: "Primary", bytes: 50 } + ] + }; + + const model = buildStatisticsViewModel(snapshot, "last24", now); + + expect(model.metrics.downloadedBytes).toMatchObject({ value: 125, available: true }); + expectUnavailable(model.metrics.files); + expectUnavailable(model.metrics.successRate); + expectUnavailable(model.metrics.averageSpeedBps); + expectUnavailable(model.metrics.errors); + expect(model.usageKind).toBe("accounts"); + expect(model.providerScope).toBe("last24"); + expect(model.providers.map((row) => [row.id, row.label, row.bytes])).toEqual([ + ["rdw_two", "Real-Debrid · Secondary", 75], + ["rdw_one", "Real-Debrid · Primary", 50] + ]); + expect(model.providers.map((row) => [row.completed, row.failed])).toEqual([[null, null], [null, null]]); + expect(model.message).toContain("seit Beginn der Aufzeichnung"); + }); + + it("shows a complete rolling description after 24 hours of statistics coverage", () => { + const snapshot = createSnapshot(); + snapshot.stats.statistics = { + version: 2, + startedAt: now - (25 * 60 * 60 * 1_000), + days: [], + minutes: [] + }; + snapshot.stats.rolling24Hours = { + from: now - (24 * 60 * 60 * 1_000), + to: now, + downloadedBytes: 0, + accounts: [] + }; + + const model = buildStatisticsViewModel(snapshot, "last24", now); + + expect(model.message).toContain("vergangenen 24 Stunden"); + expect(model.message).not.toContain("seit Beginn der Aufzeichnung"); + }); it("treats a stale daily key as a genuine zero today without stale provider rows", () => { const snapshot = createSnapshot(); @@ -340,7 +397,7 @@ describe("statistics view", () => { const html = renderToStaticMarkup(); expect(html).toContain("ui-sliding-selection ui-sliding-selection-vertical"); - expect(html.match(/data-sliding-selection-item="true"/g)).toHaveLength(5); + expect(html.match(/data-sliding-selection-item="true"/g)).toHaveLength(6); expect(html.match(/data-sliding-selection-active="true"/g)).toHaveLength(1); }); @@ -360,8 +417,8 @@ describe("statistics view", () => { for (const marker of ["statistics-sidebar", "statistics-kpis", "statistics-chart"]) { expect(html.match(new RegExp(`data-visual-region=\\"${marker}\\"`, "g"))).toHaveLength(1); } - for (const label of ["Sitzung", "Heute", "Sieben Tage", "30 Tage", "Gesamt"]) { - expect(html).toContain(`>${label}<`); + for (const label of ["Sitzung", "Heute", "Letzte 24 Stunden", "Sieben Tage", "30 Tage", "Gesamt"]) { + expect(html).toContain(`>${label}<`); } expect(html).toContain("Bestehender Bandbreitenverlauf"); expect(html).not.toContain("downloads-toolbar"); @@ -410,7 +467,7 @@ describe("statistics view", () => { expect(findButton(failedContent, "Fehler zurücksetzen").props.disabled).toBe(false); }); - it("keeps the empty provider state inside the ARIA table as a row and spanning cell", () => { + it("keeps the empty provider state inside the ARIA table as a row and spanning cell", () => { const html = renderToStaticMarkup( { ); expect(html).toContain('class="statistics-provider-empty" role="row"'); - expect(html).toContain('aria-colspan="3" role="cell"'); - }); + expect(html).toContain('aria-colspan="3" role="cell"'); + }); + + it("renders the rolling range as an account table with its own empty state", () => { + const snapshot = createSnapshot(); + snapshot.stats.rolling24Hours = { + from: now - (24 * 60 * 60 * 1_000), + to: now, + downloadedBytes: 0, + accounts: [] + }; + const model = buildStatisticsViewModel(snapshot, "last24", now); + const html = renderToStaticMarkup(<> + + } model={model} /> + ); + + expect(html).toContain("

Accounts

"); + expect(html).toContain('aria-label="Account-Nutzung"'); + expect(html).toContain('Account'); + expect(html).toContain("In den vergangenen 24 Stunden wurde noch kein Account-Traffic erfasst."); + expect(html).toContain("Accounts: 0"); + }); }); describe("bandwidth chart palette", () => { diff --git a/tests/visual-fixtures.test.ts b/tests/visual-fixtures.test.ts index 5ade9ec..3342624 100644 --- a/tests/visual-fixtures.test.ts +++ b/tests/visual-fixtures.test.ts @@ -87,6 +87,8 @@ describe("visual fixtures", () => { const update = createVisualFixture("update"); expect(Object.keys(empty.snapshot.session.packages)).toHaveLength(0); expect(Object.keys(dense.snapshot.session.packages).length).toBeGreaterThan(1); + expect(dense.snapshot.stats.rolling24Hours?.accounts).toHaveLength(2); + expect(dense.snapshot.stats.statistics?.minutes).toEqual([]); expect(update.update.latestTag).toBe("v9.9.9"); expect(createVisualFixture("dense")).toEqual(dense); }); diff --git a/tests/visual/fixtures.ts b/tests/visual/fixtures.ts index f0292e6..afbcd5c 100644 --- a/tests/visual/fixtures.ts +++ b/tests/visual/fixtures.ts @@ -496,8 +496,27 @@ function createDenseSnapshot(): UiSnapshot { } } ] + }, + rolling24Hours: { + from: 1786226400000, + to: 1786312800000, + downloadedBytes: 541165879488, + accounts: [ + { + id: "rdw_visual_primary", + provider: "realdebrid", + label: "xSucukDE", + bytes: 328565653504 + }, + { + id: "dl_visual_secondary", + provider: "debridlink", + label: "Debrid-Link Key 2", + bytes: 212600225984 + } + ] } - }; + }; snapshot.speedText = "12,0 MB/s"; snapshot.etaText = "00:17:24"; snapshot.canStart = true;