From d03b10697cb0e99652a179da3b445cb8a1bbe5bc Mon Sep 17 00:00:00 2001 From: ggq-admin Date: Mon, 20 Oct 2025 03:49:39 +0200 Subject: [PATCH] Add statistics dashboard feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- client/src/router.js | 5 + client/src/views/HomeView.vue | 6 + client/src/views/StatsView.vue | 235 +++++++++++++++++++++++++++++++++ server/index.js | 2 + server/routes/stats.js | 142 ++++++++++++++++++++ 5 files changed, 390 insertions(+) create mode 100644 client/src/views/StatsView.vue create mode 100644 server/routes/stats.js diff --git a/client/src/router.js b/client/src/router.js index af80867..8460661 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -27,6 +27,11 @@ const router = createRouter({ path: '/jobs', name: 'jobs', component: () => import('./views/JobsView.vue') + }, + { + path: '/stats', + name: 'stats', + component: () => import('./views/StatsView.vue') } ] }) diff --git a/client/src/views/HomeView.vue b/client/src/views/HomeView.vue index 83d5c27..db11023 100644 --- a/client/src/views/HomeView.vue +++ b/client/src/views/HomeView.vue @@ -17,6 +17,12 @@
+ +
+

System Statistics

+

Overview of your NaviDocs system

+
+
+ + + + +
+
+

Loading statistics...

+
+ + +
+ + + +

Failed to load statistics

+

{{ error }}

+
+ + +
+ +
+ +
+
+
+ + + +
+
+
{{ stats.overview.totalDocuments }}
+
Total Documents
+
+ + +
+
+
+ + + +
+
+
{{ stats.overview.totalPages }}
+
Total Pages
+
+ + +
+
+
+ + + +
+
+
{{ stats.overview.storageUsedFormatted }}
+
Storage Used
+
+ + +
+
+
+ + + +
+
+
{{ stats.health.healthScore }}%
+
Health Score
+
+
+ + +
+

Documents by Status

+
+
+
{{ count }}
+
{{ status }}
+
+
+
{{ stats.health.failedCount }}
+
Failed
+
+
+
{{ stats.health.processingCount }}
+
Processing
+
+
+
+ + +
+ +
+

Recent Uploads (Last 7 Days)

+
+
+ {{ formatDate(upload.date) }} + {{ upload.count }} uploads +
+
+
+ No uploads in the last 7 days +
+
+ + +
+

Recent Documents

+
+
+
+
{{ doc.title }}
+
{{ formatTimestamp(doc.createdAt) }}
+
+ + {{ doc.status }} + +
+
+
+ No documents yet +
+
+
+
+ + + + + diff --git a/server/index.js b/server/index.js index e8ae1bd..aa5810b 100644 --- a/server/index.js +++ b/server/index.js @@ -89,6 +89,7 @@ import jobsRoutes from './routes/jobs.js'; import searchRoutes from './routes/search.js'; import documentsRoutes from './routes/documents.js'; import imagesRoutes from './routes/images.js'; +import statsRoutes from './routes/stats.js'; // API routes app.use('/api/upload/quick-ocr', quickOcrRoutes); @@ -96,6 +97,7 @@ app.use('/api/upload', uploadRoutes); app.use('/api/jobs', jobsRoutes); app.use('/api/search', searchRoutes); app.use('/api/documents', documentsRoutes); +app.use('/api/stats', statsRoutes); app.use('/api', imagesRoutes); // Error handling diff --git a/server/routes/stats.js b/server/routes/stats.js new file mode 100644 index 0000000..79efa45 --- /dev/null +++ b/server/routes/stats.js @@ -0,0 +1,142 @@ +/** + * 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;