File size: 9,029 Bytes
eeb9404 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 | /**
* Admin Dashboard API
*
* GET /api/admin/dashboard - Get all dashboard statistics in a single call
*
* Returns system info, content counts, hosting stats, and traffic metrics.
* Protected by admin authentication.
*/
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';
// Read version from package.json
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';
}
}
// Parse What's New from docs/WHATS_NEW.md
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');
// Find first version heading: ## v{version} - {title}
const versionMatch = content.match(/^## v(\d+\.\d+\.\d+)\s*-\s*(.+)$/m);
if (!versionMatch) {
return defaultData;
}
const version = versionMatch[1];
const title = versionMatch[2].trim();
// Get content after the version heading until the next ## or ---
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;
// Extract bullet points (lines starting with - or *)
const bulletRegex = /^[-*]\s+\*\*(.+?)\*\*\s*[-–]?\s*(.*)$/gm;
const highlights: string[] = [];
let match;
while ((match = bulletRegex.exec(sectionContent)) !== null && highlights.length < 4) {
// Combine bold title with description if present
const boldTitle = match[1].trim();
const description = match[2]?.trim();
highlights.push(description ? `${boldTitle} - ${description}` : boldTitle);
}
// If no bold bullet points found, try regular bullets
if (highlights.length === 0) {
const simpleBulletRegex = /^[-*]\s+(.+)$/gm;
while ((match = simpleBulletRegex.exec(sectionContent)) !== null && highlights.length < 4) {
const text = match[1].trim();
// Skip if it's a link-only line
if (!text.match(/^\[.*\]\(.*\)$/)) {
highlights.push(text.replace(/\*\*/g, ''));
}
}
}
return { version, title, highlights };
} catch {
return defaultData;
}
}
// Calculate directory size recursively
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 {
// Directory doesn't exist or not accessible
}
return totalSize;
}
// Count SQLite files in deployments directory (deployment databases)
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 {
// No database for this deployment
}
}
}
} catch {
// Deployments directory doesn't exist
}
return count;
}
export async function GET() {
try {
// Verify admin authentication
const session = await getSession();
if (!session || !session.isAdmin) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Get database connection
const db = getCoreDatabase();
// Clean up old logs (7 days retention)
cleanupOldLogs(7);
// Gather all statistics in parallel where possible
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(),
]);
// Get deployment names for top deployments
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),
};
});
// Get recent projects (last 5 by updated_at)
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 }>;
// Get recent deployments (last 5 by updated_at)
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 }>;
// System info
const memoryUsage = process.memoryUsage();
// Multi-tenant stats from system database
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 {
// System database may not exist in browser mode or single-user setups
}
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 }
);
}
}
|