mirror of
https://github.com/saltbo/zpan.git
synced 2026-09-01 05:44:38 +08:00
c85e60f200
* fix(analytics): enforce trustworthy offline statistics Separate immutable counters from point-in-time snapshots, expose incomplete coverage instead of synthetic zeroes, and keep browser analytics result-only. Restore finite Free quota baselines, fail closed for invalid storage quota, reconcile traffic reports fairly, and add production-safe backfill and data-quality diagnostics. * fix(analytics): preserve global backfill totals Group generated hourly backfill rows by their projected values so SQLite cannot resolve output aliases to source organization columns and overwrite cross-organization totals.
917 lines
46 KiB
TypeScript
917 lines
46 KiB
TypeScript
import type { AdminDashboardOverviewStats } from '@shared/types'
|
|
import { sql } from 'drizzle-orm'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { createAdminStatsRepo, metricSpec } from '../adapters/repos/admin-stats'
|
|
import { captureAdminStatsSnapshot, rebuildAdminStatsHour } from '../adapters/repos/admin-stats-rollup'
|
|
import { currentTrafficPeriod } from '../domain/quota'
|
|
import { adminHeaders, createTestApp, seedProLicense } from '../test/setup.js'
|
|
|
|
describe('site stats routes', () => {
|
|
it('does not expose stats under the removed admin API group', async () => {
|
|
const { app } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
|
|
const res = await app.request('/api/admin/stats/overview', { headers })
|
|
|
|
expect(res.status).toBe(404)
|
|
})
|
|
|
|
it('does not expose legacy core or details stats endpoints', async () => {
|
|
const { app } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
|
|
const coreRes = await app.request('/api/site/stats/core', { headers })
|
|
const detailsRes = await app.request('/api/site/stats/details', { headers })
|
|
const rankingRes = await app.request('/api/site/stats/ranking', { headers })
|
|
|
|
expect(coreRes.status).toBe(404)
|
|
expect(detailsRes.status).toBe(404)
|
|
expect(rankingRes.status).toBe(404)
|
|
})
|
|
|
|
it('rejects unsupported hourly activity metric definitions', () => {
|
|
expect(() => metricSpec(['unknown_action'])).toThrow('Unsupported hourly activity metric: unknown_action')
|
|
})
|
|
|
|
it('gates advanced dashboard stats behind the analytics feature', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
await seedStatsFixture(db)
|
|
|
|
const res = await app.request('/api/site/stats/storage', { headers })
|
|
const body = (await res.json()) as { error: { details: Array<{ metadata: Record<string, string> }> } }
|
|
|
|
expect(res.status).toBe(402)
|
|
expect(body.error.details[0].metadata.feature).toBe('analytics')
|
|
})
|
|
|
|
it('normalizes date-only dashboard ranges to exact daily buckets', async () => {
|
|
const { app } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
|
|
const res = await app.request('/api/site/stats/overview?from=2026-01-01&to=2026-01-07', { headers })
|
|
const body = (await res.json()) as { from: string; to: string; trends: Array<{ date: string }> }
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(body.from).toBe('2026-01-01T00:00:00.000Z')
|
|
expect(body.to).toBe('2026-01-07T23:59:59.999Z')
|
|
expect(body.trends.map((point) => point.date)).toEqual([
|
|
'2026-01-01',
|
|
'2026-01-02',
|
|
'2026-01-03',
|
|
'2026-01-04',
|
|
'2026-01-05',
|
|
'2026-01-06',
|
|
'2026-01-07',
|
|
])
|
|
})
|
|
|
|
it('accepts exact UTC hourly boundaries', async () => {
|
|
const { app } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
const from = encodeURIComponent('2026-07-01T10:00:00.000Z')
|
|
const to = encodeURIComponent('2026-07-01T10:59:59.999Z')
|
|
|
|
const res = await app.request(`/api/site/stats/overview?from=${from}&to=${to}&timeZone=UTC`, {
|
|
headers,
|
|
})
|
|
const body = (await res.json()) as { from: string; to: string; trends: Array<{ date: string }> }
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(body.from).toBe('2026-07-01T10:00:00.000Z')
|
|
expect(body.to).toBe('2026-07-01T10:59:59.999Z')
|
|
expect(body.trends.map((point) => point.date)).toEqual(['2026-07-01'])
|
|
})
|
|
|
|
it('rejects partial-hour ranges that cannot map to offline result buckets', async () => {
|
|
const { app } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
const from = encodeURIComponent('2026-07-01T10:30:00.000Z')
|
|
const to = encodeURIComponent('2026-07-01T10:59:59.999Z')
|
|
|
|
const res = await app.request(`/api/site/stats/overview?from=${from}&to=${to}&timeZone=UTC`, { headers })
|
|
|
|
expect(res.status).toBe(400)
|
|
})
|
|
|
|
it('rejects local reporting zones because offline result buckets are UTC-only', async () => {
|
|
const { app } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
|
|
const res = await app.request('/api/site/stats/overview?from=2026-07-01&to=2026-07-01&timeZone=America%2FToronto', {
|
|
headers,
|
|
})
|
|
|
|
expect(res.status).toBe(400)
|
|
})
|
|
|
|
it('rejects dashboard ranges longer than one year', async () => {
|
|
const { app } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
|
|
const res = await app.request('/api/site/stats/overview?from=2025-01-01&to=2026-01-02', { headers })
|
|
|
|
expect(res.status).toBe(400)
|
|
})
|
|
|
|
it('rejects invalid calendar dates', async () => {
|
|
const { app } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
|
|
const res = await app.request('/api/site/stats/overview?from=2026-99-99', { headers })
|
|
|
|
expect(res.status).toBe(400)
|
|
})
|
|
|
|
it('rejects invalid IANA time zones', async () => {
|
|
const { app } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
|
|
const res = await app.request('/api/site/stats/overview?timeZone=Not%2FAZone', { headers })
|
|
|
|
expect(res.status).toBe(400)
|
|
})
|
|
|
|
it('does not publish site stats routes in the OpenAPI document', async () => {
|
|
const { app } = await createTestApp()
|
|
|
|
const res = await app.request('/api/openapi.json')
|
|
const body = (await res.json()) as { paths: Record<string, unknown> }
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(Object.keys(body.paths).some((path) => path.startsWith('/api/site/stats'))).toBe(false)
|
|
})
|
|
|
|
it('reads storage waterline trends from hourly rollups when present', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
await seedProLicense(db)
|
|
await seedStatsFixture(db)
|
|
|
|
await db.run(sql`
|
|
INSERT INTO stats_rollups_hourly (
|
|
id, bucket_start, org_id, metric_key, dimension_key, dimension_value,
|
|
count, bytes, unique_count, metadata, updated_at
|
|
)
|
|
VALUES (
|
|
'storage-marker-2026-01-01', ${Date.UTC(2026, 0, 1)}, '', 'stats.rollup_run', '', '',
|
|
1, 0, 0, '{"version":3,"scope":"full","quality":"exact"}', ${Date.UTC(2026, 0, 2)}
|
|
), (
|
|
'storage-used-2026-01-01', ${Date.UTC(2026, 0, 1)}, '', 'storage.used', '', '',
|
|
0, 4096, 0, '{"version":3,"scope":"snapshots","quality":"exact"}', ${Date.UTC(2026, 0, 2)}
|
|
)
|
|
`)
|
|
|
|
const res = await app.request('/api/site/stats/storage?from=2026-01-01&to=2026-01-01', { headers })
|
|
const body = (await res.json()) as {
|
|
storageTrend: Array<{ date: string; usedBytes: number; newBytes: number; newFiles: number }>
|
|
}
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(body.storageTrend).toEqual([{ date: '2026-01-01', usedBytes: 4096, newBytes: 0, newFiles: 0 }])
|
|
})
|
|
|
|
it('reads only hourly rollups and never falls back to raw events', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
await seedProLicense(db)
|
|
const [{ id: orgId }] = await db.all<{ id: string }>(sql`SELECT id FROM organization LIMIT 1`)
|
|
const at = Date.UTC(2026, 6, 1, 10)
|
|
await db.run(sql`
|
|
INSERT INTO activity_events (
|
|
id, org_id, user_id, actor_type, action, target_type, target_id, target_name, metadata, created_at
|
|
) VALUES (
|
|
'hourly-reader-raw', ${orgId}, NULL, 'system', 'upload_confirm', 'file', 'old-file', 'old.bin',
|
|
'{"bytes":111,"source":"upload"}', ${Math.floor(at / 1000) + 60}
|
|
)
|
|
`)
|
|
await db.run(sql`
|
|
INSERT INTO stats_rollups_hourly (
|
|
id, bucket_start, org_id, metric_key, dimension_key, dimension_value,
|
|
count, bytes, unique_count, metadata, updated_at
|
|
) VALUES
|
|
('hourly-reader-marker', ${at}, '', 'stats.rollup_run', '', '', 1, 0, 0,
|
|
'{"version":3,"scope":"full","quality":"exact"}', ${at + 3_600_000}),
|
|
('hourly-reader-upload', ${at}, ${orgId}, 'transfer.upload', 'status', 'success', 1, 999, 0,
|
|
'{"version":3,"scope":"counters","quality":"exact"}', ${at + 3_600_000})
|
|
`)
|
|
|
|
const query =
|
|
'/api/site/stats/traffic?from=2026-07-01T10%3A00%3A00.000Z&to=2026-07-01T10%3A59%3A59.999Z&timeZone=UTC'
|
|
const rollupRes = await app.request(query, { headers })
|
|
const rollupBody = (await rollupRes.json()) as {
|
|
coverage: { status: string; completedBuckets: number; expectedBuckets: number }
|
|
comparisonCoverage: { status: string; completedBuckets: number; expectedBuckets: number }
|
|
summary: { totalBytes: { value: number } }
|
|
}
|
|
|
|
await db.run(sql`DELETE FROM stats_rollups_hourly WHERE id = 'hourly-reader-upload'`)
|
|
const emptyRes = await app.request(query, { headers })
|
|
const emptyBody = (await emptyRes.json()) as { summary: { totalBytes: { value: number } } }
|
|
|
|
expect(rollupRes.status).toBe(200)
|
|
expect(rollupBody.summary.totalBytes.value).toBe(999)
|
|
expect(rollupBody.coverage).toMatchObject({ status: 'complete', completedBuckets: 1, expectedBuckets: 1 })
|
|
expect(rollupBody.comparisonCoverage).toMatchObject({ status: 'empty', completedBuckets: 0, expectedBuckets: 1 })
|
|
expect(emptyRes.status).toBe(200)
|
|
expect(emptyBody.summary.totalBytes.value).toBe(0)
|
|
})
|
|
|
|
it('hydrates completed-hour dashboard dimensions from rollups', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
await seedProLicense(db)
|
|
const [{ id: orgId }] = await db.all<{ id: string }>(sql`SELECT id FROM organization LIMIT 1`)
|
|
const at = Date.UTC(2026, 6, 1, 10)
|
|
await db.run(sql`
|
|
INSERT INTO stats_rollups_hourly
|
|
(id, bucket_start, org_id, metric_key, dimension_key, dimension_value,
|
|
count, bytes, unique_count, metadata, updated_at)
|
|
VALUES
|
|
('dimensions-marker', ${at}, '', 'stats.rollup_run', '', '', 1, 0, 0, '{"version":3,"scope":"full","quality":"exact"}', ${at}),
|
|
('dimensions-share-total', ${at}, ${orgId}, 'share.created', '', '', 1, 0, 0, '{"version":3,"scope":"counters","quality":"exact"}', ${at}),
|
|
('dimensions-share-kind', ${at}, ${orgId}, 'share.created', 'kind', 'landing', 1, 0, 0, '{"version":3,"scope":"counters","quality":"exact"}', ${at}),
|
|
('dimensions-job-total', ${at}, ${orgId}, 'background_job.finished', '', '', 1, 0, 0, '{"version":3,"scope":"counters","quality":"exact"}', ${at}),
|
|
('dimensions-job-outcome', ${at}, ${orgId}, 'background_job.finished', 'outcome', 'failed', 1, 0, 0, '{"version":3,"scope":"counters","quality":"exact"}', ${at}),
|
|
('dimensions-download-total', ${at}, ${orgId}, 'transfer.download_issued', '', '', 1, 10, 0, '{"version":3,"scope":"counters","quality":"exact"}', ${at}),
|
|
('dimensions-download-source', ${at}, ${orgId}, 'transfer.download_issued', 'source', 'object_download', 1, 10, 0, '{"version":3,"scope":"counters","quality":"exact"}', ${at}),
|
|
('dimensions-failure-total', ${at}, ${orgId}, 'transfer.download_failed', '', '', 1, 4, 0, '{"version":3,"scope":"counters","quality":"exact"}', ${at}),
|
|
('dimensions-failure-reason', ${at}, ${orgId}, 'transfer.download_failed', 'reason', 'network', 1, 4, 0, '{"version":3,"scope":"counters","quality":"exact"}', ${at}),
|
|
('dimensions-quality-total', ${at}, ${orgId}, 'stats.quality_missing_bytes', '', '', 5, 0, 0, '{"version":3,"scope":"counters","quality":"exact"}', ${at}),
|
|
('dimensions-quality-upload', ${at}, ${orgId}, 'stats.quality_missing_bytes', 'direction', 'upload', 2, 0, 0, '{"version":3,"scope":"counters","quality":"exact"}', ${at}),
|
|
('dimensions-quality-download', ${at}, ${orgId}, 'stats.quality_missing_bytes', 'direction', 'download', 3, 0, 0, '{"version":3,"scope":"counters","quality":"exact"}', ${at})
|
|
`)
|
|
const query = 'from=2026-07-01T10%3A00%3A00.000Z&to=2026-07-01T10%3A59%3A59.999Z&timeZone=UTC'
|
|
|
|
const [overviewRes, operationsRes, sharingRes, trafficRes] = await Promise.all([
|
|
app.request(`/api/site/stats/overview?${query}`, { headers }),
|
|
app.request(`/api/site/stats/operations?${query}`, { headers }),
|
|
app.request(`/api/site/stats/sharing?${query}`, { headers }),
|
|
app.request(`/api/site/stats/traffic?${query}`, { headers }),
|
|
])
|
|
const overview = (await overviewRes.json()) as { dataQuality: AdminDashboardOverviewStats['dataQuality'] }
|
|
const operations = (await operationsRes.json()) as { backgroundJobOutcomes: Array<{ name: string; value: number }> }
|
|
const sharing = (await sharingRes.json()) as {
|
|
summary: { createdShares: { value: number } }
|
|
typeBreakdown: Array<{ name: string; value: number }>
|
|
}
|
|
const traffic = (await trafficRes.json()) as {
|
|
sourceBreakdown: Array<{ name: string; requests: number; bytes: number }>
|
|
failureReasons: Array<{ name: string; value: number }>
|
|
}
|
|
|
|
expect([overviewRes.status, operationsRes.status, sharingRes.status, trafficRes.status]).toEqual([
|
|
200, 200, 200, 200,
|
|
])
|
|
expect(overview.dataQuality).toMatchObject({ missingUploadBytesEvents: 2, missingDownloadBytesEvents: 3 })
|
|
expect(operations.backgroundJobOutcomes).toContainEqual(expect.objectContaining({ name: 'failed', value: 1 }))
|
|
expect(sharing.summary.createdShares.value).toBe(1)
|
|
expect(sharing.typeBreakdown).toContainEqual(expect.objectContaining({ name: 'landing', value: 1 }))
|
|
expect(traffic.sourceBreakdown).toContainEqual(
|
|
expect.objectContaining({ name: 'object_download', requests: 1, bytes: 10 }),
|
|
)
|
|
expect(traffic.failureReasons).toContainEqual(expect.objectContaining({ name: 'network', value: 1 }))
|
|
})
|
|
|
|
it('refreshes exact hourly storage snapshots', async () => {
|
|
const { app, db } = await createTestApp()
|
|
await adminHeaders(app)
|
|
await seedStatsFixture(db)
|
|
const [{ expected }] = await db.all<{ expected: number }>(
|
|
sql`SELECT COALESCE(SUM(used), 0) AS expected FROM org_quotas`,
|
|
)
|
|
const repo = createAdminStatsRepo(db)
|
|
const firstNow = new Date('2026-07-10T04:05:00.000Z')
|
|
const secondNow = new Date('2026-07-10T18:05:00.000Z')
|
|
|
|
await repo.refreshHourlyRollups(firstNow)
|
|
await repo.refreshHourlyRollups(secondNow)
|
|
const rows = await db.all<{ bucketStart: number; bytes: number; metadata: string }>(sql`
|
|
SELECT bucket_start AS bucketStart, bytes, metadata
|
|
FROM stats_rollups_hourly
|
|
WHERE metric_key = 'storage.used' AND org_id = '' AND dimension_key = ''
|
|
ORDER BY bucket_start, org_id
|
|
`)
|
|
|
|
expect(rows.map((row) => Number(row.bucketStart))).toContain(Date.UTC(2026, 6, 10, 18))
|
|
expect(
|
|
rows
|
|
.filter((row) => Number(row.bucketStart) === Date.UTC(2026, 6, 10, 18))
|
|
.reduce((sum, row) => sum + row.bytes, 0),
|
|
).toBe(expected)
|
|
expect(JSON.parse(rows.at(-1)?.metadata ?? '{}')).toMatchObject({ version: 3, quality: 'exact' })
|
|
})
|
|
|
|
it('does not write rollups while serving storage stats', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
await seedProLicense(db)
|
|
await seedStatsFixture(db)
|
|
|
|
const before = await db.all<{ count: number }>(sql`SELECT COUNT(*) AS count FROM stats_rollups_hourly`)
|
|
const res = await app.request('/api/site/stats/storage?from=2000-01-01&to=2000-01-02', { headers })
|
|
const after = await db.all<{ count: number }>(sql`SELECT COUNT(*) AS count FROM stats_rollups_hourly`)
|
|
const body = (await res.json()) as { storageTrend: Array<{ usedBytes: number | null }> }
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(after[0].count).toBe(before[0].count)
|
|
expect(body.storageTrend.every((point) => point.usedBytes === null)).toBe(true)
|
|
})
|
|
|
|
it('returns growth lifecycle metrics from the hourly-aware repository', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
await seedProLicense(db)
|
|
await seedStatsFixture(db)
|
|
|
|
const res = await app.request('/api/site/stats/growth', { headers })
|
|
const body = (await res.json()) as {
|
|
summary: {
|
|
totalUsers: number
|
|
newUsers: { value: number }
|
|
activeUsers: { value: number }
|
|
verifiedUsers: number
|
|
bannedUsers: number
|
|
silentUsers: number
|
|
}
|
|
userScaleTrend: Array<{ date: string; newUsers: number; totalUsers: number }>
|
|
activeUserTrend: Array<{ date: string; dau: number; wau: number; mau: number }>
|
|
userStatus: Array<{ name: string; value: number; percent: number }>
|
|
registrationSources: Array<{ name: string; value: number; percent: number }>
|
|
}
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(body.summary.totalUsers).toBeGreaterThanOrEqual(1)
|
|
expect(body.summary.newUsers.value).toBeGreaterThanOrEqual(1)
|
|
expect(body.summary.activeUsers.value).toBeGreaterThanOrEqual(1)
|
|
expect(body.summary.verifiedUsers + body.summary.bannedUsers + body.summary.silentUsers).toBeGreaterThanOrEqual(0)
|
|
expect(body.userScaleTrend.length).toBeGreaterThan(0)
|
|
expect(body.activeUserTrend.length).toBeGreaterThan(0)
|
|
expect(body.userStatus.map((row) => row.name)).toEqual(['normal', 'unverified', 'banned', 'silent'])
|
|
expect(body.registrationSources.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('reads historical signup totals, trends, and providers from completed hourly rollups', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
await seedProLicense(db)
|
|
const at = Date.UTC(2026, 0, 1, 10)
|
|
await db.run(sql`
|
|
INSERT INTO stats_rollups_hourly
|
|
(id, bucket_start, org_id, metric_key, dimension_key, dimension_value,
|
|
count, bytes, unique_count, metadata, updated_at)
|
|
VALUES
|
|
('growth-rollup-marker', ${at}, '', 'stats.rollup_run', '', '', 1, 0, 0,
|
|
'{"version":3,"scope":"full","quality":"exact"}', ${at + 3_600_000}),
|
|
('growth-rollup-total', ${at}, '', 'user.signup', '', '', 2, 0, 0,
|
|
'{"version":3,"scope":"counters","quality":"exact"}', ${at + 3_600_000}),
|
|
('growth-inventory-total', ${at}, '', 'user.inventory', '', '', 2, 0, 0,
|
|
'{"version":3,"scope":"snapshots","quality":"exact"}', ${at + 3_600_000}),
|
|
('growth-rollup-credential', ${at}, '', 'user.signup', 'provider', 'credential', 1, 0, 0,
|
|
'{"version":3,"scope":"counters","quality":"exact"}', ${at + 3_600_000}),
|
|
('growth-rollup-github', ${at}, '', 'user.signup', 'provider', 'github', 1, 0, 0,
|
|
'{"version":3,"scope":"counters","quality":"exact"}', ${at + 3_600_000})
|
|
`)
|
|
|
|
const res = await app.request('/api/site/stats/growth?from=2026-01-01&to=2026-01-01', { headers })
|
|
const body = (await res.json()) as {
|
|
summary: { newUsers: { value: number } }
|
|
userScaleTrend: Array<{ newUsers: number; totalUsers: number }>
|
|
registrationSources: Array<{ name: string; value: number; percent: number }>
|
|
}
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(body.summary.newUsers.value).toBe(2)
|
|
expect(body.userScaleTrend).toEqual([{ date: '2026-01-01', newUsers: 2, totalUsers: 2 }])
|
|
expect(body.registrationSources).toEqual(
|
|
expect.arrayContaining([
|
|
{ name: 'credential', value: 1, percent: 50 },
|
|
{ name: 'github', value: 1, percent: 50 },
|
|
]),
|
|
)
|
|
})
|
|
|
|
it('does not expose raw storage changes until a snapshot rollup completes', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
await seedProLicense(db)
|
|
const { orgId, bucketStart } = await seedStatsFixture(db)
|
|
const nowSec = Math.floor(Date.now() / 1000)
|
|
for (let index = 0; index < 9; index += 1) {
|
|
await db.run(sql`
|
|
INSERT INTO matters
|
|
(id, org_id, alias, name, type, size, dirtype, parent, object, storage_id, status, created_at, updated_at)
|
|
VALUES (${`file-type-${index}`}, ${orgId}, ${`file-type-alias-${index}`}, ${`file-${index}`},
|
|
${`custom/type-${index}`}, ${index + 1}, 0, '', ${`file-${index}`}, 'stats-storage', 'active', ${nowSec}, ${nowSec})
|
|
`)
|
|
}
|
|
|
|
const beforeRes = await app.request('/api/site/stats/storage', { headers })
|
|
const before = (await beforeRes.json()) as { typeBreakdown: Array<{ type: string; files: number; bytes: number }> }
|
|
|
|
await captureAdminStatsSnapshot(db, bucketStart, new Date(bucketStart.getTime() + 45 * 60_000))
|
|
await rebuildAdminStatsHour(db, bucketStart, new Date())
|
|
const afterRes = await app.request('/api/site/stats/storage', { headers })
|
|
const after = (await afterRes.json()) as { typeBreakdown: Array<{ type: string; files: number; bytes: number }> }
|
|
|
|
expect(beforeRes.status).toBe(200)
|
|
expect(before.typeBreakdown).not.toContainEqual(expect.objectContaining({ type: 'custom' }))
|
|
expect(afterRes.status).toBe(200)
|
|
expect(after.typeBreakdown).toContainEqual(expect.objectContaining({ type: 'custom', files: 9 }))
|
|
})
|
|
|
|
it('counts quota pressure across every space while bounding the ranking to eight', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
await seedProLicense(db)
|
|
const { bucketStart } = await seedStatsFixture(db)
|
|
for (let index = 0; index < 12; index += 1) {
|
|
await db.run(sql`
|
|
INSERT INTO organization (id, name, slug, metadata, created_at, updated_at)
|
|
VALUES (
|
|
${`quota-pressure-org-${index}`},
|
|
${`Quota Pressure ${index}`},
|
|
${`quota-pressure-${index}`},
|
|
'{"type":"team"}',
|
|
${bucketStart.getTime()},
|
|
${bucketStart.getTime()}
|
|
)
|
|
`)
|
|
await db.run(sql`
|
|
INSERT INTO org_quotas (id, org_id, quota, used, traffic_quota, traffic_used, traffic_period)
|
|
VALUES (
|
|
${`quota-pressure-${index}`},
|
|
${`quota-pressure-org-${index}`},
|
|
100,
|
|
${index < 9 ? 90 : 110},
|
|
0,
|
|
0,
|
|
'2026-07'
|
|
)
|
|
`)
|
|
await db.run(sql`
|
|
INSERT INTO org_quota_entitlements
|
|
(id, org_id, resource_type, entitlement_type, source, source_id, bytes, starts_at, status, created_at, updated_at)
|
|
VALUES (
|
|
${`quota-pressure-plan-${index}`},
|
|
${`quota-pressure-org-${index}`},
|
|
'storage',
|
|
'plan',
|
|
'test',
|
|
${`quota-pressure-source-${index}`},
|
|
100,
|
|
${bucketStart.getTime()},
|
|
'active',
|
|
${bucketStart.getTime()},
|
|
${bucketStart.getTime()}
|
|
)
|
|
`)
|
|
}
|
|
await db.run(sql`
|
|
INSERT INTO organization (id, name, slug, metadata, created_at, updated_at)
|
|
VALUES ('quota-invalid-org', 'Invalid Quota', 'quota-invalid', '{"type":"team"}', ${bucketStart.getTime()}, ${bucketStart.getTime()})
|
|
`)
|
|
await db.run(sql`
|
|
INSERT INTO org_quotas (id, org_id, quota, used, traffic_quota, traffic_used, traffic_period)
|
|
VALUES ('quota-invalid', 'quota-invalid-org', 0, 1000, 0, 0, '2026-07')
|
|
`)
|
|
await captureAdminStatsSnapshot(db, bucketStart, new Date(bucketStart.getTime() + 45 * 60_000))
|
|
await rebuildAdminStatsHour(db, bucketStart, new Date())
|
|
|
|
const res = await app.request('/api/site/stats/storage', { headers })
|
|
const body = (await res.json()) as {
|
|
summary: {
|
|
quotaBytes: number | null
|
|
storageUtilization: number | null
|
|
nearQuotaSpaces: number
|
|
overQuotaSpaces: number
|
|
invalidQuotaSpaces: number
|
|
}
|
|
topSpaces: Array<{ orgId: string; utilization: number | null }>
|
|
}
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(body.summary.nearQuotaSpaces).toBe(9)
|
|
expect(body.summary.overQuotaSpaces).toBe(3)
|
|
expect(body.summary.invalidQuotaSpaces).toBe(2)
|
|
expect(body.summary.quotaBytes).toBeNull()
|
|
expect(body.summary.storageUtilization).toBeNull()
|
|
expect(body.topSpaces).toHaveLength(8)
|
|
expect(body.topSpaces[0]).toMatchObject({ orgId: 'quota-invalid-org', utilization: null })
|
|
})
|
|
|
|
it('reads historical active users from completed snapshots instead of raw activity', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
const [{ id: orgId }] = await db.all<{ id: string }>(sql`SELECT id FROM organization LIMIT 1`)
|
|
const [{ id: userId }] = await db.all<{ id: string }>(sql`SELECT id FROM user LIMIT 1`)
|
|
const created = Math.floor(Date.UTC(2026, 0, 2, 12) / 1000)
|
|
await db.run(sql`
|
|
INSERT INTO activity_events (id, org_id, user_id, actor_type, action, target_type, target_name, created_at)
|
|
VALUES
|
|
('valid-historical-activity', ${orgId}, ${userId}, 'user', 'login', 'user', 'valid', ${created}),
|
|
('orphan-historical-activity', ${orgId}, 'deleted-user', 'user', 'login', 'user', 'orphan', ${created})
|
|
`)
|
|
const at = Date.UTC(2026, 0, 2, 12)
|
|
await db.run(sql`
|
|
INSERT INTO stats_rollups_hourly
|
|
(id, bucket_start, org_id, metric_key, dimension_key, dimension_value,
|
|
count, bytes, unique_count, metadata, updated_at)
|
|
VALUES
|
|
('active-snapshot-marker', ${at}, '', 'stats.rollup_run', '', '', 1, 0, 0,
|
|
'{"version":3,"scope":"full","quality":"exact"}', ${at + 3_600_000}),
|
|
('active-snapshot-total', ${at}, '', 'user.active_snapshot', '', '', 1, 0, 0,
|
|
'{"version":3,"scope":"snapshots","quality":"exact"}', ${at + 3_600_000}),
|
|
('active-snapshot-mau', ${at}, '', 'user.active_snapshot', 'window', 'mau', 1, 0, 0,
|
|
'{"version":3,"scope":"snapshots","quality":"exact"}', ${at + 3_600_000})
|
|
`)
|
|
|
|
const res = await app.request('/api/site/stats/overview?from=2026-01-02&to=2026-01-02', { headers })
|
|
const body = (await res.json()) as { totals: { activeUsers: { value: number } } }
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(body.totals.activeUsers.value).toBe(1)
|
|
})
|
|
|
|
it('returns null for empty success-rate samples and counts upload cancellations as failures', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
await seedProLicense(db)
|
|
const { orgId, userId, bucketStart, eventSec } = await seedStatsFixture(db)
|
|
await db.run(sql`
|
|
INSERT INTO activity_events (id, org_id, user_id, actor_type, action, target_type, target_name, metadata, created_at)
|
|
VALUES ('upload-cancel-rate', ${orgId}, ${userId}, 'user', 'upload_cancel', 'file', 'cancel.bin',
|
|
'{', ${eventSec})
|
|
`)
|
|
await rebuildAdminStatsHour(db, bucketStart, new Date())
|
|
|
|
const current = await app.request('/api/site/stats/traffic', { headers })
|
|
const currentBody = (await current.json()) as {
|
|
summary: { requestCount: { changePercent: number | null } }
|
|
successTrend: Array<{ uploadSuccessRate: number | null }>
|
|
}
|
|
const empty = await app.request('/api/site/stats/traffic?from=2000-01-01&to=2000-01-01', { headers })
|
|
const emptyBody = (await empty.json()) as {
|
|
successTrend: Array<{ uploadSuccessRate: number | null; downloadSuccessRate: number | null }>
|
|
}
|
|
|
|
expect(currentBody.successTrend.some((point) => point.uploadSuccessRate === 50)).toBe(true)
|
|
expect(currentBody.summary.requestCount.changePercent).toBeNull()
|
|
expect(emptyBody.successTrend).toEqual([{ date: '2000-01-01', uploadSuccessRate: null, downloadSuccessRate: null }])
|
|
})
|
|
|
|
it('reports incomplete transfer byte metadata for the selected and comparison ranges', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
const { orgId, userId } = await seedStatsFixture(db)
|
|
await db.run(sql`
|
|
INSERT INTO activity_events
|
|
(id, org_id, user_id, actor_type, action, target_type, target_name, metadata, created_at)
|
|
VALUES
|
|
('missing-current-upload-bytes', ${orgId}, ${userId}, 'user', 'upload_confirm', 'file', 'current.bin', '{}',
|
|
${Math.floor(Date.parse('2026-07-01T12:00:00.000Z') / 1000)}),
|
|
('missing-previous-download-bytes', ${orgId}, ${userId}, 'user', 'share_download', 'share', 'previous.bin', '{}',
|
|
${Math.floor(Date.parse('2026-06-30T12:00:00.000Z') / 1000)})
|
|
`)
|
|
await rebuildAdminStatsHour(db, new Date('2026-07-01T12:00:00.000Z'), new Date())
|
|
await rebuildAdminStatsHour(db, new Date('2026-06-30T12:00:00.000Z'), new Date())
|
|
|
|
const res = await app.request('/api/site/stats/overview?from=2026-07-01&to=2026-07-01', { headers })
|
|
const body = (await res.json()) as { dataQuality: AdminDashboardOverviewStats['dataQuality'] }
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(body.dataQuality).toEqual({
|
|
missingBytesEvents: 1,
|
|
previousMissingBytesEvents: 1,
|
|
missingUploadBytesEvents: 1,
|
|
previousMissingUploadBytesEvents: 0,
|
|
missingDownloadBytesEvents: 0,
|
|
previousMissingDownloadBytesEvents: 1,
|
|
})
|
|
})
|
|
|
|
it('excludes expired and exhausted shares from the active-share total', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
await seedProLicense(db)
|
|
const { orgId, userId } = await seedStatsFixture(db)
|
|
const nowSec = Math.floor(Date.now() / 1000)
|
|
await db.run(sql`
|
|
INSERT INTO shares (id, token, kind, matter_id, org_id, creator_id, expires_at, download_limit, views, downloads, status, created_at)
|
|
VALUES
|
|
('expired-share', 'expired-share', 'landing', 'stats-file', ${orgId}, ${userId}, ${nowSec - 60}, NULL, 0, 0, 'active', ${nowSec}),
|
|
('exhausted-share', 'exhausted-share', 'direct', 'stats-file', ${orgId}, ${userId}, NULL, 1, 0, 1, 'active', ${nowSec})
|
|
`)
|
|
|
|
const res = await app.request('/api/site/stats/sharing', { headers })
|
|
const body = (await res.json()) as { summary: { activeShares: number } }
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(body.summary.activeShares).toBe(1)
|
|
})
|
|
|
|
it('returns traffic dashboard stats from audit-backed download events for Pro admins', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
await seedProLicense(db)
|
|
await seedStatsFixture(db)
|
|
|
|
const res = await app.request('/api/site/stats/traffic', { headers })
|
|
const body = (await res.json()) as {
|
|
summary: {
|
|
totalBytes: { value: number }
|
|
requestCount: { value: number }
|
|
issuedDownloads: number
|
|
blockedDownloads: number
|
|
}
|
|
sourceBreakdown: Array<{ name: string; bytes: number; requests: number }>
|
|
successTrend: Array<{ uploadSuccessRate: number; downloadSuccessRate: number }>
|
|
failureReasons: Array<{ name: string; value: number; percent: number }>
|
|
}
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(body.summary.totalBytes.value).toBe(896)
|
|
expect(body.summary.requestCount.value).toBe(3)
|
|
expect(body.summary.issuedDownloads).toBe(2)
|
|
expect(body.summary.blockedDownloads).toBe(0)
|
|
expect(body.sourceBreakdown).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ name: 'upload', bytes: 128, requests: 1 }),
|
|
expect.objectContaining({ name: 'landing_share', bytes: 512, requests: 1 }),
|
|
expect.objectContaining({ name: 'object_download', bytes: 256, requests: 1 }),
|
|
]),
|
|
)
|
|
expect(
|
|
body.successTrend.some((point) => point.uploadSuccessRate === 100 && point.downloadSuccessRate === 100),
|
|
).toBe(true)
|
|
expect(body.failureReasons).toEqual([])
|
|
})
|
|
|
|
it('separates operational health from transfer analytics', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
await seedProLicense(db)
|
|
const { orgId, bucketStart, eventMs } = await seedStatsFixture(db)
|
|
const previousBucket = new Date(bucketStart.getTime() - 3_600_000)
|
|
const reportAt = previousBucket.getTime() + 60_000
|
|
await db.run(sql`UPDATE download_tasks SET finished_at = updated_at WHERE id IN ('task-1', 'task-2')`)
|
|
await db.run(sql`
|
|
INSERT INTO cloud_traffic_reports
|
|
(id, org_id, period, source, source_id, event_id, bytes, status, created_at, updated_at)
|
|
VALUES ('operations-cloud-report', ${orgId}, '2026-07', 'object_download', 'stats-file',
|
|
'operations-cloud-report', 64, 'pending', ${reportAt}, ${reportAt})
|
|
`)
|
|
await rebuildAdminStatsHour(db, previousBucket, new Date(reportAt + 3_600_000))
|
|
await db.run(sql`
|
|
UPDATE cloud_traffic_reports
|
|
SET status = 'reported', updated_at = ${eventMs}
|
|
WHERE id = 'operations-cloud-report'
|
|
`)
|
|
await captureAdminStatsSnapshot(db, bucketStart, new Date(bucketStart.getTime() + 45 * 60_000))
|
|
await rebuildAdminStatsHour(db, bucketStart, new Date())
|
|
|
|
const res = await app.request('/api/site/stats/operations', { headers })
|
|
const body = (await res.json()) as {
|
|
summary: {
|
|
onlineDownloaders: number
|
|
backgroundJobFailureRate: number | null
|
|
remoteDownloadSuccessRate: number | null
|
|
}
|
|
backgroundJobOutcomes: Array<{ name: string; value: number }>
|
|
remoteDownloadOutcomes: Array<{ name: string; value: number }>
|
|
cloudReportStatus: Array<{ name: string; value: number }>
|
|
}
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(body.summary.onlineDownloaders).toBe(1)
|
|
expect(body.summary.backgroundJobFailureRate).toBe(100)
|
|
expect(body.summary.remoteDownloadSuccessRate).toBe(50)
|
|
expect(body.backgroundJobOutcomes).toEqual(
|
|
expect.arrayContaining([expect.objectContaining({ name: 'failed', value: 1 })]),
|
|
)
|
|
expect(body.remoteDownloadOutcomes).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ name: 'completed', value: 1 }),
|
|
expect.objectContaining({ name: 'failed', value: 1 }),
|
|
]),
|
|
)
|
|
expect(body.cloudReportStatus).toEqual([{ name: 'reported', value: 1, percent: 100 }])
|
|
const [{ count: mutableCounters }] = await db.all<{ count: number }>(sql`
|
|
SELECT COUNT(*) AS count FROM stats_rollups_hourly WHERE metric_key = 'traffic.report_sync'
|
|
`)
|
|
expect(mutableCounters).toBe(0)
|
|
})
|
|
|
|
it('applies dashboard ranges to sharing drill-down data', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
await seedProLicense(db)
|
|
await seedStatsFixture(db)
|
|
|
|
const currentSharingRes = await app.request('/api/site/stats/sharing', { headers })
|
|
const currentSharing = (await currentSharingRes.json()) as {
|
|
dataQuality: { unlocatedViews: number; unlocatedDownloads: number; unlocatedEvents: number }
|
|
summary: {
|
|
views: { value: number; change: number | null }
|
|
downloads: { value: number; change: number | null }
|
|
downloadsPer100Views: number | null
|
|
}
|
|
topShares: Array<{
|
|
token: string
|
|
views: number
|
|
downloads: number
|
|
viewPercent: number
|
|
downloadPercent: number
|
|
}>
|
|
}
|
|
const oldSharingRes = await app.request('/api/site/stats/sharing?from=2000-01-01&to=2000-01-02', { headers })
|
|
const oldSharing = (await oldSharingRes.json()) as {
|
|
summary: { views: { value: number }; downloads: { value: number } }
|
|
topShares: unknown[]
|
|
}
|
|
expect(currentSharingRes.status).toBe(200)
|
|
expect(currentSharing.summary.views.value).toBe(1)
|
|
expect(currentSharing.summary.downloads.value).toBe(1)
|
|
expect(currentSharing.dataQuality).toEqual({ unlocatedViews: 11, unlocatedDownloads: 3, unlocatedEvents: 14 })
|
|
expect(currentSharing.summary.views.change).toBeNull()
|
|
expect(currentSharing.summary.downloads.change).toBeNull()
|
|
expect(currentSharing.summary.downloadsPer100Views).toBeNull()
|
|
expect(currentSharing.topShares[0]).toMatchObject({
|
|
token: 'share-token-1',
|
|
views: 1,
|
|
downloads: 1,
|
|
viewPercent: 100,
|
|
downloadPercent: 100,
|
|
})
|
|
expect(oldSharing.summary.views.value).toBe(0)
|
|
expect(oldSharing.summary.downloads.value).toBe(0)
|
|
expect(oldSharing.topShares).toEqual([])
|
|
})
|
|
|
|
it('keeps deleted shares in historical rankings without reading their counters live', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
await seedProLicense(db)
|
|
await seedStatsFixture(db)
|
|
await db.run(sql`DELETE FROM shares WHERE id = 'share-1'`)
|
|
|
|
const res = await app.request('/api/site/stats/sharing', { headers })
|
|
const body = (await res.json()) as {
|
|
topShares: Array<{ id: string; token: string; name: string; status: string; views: number; downloads: number }>
|
|
}
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(body.topShares[0]).toMatchObject({
|
|
id: 'share-1',
|
|
token: '',
|
|
name: '已删除的分享',
|
|
status: 'deleted',
|
|
views: 1,
|
|
downloads: 1,
|
|
})
|
|
})
|
|
|
|
it('orders top share rankings by views before downloads', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
await seedProLicense(db)
|
|
const { orgId, userId, bucketStart, eventSec } = await seedStatsFixture(db)
|
|
const futureSec = eventSec + 7 * 24 * 60 * 60
|
|
|
|
await db.run(sql`
|
|
INSERT INTO shares (id, token, kind, matter_id, org_id, creator_id, expires_at, download_limit, views, downloads, status, created_at)
|
|
VALUES ('share-download-heavy', 'share-download-heavy', 'landing', 'stats-file', ${orgId}, ${userId}, ${futureSec}, 10, 0, 3, 'active', ${eventSec})
|
|
`)
|
|
await db.run(sql`
|
|
INSERT INTO activity_events (id, org_id, user_id, actor_type, action, target_type, target_id, target_name, metadata, created_at)
|
|
VALUES
|
|
('activity-download-heavy-1', ${orgId}, NULL, 'anonymous', 'share_download', 'share', 'share-download-heavy', 'report.pdf', '{"bytes":512,"source":"landing_share","anonymous":true}', ${eventSec}),
|
|
('activity-download-heavy-2', ${orgId}, NULL, 'anonymous', 'share_download', 'share', 'share-download-heavy', 'report.pdf', '{"bytes":512,"source":"landing_share","anonymous":true}', ${eventSec}),
|
|
('activity-download-heavy-3', ${orgId}, NULL, 'anonymous', 'share_download', 'share', 'share-download-heavy', 'report.pdf', '{"bytes":512,"source":"landing_share","anonymous":true}', ${eventSec})
|
|
`)
|
|
await rebuildAdminStatsHour(db, bucketStart, new Date())
|
|
await db.run(sql`
|
|
INSERT INTO stats_rollups_hourly
|
|
(id, bucket_start, org_id, metric_key, dimension_key, dimension_value,
|
|
count, bytes, unique_count, metadata, updated_at)
|
|
VALUES
|
|
('ranking-invalid-quality', ${bucketStart.getTime()}, ${orgId}, 'share.view', 'share_id',
|
|
'share-download-heavy', 1000, 0, 0, '{"version":3,"scope":"counters"}', ${bucketStart.getTime()})
|
|
`)
|
|
|
|
const res = await app.request('/api/site/stats/sharing', { headers })
|
|
const body = (await res.json()) as { topShares: Array<{ token: string; views: number; downloads: number }> }
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(body.topShares[0]).toMatchObject({ token: 'share-token-1', views: 1, downloads: 1 })
|
|
expect(body.topShares[1]).toMatchObject({ token: 'share-download-heavy', views: 0, downloads: 3 })
|
|
})
|
|
|
|
it('calculates top-share percentages against all matching shares before the top-eight limit', async () => {
|
|
const { app, db } = await createTestApp()
|
|
const headers = await adminHeaders(app)
|
|
await seedProLicense(db)
|
|
const { orgId, userId, bucketStart, eventSec } = await seedStatsFixture(db)
|
|
for (let index = 2; index <= 9; index += 1) {
|
|
const id = `share-percent-${index}`
|
|
await db.run(sql`
|
|
INSERT INTO shares (id, token, kind, matter_id, org_id, creator_id, expires_at, download_limit, views, downloads, status, created_at)
|
|
VALUES (${id}, ${id}, 'landing', 'stats-file', ${orgId}, ${userId}, NULL, NULL, 1, 0, 'active', ${eventSec})
|
|
`)
|
|
await db.run(sql`
|
|
INSERT INTO activity_events (id, org_id, user_id, actor_type, action, target_type, target_id, target_name, metadata, created_at)
|
|
VALUES (${`view-percent-${index}`}, ${orgId}, NULL, 'anonymous', 'share_view', 'share', ${id}, 'report.pdf',
|
|
'{"source":"landing_share"}', ${eventSec})
|
|
`)
|
|
}
|
|
await rebuildAdminStatsHour(db, bucketStart, new Date())
|
|
|
|
const res = await app.request('/api/site/stats/sharing', { headers })
|
|
const body = (await res.json()) as { topShares: Array<{ viewPercent: number }> }
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(body.topShares).toHaveLength(8)
|
|
expect(body.topShares.every((share) => share.viewPercent === 11.1)).toBe(true)
|
|
})
|
|
})
|
|
|
|
async function seedStatsFixture(db: Awaited<ReturnType<typeof createTestApp>>['db']) {
|
|
const generatedAt = Date.now()
|
|
const bucketStart = new Date(Math.floor(generatedAt / 3_600_000) * 3_600_000 - 3_600_000)
|
|
const now = bucketStart.getTime() + 30 * 60 * 1000
|
|
const nowSec = Math.floor(now / 1000)
|
|
const future = generatedAt + 7 * 24 * 60 * 60 * 1000
|
|
const futureSec = Math.floor(future / 1000)
|
|
const period = currentTrafficPeriod(new Date(generatedAt))
|
|
const [{ id: orgId }] = await db.all<{ id: string }>(
|
|
sql`SELECT id FROM organization WHERE metadata LIKE '%"type":"personal"%' LIMIT 1`,
|
|
)
|
|
const [{ id: userId }] = await db.all<{ id: string }>(sql`SELECT id FROM user LIMIT 1`)
|
|
|
|
await db.run(sql`UPDATE user SET created_at = ${now}, updated_at = ${now} WHERE id = ${userId}`)
|
|
await db.run(sql`UPDATE account SET created_at = ${now}, updated_at = ${now} WHERE user_id = ${userId}`)
|
|
await db.run(sql`UPDATE session SET created_at = ${now}, updated_at = ${now} WHERE user_id = ${userId}`)
|
|
|
|
await db.run(sql`
|
|
UPDATE org_quotas
|
|
SET used = 512, traffic_used = 256, traffic_period = ${period}
|
|
WHERE org_id = ${orgId}
|
|
`)
|
|
await db.run(sql`
|
|
INSERT INTO storages (id, bucket, endpoint, region, access_key, secret_key, file_path, custom_host, capacity, used, status, created_at, updated_at)
|
|
VALUES ('stats-storage', 'stats-bucket', 'https://s3.example', 'auto', 'AK', 'SK', '', '', 2048, 512, 'active', ${nowSec}, ${nowSec})
|
|
`)
|
|
await db.run(sql`
|
|
INSERT INTO matters (id, org_id, alias, name, type, size, dirtype, parent, object, storage_id, status, created_at, updated_at)
|
|
VALUES ('stats-file', ${orgId}, 'stats-file-alias', 'report.pdf', 'application/pdf', 512, 0, '', 'files/report.pdf', 'stats-storage', 'active', ${nowSec}, ${nowSec})
|
|
`)
|
|
await db.run(sql`
|
|
INSERT INTO shares (id, token, kind, matter_id, org_id, creator_id, expires_at, download_limit, views, downloads, status, created_at)
|
|
VALUES ('share-1', 'share-token-1', 'landing', 'stats-file', ${orgId}, ${userId}, ${futureSec}, 10, 12, 4, 'active', ${nowSec})
|
|
`)
|
|
await db.run(sql`
|
|
INSERT INTO site_invitations (id, email, token, invited_by, accepted_by, accepted_at, revoked_by, revoked_at, expires_at, created_at, updated_at)
|
|
VALUES ('invite-1', 'new@example.com', 'invite-token', ${userId}, NULL, NULL, NULL, NULL, ${futureSec}, ${nowSec}, ${nowSec})
|
|
`)
|
|
await db.run(sql`
|
|
INSERT INTO activity_events (id, org_id, user_id, action, target_type, target_id, target_name, metadata, created_at)
|
|
VALUES ('activity-1', ${orgId}, ${userId}, 'upload', 'file', 'stats-file', 'report.pdf', NULL, ${nowSec})
|
|
`)
|
|
await db.run(sql`
|
|
INSERT INTO activity_events (id, org_id, user_id, actor_type, action, target_type, target_id, target_name, metadata, created_at)
|
|
VALUES
|
|
('activity-upload-confirm', ${orgId}, ${userId}, 'user', 'upload_confirm', 'file', 'stats-file', 'report.pdf', '{"bytes":128,"source":"upload"}', ${nowSec}),
|
|
('activity-share-view', ${orgId}, NULL, 'anonymous', 'share_view', 'share', 'share-1', 'report.pdf', '{"source":"landing_share","anonymous":true}', ${nowSec}),
|
|
('activity-share-password', ${orgId}, NULL, 'anonymous', 'share_password_passed', 'share', 'share-1', 'report.pdf', '{"source":"landing_share","anonymous":true}', ${nowSec}),
|
|
('activity-share-download', ${orgId}, NULL, 'anonymous', 'share_download', 'share', 'share-1', 'report.pdf', '{"bytes":512,"source":"landing_share","anonymous":true}', ${nowSec}),
|
|
('activity-object-download', ${orgId}, ${userId}, 'user', 'object_download', 'file', 'stats-file', 'report.pdf', '{"bytes":256,"source":"object_download"}', ${nowSec}),
|
|
('stats-fixture-share-created', ${orgId}, NULL, 'system', 'stats_share_created', 'share', 'share-1', 'share-1', '{"kind":"landing","statsQuality":"exact"}', ${nowSec}),
|
|
('stats-fixture-task-completed', ${orgId}, NULL, 'system', 'stats_remote_download_finished', 'remote_download', 'task-1', 'task-1', '{"category":"direct","outcome":"completed","bytes":0,"statsQuality":"exact"}', ${nowSec}),
|
|
('stats-fixture-task-failed', ${orgId}, NULL, 'system', 'stats_remote_download_finished', 'remote_download', 'task-2', 'task-2', '{"category":"direct","outcome":"failed","bytes":0,"statsQuality":"exact"}', ${nowSec}),
|
|
('stats-fixture-job-failed', ${orgId}, NULL, 'system', 'stats_background_job_finished', 'background_job', 'job-1', 'job-1', '{"jobType":"extract","outcome":"failed","statsQuality":"exact"}', ${nowSec})
|
|
`)
|
|
await db.run(sql`
|
|
UPDATE activity_events
|
|
SET created_at = ${nowSec}, metadata = '{"provider":"credential","statsQuality":"exact"}'
|
|
WHERE action = 'stats_user_signup' AND target_id = ${userId}
|
|
`)
|
|
await db.run(sql`
|
|
INSERT INTO downloaders (id, name, token_hash, token_jti, status, enabled, version, hostname, platform, arch, engine, capabilities, max_concurrent_tasks, current_tasks, download_bps, upload_bps, free_disk_bytes, created_by, last_heartbeat_at, created_at, updated_at)
|
|
VALUES ('downloader-1', 'Downloader One', 'hash', 'jti', 'online', 1, '1.0.0', 'host', 'linux', 'x64', 'http', '[]', 2, 0, 0, 0, 1000, ${userId}, ${now}, ${now}, ${now})
|
|
`)
|
|
await db.run(sql`
|
|
INSERT INTO download_tasks (id, org_id, created_by_user_id, source_type, source_uri, display_name, target_folder, category, tags, assigned_downloader_id, status, error_code, error_message, created_at, updated_at)
|
|
VALUES
|
|
('task-1', ${orgId}, ${userId}, 'http', 'https://example.com/ok.bin', 'ok.bin', '', 'direct', '[]', 'downloader-1', 'completed', NULL, NULL, ${now}, ${now}),
|
|
('task-2', ${orgId}, ${userId}, 'http', 'https://example.com/bad.bin', 'bad.bin', '', 'direct', '[]', 'downloader-1', 'failed', 'network', 'Network error', ${now}, ${now})
|
|
`)
|
|
await db.run(sql`
|
|
INSERT INTO background_jobs (id, org_id, user_id, type, status, target_folder, target_path, metadata, input_bytes, output_bytes, processed_bytes, file_count, current_filename, error_message, result_metadata, retryable, cancelable, retried_from_job_id, created_at, updated_at, started_at, finished_at)
|
|
VALUES ('job-1', ${orgId}, ${userId}, 'extract', 'failed', '', '', NULL, 0, 0, 0, 0, NULL, 'bad zip', NULL, 1, 0, NULL, ${now}, ${now}, ${now}, ${now})
|
|
`)
|
|
|
|
await captureAdminStatsSnapshot(db, bucketStart, new Date(now))
|
|
await rebuildAdminStatsHour(db, bucketStart, new Date(generatedAt))
|
|
return { orgId, userId, bucketStart, eventMs: now, eventSec: nowSec }
|
|
}
|