Backend changes: - Created /api/stats endpoint in server/routes/stats.js - Provides system overview (documents, pages, storage) - Shows document status breakdown - Lists recent uploads and documents - Calculates health score - Registered stats route in server/index.js Frontend changes: - Created StatsView.vue with responsive dashboard layout - Added 4 overview metric cards (documents, pages, storage, health) - Document status breakdown section - Recent uploads chart (last 7 days) - Recent documents list with click-to-view - Added /stats route to router.js - Added Stats button to HomeView header navigation Features: - Real-time statistics with refresh button - Loading and error states - Responsive grid layout - Click on recent docs to view details - Formatted timestamps and file sizes - Health score calculation (success vs failed ratio) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
142 lines
3.7 KiB
JavaScript
142 lines
3.7 KiB
JavaScript
/**
|
|
* Statistics API
|
|
* Provides system statistics and analytics
|
|
*/
|
|
|
|
import express from 'express';
|
|
import { getDb } from '../db/db.js';
|
|
import { readdirSync, statSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { loggers } from '../utils/logger.js';
|
|
|
|
const router = express.Router();
|
|
const logger = loggers.app.child('Stats');
|
|
|
|
/**
|
|
* GET /api/stats
|
|
* Get system statistics
|
|
*/
|
|
router.get('/', async (req, res) => {
|
|
try {
|
|
const db = getDb();
|
|
|
|
// Total documents
|
|
const { totalDocuments } = db.prepare(`
|
|
SELECT COUNT(*) as totalDocuments FROM documents
|
|
`).get();
|
|
|
|
// Total pages
|
|
const { totalPages } = db.prepare(`
|
|
SELECT COUNT(*) as totalPages FROM document_pages
|
|
`).get();
|
|
|
|
// Documents by status
|
|
const documentsByStatus = db.prepare(`
|
|
SELECT status, COUNT(*) as count
|
|
FROM documents
|
|
GROUP BY status
|
|
`).all();
|
|
|
|
// Storage used (calculate from uploads directory)
|
|
let storageUsed = 0;
|
|
try {
|
|
const uploadsDir = join(process.cwd(), '../uploads');
|
|
function calculateDirSize(dir) {
|
|
let size = 0;
|
|
const files = readdirSync(dir, { withFileTypes: true });
|
|
for (const file of files) {
|
|
const filePath = join(dir, file.name);
|
|
if (file.isDirectory()) {
|
|
size += calculateDirSize(filePath);
|
|
} else {
|
|
try {
|
|
size += statSync(filePath).size;
|
|
} catch (e) {
|
|
// Skip files that can't be read
|
|
}
|
|
}
|
|
}
|
|
return size;
|
|
}
|
|
storageUsed = calculateDirSize(uploadsDir);
|
|
} catch (err) {
|
|
logger.warn('Failed to calculate storage:', err);
|
|
}
|
|
|
|
// Recent uploads (last 7 days)
|
|
const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
|
|
const recentUploads = db.prepare(`
|
|
SELECT DATE(created_at / 1000, 'unixepoch') as date, COUNT(*) as count
|
|
FROM documents
|
|
WHERE created_at > ?
|
|
GROUP BY DATE(created_at / 1000, 'unixepoch')
|
|
ORDER BY date ASC
|
|
`).all(sevenDaysAgo);
|
|
|
|
// Recent documents
|
|
const recentDocuments = db.prepare(`
|
|
SELECT id, title, status, created_at, page_count
|
|
FROM documents
|
|
ORDER BY created_at DESC
|
|
LIMIT 5
|
|
`).all();
|
|
|
|
// Document health (issues)
|
|
const { failedCount } = db.prepare(`
|
|
SELECT COUNT(*) as failedCount
|
|
FROM documents
|
|
WHERE status = 'failed'
|
|
`).get();
|
|
|
|
const { processingCount } = db.prepare(`
|
|
SELECT COUNT(*) as processingCount
|
|
FROM documents
|
|
WHERE status IN ('processing', 'pending', 'queued')
|
|
`).get();
|
|
|
|
res.json({
|
|
overview: {
|
|
totalDocuments,
|
|
totalPages,
|
|
storageUsed,
|
|
storageUsedFormatted: formatBytes(storageUsed)
|
|
},
|
|
documentsByStatus: documentsByStatus.reduce((acc, { status, count }) => {
|
|
acc[status] = count;
|
|
return acc;
|
|
}, {}),
|
|
recentUploads,
|
|
recentDocuments: recentDocuments.map(doc => ({
|
|
id: doc.id,
|
|
title: doc.title,
|
|
status: doc.status,
|
|
createdAt: doc.created_at,
|
|
pageCount: doc.page_count
|
|
})),
|
|
health: {
|
|
failedCount,
|
|
processingCount,
|
|
healthScore: totalDocuments > 0
|
|
? Math.round(((totalDocuments - failedCount) / totalDocuments) * 100)
|
|
: 100
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Stats retrieval error:', error);
|
|
res.status(500).json({
|
|
error: 'Failed to retrieve statistics',
|
|
message: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
function formatBytes(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
}
|
|
|
|
export default router;
|