| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { NextResponse } from 'next/server'; |
| import { getSession } from '@/lib/auth/session'; |
| import { getCoreDatabase } from '@/lib/vfs/adapters/sqlite-connection'; |
| import { getRequestStats, cleanupOldLogs } from '@/lib/logging/request-logger'; |
| import { getSystemDatabase } from '@/lib/auth/system-database'; |
| import { promises as fs } from 'fs'; |
| import path from 'path'; |
|
|
| |
| async function getVersion(): Promise<string> { |
| try { |
| const packagePath = path.join(process.cwd(), 'package.json'); |
| const content = await fs.readFile(packagePath, 'utf-8'); |
| const pkg = JSON.parse(content); |
| return pkg.version || 'unknown'; |
| } catch { |
| return 'unknown'; |
| } |
| } |
|
|
| |
| interface WhatsNewData { |
| version: string; |
| title: string; |
| highlights: string[]; |
| } |
|
|
| async function getWhatsNew(): Promise<WhatsNewData> { |
| const defaultData: WhatsNewData = { |
| version: 'unknown', |
| title: 'Welcome to OSW Studio', |
| highlights: [], |
| }; |
|
|
| try { |
| const whatsNewPath = path.join(process.cwd(), 'docs', 'WHATS_NEW.md'); |
| const content = await fs.readFile(whatsNewPath, 'utf-8'); |
|
|
| |
| const versionMatch = content.match(/^## v(\d+\.\d+\.\d+)\s*-\s*(.+)$/m); |
| if (!versionMatch) { |
| return defaultData; |
| } |
|
|
| const version = versionMatch[1]; |
| const title = versionMatch[2].trim(); |
|
|
| |
| const versionIndex = content.indexOf(versionMatch[0]); |
| const afterVersion = content.substring(versionIndex + versionMatch[0].length); |
| const nextSectionMatch = afterVersion.match(/^(?:## |---)/m); |
| const sectionContent = nextSectionMatch |
| ? afterVersion.substring(0, nextSectionMatch.index) |
| : afterVersion; |
|
|
| |
| const bulletRegex = /^[-*]\s+\*\*(.+?)\*\*\s*[-–]?\s*(.*)$/gm; |
| const highlights: string[] = []; |
| let match; |
|
|
| while ((match = bulletRegex.exec(sectionContent)) !== null && highlights.length < 4) { |
| |
| const boldTitle = match[1].trim(); |
| const description = match[2]?.trim(); |
| highlights.push(description ? `${boldTitle} - ${description}` : boldTitle); |
| } |
|
|
| |
| if (highlights.length === 0) { |
| const simpleBulletRegex = /^[-*]\s+(.+)$/gm; |
| while ((match = simpleBulletRegex.exec(sectionContent)) !== null && highlights.length < 4) { |
| const text = match[1].trim(); |
| |
| if (!text.match(/^\[.*\]\(.*\)$/)) { |
| highlights.push(text.replace(/\*\*/g, '')); |
| } |
| } |
| } |
|
|
| return { version, title, highlights }; |
| } catch { |
| return defaultData; |
| } |
| } |
|
|
| |
| async function getDirectorySize(dirPath: string): Promise<number> { |
| let totalSize = 0; |
|
|
| try { |
| const entries = await fs.readdir(dirPath, { withFileTypes: true }); |
|
|
| for (const entry of entries) { |
| const entryPath = path.join(dirPath, entry.name); |
|
|
| if (entry.isDirectory()) { |
| totalSize += await getDirectorySize(entryPath); |
| } else if (entry.isFile()) { |
| const stat = await fs.stat(entryPath); |
| totalSize += stat.size; |
| } |
| } |
| } catch { |
| |
| } |
|
|
| return totalSize; |
| } |
|
|
| |
| async function countDeploymentDatabases(): Promise<number> { |
| const deploymentsDir = path.join(process.cwd(), 'deployments'); |
| let count = 0; |
|
|
| try { |
| const entries = await fs.readdir(deploymentsDir, { withFileTypes: true }); |
|
|
| for (const entry of entries) { |
| if (entry.isDirectory()) { |
| const dbPath = path.join(deploymentsDir, entry.name, 'runtime.sqlite'); |
| try { |
| await fs.access(dbPath); |
| count++; |
| } catch { |
| |
| } |
| } |
| } |
| } catch { |
| |
| } |
|
|
| return count; |
| } |
|
|
| export async function GET() { |
| try { |
| |
| const session = await getSession(); |
| if (!session || !session.isAdmin) { |
| return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); |
| } |
|
|
| |
| const db = getCoreDatabase(); |
|
|
| |
| cleanupOldLogs(7); |
|
|
| |
| const [ |
| version, |
| projectCount, |
| templateCount, |
| skillCount, |
| fileCount, |
| publishedSiteCount, |
| deploymentsWithDb, |
| storageSize, |
| trafficStats, |
| whatsNew, |
| ] = await Promise.all([ |
| getVersion(), |
| Promise.resolve((db.prepare('SELECT COUNT(*) as count FROM projects').get() as { count: number }).count), |
| Promise.resolve((db.prepare('SELECT COUNT(*) as count FROM custom_templates').get() as { count: number }).count), |
| Promise.resolve((db.prepare('SELECT COUNT(*) as count FROM skills').get() as { count: number }).count), |
| Promise.resolve((db.prepare('SELECT COUNT(*) as count FROM files').get() as { count: number }).count), |
| Promise.resolve((db.prepare('SELECT COUNT(*) as count FROM deployments WHERE published_at IS NOT NULL').get() as { count: number }).count), |
| countDeploymentDatabases(), |
| getDirectorySize(path.join(process.cwd(), 'public', 'deployments')), |
| Promise.resolve(getRequestStats(24)), |
| getWhatsNew(), |
| ]); |
|
|
| |
| const topDeploymentsWithNames = trafficStats.topDeployments.map((deployment) => { |
| const deploymentInfo = db.prepare('SELECT name FROM deployments WHERE id = ?').get(deployment.deploymentId) as { name: string } | undefined; |
| return { |
| ...deployment, |
| deploymentName: deploymentInfo?.name || deployment.deploymentId.substring(0, 8), |
| }; |
| }); |
|
|
| |
| const recentProjects = db.prepare(` |
| SELECT id, name, description, updated_at as updatedAt |
| FROM projects |
| ORDER BY updated_at DESC |
| LIMIT 5 |
| `).all() as Array<{ id: string; name: string; description: string | null; updatedAt: string }>; |
|
|
| |
| const recentDeployments = db.prepare(` |
| SELECT id, name, slug, enabled, published_at as publishedAt, updated_at as updatedAt |
| FROM deployments |
| ORDER BY updated_at DESC |
| LIMIT 5 |
| `).all() as Array<{ id: string; name: string; slug: string; enabled: number; publishedAt: string | null; updatedAt: string }>; |
|
|
| |
| const memoryUsage = process.memoryUsage(); |
|
|
| |
| let userStats = { totalUsers: 0, activeUsers: 0, totalDeploymentRoutes: 0 }; |
| try { |
| const sysDb = getSystemDatabase(); |
| const total = sysDb.prepare('SELECT COUNT(*) as count FROM users').get() as { count: number }; |
| const active = sysDb.prepare('SELECT COUNT(*) as count FROM users WHERE active = 1').get() as { count: number }; |
| const routes = sysDb.prepare('SELECT COUNT(*) as count FROM deployment_routing').get() as { count: number }; |
| userStats = { totalUsers: total.count, activeUsers: active.count, totalDeploymentRoutes: routes.count }; |
| } catch { |
| |
| } |
|
|
| return NextResponse.json({ |
| system: { |
| version, |
| instanceId: process.env.INSTANCE_ID || null, |
| nodeVersion: process.version, |
| uptime: process.uptime(), |
| memoryUsed: memoryUsage.heapUsed, |
| memoryTotal: memoryUsage.heapTotal, |
| }, |
| users: userStats, |
| content: { |
| projects: projectCount, |
| templates: templateCount, |
| skills: skillCount, |
| totalFiles: fileCount, |
| }, |
| hosting: { |
| publishedDeployments: publishedSiteCount, |
| deploymentsWithDb, |
| storageUsed: storageSize, |
| }, |
| traffic: { |
| requestsLastHour: trafficStats.requestsLastHour, |
| requestsLastDay: trafficStats.requestsLastDay, |
| errorCount: trafficStats.errorCount, |
| topDeployments: topDeploymentsWithNames, |
| recentErrors: trafficStats.recentErrors, |
| }, |
| whatsNew, |
| recentProjects, |
| recentDeployments: recentDeployments.map(deployment => ({ |
| ...deployment, |
| enabled: Boolean(deployment.enabled), |
| })), |
| }); |
|
|
| } catch (error) { |
| console.error('[Dashboard API] Error:', error); |
| return NextResponse.json( |
| { error: 'Failed to fetch dashboard data' }, |
| { status: 500 } |
| ); |
| } |
| } |
|
|