diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 1c622dbcd9..ee2790a2ba 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -17,9 +17,9 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.3.8 diff --git a/.github/workflows/dockerize-profiles.yml b/.github/workflows/dockerize-profiles.yml index aafa95f324..cc26e115e9 100644 --- a/.github/workflows/dockerize-profiles.yml +++ b/.github/workflows/dockerize-profiles.yml @@ -14,20 +14,20 @@ jobs: steps: - name: Checkout the repo - uses: actions/checkout@v2 + uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2.8.0 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 - name: Log in to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ vars.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: appwrite/console-profiles tags: | @@ -40,7 +40,7 @@ jobs: - name: Build and push Docker image id: push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: . push: true diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 924f4c52cf..a5ac621fa7 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -11,9 +11,9 @@ jobs: e2e: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.3.8 @@ -21,7 +21,7 @@ jobs: run: bun install --frozen-lockfile - name: Cache Playwright browsers - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 id: playwright-cache with: path: ~/.cache/ms-playwright @@ -34,7 +34,7 @@ jobs: - name: E2E Tests run: bun test:e2e - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: ${{ !cancelled() }} with: name: playwright-report diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4bb77fbcbc..31bf8a1985 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,19 +9,19 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repo - uses: actions/checkout@v2 + uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2.8.0 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 - name: Log in to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ vars.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: appwrite/console-cloud tags: | @@ -30,7 +30,7 @@ jobs: type=semver,pattern={{major}} - name: Build and push Docker image id: push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: . push: true @@ -51,19 +51,19 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repo - uses: actions/checkout@v2 + uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2.8.0 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 - name: Log in to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ vars.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: appwrite/console-cloud-stage tags: | @@ -72,7 +72,7 @@ jobs: type=semver,pattern={{major}} - name: Build and push Docker image id: push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: . push: true @@ -91,13 +91,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repo - uses: actions/checkout@v2 + uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2.8.0 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 - name: Log in to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ vars.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -112,7 +112,7 @@ jobs: type=semver,pattern={{major}} - name: Build and push Docker image id: push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: . push: true @@ -130,19 +130,19 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repo - uses: actions/checkout@v2 + uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2.8.0 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 - name: Log in to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ vars.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: appwrite/console-cloud-no-regions tags: | @@ -151,7 +151,7 @@ jobs: type=semver,pattern={{major}} - name: Build and push Docker image id: push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: . push: true diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index d018dbf919..f950a650f9 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9 + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: "This issue has been labeled as a 'question', indicating that it requires additional information from the requestor. It has been inactive for 7 days. If no further activity occurs, this issue will be closed in 14 days." diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fa21cfc861..c94731c418 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,9 +14,9 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.3.8 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 fd5d4c410a..effb2dc424 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'; @@ -196,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 d335aba742..ba258eb50a 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}