Repository: storacha/bluesky-backup-webapp-server
Pull request #171: feat: list all snapshots
Author: login: kaf-lamed-beyt
Created: 2025-05-18T15:21:21Z | Merged: 2025-05-21T17:32:32Z

I've updated the `findSnaphots` method in the storage context to return paged results and updated the API routes to access this data with query parameters

In the snapshot component, there's a tiny UX hack where I'm rendering the Snapshots in a div that is hidden. This ensures that the SWR cache is always updated with fresh data for the next page, allowing navigation to be instant.

Closes [#448](https://github.com/storacha/project-tracking/issues/448)



Diff:
diff --git a/src/app/api/backups/[id]/snapshots/route.ts b/src/app/api/backups/[id]/snapshots/route.ts
index a9697fda..fc78bd59 100644
--- a/src/app/api/backups/[id]/snapshots/route.ts
+++ b/src/app/api/backups/[id]/snapshots/route.ts
@@ -1,4 +1,5 @@
 import { Delegation } from '@ucanto/core'
+import { NextRequest } from 'next/server'
 
 import { backupOwnedByAccount, isCronjobAuthed } from '@/lib/server/auth'
 import { createSnapshotForBackup } from '@/lib/server/backups'
@@ -7,18 +8,31 @@ import { getSession } from '@/lib/sessions'
 import { cidUrl } from '@/lib/storacha'
 
 export async function GET(
-  request: Request,
+  request: NextRequest,
   { params }: { params: Promise<{ id: string }> }
 ) {
   const { id } = await params
   const { db } = getStorageContext()
   const { did: account } = await getSession()
+
+  const searchParams = request.nextUrl.searchParams
+  const limit = Number(searchParams.get('limit'))
+  const page = Number(searchParams.get('page'))
   if (!(await backupOwnedByAccount(db, id, account))) {
     return new Response('Not authorized', { status: 401 })
   }
-  const { results } = await db.findSnapshots(id)
+  const data = await db.findSnapshots(id, { limit, page })
+  const count = data.count
+  const totalPages = Math.ceil(count / limit)
+
+  const getPageUrl = (pageNumber: number) =>
+    `${process.env.NEXT_PUBLIC_APP_URI!}/api/backups/${id}/snapshots?page=${pageNumber}&limit=${limit}`
 
-  return Response.json(results)
+  return Response.json({
+    ...data,
+    next: page < totalPages ? getPageUrl(page + 1) : null,
+    prev: page > 1 ? getPageUrl(page - 1) : null,
+  })
 }
 
 export async function POST(
diff --git a/src/app/backups/[id]/BackupPage.tsx b/src/app/backups/[id]/BackupPage.tsx
index 1a2e4b48..81793b1f 100644
--- a/src/app/backups/[id]/BackupPage.tsx
+++ b/src/app/backups/[id]/BackupPage.tsx
@@ -79,19 +79,21 @@ const RightSidebarContent = ({ backup }: { backup: Backup }) => {
       <SnapshotContainer $gap="1rem">
         <Stack $gap="0.5em" $direction="row" $alignItems="center">
           <CreateSnapshotButton backup={backup} />
-          <SnapshotsLink href={`/backups/${backup.id}/snapshots`}>
-            All Snapshots
-          </SnapshotsLink>
+          {snapshots && snapshots?.count > 5 && (
+            <SnapshotsLink href={`/backups/${backup.id}/snapshots`}>
+              All Snapshots
+            </SnapshotsLink>
+          )}
         </Stack>
         <SubHeading>Recent Snapshots</SubHeading>
-        <>
+        <Stack $gap="0.6rem">
           {isLoading ? (
             <Center $height="200px">
               <Loader />
             </Center>
           ) : (
             <>
-              {snapshots?.slice(0, 5).map((snapshot) => (
+              {snapshots?.results.slice(0, 5).map((snapshot) => (
                 <SnapshotSummary
                   key={snapshot.id}
                   $background="var(--color-white)"
@@ -112,7 +114,7 @@ const RightSidebarContent = ({ backup }: { backup: Backup }) => {
               ))}
             </>
           )}
-        </>
+        </Stack>
       </SnapshotContainer>
     </>
   )
diff --git a/src/app/backups/[id]/page.stories.tsx b/src/app/backups/[id]/page.stories.tsx
index a06a62b0..4741f479 100644
--- a/src/app/backups/[id]/page.stories.tsx
+++ b/src/app/backups/[id]/page.stories.tsx
@@ -62,9 +62,11 @@ export const WithNoSnapshots: Story = {}
 
 export const WithSnapshots: Story = {
   decorators: [
-    withData(
-      ['api', '/api/backups/abc/snapshots'],
-      [
+    withData(['api', '/api/backups/abc/snapshots'], {
+      count: 2,
+      next: null,
+      prev: null,
+      results: [
         {
           id: 'abc',
           backupId: 'abc',
@@ -87,7 +89,7 @@ export const WithSnapshots: Story = {
             'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy552pref',
           createdAt: '2025-04-07 20:51:56',
         },
-      ]
-    ),
+      ],
+    }),
   ],
 }
diff --git a/src/app/backups/[id]/snapshots/SnapshotPage.tsx b/src/app/backups/[id]/snapshots/SnapshotPage.tsx
index 45a14991..61cad2a0 100644
--- a/src/app/backups/[id]/snapshots/SnapshotPage.tsx
+++ b/src/app/backups/[id]/snapshots/SnapshotPage.tsx
@@ -2,15 +2,20 @@
 
 import Link from 'next/link'
 import { styled } from 'next-yak'
+import { useState } from 'react'
 
 import { AppLayout } from '@/app/AppLayout'
+import { BackButton } from '@/components/BackButton'
 import { Loader } from '@/components/Loader'
+import { PaginationControls } from '@/components/Pagination'
 import { Box, Center, Heading, Stack } from '@/components/ui'
+import { PAGINATED_RESULTS_LIMIT } from '@/lib/constants'
 import { useSWR } from '@/lib/swr'
 import { formatDate } from '@/lib/ui'
+import { Snapshot } from '@/types'
 
 const SnapshotContainer = styled(Stack)`
-  margin: 4rem;
+  margin: 2rem;
 `
 
 const SnapshotSummary = styled(Box)`
@@ -24,46 +29,77 @@ const SnapshotLink = styled(Link)`
   padding: 1rem;
 `
 
-export default function SnapshotPage({ backupId }: { backupId: string }) {
+interface SnapshotPageProps {
+  backupId: string
+  snapshots: Snapshot[]
+  loading: boolean
+}
+
+function Snapshots({ snapshots, loading, backupId }: SnapshotPageProps) {
+  return (
+    <SnapshotContainer $gap="1rem">
+      <Stack $direction="row" $gap="1rem">
+        <BackButton path={`/backups/${backupId}`} />
+        <Heading>Snapshots</Heading>
+      </Stack>
+      {loading ? (
+        <Center $height="200px">
+          <Loader />
+        </Center>
+      ) : (
+        <>
+          {snapshots?.map((snapshot) => (
+            <SnapshotSummary key={snapshot.id} $background="var(--color-white)">
+              <SnapshotLink href={`/snapshots/${snapshot.id}`}>
+                <Stack
+                  $direction="row"
+                  $alignItems="center"
+                  $justifyContent="space-between"
+                  $width="100%"
+                >
+                  <Stack $direction="column" $alignItems="flex-start">
+                    <h3>{formatDate(snapshot.createdAt)} Snapshot</h3>
+                  </Stack>
+                </Stack>
+              </SnapshotLink>
+            </SnapshotSummary>
+          ))}
+        </>
+      )}
+    </SnapshotContainer>
+  )
+}
+
+export default function SnapshotPage({
+  backupId,
+}: Pick<SnapshotPageProps, 'backupId'>) {
+  const [pageNumber, setPageNumber] = useState(1)
+
   const { data: snapshots, isLoading } = useSWR([
     'api',
     `/api/backups/${backupId}/snapshots`,
+    { page: pageNumber.toString() },
   ])
 
+  const totalPages =
+    snapshots && Math.ceil(snapshots?.count / PAGINATED_RESULTS_LIMIT)
+
+  if (!snapshots) return null
+
   return (
     <AppLayout selectedBackupId={backupId}>
-      <SnapshotContainer $gap="1rem">
-        <Heading>Snapshots</Heading>
-        <>
-          {isLoading ? (
-            <Center $height="200px">
-              <Loader />
-            </Center>
-          ) : (
-            <>
-              {snapshots?.map((snapshot) => (
-                <SnapshotSummary
-                  key={snapshot.id}
-                  $background="var(--color-white)"
-                >
-                  <SnapshotLink href={`/snapshots/${snapshot.id}`}>
-                    <Stack
-                      $direction="row"
-                      $alignItems="center"
-                      $justifyContent="space-between"
-                      $width="100%"
-                    >
-                      <Stack $direction="column" $alignItems="flex-start">
-                        <h3>{formatDate(snapshot.createdAt)} Snapshot</h3>
-                      </Stack>
-                    </Stack>
-                  </SnapshotLink>
-                </SnapshotSummary>
-              ))}
-            </>
-          )}
-        </>
-      </SnapshotContainer>
+      <Stack $gap="0.8rem">
+        <Snapshots
+          backupId={backupId}
+          loading={isLoading}
+          snapshots={snapshots?.results || []}
+        />
+        <PaginationControls
+          totalPages={totalPages ?? 1}
+          currentPage={pageNumber}
+          onChange={(newPage) => setPageNumber(newPage)}
+        />
+      </Stack>
     </AppLayout>
   )
 }
diff --git a/src/components/BackButton.tsx b/src/components/BackButton.tsx
index 889c40f0..13171081 100644
--- a/src/components/BackButton.tsx
+++ b/src/components/BackButton.tsx
@@ -34,7 +34,7 @@ export const BackButton = ({ path }: { path?: string }) => {
         $alignItems="center"
         $gap="1rem"
         onClick={goBack}
-        noPadding
+        $noPadding
       >
         <ArrowLeft size={18} />
       </Button>
diff --git a/src/components/Pagination.stories.tsx b/src/components/Pagination.stories.tsx
new file mode 100644
index 00000000..c92df5d0
--- /dev/null
+++ b/src/components/Pagination.stories.tsx
@@ -0,0 +1,53 @@
+import { Meta, StoryObj } from '@storybook/react'
+import { useState } from 'react'
+
+import { PaginationControls } from './Pagination'
+
+const meta: Meta<typeof PaginationControls> = {
+  title: 'components/Pagination',
+  component: PaginationControls,
+  args: {
+    totalPages: 20,
+    delta: 3,
+  },
+}
+export default meta
+
+type Story = StoryObj<typeof PaginationControls>
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+function PaginationStoryWrapper(args: any) {
+  const [currentPage, setCurrentPage] = useState(1)
+
+  return (
+    <PaginationControls
+      {...args}
+      currentPage={currentPage}
+      onChange={(page) => setCurrentPage(page)}
+    />
+  )
+}
+
+export const Default: Story = {
+  render: (args) => <PaginationStoryWrapper {...args} />,
+  args: {
+    totalPages: 10,
+    delta: 1,
+  },
+}
+
+export const WithManyPages: Story = {
+  render: (args) => <PaginationStoryWrapper {...args} />,
+  args: {
+    totalPages: 32,
+    delta: 2,
+  },
+}
+
+export const Minimal: Story = {
+  render: (args) => <PaginationStoryWrapper {...args} />,
+  args: {
+    totalPages: 3,
+    delta: 1,
+  },
+}
diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx
new file mode 100644
index 00000000..48fc05e8
--- /dev/null
+++ b/src/components/Pagination.tsx
@@ -0,0 +1,101 @@
+import { CaretLeft, CaretRight } from '@phosphor-icons/react'
+import { styled } from 'next-yak'
+
+import { Box, Button, Stack } from './ui'
+
+const PaginationContainer = styled.div`
+  display: flex;
+  align-items: center;
+  gap: 1rem;
+  padding: 0.3rem 2rem;
+`
+
+interface PaginationProps {
+  /** Total number of pages available */
+  totalPages: number
+  /** Current active page number (1-based) */
+  currentPage: number
+  /**
+   * Called when a different page number is selected
+   * @param page - The new page number
+   */
+  onChange: (page: number) => void
+  /**
+   * Number of sibling pages to show on either side of the current page
+   * @default 2
+   */
+  delta?: number
+}
+
+export const PaginationControls = ({
+  totalPages,
+  currentPage,
+  onChange,
+  delta = 2,
+}: PaginationProps) => {
+  if (totalPages <= 1) return null
+
+  const range: (number | 'dots')[] = []
+
+  const left = Math.max(2, currentPage - delta)
+  const right = Math.min(totalPages - 1, currentPage + delta)
+
+  for (let i = 1; i <= totalPages; i++) {
+    if (i === 1 || i === totalPages || (i >= left && i <= right)) {
+      range.push(i)
+    } else if (
+      range[range.length - 1] !== 'dots' &&
+      (i === left - 1 || i === right + 1)
+    ) {
+      range.push('dots')
+    }
+  }
+
+  return (
+    <PaginationContainer>
+      <Button
+        $background="var(--color-white)"
+        $color="var(--color-black)"
+        onClick={() => onChange(currentPage - 1)}
+        disabled={currentPage === 1}
+      >
+        <CaretLeft size={20} />
+      </Button>
+      <Stack
+        $gap="0.6rem"
+        $direction="row"
+        $height="fit-content"
+        $alignItems="center"
+      >
+        {range.map((value, idx) =>
+          value === 'dots' ? (
+            <Box $height="100%" key={`dots-${idx}`}>
+              ...
+            </Box>
+          ) : (
+            <Button
+              key={value}
+              onClick={() => onChange(value)}
+              $background="none"
+              $color={value === currentPage ? 'var(--color-dark-blue)' : ''}
+              $fontWeight={value === currentPage ? '600' : 'normal'}
+              style={{
+                textDecoration: value === currentPage ? 'underline' : 'none',
+              }}
+            >
+              {value}
+            </Button>
+          )
+        )}
+      </Stack>
+      <Button
+        $background="var(--color-white)"
+        $color="var(--color-black)"
+        onClick={() => onChange(currentPage + 1)}
+        disabled={currentPage >= totalPages}
+      >
+        <CaretRight size={20} />
+      </Button>
+    </PaginationContainer>
+  )
+}
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx
index 14ac027f..b7d34b45 100644
--- a/src/components/ui/Button.tsx
+++ b/src/components/ui/Button.tsx
@@ -17,11 +17,13 @@ const buttonBackgroundColor = (
   }
 }
 
-const ButtonElement = styled.button<Partial<BtnProps & { noPadding: boolean }>>`
+const ButtonElement = styled.button<
+  Partial<BtnProps & { $noPadding: boolean }>
+>`
   font-family: ${({ $fontFamily = 'var(--font-dm-mono)' }) => $fontFamily};
   font-weight: ${({ $fontWeight = '300' }) => $fontWeight};
-  padding: ${({ $py, $px, noPadding }) =>
-    `${$py || (noPadding ? '0' : '0.75rem')} ${$px || (noPadding ? '0' : '1rem')}`};
+  padding: ${({ $py, $px, $noPadding }) =>
+    `${$py || ($noPadding ? '0' : '0.75rem')} ${$px || ($noPadding ? '0' : '1rem')}`};
   border-radius: ${({ $borderRadius = '0.75rem' }) => $borderRadius};
   background-color: ${({
     $background = 'var(--color-dark-blue)',
@@ -42,6 +44,7 @@ const ButtonElement = styled.button<Partial<BtnProps & { noPadding: boolean }>>`
   transition: background-color 0.2s ease;
   border: none;
   outline: none;
+  align-items: center;
   &:disabled {
     background-color: var(--color-gray-light);
     color: var(--color-gray-medium);
@@ -56,7 +59,7 @@ export const Button = ({
   children,
   ...props
 }: ButtonHTMLAttributes<HTMLButtonElement> &
-  Partial<BtnProps & { noPadding: boolean }>) => (
+  Partial<BtnProps & { $noPadding: boolean }>) => (
   <ButtonElement {...props}>
     {props.$leftIcon}
     {children}
diff --git a/src/components/ui/Stack.tsx b/src/components/ui/Stack.tsx
index 55dc4885..67c2f118 100644
--- a/src/components/ui/Stack.tsx
+++ b/src/components/ui/Stack.tsx
@@ -13,6 +13,7 @@ export const Stack = styled.div<{ $even?: boolean } & Partial<StyleProps>>`
   height: ${({ $height = '' }) => $height};
   border-bottom: ${({ $borderBottom = '' }) => $borderBottom};
   border: ${({ $border = '' }) => $border};
+  align-items: ${({ $alignItems = '' }) => $alignItems};
 
   ${({ $even }) =>
     $even &&
diff --git a/src/components/ui/style.ts b/src/components/ui/style.ts
index 0d859bf1..7946620e 100644
--- a/src/components/ui/style.ts
+++ b/src/components/ui/style.ts
@@ -43,6 +43,10 @@ export type StyleProps = {
   $py: `${Property.PaddingTop} ${Property.PaddingBottom}`
   $my: `${Property.MarginTop} ${Property.MarginBottom}`
   $pt: `${Property.PaddingTop}`
+  $overflow: Property.Overflow
+  $overflowX: Property.OverflowX
+  $overflowY: Property.OverflowY
+  $textDecor: Property.TextDecoration
 }
 
 export interface BtnProps extends StyleProps {
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index ea4dea4e..7f5c3a64 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -53,3 +53,5 @@ const toWebDID = (input?: string) =>
 export const GATEWAY_ID =
   toWebDID(process.env.NEXT_PUBLIC_STORACHA_GATEWAY_ID) ??
   toWebDID('did:web:w3s.link')
+
+export const PAGINATED_RESULTS_LIMIT = 10
diff --git a/src/lib/server/db.ts b/src/lib/server/db.ts
index 78a7bd2f..6ec75c90 100644
--- a/src/lib/server/db.ts
+++ b/src/lib/server/db.ts
@@ -11,12 +11,16 @@ import {
   ATBlobInput,
   Backup,
   BackupInput,
+  PaginatedResult,
+  PaginatedResultParams,
   RotationKey,
   RotationKeyInput,
   Snapshot,
   SnapshotInput,
 } from '@/types'
 
+import { PAGINATED_RESULTS_LIMIT } from '../constants'
+
 // will use psql environment variables
 // https://github.com/porsager/postgres?tab=readme-ov-file#environmental-variables
 
@@ -187,7 +191,10 @@ export const requestLock: RuntimeLock = async (key, fn) => {
 export interface BBDatabase {
   addSnapshot: (input: SnapshotInput) => Promise<Snapshot>
   updateSnapshot: (id: string, input: Partial<Snapshot>) => Promise<Snapshot>
-  findSnapshots: (backupId: string) => Promise<{ results: Snapshot[] }>
+  findSnapshots: (
+    backupId: string,
+    options?: Partial<PaginatedResultParams>
+  ) => Promise<PaginatedResult<Snapshot>>
   findSnapshot: (id: string) => Promise<{ result: Snapshot | undefined }>
   findBackups: (account: string) => Promise<{ results: Backup[] }>
   findBackup: (id: string) => Promise<{ result: Backup | undefined }>
@@ -295,17 +302,38 @@ export function getStorageContext(): StorageContext {
         }
         return results[0]
       },
-      async findSnapshots(backupId) {
-        if (!validateUUID(backupId)) return { results: [] }
+      async findSnapshots(backupId, options?: PaginatedResultParams) {
+        const { limit = PAGINATED_RESULTS_LIMIT, page = 1 } = options ?? {}
+        const offset = (page - 1) * limit
+
+        if (!validateUUID(backupId))
+          return {
+            count: 0,
+            next: null,
+            prev: null,
+            results: [],
+          }
+
+        const total = await sql<{ count: number }[]>`
+          select count(*) as count
+          from snapshots
+          where backup_id = ${backupId}
+        `
 
         const results = await sql<Snapshot[]>`
           select *
           from snapshots
           where backup_id = ${backupId}
           order by created_at desc
+          limit ${limit}
+          offset ${offset}
         `
+
+        const count = Number(total[0]?.count) ?? 0
+
         return {
-          results,
+          count: count,
+          results: results,
         }
       },
       async addBackup(input) {
diff --git a/src/lib/swr.tsx b/src/lib/swr.tsx
index 6f97709d..fd25efc4 100644
--- a/src/lib/swr.tsx
+++ b/src/lib/swr.tsx
@@ -6,7 +6,14 @@ import React from 'react'
 import useSWRBase, { SWRConfig, SWRConfiguration, SWRResponse } from 'swr'
 import useSWRMutationBase, { MutationFetcher } from 'swr/mutation'
 
-import { ATBlob, Backup, ProfileData, RotationKey, Snapshot } from '@/types'
+import {
+  ATBlob,
+  Backup,
+  PaginatedResult,
+  ProfileData,
+  RotationKey,
+  Snapshot,
+} from '@/types'
 
 // This type defines what's fetchable with `useSWR`. It is a union of key/data
 // pairs. The key can match a pattern by being as wide as it needs to be.
@@ -14,8 +21,13 @@ type Fetchable =
   | [['api', '/session/did', Record<never, string>?], string]
   | [['api', '/api/backups', Record<string, string>?], Backup[]]
   | [
-      ['api', `/api/backups/${string}/snapshots`, Record<string, string>?],
-      Snapshot[],
+      [
+        'api',
+        `/api/backups/${string}/snapshots`,
+        { page?: string; limit?: string }?,
+        Record<string, string>?,
+      ],
+      PaginatedResult<Snapshot>,
     ]
   | [['api', `/api/backups/${string}/blobs`, Record<string, string>?], ATBlob[]]
   | [['api', `/api/snapshots/${string}`, Record<string, string>?], Snapshot]
diff --git a/src/types.tsx b/src/types.tsx
index 7666887f..cb0519c3 100644
--- a/src/types.tsx
+++ b/src/types.tsx
@@ -176,3 +176,15 @@ export interface RotationKey {
 export type RotationKeyInput = Input<RotationKey, 'createdAt' | 'keypair'>
 
 export type RotationKeyClientInput = Input<RotationKeyInput, 'storachaAccount'>
+
+export type PaginatedResult<T> = {
+  count: number
+  results: T[]
+  next?: string | null
+  prev?: string | null
+}
+
+export type PaginatedResultParams = {
+  limit?: number
+  page?: number
+}


Reviews:
user: login: Peeja
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-19T15:44:40Z
user: login: kaf-lamed-beyt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T16:03:34Z
user: login: kaf-lamed-beyt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T16:14:52Z
user: login: kaf-lamed-beyt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T16:16:45Z
user: login: kaf-lamed-beyt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T16:25:52Z
user: login: kaf-lamed-beyt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T16:54:46Z
user: login: kaf-lamed-beyt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T16:58:59Z
user: login: Peeja
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T18:46:19Z
user: login: Peeja
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T18:46:47Z
user: login: Peeja
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T18:48:09Z
user: login: Peeja
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T18:48:36Z
user: login: kaf-lamed-beyt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T19:03:47Z
user: login: kaf-lamed-beyt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T19:08:10Z
user: login: kaf-lamed-beyt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T22:34:19Z
user: login: Peeja
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-20T14:49:27Z
user: login: kaf-lamed-beyt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T14:57:06Z
user: login: travis
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T17:30:58Z

Review comments:
diff_hunk: @@ -24,46 +29,90 @@ const SnapshotLink = styled(Link)`
   padding: 1rem;
 `
 
-export default function SnapshotPage({ backupId }: { backupId: string }) {
+interface SnapshotPageProps {
+  backupId: string
+  pageNumber: number
+  snapshots: Snapshot[]
+  loading: boolean
+}
+
+function Snapshots({ snapshots, loading, backupId }: SnapshotPageProps) {
+  return (
+    <SnapshotContainer $gap="1rem">
+      <Stack $direction="row" $gap="1rem">
+        <BackButton path={`/backups/${backupId}`} />
+        <Heading>Snapshots</Heading>
+      </Stack>
+      <>
+        {loading ? (
+          <Center $height="200px">
+            <Loader />
+          </Center>
+        ) : (
+          <>
+            {snapshots?.map((snapshot) => (
+              <SnapshotSummary
+                key={snapshot.id}
+                $background="var(--color-white)"
+              >
+                <SnapshotLink href={`/snapshots/${snapshot.id}`}>
+                  <Stack
+                    $direction="row"
+                    $alignItems="center"
+                    $justifyContent="space-between"
+                    $width="100%"
+                  >
+                    <Stack $direction="column" $alignItems="flex-start">
+                      <h3>{formatDate(snapshot.createdAt)} Snapshot</h3>
+                    </Stack>
+                  </Stack>
+                </SnapshotLink>
+              </SnapshotSummary>
+            ))}
+          </>
+        )}
+      </>
+    </SnapshotContainer>
+  )
+}
+
+export default function SnapshotPage({
+  backupId,
+}: Pick<SnapshotPageProps, 'backupId'>) {
+  const [pageNumber, setPageNumber] = useState(1)
+
   const { data: snapshots, isLoading } = useSWR([
     'api',
     `/api/backups/${backupId}/snapshots`,
+    { page: pageNumber.toString() },
   ])
 
+  const totalPages =
+    snapshots && Math.ceil(snapshots?.count / PAGINATED_RESULTS_LIMIT)
+
   return (
     <AppLayout selectedBackupId={backupId}>
-      <SnapshotContainer $gap="1rem">
-        <Heading>Snapshots</Heading>
-        <>
-          {isLoading ? (
-            <Center $height="200px">
-              <Loader />
-            </Center>
-          ) : (
-            <>
-              {snapshots?.map((snapshot) => (
-                <SnapshotSummary
-                  key={snapshot.id}
-                  $background="var(--color-white)"
-                >
-                  <SnapshotLink href={`/snapshots/${snapshot.id}`}>
-                    <Stack
-                      $direction="row"
-                      $alignItems="center"
-                      $justifyContent="space-between"
-                      $width="100%"
-                    >
-                      <Stack $direction="column" $alignItems="flex-start">
-                        <h3>{formatDate(snapshot.createdAt)} Snapshot</h3>
-                      </Stack>
-                    </Stack>
-                  </SnapshotLink>
-                </SnapshotSummary>
-              ))}
-            </>
-          )}
-        </>
-      </SnapshotContainer>
+      <Stack $gap="0.8rem">
+        <Snapshots
+          backupId={backupId}
+          pageNumber={pageNumber}
+          loading={isLoading}
+          snapshots={snapshots?.results as Snapshot[]}
+        />
+        <PaginationControls
+          totalPages={totalPages ?? 1}
+          currentPage={pageNumber}
+          onChange={(newPage) => setPageNumber(newPage)}
+        />
+        <div style={{ display: 'none' }}>
+          <Snapshots
+            backupId={backupId}
+            pageNumber={pageNumber}
+            loading={isLoading}
+            snapshots={snapshots?.results as Snapshot[]}
+          />
+        </div>
path: src/app/backups/[id]/snapshots/SnapshotPage.tsx
user: login: Peeja
body: **question:** I'm not following. How does this affect the cache? Aren't we by definition already rendering exactly what's here above, visibly?
created_at: 2025-05-19T15:25:59Z
author_association: MEMBER
diff_hunk: @@ -24,46 +29,90 @@ const SnapshotLink = styled(Link)`
   padding: 1rem;
 `
 
-export default function SnapshotPage({ backupId }: { backupId: string }) {
+interface SnapshotPageProps {
+  backupId: string
+  pageNumber: number
path: src/app/backups/[id]/snapshots/SnapshotPage.tsx
user: login: Peeja
body: **suggestion:** Looks like this is unused. Remove?
created_at: 2025-05-19T15:26:21Z
author_association: MEMBER
diff_hunk: @@ -24,46 +29,90 @@ const SnapshotLink = styled(Link)`
   padding: 1rem;
 `
 
-export default function SnapshotPage({ backupId }: { backupId: string }) {
+interface SnapshotPageProps {
+  backupId: string
+  pageNumber: number
+  snapshots: Snapshot[]
+  loading: boolean
+}
+
+function Snapshots({ snapshots, loading, backupId }: SnapshotPageProps) {
+  return (
+    <SnapshotContainer $gap="1rem">
+      <Stack $direction="row" $gap="1rem">
+        <BackButton path={`/backups/${backupId}`} />
+        <Heading>Snapshots</Heading>
+      </Stack>
+      <>
+        {loading ? (
+          <Center $height="200px">
+            <Loader />
+          </Center>
+        ) : (
+          <>
+            {snapshots?.map((snapshot) => (
+              <SnapshotSummary
+                key={snapshot.id}
+                $background="var(--color-white)"
+              >
+                <SnapshotLink href={`/snapshots/${snapshot.id}`}>
+                  <Stack
+                    $direction="row"
+                    $alignItems="center"
+                    $justifyContent="space-between"
+                    $width="100%"
+                  >
+                    <Stack $direction="column" $alignItems="flex-start">
+                      <h3>{formatDate(snapshot.createdAt)} Snapshot</h3>
+                    </Stack>
+                  </Stack>
+                </SnapshotLink>
+              </SnapshotSummary>
+            ))}
+          </>
+        )}
+      </>
+    </SnapshotContainer>
+  )
+}
+
+export default function SnapshotPage({
+  backupId,
+}: Pick<SnapshotPageProps, 'backupId'>) {
+  const [pageNumber, setPageNumber] = useState(1)
+
   const { data: snapshots, isLoading } = useSWR([
     'api',
     `/api/backups/${backupId}/snapshots`,
+    { page: pageNumber.toString() },
   ])
 
+  const totalPages =
+    snapshots && Math.ceil(snapshots?.count / PAGINATED_RESULTS_LIMIT)
+
   return (
     <AppLayout selectedBackupId={backupId}>
-      <SnapshotContainer $gap="1rem">
-        <Heading>Snapshots</Heading>
-        <>
-          {isLoading ? (
-            <Center $height="200px">
-              <Loader />
-            </Center>
-          ) : (
-            <>
-              {snapshots?.map((snapshot) => (
-                <SnapshotSummary
-                  key={snapshot.id}
-                  $background="var(--color-white)"
-                >
-                  <SnapshotLink href={`/snapshots/${snapshot.id}`}>
-                    <Stack
-                      $direction="row"
-                      $alignItems="center"
-                      $justifyContent="space-between"
-                      $width="100%"
-                    >
-                      <Stack $direction="column" $alignItems="flex-start">
-                        <h3>{formatDate(snapshot.createdAt)} Snapshot</h3>
-                      </Stack>
-                    </Stack>
-                  </SnapshotLink>
-                </SnapshotSummary>
-              ))}
-            </>
-          )}
-        </>
-      </SnapshotContainer>
+      <Stack $gap="0.8rem">
+        <Snapshots
+          backupId={backupId}
+          pageNumber={pageNumber}
+          loading={isLoading}
+          snapshots={snapshots?.results as Snapshot[]}
path: src/app/backups/[id]/snapshots/SnapshotPage.tsx
user: login: Peeja
body: **issue:** We're hiding a real potential issue with this type assertion.

**suggestion:** Decide what to actually do if `snapshots` is `undefined`—probably not render `Snapshots` at all. In which case, I'd move the responsibility for rendering the `Loader` to here, and no longer pass `loading` in. It makes sense to me to keep that with the component that's doing the loading itself.
created_at: 2025-05-19T15:29:03Z
author_association: MEMBER
diff_hunk: @@ -295,16 +302,41 @@ export function getStorageContext(): StorageContext {
         }
         return results[0]
       },
-      async findSnapshots(backupId) {
-        if (!validateUUID(backupId)) return { results: [] }
+      async findSnapshots(backupId, options?: Partial<PaginatedResultParams>) {
path: src/lib/server/db.ts
user: login: Peeja
body: **suggestion:** This is always used as a `Partial<>`, so let's just make the two properties optional in the actual interface.
created_at: 2025-05-19T15:33:06Z
author_association: MEMBER
diff_hunk: @@ -1,24 +1,30 @@
 import { Delegation } from '@ucanto/core'
+import { NextRequest } from 'next/server'
 
+import { PAGINATED_RESULTS_LIMIT } from '@/lib/constants'
 import { backupOwnedByAccount, isCronjobAuthed } from '@/lib/server/auth'
 import { createSnapshotForBackup } from '@/lib/server/backups'
 import { getStorageContext } from '@/lib/server/db'
 import { getSession } from '@/lib/sessions'
 import { cidUrl } from '@/lib/storacha'
 
 export async function GET(
-  request: Request,
+  request: NextRequest,
   { params }: { params: Promise<{ id: string }> }
 ) {
   const { id } = await params
   const { db } = getStorageContext()
   const { did: account } = await getSession()
+
+  const searchParams = request.nextUrl.searchParams
+  const limit = Number(searchParams.get('limit') ?? PAGINATED_RESULTS_LIMIT)
+  const page = Number(searchParams.get('page') ?? 1)
path: src/app/api/backups/[id]/snapshots/route.ts
user: login: Peeja
body: **suggestion:** We already do this defaulting in the DB access layer, so let's just pass `undefined` down when it's not specified.
created_at: 2025-05-19T15:33:38Z
author_association: MEMBER
diff_hunk: @@ -295,16 +302,41 @@ export function getStorageContext(): StorageContext {
         }
         return results[0]
       },
-      async findSnapshots(backupId) {
-        if (!validateUUID(backupId)) return { results: [] }
+      async findSnapshots(backupId, options?: Partial<PaginatedResultParams>) {
+        const { limit = PAGINATED_RESULTS_LIMIT, page = 1 } = options ?? {}
+        const offset = (page - 1) * limit
+
+        if (!validateUUID(backupId))
+          return {
+            count: 0,
+            next: null,
+            prev: null,
+            results: [],
+          }
+
+        const total = await sql<{ count: number }[]>`
+          select count(*) as count
+          from snapshots
+          where backup_id = ${backupId}
+        `
 
         const results = await sql<Snapshot[]>`
           select *
           from snapshots
           where backup_id = ${backupId}
           order by created_at desc
+          limit ${limit}
+          offset ${offset}
         `
+
+        const count = Number(total[0]?.count) ?? 0
+        const totalPages = Math.ceil(count / limit)
+        const getPageUrl = (pageNumber: number) =>
+          `${process.env.NEXT_PUBLIC_APP_URI!}/api/backups/${backupId}/snapshots?page=${pageNumber}&limit=${limit}`
path: src/lib/server/db.ts
user: login: Peeja
body: **suggestion:** The DB access layer doesn't know anything about URLs. Let's move `next` and `prev` into the route handler, and just return the `count` and `results` here (which is, after all, what we query the DB for here).
created_at: 2025-05-19T15:35:30Z
author_association: MEMBER
diff_hunk: @@ -24,46 +29,90 @@ const SnapshotLink = styled(Link)`
   padding: 1rem;
 `
 
-export default function SnapshotPage({ backupId }: { backupId: string }) {
+interface SnapshotPageProps {
+  backupId: string
+  pageNumber: number
+  snapshots: Snapshot[]
+  loading: boolean
+}
+
+function Snapshots({ snapshots, loading, backupId }: SnapshotPageProps) {
+  return (
+    <SnapshotContainer $gap="1rem">
+      <Stack $direction="row" $gap="1rem">
+        <BackButton path={`/backups/${backupId}`} />
+        <Heading>Snapshots</Heading>
+      </Stack>
+      <>
path: src/app/backups/[id]/snapshots/SnapshotPage.tsx
user: login: Peeja
body: **suggestion:** This fragment can be unwrapped.
created_at: 2025-05-19T15:37:29Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,53 @@
+import { Meta, StoryObj } from '@storybook/react'
+import { useState } from 'react'
+
+import { PaginationControls } from './Pagination'
+
+const meta: Meta<typeof PaginationControls> = {
+  title: 'components/Pagination',
+  component: PaginationControls,
+  args: {
+    totalPages: 20,
+    delta: 3,
+  },
+}
+export default meta
+
+type Story = StoryObj<typeof PaginationControls>
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+function PaginationStoryWrapper(args: any) {
+  const [currentPage, setCurrentPage] = useState(1)
+
+  return (
+    <PaginationControls
+      {...args}
+      currentPage={currentPage}
+      onChange={(page) => setCurrentPage(page)}
+    />
+  )
+}
+
+export const Default: Story = {
+  render: (args) => <PaginationStoryWrapper {...args} />,
+  args: {
+    totalPages: 10,
+    delta: 1,
+  },
+}
+
+export const WithManyPages: Story = {
path: src/components/Pagination.stories.tsx
user: login: Peeja
body: **issue:** [The ellipses look a little wonky.](https://www.chromatic.com/test?appId=6810f7eed73ae95e1f9b2d85&id=682a6ac86a11424cdba6001e) Should those be vertically centered?
created_at: 2025-05-19T15:44:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,53 @@
+import { Meta, StoryObj } from '@storybook/react'
+import { useState } from 'react'
+
+import { PaginationControls } from './Pagination'
+
+const meta: Meta<typeof PaginationControls> = {
+  title: 'components/Pagination',
+  component: PaginationControls,
+  args: {
+    totalPages: 20,
+    delta: 3,
+  },
+}
+export default meta
+
+type Story = StoryObj<typeof PaginationControls>
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+function PaginationStoryWrapper(args: any) {
+  const [currentPage, setCurrentPage] = useState(1)
+
+  return (
+    <PaginationControls
+      {...args}
+      currentPage={currentPage}
+      onChange={(page) => setCurrentPage(page)}
+    />
+  )
+}
+
+export const Default: Story = {
+  render: (args) => <PaginationStoryWrapper {...args} />,
+  args: {
+    totalPages: 10,
+    delta: 1,
+  },
+}
+
+export const WithManyPages: Story = {
path: src/components/Pagination.stories.tsx
user: login: kaf-lamed-beyt
body: ah, shoot! align-items: center should fix that.
created_at: 2025-05-19T16:03:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,53 @@
+import { Meta, StoryObj } from '@storybook/react'
+import { useState } from 'react'
+
+import { PaginationControls } from './Pagination'
+
+const meta: Meta<typeof PaginationControls> = {
+  title: 'components/Pagination',
+  component: PaginationControls,
+  args: {
+    totalPages: 20,
+    delta: 3,
+  },
+}
+export default meta
+
+type Story = StoryObj<typeof PaginationControls>
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+function PaginationStoryWrapper(args: any) {
+  const [currentPage, setCurrentPage] = useState(1)
+
+  return (
+    <PaginationControls
+      {...args}
+      currentPage={currentPage}
+      onChange={(page) => setCurrentPage(page)}
+    />
+  )
+}
+
+export const Default: Story = {
+  render: (args) => <PaginationStoryWrapper {...args} />,
+  args: {
+    totalPages: 10,
+    delta: 1,
+  },
+}
+
+export const WithManyPages: Story = {
path: src/components/Pagination.stories.tsx
user: login: kaf-lamed-beyt
body: okay, so... i wrapped it in `<Box />` instead of `<span />`. i think it is better at the bottom. wdyt?


https://github.com/user-attachments/assets/e84376c8-1904-4fbd-9d7b-da3e92b43e49


created_at: 2025-05-19T16:14:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -295,16 +302,41 @@ export function getStorageContext(): StorageContext {
         }
         return results[0]
       },
-      async findSnapshots(backupId) {
-        if (!validateUUID(backupId)) return { results: [] }
+      async findSnapshots(backupId, options?: Partial<PaginatedResultParams>) {
+        const { limit = PAGINATED_RESULTS_LIMIT, page = 1 } = options ?? {}
+        const offset = (page - 1) * limit
+
+        if (!validateUUID(backupId))
+          return {
+            count: 0,
+            next: null,
+            prev: null,
+            results: [],
+          }
+
+        const total = await sql<{ count: number }[]>`
+          select count(*) as count
+          from snapshots
+          where backup_id = ${backupId}
+        `
 
         const results = await sql<Snapshot[]>`
           select *
           from snapshots
           where backup_id = ${backupId}
           order by created_at desc
+          limit ${limit}
+          offset ${offset}
         `
+
+        const count = Number(total[0]?.count) ?? 0
+        const totalPages = Math.ceil(count / limit)
+        const getPageUrl = (pageNumber: number) =>
+          `${process.env.NEXT_PUBLIC_APP_URI!}/api/backups/${backupId}/snapshots?page=${pageNumber}&limit=${limit}`
path: src/lib/server/db.ts
user: login: kaf-lamed-beyt
body: will do 🫡 
created_at: 2025-05-19T16:16:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,46 +29,90 @@ const SnapshotLink = styled(Link)`
   padding: 1rem;
 `
 
-export default function SnapshotPage({ backupId }: { backupId: string }) {
+interface SnapshotPageProps {
+  backupId: string
+  pageNumber: number
+  snapshots: Snapshot[]
+  loading: boolean
+}
+
+function Snapshots({ snapshots, loading, backupId }: SnapshotPageProps) {
+  return (
+    <SnapshotContainer $gap="1rem">
+      <Stack $direction="row" $gap="1rem">
+        <BackButton path={`/backups/${backupId}`} />
+        <Heading>Snapshots</Heading>
+      </Stack>
+      <>
+        {loading ? (
+          <Center $height="200px">
+            <Loader />
+          </Center>
+        ) : (
+          <>
+            {snapshots?.map((snapshot) => (
+              <SnapshotSummary
+                key={snapshot.id}
+                $background="var(--color-white)"
+              >
+                <SnapshotLink href={`/snapshots/${snapshot.id}`}>
+                  <Stack
+                    $direction="row"
+                    $alignItems="center"
+                    $justifyContent="space-between"
+                    $width="100%"
+                  >
+                    <Stack $direction="column" $alignItems="flex-start">
+                      <h3>{formatDate(snapshot.createdAt)} Snapshot</h3>
+                    </Stack>
+                  </Stack>
+                </SnapshotLink>
+              </SnapshotSummary>
+            ))}
+          </>
+        )}
+      </>
+    </SnapshotContainer>
+  )
+}
+
+export default function SnapshotPage({
+  backupId,
+}: Pick<SnapshotPageProps, 'backupId'>) {
+  const [pageNumber, setPageNumber] = useState(1)
+
   const { data: snapshots, isLoading } = useSWR([
     'api',
     `/api/backups/${backupId}/snapshots`,
+    { page: pageNumber.toString() },
   ])
 
+  const totalPages =
+    snapshots && Math.ceil(snapshots?.count / PAGINATED_RESULTS_LIMIT)
+
   return (
     <AppLayout selectedBackupId={backupId}>
-      <SnapshotContainer $gap="1rem">
-        <Heading>Snapshots</Heading>
-        <>
-          {isLoading ? (
-            <Center $height="200px">
-              <Loader />
-            </Center>
-          ) : (
-            <>
-              {snapshots?.map((snapshot) => (
-                <SnapshotSummary
-                  key={snapshot.id}
-                  $background="var(--color-white)"
-                >
-                  <SnapshotLink href={`/snapshots/${snapshot.id}`}>
-                    <Stack
-                      $direction="row"
-                      $alignItems="center"
-                      $justifyContent="space-between"
-                      $width="100%"
-                    >
-                      <Stack $direction="column" $alignItems="flex-start">
-                        <h3>{formatDate(snapshot.createdAt)} Snapshot</h3>
-                      </Stack>
-                    </Stack>
-                  </SnapshotLink>
-                </SnapshotSummary>
-              ))}
-            </>
-          )}
-        </>
-      </SnapshotContainer>
+      <Stack $gap="0.8rem">
+        <Snapshots
+          backupId={backupId}
+          pageNumber={pageNumber}
+          loading={isLoading}
+          snapshots={snapshots?.results as Snapshot[]}
path: src/app/backups/[id]/snapshots/SnapshotPage.tsx
user: login: kaf-lamed-beyt
body: Ah, I see. If the snapshot is `undefined`. Is it okay for us to render the loader indefinitely, or show a text saying "no snapshots found" if there are none?
created_at: 2025-05-19T16:25:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,46 +29,90 @@ const SnapshotLink = styled(Link)`
   padding: 1rem;
 `
 
-export default function SnapshotPage({ backupId }: { backupId: string }) {
+interface SnapshotPageProps {
+  backupId: string
+  pageNumber: number
path: src/app/backups/[id]/snapshots/SnapshotPage.tsx
user: login: kaf-lamed-beyt
body: are you referring to the `pageNumber` here, @Peeja? if yes &mdash; it is in use, here 👇🏼 

```tsx
<Snapshots
   backupId={backupId}
   pageNumber={pageNumber}
   loading={isLoading}
   snapshots={snapshots?.results || []}
 />
```
created_at: 2025-05-19T16:54:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,46 +29,90 @@ const SnapshotLink = styled(Link)`
   padding: 1rem;
 `
 
-export default function SnapshotPage({ backupId }: { backupId: string }) {
+interface SnapshotPageProps {
+  backupId: string
+  pageNumber: number
+  snapshots: Snapshot[]
+  loading: boolean
+}
+
+function Snapshots({ snapshots, loading, backupId }: SnapshotPageProps) {
+  return (
+    <SnapshotContainer $gap="1rem">
+      <Stack $direction="row" $gap="1rem">
+        <BackButton path={`/backups/${backupId}`} />
+        <Heading>Snapshots</Heading>
+      </Stack>
+      <>
+        {loading ? (
+          <Center $height="200px">
+            <Loader />
+          </Center>
+        ) : (
+          <>
+            {snapshots?.map((snapshot) => (
+              <SnapshotSummary
+                key={snapshot.id}
+                $background="var(--color-white)"
+              >
+                <SnapshotLink href={`/snapshots/${snapshot.id}`}>
+                  <Stack
+                    $direction="row"
+                    $alignItems="center"
+                    $justifyContent="space-between"
+                    $width="100%"
+                  >
+                    <Stack $direction="column" $alignItems="flex-start">
+                      <h3>{formatDate(snapshot.createdAt)} Snapshot</h3>
+                    </Stack>
+                  </Stack>
+                </SnapshotLink>
+              </SnapshotSummary>
+            ))}
+          </>
+        )}
+      </>
+    </SnapshotContainer>
+  )
+}
+
+export default function SnapshotPage({
+  backupId,
+}: Pick<SnapshotPageProps, 'backupId'>) {
+  const [pageNumber, setPageNumber] = useState(1)
+
   const { data: snapshots, isLoading } = useSWR([
     'api',
     `/api/backups/${backupId}/snapshots`,
+    { page: pageNumber.toString() },
   ])
 
+  const totalPages =
+    snapshots && Math.ceil(snapshots?.count / PAGINATED_RESULTS_LIMIT)
+
   return (
     <AppLayout selectedBackupId={backupId}>
-      <SnapshotContainer $gap="1rem">
-        <Heading>Snapshots</Heading>
-        <>
-          {isLoading ? (
-            <Center $height="200px">
-              <Loader />
-            </Center>
-          ) : (
-            <>
-              {snapshots?.map((snapshot) => (
-                <SnapshotSummary
-                  key={snapshot.id}
-                  $background="var(--color-white)"
-                >
-                  <SnapshotLink href={`/snapshots/${snapshot.id}`}>
-                    <Stack
-                      $direction="row"
-                      $alignItems="center"
-                      $justifyContent="space-between"
-                      $width="100%"
-                    >
-                      <Stack $direction="column" $alignItems="flex-start">
-                        <h3>{formatDate(snapshot.createdAt)} Snapshot</h3>
-                      </Stack>
-                    </Stack>
-                  </SnapshotLink>
-                </SnapshotSummary>
-              ))}
-            </>
-          )}
-        </>
-      </SnapshotContainer>
+      <Stack $gap="0.8rem">
+        <Snapshots
+          backupId={backupId}
+          pageNumber={pageNumber}
+          loading={isLoading}
+          snapshots={snapshots?.results as Snapshot[]}
+        />
+        <PaginationControls
+          totalPages={totalPages ?? 1}
+          currentPage={pageNumber}
+          onChange={(newPage) => setPageNumber(newPage)}
+        />
+        <div style={{ display: 'none' }}>
+          <Snapshots
+            backupId={backupId}
+            pageNumber={pageNumber}
+            loading={isLoading}
+            snapshots={snapshots?.results as Snapshot[]}
+          />
+        </div>
path: src/app/backups/[id]/snapshots/SnapshotPage.tsx
user: login: kaf-lamed-beyt
body: it is quite similar to preloading, @Peeja. so the component already has the data needed for the next page before people click on it. 

excerpt from the SWR docs
> ...because of SWR's cache, we get the benefit to preload the next page. We render the next page inside a hidden div, so SWR will trigger the data fetching of the next page. When the user navigates to the next page, the data is already there:
created_at: 2025-05-19T16:58:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,46 +29,90 @@ const SnapshotLink = styled(Link)`
   padding: 1rem;
 `
 
-export default function SnapshotPage({ backupId }: { backupId: string }) {
+interface SnapshotPageProps {
+  backupId: string
+  pageNumber: number
+  snapshots: Snapshot[]
+  loading: boolean
+}
+
+function Snapshots({ snapshots, loading, backupId }: SnapshotPageProps) {
+  return (
+    <SnapshotContainer $gap="1rem">
+      <Stack $direction="row" $gap="1rem">
+        <BackButton path={`/backups/${backupId}`} />
+        <Heading>Snapshots</Heading>
+      </Stack>
+      <>
+        {loading ? (
+          <Center $height="200px">
+            <Loader />
+          </Center>
+        ) : (
+          <>
+            {snapshots?.map((snapshot) => (
+              <SnapshotSummary
+                key={snapshot.id}
+                $background="var(--color-white)"
+              >
+                <SnapshotLink href={`/snapshots/${snapshot.id}`}>
+                  <Stack
+                    $direction="row"
+                    $alignItems="center"
+                    $justifyContent="space-between"
+                    $width="100%"
+                  >
+                    <Stack $direction="column" $alignItems="flex-start">
+                      <h3>{formatDate(snapshot.createdAt)} Snapshot</h3>
+                    </Stack>
+                  </Stack>
+                </SnapshotLink>
+              </SnapshotSummary>
+            ))}
+          </>
+        )}
+      </>
+    </SnapshotContainer>
+  )
+}
+
+export default function SnapshotPage({
+  backupId,
+}: Pick<SnapshotPageProps, 'backupId'>) {
+  const [pageNumber, setPageNumber] = useState(1)
+
   const { data: snapshots, isLoading } = useSWR([
     'api',
     `/api/backups/${backupId}/snapshots`,
+    { page: pageNumber.toString() },
   ])
 
+  const totalPages =
+    snapshots && Math.ceil(snapshots?.count / PAGINATED_RESULTS_LIMIT)
+
   return (
     <AppLayout selectedBackupId={backupId}>
-      <SnapshotContainer $gap="1rem">
-        <Heading>Snapshots</Heading>
-        <>
-          {isLoading ? (
-            <Center $height="200px">
-              <Loader />
-            </Center>
-          ) : (
-            <>
-              {snapshots?.map((snapshot) => (
-                <SnapshotSummary
-                  key={snapshot.id}
-                  $background="var(--color-white)"
-                >
-                  <SnapshotLink href={`/snapshots/${snapshot.id}`}>
-                    <Stack
-                      $direction="row"
-                      $alignItems="center"
-                      $justifyContent="space-between"
-                      $width="100%"
-                    >
-                      <Stack $direction="column" $alignItems="flex-start">
-                        <h3>{formatDate(snapshot.createdAt)} Snapshot</h3>
-                      </Stack>
-                    </Stack>
-                  </SnapshotLink>
-                </SnapshotSummary>
-              ))}
-            </>
-          )}
-        </>
-      </SnapshotContainer>
+      <Stack $gap="0.8rem">
+        <Snapshots
+          backupId={backupId}
+          pageNumber={pageNumber}
+          loading={isLoading}
+          snapshots={snapshots?.results as Snapshot[]}
+        />
+        <PaginationControls
+          totalPages={totalPages ?? 1}
+          currentPage={pageNumber}
+          onChange={(newPage) => setPageNumber(newPage)}
+        />
+        <div style={{ display: 'none' }}>
+          <Snapshots
+            backupId={backupId}
+            pageNumber={pageNumber}
+            loading={isLoading}
+            snapshots={snapshots?.results as Snapshot[]}
+          />
+        </div>
path: src/app/backups/[id]/snapshots/SnapshotPage.tsx
user: login: Peeja
body: But it's not loading the next page. It's rendering the _current_ page. Right? Or am I missing something?
created_at: 2025-05-19T18:46:19Z
author_association: MEMBER
diff_hunk: @@ -24,46 +29,90 @@ const SnapshotLink = styled(Link)`
   padding: 1rem;
 `
 
-export default function SnapshotPage({ backupId }: { backupId: string }) {
+interface SnapshotPageProps {
+  backupId: string
+  pageNumber: number
path: src/app/backups/[id]/snapshots/SnapshotPage.tsx
user: login: Peeja
body: Well, yeah, okay. 😄 But *then* we don't use it. It's never read.
created_at: 2025-05-19T18:46:47Z
author_association: MEMBER
diff_hunk: @@ -24,46 +29,90 @@ const SnapshotLink = styled(Link)`
   padding: 1rem;
 `
 
-export default function SnapshotPage({ backupId }: { backupId: string }) {
+interface SnapshotPageProps {
+  backupId: string
+  pageNumber: number
+  snapshots: Snapshot[]
+  loading: boolean
+}
+
+function Snapshots({ snapshots, loading, backupId }: SnapshotPageProps) {
+  return (
+    <SnapshotContainer $gap="1rem">
+      <Stack $direction="row" $gap="1rem">
+        <BackButton path={`/backups/${backupId}`} />
+        <Heading>Snapshots</Heading>
+      </Stack>
+      <>
+        {loading ? (
+          <Center $height="200px">
+            <Loader />
+          </Center>
+        ) : (
+          <>
+            {snapshots?.map((snapshot) => (
+              <SnapshotSummary
+                key={snapshot.id}
+                $background="var(--color-white)"
+              >
+                <SnapshotLink href={`/snapshots/${snapshot.id}`}>
+                  <Stack
+                    $direction="row"
+                    $alignItems="center"
+                    $justifyContent="space-between"
+                    $width="100%"
+                  >
+                    <Stack $direction="column" $alignItems="flex-start">
+                      <h3>{formatDate(snapshot.createdAt)} Snapshot</h3>
+                    </Stack>
+                  </Stack>
+                </SnapshotLink>
+              </SnapshotSummary>
+            ))}
+          </>
+        )}
+      </>
+    </SnapshotContainer>
+  )
+}
+
+export default function SnapshotPage({
+  backupId,
+}: Pick<SnapshotPageProps, 'backupId'>) {
+  const [pageNumber, setPageNumber] = useState(1)
+
   const { data: snapshots, isLoading } = useSWR([
     'api',
     `/api/backups/${backupId}/snapshots`,
+    { page: pageNumber.toString() },
   ])
 
+  const totalPages =
+    snapshots && Math.ceil(snapshots?.count / PAGINATED_RESULTS_LIMIT)
+
   return (
     <AppLayout selectedBackupId={backupId}>
-      <SnapshotContainer $gap="1rem">
-        <Heading>Snapshots</Heading>
-        <>
-          {isLoading ? (
-            <Center $height="200px">
-              <Loader />
-            </Center>
-          ) : (
-            <>
-              {snapshots?.map((snapshot) => (
-                <SnapshotSummary
-                  key={snapshot.id}
-                  $background="var(--color-white)"
-                >
-                  <SnapshotLink href={`/snapshots/${snapshot.id}`}>
-                    <Stack
-                      $direction="row"
-                      $alignItems="center"
-                      $justifyContent="space-between"
-                      $width="100%"
-                    >
-                      <Stack $direction="column" $alignItems="flex-start">
-                        <h3>{formatDate(snapshot.createdAt)} Snapshot</h3>
-                      </Stack>
-                    </Stack>
-                  </SnapshotLink>
-                </SnapshotSummary>
-              ))}
-            </>
-          )}
-        </>
-      </SnapshotContainer>
+      <Stack $gap="0.8rem">
+        <Snapshots
+          backupId={backupId}
+          pageNumber={pageNumber}
+          loading={isLoading}
+          snapshots={snapshots?.results as Snapshot[]}
path: src/app/backups/[id]/snapshots/SnapshotPage.tsx
user: login: Peeja
body: `snapshots` should only be `undefined` if the request is loading or errored, unless we're doing something wrong. If there are no snapshots, it would be `[]`.
created_at: 2025-05-19T18:48:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,53 @@
+import { Meta, StoryObj } from '@storybook/react'
+import { useState } from 'react'
+
+import { PaginationControls } from './Pagination'
+
+const meta: Meta<typeof PaginationControls> = {
+  title: 'components/Pagination',
+  component: PaginationControls,
+  args: {
+    totalPages: 20,
+    delta: 3,
+  },
+}
+export default meta
+
+type Story = StoryObj<typeof PaginationControls>
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+function PaginationStoryWrapper(args: any) {
+  const [currentPage, setCurrentPage] = useState(1)
+
+  return (
+    <PaginationControls
+      {...args}
+      currentPage={currentPage}
+      onChange={(page) => setCurrentPage(page)}
+    />
+  )
+}
+
+export const Default: Story = {
+  render: (args) => <PaginationStoryWrapper {...args} />,
+  args: {
+    totalPages: 10,
+    delta: 1,
+  },
+}
+
+export const WithManyPages: Story = {
path: src/components/Pagination.stories.tsx
user: login: Peeja
body: Ooh, yeah, that's much better than centered. ✨ 
created_at: 2025-05-19T18:48:36Z
author_association: MEMBER
diff_hunk: @@ -24,46 +29,90 @@ const SnapshotLink = styled(Link)`
   padding: 1rem;
 `
 
-export default function SnapshotPage({ backupId }: { backupId: string }) {
+interface SnapshotPageProps {
+  backupId: string
+  pageNumber: number
path: src/app/backups/[id]/snapshots/SnapshotPage.tsx
user: login: kaf-lamed-beyt
body: ah, i see. it is still quite strange 😅. i'll remove it.
created_at: 2025-05-19T19:03:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,46 +29,90 @@ const SnapshotLink = styled(Link)`
   padding: 1rem;
 `
 
-export default function SnapshotPage({ backupId }: { backupId: string }) {
+interface SnapshotPageProps {
+  backupId: string
+  pageNumber: number
+  snapshots: Snapshot[]
+  loading: boolean
+}
+
+function Snapshots({ snapshots, loading, backupId }: SnapshotPageProps) {
+  return (
+    <SnapshotContainer $gap="1rem">
+      <Stack $direction="row" $gap="1rem">
+        <BackButton path={`/backups/${backupId}`} />
+        <Heading>Snapshots</Heading>
+      </Stack>
+      <>
+        {loading ? (
+          <Center $height="200px">
+            <Loader />
+          </Center>
+        ) : (
+          <>
+            {snapshots?.map((snapshot) => (
+              <SnapshotSummary
+                key={snapshot.id}
+                $background="var(--color-white)"
+              >
+                <SnapshotLink href={`/snapshots/${snapshot.id}`}>
+                  <Stack
+                    $direction="row"
+                    $alignItems="center"
+                    $justifyContent="space-between"
+                    $width="100%"
+                  >
+                    <Stack $direction="column" $alignItems="flex-start">
+                      <h3>{formatDate(snapshot.createdAt)} Snapshot</h3>
+                    </Stack>
+                  </Stack>
+                </SnapshotLink>
+              </SnapshotSummary>
+            ))}
+          </>
+        )}
+      </>
+    </SnapshotContainer>
+  )
+}
+
+export default function SnapshotPage({
+  backupId,
+}: Pick<SnapshotPageProps, 'backupId'>) {
+  const [pageNumber, setPageNumber] = useState(1)
+
   const { data: snapshots, isLoading } = useSWR([
     'api',
     `/api/backups/${backupId}/snapshots`,
+    { page: pageNumber.toString() },
   ])
 
+  const totalPages =
+    snapshots && Math.ceil(snapshots?.count / PAGINATED_RESULTS_LIMIT)
+
   return (
     <AppLayout selectedBackupId={backupId}>
-      <SnapshotContainer $gap="1rem">
-        <Heading>Snapshots</Heading>
-        <>
-          {isLoading ? (
-            <Center $height="200px">
-              <Loader />
-            </Center>
-          ) : (
-            <>
-              {snapshots?.map((snapshot) => (
-                <SnapshotSummary
-                  key={snapshot.id}
-                  $background="var(--color-white)"
-                >
-                  <SnapshotLink href={`/snapshots/${snapshot.id}`}>
-                    <Stack
-                      $direction="row"
-                      $alignItems="center"
-                      $justifyContent="space-between"
-                      $width="100%"
-                    >
-                      <Stack $direction="column" $alignItems="flex-start">
-                        <h3>{formatDate(snapshot.createdAt)} Snapshot</h3>
-                      </Stack>
-                    </Stack>
-                  </SnapshotLink>
-                </SnapshotSummary>
-              ))}
-            </>
-          )}
-        </>
-      </SnapshotContainer>
+      <Stack $gap="0.8rem">
+        <Snapshots
+          backupId={backupId}
+          pageNumber={pageNumber}
+          loading={isLoading}
+          snapshots={snapshots?.results as Snapshot[]}
path: src/app/backups/[id]/snapshots/SnapshotPage.tsx
user: login: kaf-lamed-beyt
body: @Peeja, I did this... 
```ts
snapshots={snapshots?.results || []}
```

That should solve it, no?
created_at: 2025-05-19T19:08:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,46 +29,90 @@ const SnapshotLink = styled(Link)`
   padding: 1rem;
 `
 
-export default function SnapshotPage({ backupId }: { backupId: string }) {
+interface SnapshotPageProps {
+  backupId: string
+  pageNumber: number
+  snapshots: Snapshot[]
+  loading: boolean
+}
+
+function Snapshots({ snapshots, loading, backupId }: SnapshotPageProps) {
+  return (
+    <SnapshotContainer $gap="1rem">
+      <Stack $direction="row" $gap="1rem">
+        <BackButton path={`/backups/${backupId}`} />
+        <Heading>Snapshots</Heading>
+      </Stack>
+      <>
+        {loading ? (
+          <Center $height="200px">
+            <Loader />
+          </Center>
+        ) : (
+          <>
+            {snapshots?.map((snapshot) => (
+              <SnapshotSummary
+                key={snapshot.id}
+                $background="var(--color-white)"
+              >
+                <SnapshotLink href={`/snapshots/${snapshot.id}`}>
+                  <Stack
+                    $direction="row"
+                    $alignItems="center"
+                    $justifyContent="space-between"
+                    $width="100%"
+                  >
+                    <Stack $direction="column" $alignItems="flex-start">
+                      <h3>{formatDate(snapshot.createdAt)} Snapshot</h3>
+                    </Stack>
+                  </Stack>
+                </SnapshotLink>
+              </SnapshotSummary>
+            ))}
+          </>
+        )}
+      </>
+    </SnapshotContainer>
+  )
+}
+
+export default function SnapshotPage({
+  backupId,
+}: Pick<SnapshotPageProps, 'backupId'>) {
+  const [pageNumber, setPageNumber] = useState(1)
+
   const { data: snapshots, isLoading } = useSWR([
     'api',
     `/api/backups/${backupId}/snapshots`,
+    { page: pageNumber.toString() },
   ])
 
+  const totalPages =
+    snapshots && Math.ceil(snapshots?.count / PAGINATED_RESULTS_LIMIT)
+
   return (
     <AppLayout selectedBackupId={backupId}>
-      <SnapshotContainer $gap="1rem">
-        <Heading>Snapshots</Heading>
-        <>
-          {isLoading ? (
-            <Center $height="200px">
-              <Loader />
-            </Center>
-          ) : (
-            <>
-              {snapshots?.map((snapshot) => (
-                <SnapshotSummary
-                  key={snapshot.id}
-                  $background="var(--color-white)"
-                >
-                  <SnapshotLink href={`/snapshots/${snapshot.id}`}>
-                    <Stack
-                      $direction="row"
-                      $alignItems="center"
-                      $justifyContent="space-between"
-                      $width="100%"
-                    >
-                      <Stack $direction="column" $alignItems="flex-start">
-                        <h3>{formatDate(snapshot.createdAt)} Snapshot</h3>
-                      </Stack>
-                    </Stack>
-                  </SnapshotLink>
-                </SnapshotSummary>
-              ))}
-            </>
-          )}
-        </>
-      </SnapshotContainer>
+      <Stack $gap="0.8rem">
+        <Snapshots
+          backupId={backupId}
+          pageNumber={pageNumber}
+          loading={isLoading}
+          snapshots={snapshots?.results as Snapshot[]}
+        />
+        <PaginationControls
+          totalPages={totalPages ?? 1}
+          currentPage={pageNumber}
+          onChange={(newPage) => setPageNumber(newPage)}
+        />
+        <div style={{ display: 'none' }}>
+          <Snapshots
+            backupId={backupId}
+            pageNumber={pageNumber}
+            loading={isLoading}
+            snapshots={snapshots?.results as Snapshot[]}
+          />
+        </div>
path: src/app/backups/[id]/snapshots/SnapshotPage.tsx
user: login: kaf-lamed-beyt
body: Yes, looking at it again, where I mixed it all up was from the `pageNumber` prop that is not been read, like you mentioned. I looked at the component closely again and realized this 🙏🏼 
created_at: 2025-05-19T22:34:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -330,14 +330,10 @@ export function getStorageContext(): StorageContext {
         `
 
         const count = Number(total[0]?.count) ?? 0
-        const totalPages = Math.ceil(count / limit)
-        const getPageUrl = (pageNumber: number) =>
-          `${process.env.NEXT_PUBLIC_APP_URI!}/api/backups/${backupId}/snapshots?page=${pageNumber}&limit=${limit}`
+
         return {
           count: count,
-          next: page < totalPages ? getPageUrl(page + 1) : null,
-          prev: page > 1 ? getPageUrl(page - 1) : null,
-          results,
+          results: results || [],
path: src/lib/server/db.ts
user: login: Peeja
body: **suggestion:**

```suggestion
          results: results,
```

`results` is never falsy (unless I'm misreading).
created_at: 2025-05-20T14:34:08Z
author_association: MEMBER
diff_hunk: @@ -24,46 +29,90 @@ const SnapshotLink = styled(Link)`
   padding: 1rem;
 `
 
-export default function SnapshotPage({ backupId }: { backupId: string }) {
+interface SnapshotPageProps {
+  backupId: string
+  pageNumber: number
+  snapshots: Snapshot[]
+  loading: boolean
+}
+
+function Snapshots({ snapshots, loading, backupId }: SnapshotPageProps) {
+  return (
+    <SnapshotContainer $gap="1rem">
+      <Stack $direction="row" $gap="1rem">
+        <BackButton path={`/backups/${backupId}`} />
+        <Heading>Snapshots</Heading>
+      </Stack>
+      <>
+        {loading ? (
+          <Center $height="200px">
+            <Loader />
+          </Center>
+        ) : (
+          <>
+            {snapshots?.map((snapshot) => (
+              <SnapshotSummary
+                key={snapshot.id}
+                $background="var(--color-white)"
+              >
+                <SnapshotLink href={`/snapshots/${snapshot.id}`}>
+                  <Stack
+                    $direction="row"
+                    $alignItems="center"
+                    $justifyContent="space-between"
+                    $width="100%"
+                  >
+                    <Stack $direction="column" $alignItems="flex-start">
+                      <h3>{formatDate(snapshot.createdAt)} Snapshot</h3>
+                    </Stack>
+                  </Stack>
+                </SnapshotLink>
+              </SnapshotSummary>
+            ))}
+          </>
+        )}
+      </>
+    </SnapshotContainer>
+  )
+}
+
+export default function SnapshotPage({
+  backupId,
+}: Pick<SnapshotPageProps, 'backupId'>) {
+  const [pageNumber, setPageNumber] = useState(1)
+
   const { data: snapshots, isLoading } = useSWR([
     'api',
     `/api/backups/${backupId}/snapshots`,
+    { page: pageNumber.toString() },
   ])
 
+  const totalPages =
+    snapshots && Math.ceil(snapshots?.count / PAGINATED_RESULTS_LIMIT)
+
   return (
     <AppLayout selectedBackupId={backupId}>
-      <SnapshotContainer $gap="1rem">
-        <Heading>Snapshots</Heading>
-        <>
-          {isLoading ? (
-            <Center $height="200px">
-              <Loader />
-            </Center>
-          ) : (
-            <>
-              {snapshots?.map((snapshot) => (
-                <SnapshotSummary
-                  key={snapshot.id}
-                  $background="var(--color-white)"
-                >
-                  <SnapshotLink href={`/snapshots/${snapshot.id}`}>
-                    <Stack
-                      $direction="row"
-                      $alignItems="center"
-                      $justifyContent="space-between"
-                      $width="100%"
-                    >
-                      <Stack $direction="column" $alignItems="flex-start">
-                        <h3>{formatDate(snapshot.createdAt)} Snapshot</h3>
-                      </Stack>
-                    </Stack>
-                  </SnapshotLink>
-                </SnapshotSummary>
-              ))}
-            </>
-          )}
-        </>
-      </SnapshotContainer>
+      <Stack $gap="0.8rem">
+        <Snapshots
+          backupId={backupId}
+          pageNumber={pageNumber}
+          loading={isLoading}
+          snapshots={snapshots?.results as Snapshot[]}
path: src/app/backups/[id]/snapshots/SnapshotPage.tsx
user: login: Peeja
body: That'll work, but I'd recommend having `SnapshotPage` render the `Loader` instead of `Snapshots`. Then we _never_ render a `Snapshots` when `snapshots` is `null`, and we don't have to worry about it. We might not be able to convince TS that `snapshots` is definitely non-null, though, we we may still have to do `snapshots?.results || []` for its sake, but it makes the interface easier to reason about.
created_at: 2025-05-20T14:39:17Z
author_association: MEMBER
diff_hunk: @@ -90,28 +84,21 @@ export default function SnapshotPage({
   const totalPages =
     snapshots && Math.ceil(snapshots?.count / PAGINATED_RESULTS_LIMIT)
 
+  if (!snapshots && !isLoading) return <Text>No Snapshots found</Text>
path: src/app/backups/[id]/snapshots/SnapshotPage.tsx
user: login: Peeja
body: **issue:** This state is never true when there are no snapshots (`snapshots.length === 0`), and is true (hopefully only) when there's an error from the fetch. Granted our error handling is pretty rough in this app so far, but…

**suggestion:** …let's show something else here, or even just `null`, which is what we're doing so far in most places for errors.
created_at: 2025-05-20T14:48:55Z
author_association: MEMBER
diff_hunk: @@ -90,28 +84,21 @@ export default function SnapshotPage({
   const totalPages =
     snapshots && Math.ceil(snapshots?.count / PAGINATED_RESULTS_LIMIT)
 
+  if (!snapshots && !isLoading) return <Text>No Snapshots found</Text>
path: src/app/backups/[id]/snapshots/SnapshotPage.tsx
user: login: kaf-lamed-beyt
body: huh. i see... 

so, `if (!snapshots) return null`
created_at: 2025-05-20T14:57:06Z
author_association: CONTRIBUTOR

PR comments:
user: login: kaf-lamed-beyt
created_at: 2025-05-18T18:36:46Z
body: 
[![demo](https://github.com/user-attachments/assets/51387c95-7cce-422a-aab9-5f10bd7434a8)](https://github.com/user-attachments/assets/51387c95-7cce-422a-aab9-5f10bd7434a8)




author_association: CONTRIBUTOR
user: login: kaf-lamed-beyt
created_at: 2025-05-20T15:36:20Z
body: @Peeja, is it better now?
author_association: CONTRIBUTOR