Преглед изворни кода

fix(frontend): don't drift a client's byte quota on a no-op save

The quota field shows the total in GB rounded to two decimals; editing a
client and saving converted that display value straight back to bytes. A
byte total not aligned to 0.01 GB — one set via the API or an import — was
therefore rewritten to the rounded value on any save that never touched the
field, losing a few MB each time.

Add resolveTotalBytes: keep the original byte total when the displayed GB
still matches it, and only re-derive from GB when the user actually changed
the field.
MHSanaei пре 1 дан
родитељ
комит
eb669e1d83

+ 15 - 2
frontend/src/pages/clients/ClientFormModal.tsx

@@ -146,11 +146,23 @@ function bytesToGB(bytes: number): number {
   return Math.round((bytes / (1024 * 1024 * 1024)) * 100) / 100;
 }
 
-function gbToBytes(gb: number): number {
+export function gbToBytes(gb: number): number {
   if (!gb || gb <= 0) return 0;
   return Math.round(gb * 1024 * 1024 * 1024);
 }
 
+// The quota field displays whole/2-decimal GB, so a byte total that isn't
+// aligned to 0.01 GB (e.g. one set via the API or an import) would drift on a
+// save that never touched the field. Keep the original byte total when the
+// displayed GB still matches it, and only re-derive from GB when the user
+// actually changed the value.
+export function resolveTotalBytes(originalBytes: number | null | undefined, displayedGB: number): number {
+  if (originalBytes != null && displayedGB === bytesToGB(originalBytes)) {
+    return originalBytes;
+  }
+  return gbToBytes(displayedGB);
+}
+
 export default function ClientFormModal({
   open,
   mode,
@@ -491,6 +503,7 @@ export default function ClientFormModal({
     const expiryTime = values.delayedStart
       ? -86400000 * (Number(values.delayedDays) || 0)
       : (values.expiryDate || 0);
+    const totalBytes = resolveTotalBytes(client ? (client.totalGB ?? 0) : null, values.totalGB);
     const clientPayload: Record<string, unknown> = {
       email: values.email.trim(),
       subId: values.subId,
@@ -499,7 +512,7 @@ export default function ClientFormModal({
       auth: values.auth,
       flow: showFlow ? (values.flow || '') : '',
       security: showSecurity ? (values.security || 'auto') : 'auto',
-      totalGB: gbToBytes(values.totalGB),
+      totalGB: totalBytes,
       expiryTime,
       reset: Number(values.reset) || 0,
       limitIp: Number(values.limitIp) || 0,

+ 20 - 0
frontend/src/test/client-total-bytes.test.tsx

@@ -0,0 +1,20 @@
+import { describe, it, expect } from 'vitest';
+
+import { resolveTotalBytes, gbToBytes } from '@/pages/clients/ClientFormModal';
+
+describe('resolveTotalBytes', () => {
+  it('preserves the original byte total on a no-op save of a non-GB-aligned quota', () => {
+    const original = 11_505_016_832;
+    const displayedGB = Math.round((original / 1024 ** 3) * 100) / 100;
+    expect(resolveTotalBytes(original, displayedGB)).toBe(original);
+  });
+
+  it('re-derives bytes from GB when the user changed the quota', () => {
+    const original = 11_505_016_832;
+    expect(resolveTotalBytes(original, 20)).toBe(gbToBytes(20));
+  });
+
+  it('uses the GB value directly when there is no original (add mode)', () => {
+    expect(resolveTotalBytes(null, 5)).toBe(gbToBytes(5));
+  });
+});