diff --git a/server/adapters/repos/admin-stats-rollup.integration.test.ts b/server/adapters/repos/admin-stats-rollup.integration.test.ts index b3bdd4c9..5eedb0f6 100644 --- a/server/adapters/repos/admin-stats-rollup.integration.test.ts +++ b/server/adapters/repos/admin-stats-rollup.integration.test.ts @@ -310,6 +310,29 @@ describe('admin hourly stats rollup', () => { ).rejects.toThrow('stats_bucket_must_align_to_utc_hour') }) + it('counts recent registered users live even when signup rollups are unavailable', async () => { + const { app, db } = await createTestApp() + await adminHeaders(app) + const now = new Date('2026-07-20T18:30:00.000Z') + const recentCreatedAt = new Date('2026-07-15T09:00:00.000Z').getTime() + const oldCreatedAt = new Date('2026-07-13T23:59:59.999Z').getTime() + const [{ id: seededUserId }] = await db.all<{ id: string }>(sql`SELECT id FROM user LIMIT 1`) + await db.run(sql`UPDATE user SET created_at = ${oldCreatedAt}`) + await db.run(sql`UPDATE user SET created_at = ${recentCreatedAt} WHERE id = ${seededUserId}`) + await db.run(sql` + INSERT INTO user (id, name, email, email_verified, created_at, updated_at) + VALUES ('old-overview-user', 'Old User', 'old-overview@example.com', 1, ${oldCreatedAt}, ${oldCreatedAt}) + `) + + const overview = await createAdminStatsRepo(db).getOverviewStatistics(now, { + from: new Date('2026-06-21T00:00:00.000Z'), + to: new Date('2026-07-20T17:59:59.999Z'), + timeZone: 'UTC', + }) + + expect(overview.users.new7Days).toBe(1) + }) + it('rolls exact storage writes and releases into the overview trend', async () => { const { app, db } = await createTestApp() await adminHeaders(app) diff --git a/server/adapters/repos/admin-stats.ts b/server/adapters/repos/admin-stats.ts index 81fd2bd1..c7496b5c 100644 --- a/server/adapters/repos/admin-stats.ts +++ b/server/adapters/repos/admin-stats.ts @@ -13,7 +13,7 @@ import type { AdminTopShare, AdminTransferDataQuality, } from '@shared/types' -import { and, eq, gte, inArray, lte } from 'drizzle-orm' +import { and, eq, gte, inArray, lte, sql } from 'drizzle-orm' import { member, organization, user } from '../../db/auth-schema' import { matters, shares, statsRollupsHourly } from '../../db/schema' import { @@ -55,6 +55,7 @@ async function getOverviewStatistics( const [ inventory, active, + recentRegisteredUsers, newUsersByDay, totalUsersByDay, activeByDay, @@ -66,6 +67,7 @@ async function getOverviewStatistics( ] = await Promise.all([ getUserInventory(reader), getActiveUserSnapshot(reader), + getRecentRegisteredUserCount(db, now), getSignupsByDay(reader), getUserTotalsByDay(reader), getRollingActiveUserTrend(reader, effective), @@ -80,7 +82,6 @@ async function getOverviewStatistics( const dau = active?.dau ?? null const wau = active?.wau ?? null const mau = active?.mau ?? null - const recentSignups = dates.slice(-7).map((date) => (newUsersByDay.has(date) ? (newUsersByDay.get(date) ?? null) : 0)) const exactUsage = storageDataQuality.usageDriftSpaces === null || storageDataQuality.usageDriftSpaces === 0 const exactLedger = storageDataQuality.ledgerDriftSpaces === null || storageDataQuality.ledgerDriftSpaces === 0 const storageChangesExactFrom = fullStorageChangeDayFrom(storageLedgerOpening) @@ -89,9 +90,7 @@ async function getOverviewStatistics( users: { total: inventory?.total ?? null, active30Days: mau, - new7Days: recentSignups.some((value) => value === null) - ? null - : recentSignups.reduce((total, value) => total + (value ?? 0), 0), + new7Days: recentRegisteredUsers, activity: { today: dau, last7Days: dau === null || wau === null ? null : Math.max(0, wau - dau), @@ -120,6 +119,15 @@ async function getOverviewStatistics( } } +async function getRecentRegisteredUserCount(db: Database, now: Date): Promise { + const from = utcDateStart(addCalendarDays(dayKey(now), -6)) + const [row] = await db + .select({ count: sql`COUNT(*)` }) + .from(user) + .where(and(gte(user.createdAt, from), lte(user.createdAt, now))) + return Number(row.count) +} + async function getTopPersonalUsage( db: Database, reader: AdminStatsHourlyReader,