feat(statistics): show rolling account usage

This commit is contained in:
Sucukdeluxe
2026-08-21 08:30:50 +02:00
parent e24f685a71
commit 4de4f4a0be
5 changed files with 173 additions and 26 deletions
@@ -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<StatisticsViewProps, "mo
metrics.files.available ? `Dateien: ${formatMetric(metrics.files, "count")}` : null,
metrics.successRate.available ? `Erfolg: ${formatMetric(metrics.successRate, "percent")}` : null,
metrics.errors.available ? `Fehler: ${formatMetric(metrics.errors, "count")}` : null,
model.providerScope ? `Provider: ${model.providers.length}` : null
model.providerScope ? `${model.usageKind === "accounts" ? "Accounts" : "Provider"}: ${model.providers.length}` : null
].filter((value): value is string => value !== null);
if (rows.length === 0) {
return null;
@@ -156,8 +163,10 @@ export function StatisticsSidebarStatus({ model }: Pick<StatisticsViewProps, "mo
);
}
export function StatisticsContent({ model, actions, chart }: StatisticsViewProps): ReactElement {
return (
export function StatisticsContent({ model, actions, chart }: StatisticsViewProps): ReactElement {
const usageHeading = model.usageKind === "accounts" ? "Accounts" : "Provider";
const usageColumnHeading = model.usageKind === "accounts" ? "Account" : "Provider";
return (
<section aria-label="Statistik-Dashboard" className="statistics-content">
<header className="statistics-heading">
<div>
@@ -193,14 +202,14 @@ export function StatisticsContent({ model, actions, chart }: StatisticsViewProps
<div className="statistics-chart-canvas">{chart}</div>
</section>
<section className="statistics-providers">
<div className="statistics-section-heading">
<h3>Provider</h3>
<span>{providerScopeLabel(model.providerScope)}</span>
</div>
<div aria-label="Provider-Nutzung" className="statistics-provider-table" role="table">
<div className="statistics-provider-header" role="row">
<span role="columnheader">Provider</span>
<section className="statistics-providers">
<div className="statistics-section-heading">
<h3>{usageHeading}</h3>
<span>{providerScopeLabel(model.providerScope)}</span>
</div>
<div aria-label={`${usageColumnHeading}-Nutzung`} className="statistics-provider-table" role="table">
<div className="statistics-provider-header" role="row">
<span role="columnheader">{usageColumnHeading}</span>
<span role="columnheader">Daten</span>
<span role="columnheader">Ergebnisse</span>
</div>
@@ -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
};