feat: doc status & share status (#14426)

#### PR Dependency Tree


* **PR #14426** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Admin dashboard: view workspace analytics (storage, sync activity, top
shared links) with charts and configurable windows.
* Document analytics tab: see total/unique/guest views and trends over
selectable time windows.
* Last-accessed members: view who last accessed a document, with
pagination.
* Shared links analytics: browse and paginate all shared links with
view/unique/guest metrics and share URLs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-02-13 01:01:29 +08:00
committed by GitHub
parent b46bf91575
commit b4be9118ad
44 changed files with 5701 additions and 86 deletions
@@ -1,3 +1,4 @@
import { PrismaClient } from '@prisma/client';
import test, { type ExecutionContext } from 'ava';
import { io, type Socket as SocketIOClient } from 'socket.io-client';
import { Doc, encodeStateAsUpdate } from 'yjs';
@@ -146,6 +147,44 @@ function createYjsUpdateBase64() {
return Buffer.from(update).toString('base64');
}
async function ensureSyncActiveUsersTable(db: PrismaClient) {
await db.$executeRawUnsafe(`
CREATE TABLE IF NOT EXISTS sync_active_users_minutely (
minute_ts TIMESTAMPTZ(3) NOT NULL PRIMARY KEY,
active_users INTEGER NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ(3) NOT NULL DEFAULT NOW()
)
`);
}
async function latestActiveUsers(db: PrismaClient) {
const rows = await db.$queryRaw<{ activeUsers: number }[]>`
SELECT active_users::integer AS "activeUsers"
FROM sync_active_users_minutely
ORDER BY minute_ts DESC
LIMIT 1
`;
if (!rows[0]) {
return null;
}
return Number(rows[0].activeUsers);
}
async function waitForActiveUsers(db: PrismaClient, expected: number) {
const deadline = Date.now() + WS_TIMEOUT_MS;
while (Date.now() < deadline) {
const current = await latestActiveUsers(db);
if (current === expected) {
return;
}
await new Promise(resolve => setTimeout(resolve, 100));
}
throw new Error(`Timed out waiting active users=${expected}`);
}
let app: TestingApp;
let url: string;
@@ -461,3 +500,22 @@ test('space:join-awareness should reject clientVersion<0.25.0', async t => {
socket.disconnect();
}
});
test('active users metric should dedupe multiple sockets for one user', async t => {
const db = app.get(PrismaClient);
await ensureSyncActiveUsersTable(db);
const { cookieHeader } = await login(app);
const first = createClient(url, cookieHeader);
const second = createClient(url, cookieHeader);
try {
await Promise.all([waitForConnect(first), waitForConnect(second)]);
await waitForActiveUsers(db, 1);
t.pass();
} finally {
first.disconnect();
second.disconnect();
await Promise.all([waitForDisconnect(first), waitForDisconnect(second)]);
}
});