|
| 1 | +import { Hono } from 'hono'; |
| 2 | +import type { AppContext } from '@/types/app.js'; |
| 3 | +import { prisma } from '@/db.js'; |
| 4 | + |
| 5 | +const admin = new Hono<AppContext>(); |
| 6 | + |
| 7 | +admin.use('*', async (c, next) => { |
| 8 | + if (!c.get('authenticated')) { |
| 9 | + return c.json({ error: 'Unauthorized' }, 401); |
| 10 | + } |
| 11 | + await next(); |
| 12 | +}); |
| 13 | + |
| 14 | +admin.get('/telemetry/system-info', async (c) => { |
| 15 | + const limit = Math.min(Number(c.req.query('limit') || 100), 500); |
| 16 | + const offset = Number(c.req.query('offset') || 0); |
| 17 | + |
| 18 | + const rows = await prisma.telemetrySystemInfo.findMany({ |
| 19 | + orderBy: { createdAt: 'desc' }, |
| 20 | + take: limit, |
| 21 | + skip: offset, |
| 22 | + }); |
| 23 | + |
| 24 | + return c.json({ |
| 25 | + data: rows.map((r) => ({ |
| 26 | + ...r, |
| 27 | + desktops: JSON.parse(r.desktops), |
| 28 | + screens: JSON.parse(r.screens), |
| 29 | + })), |
| 30 | + limit, |
| 31 | + offset, |
| 32 | + }); |
| 33 | +}); |
| 34 | + |
| 35 | +const GRANULARITIES = ['daily', 'weekly', 'monthly', 'yearly'] as const; |
| 36 | +type Granularity = typeof GRANULARITIES[number]; |
| 37 | + |
| 38 | +function mondayOfWeek(date: Date): string { |
| 39 | + const d = new Date(date); |
| 40 | + const day = d.getUTCDay(); |
| 41 | + const diff = day === 0 ? -6 : 1 - day; |
| 42 | + d.setUTCDate(d.getUTCDate() + diff); |
| 43 | + return d.toISOString().slice(0, 10); |
| 44 | +} |
| 45 | + |
| 46 | +function bucketKey(date: Date, granularity: Granularity): string { |
| 47 | + const iso = date.toISOString(); |
| 48 | + switch (granularity) { |
| 49 | + case 'daily': return iso.slice(0, 10); |
| 50 | + case 'weekly': return mondayOfWeek(date); |
| 51 | + case 'monthly': return iso.slice(0, 7) + '-01'; |
| 52 | + case 'yearly': return iso.slice(0, 4) + '-01-01'; |
| 53 | + } |
| 54 | +} |
| 55 | + |
| 56 | +admin.get('/telemetry/system-info/stats', async (c) => { |
| 57 | + const granularity = (c.req.query('granularity') || 'daily') as Granularity; |
| 58 | + if (!GRANULARITIES.includes(granularity)) { |
| 59 | + return c.json({ error: `Invalid granularity. Must be one of: ${GRANULARITIES.join(', ')}` }, 400); |
| 60 | + } |
| 61 | + |
| 62 | + const periods = Math.min(Number(c.req.query('periods') || 30), 365); |
| 63 | + const since = new Date(); |
| 64 | + switch (granularity) { |
| 65 | + case 'daily': since.setDate(since.getDate() - periods); break; |
| 66 | + case 'weekly': since.setDate(since.getDate() - periods * 7); break; |
| 67 | + case 'monthly': since.setMonth(since.getMonth() - periods); break; |
| 68 | + case 'yearly': since.setFullYear(since.getFullYear() - periods); break; |
| 69 | + } |
| 70 | + |
| 71 | + const rows = await prisma.telemetrySystemInfo.findMany({ |
| 72 | + where: { date: { gte: since } }, |
| 73 | + orderBy: { date: 'desc' }, |
| 74 | + }); |
| 75 | + |
| 76 | + const buckets = new Map<string, { |
| 77 | + activeUsers: Set<string>; |
| 78 | + operatingSystems: Map<string, number>; |
| 79 | + architectures: Map<string, number>; |
| 80 | + versions: Map<string, number>; |
| 81 | + displayProtocols: Map<string, number>; |
| 82 | + chassisTypes: Map<string, number>; |
| 83 | + }>(); |
| 84 | + |
| 85 | + for (const row of rows) { |
| 86 | + const key = bucketKey(row.date, granularity); |
| 87 | + if (!buckets.has(key)) { |
| 88 | + buckets.set(key, { |
| 89 | + activeUsers: new Set(), |
| 90 | + operatingSystems: new Map(), |
| 91 | + architectures: new Map(), |
| 92 | + versions: new Map(), |
| 93 | + displayProtocols: new Map(), |
| 94 | + chassisTypes: new Map(), |
| 95 | + }); |
| 96 | + } |
| 97 | + const bucket = buckets.get(key)!; |
| 98 | + bucket.activeUsers.add(row.userId); |
| 99 | + bucket.operatingSystems.set(row.operatingSystem, (bucket.operatingSystems.get(row.operatingSystem) || 0) + 1); |
| 100 | + bucket.architectures.set(row.architecture, (bucket.architectures.get(row.architecture) || 0) + 1); |
| 101 | + bucket.versions.set(row.vicinaeVersion, (bucket.versions.get(row.vicinaeVersion) || 0) + 1); |
| 102 | + bucket.displayProtocols.set(row.displayProtocol, (bucket.displayProtocols.get(row.displayProtocol) || 0) + 1); |
| 103 | + bucket.chassisTypes.set(row.chassisType, (bucket.chassisTypes.get(row.chassisType) || 0) + 1); |
| 104 | + } |
| 105 | + |
| 106 | + const data = [...buckets.entries()].map(([period, bucket]) => ({ |
| 107 | + period, |
| 108 | + activeUsers: bucket.activeUsers.size, |
| 109 | + operatingSystems: Object.fromEntries(bucket.operatingSystems), |
| 110 | + architectures: Object.fromEntries(bucket.architectures), |
| 111 | + versions: Object.fromEntries(bucket.versions), |
| 112 | + displayProtocols: Object.fromEntries(bucket.displayProtocols), |
| 113 | + chassisTypes: Object.fromEntries(bucket.chassisTypes), |
| 114 | + })); |
| 115 | + |
| 116 | + return c.json({ data, granularity, periods }); |
| 117 | +}); |
| 118 | + |
| 119 | +export default admin; |
0 commit comments