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 }
    );
  }
}