From 06ca3c2d9af6d6044dae5a33a0b2ebb839ebc47c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sun, 30 Aug 2026 23:46:41 +1200 Subject: [PATCH 1/2] (fix): render a zero replication lag as a bound, not an exact reading The three dedicated-database panels printed `{lagSeconds}s` raw, so a zero came out as `0s` -- a claim of exact currency that only PostgreSQL can actually make. MySQL reports Seconds_Behind_Source in whole seconds and MongoDB derives its lag from the oplog timestamp's second granularity, so neither can tell "caught up" from "900ms behind", and the reading carries no engine to tell them apart. `< 1s` is true on every engine. Sub-second readings now render in milliseconds through calculateTime, which is what the backend started publishing once it stopped rounding the reading to whole seconds. Cross-region already published the raw float, so that panel was printing six decimals. CrossRegionStatus.lagSeconds is widened to number | null to match what the endpoint returns when no reading was taken. Co-Authored-By: Claude Opus 5 --- src/lib/helpers/timeConversion.test.ts | 23 +++++++++++++++++++ src/lib/helpers/timeConversion.ts | 21 +++++++++++++++++ .../settings/updateCrossRegion.svelte | 6 +++-- .../settings/updateHAStatus.svelte | 11 ++++++--- .../settings/updateReadReplicas.svelte | 5 +++- 5 files changed, 60 insertions(+), 6 deletions(-) diff --git a/src/lib/helpers/timeConversion.test.ts b/src/lib/helpers/timeConversion.test.ts index 951de79f4c..37026df400 100644 --- a/src/lib/helpers/timeConversion.test.ts +++ b/src/lib/helpers/timeConversion.test.ts @@ -1,5 +1,6 @@ import { calculateTime, + formatReplicationLag, timeToSeconds, timeToMinutes, secsToUnit @@ -169,3 +170,25 @@ describe('Seconds to unit', () => { }); }); }); + +describe('Format replication lag', () => { + it('renders an unknown lag as a dash', () => { + expect(formatReplicationLag(null)).toBe('-'); + expect(formatReplicationLag(undefined)).toBe('-'); + }); + + it('renders zero as a bound rather than an exact reading', () => { + // MySQL and MongoDB report whole seconds, so their 0 covers everything + // up to a second. Printing it as 0s claimed a precision no engine gives. + expect(formatReplicationLag(0)).toBe('< 1s'); + }); + + it('renders a sub-second lag in milliseconds', () => { + expect(formatReplicationLag(0.184)).toBe('184ms'); + }); + + it('renders a lag of a second or more in the largest fitting unit', () => { + expect(formatReplicationLag(7)).toBe('7s'); + expect(formatReplicationLag(90)).toBe('1m'); + }); +}); diff --git a/src/lib/helpers/timeConversion.ts b/src/lib/helpers/timeConversion.ts index 7cf8de1667..bec4690298 100644 --- a/src/lib/helpers/timeConversion.ts +++ b/src/lib/helpers/timeConversion.ts @@ -31,6 +31,27 @@ export function calculateTime(time: number) { } } +/** + * Formats a replication lag reading for display. + * + * A zero renders as `< 1s` rather than `0s` because it does not mean the same + * thing on every engine: MySQL reports `Seconds_Behind_Source` in whole seconds + * and MongoDB derives its lag from the oplog timestamp's second granularity, so + * neither can distinguish "caught up" from "900ms behind". Only PostgreSQL + * reports a true zero, and the reading carries no engine to tell them apart. + * + * @export + * @param {number | null | undefined} lagSeconds - The lag in seconds, or null when the backend took no reading. + * @returns {string} The formatted lag, or `-` when the lag is unknown. + */ +export function formatReplicationLag(lagSeconds: number | null | undefined): string { + if (typeof lagSeconds !== 'number' || Number.isNaN(lagSeconds)) { + return '-'; + } + + return lagSeconds <= 0 ? '< 1s' : calculateTime(lagSeconds); +} + type Unit = 'ms' | 's' | 'm' | 'h' | 'd' | 'M' | 'y'; export function timeToSeconds(time: number, unit: Unit) { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateCrossRegion.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateCrossRegion.svelte index 4b88ab9857..26134078cb 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateCrossRegion.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateCrossRegion.svelte @@ -11,6 +11,7 @@ import type { Models } from '@appwrite.io/console'; import { Badge, Layout } from '@appwrite.io/pink-svelte'; import { toLocaleDateTime } from '$lib/helpers/date'; + import { formatReplicationLag } from '$lib/helpers/timeConversion'; let { database @@ -35,7 +36,7 @@ primaryRegion: string; standbyRegion: string; standbyStatus: 'healthy' | 'degraded' | 'unhealthy' | 'provisioning'; - lagSeconds: number; + lagSeconds: number | null; lastSyncedAt: string; }; @@ -209,7 +210,8 @@ • Standby: {crossRegionStatus.standbyRegion} - Lag: {crossRegionStatus.lagSeconds}s • Last synced: {toLocaleDateTime( + Lag: {formatReplicationLag(crossRegionStatus.lagSeconds)} • + Last synced: {toLocaleDateTime( crossRegionStatus.lastSyncedAt )} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateHAStatus.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateHAStatus.svelte index bd6a614be8..5c11d46b18 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateHAStatus.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateHAStatus.svelte @@ -4,6 +4,7 @@ import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { CardGrid, Modal } from '$lib/components'; import { Dependencies } from '$lib/constants'; + import { formatReplicationLag } from '$lib/helpers/timeConversion'; import { Button, Form, InputSwitch, InputNumber, InputSelect } from '$lib/elements/forms'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; @@ -30,11 +31,15 @@ let haEnabled: boolean = $state(database.highAvailability); let replicaCount: number = $state(database.highAvailabilityReplicaCount); - let syncMode: HighAvailabilitySyncMode = $state((database.highAvailabilitySyncMode ?? 'async') as HighAvailabilitySyncMode); + let syncMode: HighAvailabilitySyncMode = $state( + (database.highAvailabilitySyncMode ?? 'async') as HighAvailabilitySyncMode + ); let initialEnabled = $state(database.highAvailability); let initialReplicaCount = $state(database.highAvailabilityReplicaCount); - let initialSyncMode: HighAvailabilitySyncMode = $state((database.highAvailabilitySyncMode ?? 'async') as HighAvailabilitySyncMode); + let initialSyncMode: HighAvailabilitySyncMode = $state( + (database.highAvailabilitySyncMode ?? 'async') as HighAvailabilitySyncMode + ); let showFailoverConfirm = $state(false); let isFailingOver = $state(false); @@ -192,7 +197,7 @@ type={getHealthType(replica.status)} content={replica.status} /> - Lag: {replica.lagSeconds}s + Lag: {formatReplicationLag(replica.lagSeconds)} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateReadReplicas.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateReadReplicas.svelte index cb920978e9..d162d6d956 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateReadReplicas.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateReadReplicas.svelte @@ -4,6 +4,7 @@ import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { CardGrid, Modal } from '$lib/components'; import { Dependencies } from '$lib/constants'; + import { formatReplicationLag } from '$lib/helpers/timeConversion'; import { Button, Form, InputSelect, InputCheckbox } from '$lib/elements/forms'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; @@ -176,7 +177,9 @@ {replica.sourceRegion} → {replica.targetRegion} - • Lag: {replica.lagSeconds}s • {replica.hostname} + • Lag: {formatReplicationLag( + replica.lagSeconds + )} • {replica.hostname}