From 8aa21a20cae72befa62dd84d6222b521463b6cc6 Mon Sep 17 00:00:00 2001 From: drondeseries Date: Mon, 6 Jul 2026 11:31:14 -0400 Subject: [PATCH 1/4] fix(quota): restore provider quota display and fix automatic resets + persistence --- .../components/system/ProviderStatusTable.tsx | 41 ++++++++++++------- internal/pool/manager.go | 30 ++++++++++---- 2 files changed, 50 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/system/ProviderStatusTable.tsx b/frontend/src/components/system/ProviderStatusTable.tsx index 8bf383630..461f394db 100644 --- a/frontend/src/components/system/ProviderStatusTable.tsx +++ b/frontend/src/components/system/ProviderStatusTable.tsx @@ -18,6 +18,7 @@ import { useProviders } from "../../hooks/useProviders"; import { formatBytes, formatExpirationDate, + formatFutureTime, formatRelativeTime, getProviderBrandName, } from "../../lib/utils"; @@ -476,20 +477,32 @@ export function ProviderStatusTable({ )} - {provider.byte_count > 0 ? ( -
- - {formatBytes(provider.byte_count)} - - - {formatBytes(provider.byte_count_24h)} / 24h - -
- ) : ( - - - - - )} +
+ {provider.byte_count > 0 ? ( + <> + + {formatBytes(provider.byte_count)} + + + {formatBytes(provider.byte_count_24h)} / 24h + + + ) : ( + - + )} + {hasQuota && ( +
+
+ Quota: {formatBytes(provider.quota_used || 0)} / {formatBytes(provider.quota_bytes || 0)} +
+ {provider.quota_reset_at && ( +
+ Resets: {formatFutureTime(provider.quota_reset_at)} +
+ )} +
+ )} +
{(() => { diff --git a/internal/pool/manager.go b/internal/pool/manager.go index d9bebf8c5..475e44bd4 100644 --- a/internal/pool/manager.go +++ b/internal/pool/manager.go @@ -171,13 +171,29 @@ func (m *manager) injectQuotaState(providers []nntppool.Provider) { for i := range providers { name := providerPoolName(providers[i]) - if used, ok := quotaUsed[name]; ok && used > 0 { - providers[i].QuotaUsed = used - } + hasResetTime := false + var resetTime time.Time if resetNano, ok := quotaResetAt[name]; ok && resetNano > 0 { - t := time.Unix(0, resetNano) - if t.After(time.Now()) { - providers[i].QuotaResetAt = t + resetTime = time.Unix(0, resetNano) + hasResetTime = true + } + + if hasResetTime { + if resetTime.After(time.Now()) { + // The quota period is still active: restore the used bytes and the reset time + if used, ok := quotaUsed[name]; ok && used > 0 { + providers[i].QuotaUsed = used + } + providers[i].QuotaResetAt = resetTime + } else { + // The quota period has already expired: discard the old usage (it resets to 0) + providers[i].QuotaUsed = 0 + providers[i].QuotaResetAt = time.Time{} + } + } else { + // No persisted reset time: restore whatever usage we have (fallback) + if used, ok := quotaUsed[name]; ok && used > 0 { + providers[i].QuotaUsed = used } } } @@ -567,7 +583,7 @@ func (m *manager) checkAndResetExpiredQuotas(ctx context.Context) { now := time.Now() for _, ps := range m.pool.Stats().Providers { - if ps.QuotaBytes == 0 || !ps.QuotaExceeded { + if ps.QuotaBytes == 0 { continue } if ps.QuotaResetAt.IsZero() || !ps.QuotaResetAt.Before(now) { From 1da23c7375ab3bd470a7e6caacd5ec55c692b7fc Mon Sep 17 00:00:00 2001 From: drondeseries Date: Mon, 6 Jul 2026 15:06:11 -0400 Subject: [PATCH 2/4] fix(review): guard quota reset on QuotaUsed==0 to skip idle providers --- internal/pool/manager.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/pool/manager.go b/internal/pool/manager.go index 475e44bd4..155285a21 100644 --- a/internal/pool/manager.go +++ b/internal/pool/manager.go @@ -583,7 +583,11 @@ func (m *manager) checkAndResetExpiredQuotas(ctx context.Context) { now := time.Now() for _, ps := range m.pool.Stats().Providers { - if ps.QuotaBytes == 0 { + // Skip providers with no quota configured or no tracked usage to reset. + // Without the QuotaUsed guard, providers that simply have a quota period + // configured but have never downloaded anything would trigger a spurious + // reset + DB write on every tick once their reset time expires. + if ps.QuotaBytes == 0 || ps.QuotaUsed == 0 { continue } if ps.QuotaResetAt.IsZero() || !ps.QuotaResetAt.Before(now) { From ac45ce44107199507bebe478a156071f66a2360a Mon Sep 17 00:00:00 2001 From: drondeseries Date: Mon, 6 Jul 2026 18:22:52 -0400 Subject: [PATCH 3/4] fix(quota): support decimal-based gigabyte/terabyte conversion for provider quotas --- frontend/src/components/config/ProviderModal.tsx | 2 +- frontend/src/components/system/ProviderStatusTable.tsx | 2 +- frontend/src/lib/utils.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/config/ProviderModal.tsx b/frontend/src/components/config/ProviderModal.tsx index ac61ab0b0..fb9eef536 100644 --- a/frontend/src/components/config/ProviderModal.tsx +++ b/frontend/src/components/config/ProviderModal.tsx @@ -15,7 +15,7 @@ interface ProviderModalProps { onCancel: () => void; } -const BYTES_PER_GB = 1_073_741_824; +const BYTES_PER_GB = 1_000_000_000; const defaultFormData: ProviderFormData = { name: "", diff --git a/frontend/src/components/system/ProviderStatusTable.tsx b/frontend/src/components/system/ProviderStatusTable.tsx index 461f394db..3fa369cfb 100644 --- a/frontend/src/components/system/ProviderStatusTable.tsx +++ b/frontend/src/components/system/ProviderStatusTable.tsx @@ -493,7 +493,7 @@ export function ProviderStatusTable({ {hasQuota && (
- Quota: {formatBytes(provider.quota_used || 0)} / {formatBytes(provider.quota_bytes || 0)} + Quota: {formatBytes(provider.quota_used || 0, 2, false, true)} / {formatBytes(provider.quota_bytes || 0, 2, false, true)}
{provider.quota_reset_at && (
diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index bcdbeacef..c6340ddaf 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -37,14 +37,14 @@ export async function copyToClipboard(text: string): Promise { } } -export function formatBytes(bytes: number, decimals = 2, shortUnit = false) { +export function formatBytes(bytes: number, decimals = 2, shortUnit = false, useDecimal = false) { const sizes = shortUnit ? ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] : ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; if (bytes === 0) return `0 ${sizes[0]}`; - const k = 1024; + const k = useDecimal ? 1000 : 1024; const dm = decimals < 0 ? 0 : decimals; const i = Math.floor(Math.log(bytes) / Math.log(k)); From ea8fdda1d0c4403c45f3d17ae2a09cdd42a7807e Mon Sep 17 00:00:00 2001 From: drondeseries Date: Mon, 6 Jul 2026 18:24:30 -0400 Subject: [PATCH 4/4] fix(quota): format all provider volume metrics using decimal base for unit consistency --- frontend/src/components/system/ProviderChart.tsx | 8 +++++--- frontend/src/components/system/ProviderStatusTable.tsx | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/system/ProviderChart.tsx b/frontend/src/components/system/ProviderChart.tsx index c30fc68f0..890bdf92d 100644 --- a/frontend/src/components/system/ProviderChart.tsx +++ b/frontend/src/components/system/ProviderChart.tsx @@ -98,12 +98,14 @@ export function ProviderChart() {
); + const formatDecimalBytes = (v: number) => formatBytes(v, 2, false, true); + return ( ); } diff --git a/frontend/src/components/system/ProviderStatusTable.tsx b/frontend/src/components/system/ProviderStatusTable.tsx index 3fa369cfb..66de8d6ed 100644 --- a/frontend/src/components/system/ProviderStatusTable.tsx +++ b/frontend/src/components/system/ProviderStatusTable.tsx @@ -481,10 +481,10 @@ export function ProviderStatusTable({ {provider.byte_count > 0 ? ( <> - {formatBytes(provider.byte_count)} + {formatBytes(provider.byte_count, 2, false, true)} - {formatBytes(provider.byte_count_24h)} / 24h + {formatBytes(provider.byte_count_24h, 2, false, true)} / 24h ) : (