diff --git a/logs/sessions/2025-07-15T02-56-56-617Z/.4124dcd56ceea5c9f7cd347e6b5c95260ffd2de7-audit.json b/logs/sessions/2025-07-15T02-56-56-617Z/.4124dcd56ceea5c9f7cd347e6b5c95260ffd2de7-audit.json new file mode 100644 index 0000000..19dd87d --- /dev/null +++ b/logs/sessions/2025-07-15T02-56-56-617Z/.4124dcd56ceea5c9f7cd347e6b5c95260ffd2de7-audit.json @@ -0,0 +1,15 @@ +{ + "keep": { + "days": true, + "amount": 30 + }, + "auditLog": "/srv/projects/jasonpoage.com/expressjs-blog/logs/sessions/2025-07-15T02-56-56-617Z/.4124dcd56ceea5c9f7cd347e6b5c95260ffd2de7-audit.json", + "files": [ + { + "date": 1752548216657, + "name": "/srv/projects/jasonpoage.com/expressjs-blog/logs/sessions/2025-07-15T02-56-56-617Z/session-2025-07-14.log", + "hash": "e9bf52fdaab6da908abb599fb21a6be696386189c1e172b9bb5246aeeca3b2de" + } + ], + "hashType": "sha256" +} \ No newline at end of file diff --git a/logs/sessions/2025-07-15T03-05-11-391Z/.e3983f43b81e64287da532331f367fc4071c9981-audit.json b/logs/sessions/2025-07-15T03-05-11-391Z/.e3983f43b81e64287da532331f367fc4071c9981-audit.json new file mode 100644 index 0000000..dbc2054 --- /dev/null +++ b/logs/sessions/2025-07-15T03-05-11-391Z/.e3983f43b81e64287da532331f367fc4071c9981-audit.json @@ -0,0 +1,15 @@ +{ + "keep": { + "days": true, + "amount": 30 + }, + "auditLog": "/srv/projects/jasonpoage.com/expressjs-blog/logs/sessions/2025-07-15T03-05-11-391Z/.e3983f43b81e64287da532331f367fc4071c9981-audit.json", + "files": [ + { + "date": 1752548711433, + "name": "/srv/projects/jasonpoage.com/expressjs-blog/logs/sessions/2025-07-15T03-05-11-391Z/session-2025-07-14.log", + "hash": "0564270eb11d320611b60a8b0aaaa707df77a491a55fb4c4f4473b74397da6ba" + } + ], + "hashType": "sha256" +} \ No newline at end of file diff --git a/logs/sessions/2025-07-15T03-05-13-725Z/.99292ee5a0171483673b5876e8433ea9af39888f-audit.json b/logs/sessions/2025-07-15T03-05-13-725Z/.99292ee5a0171483673b5876e8433ea9af39888f-audit.json new file mode 100644 index 0000000..d32d556 --- /dev/null +++ b/logs/sessions/2025-07-15T03-05-13-725Z/.99292ee5a0171483673b5876e8433ea9af39888f-audit.json @@ -0,0 +1,15 @@ +{ + "keep": { + "days": true, + "amount": 30 + }, + "auditLog": "/srv/projects/jasonpoage.com/expressjs-blog/logs/sessions/2025-07-15T03-05-13-725Z/.99292ee5a0171483673b5876e8433ea9af39888f-audit.json", + "files": [ + { + "date": 1752548713761, + "name": "/srv/projects/jasonpoage.com/expressjs-blog/logs/sessions/2025-07-15T03-05-13-725Z/session-2025-07-14.log", + "hash": "c97fe6758199443ea238bf1775ea67e9a6fd302358510ead38830b3bfba6b166" + } + ], + "hashType": "sha256" +} \ No newline at end of file diff --git a/logs/sessions/2025-07-15T03-25-57-454Z/.c2d341cd4b53c5d7ea0f1f43960bbbaf0fbfa6e8-audit.json b/logs/sessions/2025-07-15T03-25-57-454Z/.c2d341cd4b53c5d7ea0f1f43960bbbaf0fbfa6e8-audit.json new file mode 100644 index 0000000..de20f74 --- /dev/null +++ b/logs/sessions/2025-07-15T03-25-57-454Z/.c2d341cd4b53c5d7ea0f1f43960bbbaf0fbfa6e8-audit.json @@ -0,0 +1,15 @@ +{ + "keep": { + "days": true, + "amount": 30 + }, + "auditLog": "/srv/projects/jasonpoage.com/expressjs-blog/logs/sessions/2025-07-15T03-25-57-454Z/.c2d341cd4b53c5d7ea0f1f43960bbbaf0fbfa6e8-audit.json", + "files": [ + { + "date": 1752549957485, + "name": "/srv/projects/jasonpoage.com/expressjs-blog/logs/sessions/2025-07-15T03-25-57-454Z/session-2025-07-14.log", + "hash": "19219eb3b29e5205d57440a2d1e6377650565e66fb9babe9131f3f0e398c7136" + } + ], + "hashType": "sha256" +} \ No newline at end of file diff --git a/logs/sessions/2025-07-15T03-27-59-466Z/.4d1837ccb55df117edaf40a8ea52e7d805053895-audit.json b/logs/sessions/2025-07-15T03-27-59-466Z/.4d1837ccb55df117edaf40a8ea52e7d805053895-audit.json new file mode 100644 index 0000000..4f78681 --- /dev/null +++ b/logs/sessions/2025-07-15T03-27-59-466Z/.4d1837ccb55df117edaf40a8ea52e7d805053895-audit.json @@ -0,0 +1,15 @@ +{ + "keep": { + "days": true, + "amount": 30 + }, + "auditLog": "/srv/projects/jasonpoage.com/expressjs-blog/logs/sessions/2025-07-15T03-27-59-466Z/.4d1837ccb55df117edaf40a8ea52e7d805053895-audit.json", + "files": [ + { + "date": 1752550079507, + "name": "/srv/projects/jasonpoage.com/expressjs-blog/logs/sessions/2025-07-15T03-27-59-466Z/session-2025-07-14.log", + "hash": "7f794a6fad9d608287397b1f5f2f0793cb9c8be536c88904eee614eb0560bd63" + } + ], + "hashType": "sha256" +} \ No newline at end of file diff --git a/logs/sessions/2025-07-15T03-28-59-461Z/.dd471705688f18d97d1b8da76bb9d23e6bdef615-audit.json b/logs/sessions/2025-07-15T03-28-59-461Z/.dd471705688f18d97d1b8da76bb9d23e6bdef615-audit.json new file mode 100644 index 0000000..2ada9ed --- /dev/null +++ b/logs/sessions/2025-07-15T03-28-59-461Z/.dd471705688f18d97d1b8da76bb9d23e6bdef615-audit.json @@ -0,0 +1,15 @@ +{ + "keep": { + "days": true, + "amount": 30 + }, + "auditLog": "/srv/projects/jasonpoage.com/expressjs-blog/logs/sessions/2025-07-15T03-28-59-461Z/.dd471705688f18d97d1b8da76bb9d23e6bdef615-audit.json", + "files": [ + { + "date": 1752550139500, + "name": "/srv/projects/jasonpoage.com/expressjs-blog/logs/sessions/2025-07-15T03-28-59-461Z/session-2025-07-14.log", + "hash": "ccfb43a01c5cacce778cc7874996adfd8265e914c12d3531acf94a305cc0d0e8" + } + ], + "hashType": "sha256" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a1bebb0..fa3795a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "js-beautify": "^1.15.4", "marked": "^15.0.11", "morgan": "^1.10.0", + "node-disk-info": "^1.3.0", "node-fetch": "^2.7.0", "nodemailer": "^7.0.3", "nodemon": "^3.1.10", @@ -4908,6 +4909,18 @@ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT" }, + "node_modules/node-disk-info": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/node-disk-info/-/node-disk-info-1.3.0.tgz", + "integrity": "sha512-NEx858vJZ0AoBtmD/ChBIHLjFTF28xCsDIgmFl4jtGKsvlUx9DU/OrMDjvj3qp/E4hzLN0HvTg7eJx5XFQvbeg==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.2" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", diff --git a/package.json b/package.json index 5a94c8c..05be650 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "js-beautify": "^1.15.4", "marked": "^15.0.11", "morgan": "^1.10.0", + "node-disk-info": "^1.3.0", "node-fetch": "^2.7.0", "nodemailer": "^7.0.3", "nodemon": "^3.1.10", diff --git a/public/js/logs.js b/public/js/logs.js index f87fe18..e92c9a0 100644 --- a/public/js/logs.js +++ b/public/js/logs.js @@ -148,7 +148,7 @@ params.append("limit", this.limit); try { - const res = await fetch("/logs", { + const res = await fetch("/admin/logs", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", diff --git a/public/locales/en/consent.json b/public/locales/en/consent.json new file mode 100644 index 0000000..5752e5b --- /dev/null +++ b/public/locales/en/consent.json @@ -0,0 +1,6 @@ +{ + "consentTitle": "We value your privacy", + "consentMessage": "This site uses cookies to improve your experience.", + "acceptButton": "Accept", + "rejectButton": "Reject" +} diff --git a/src/routes/admin/dskManager.js b/src/routes/admin/dskManager.js new file mode 100644 index 0000000..61471c1 --- /dev/null +++ b/src/routes/admin/dskManager.js @@ -0,0 +1,128 @@ +// routes/admin.js +const express = require("express"); +const { diskSpaceMonitor } = require("../utils/logging"); + +const router = express.Router(); + +// Middleware to check admin authentication (implement as needed) +const requireAdmin = (req, res, next) => { + // Add your admin authentication logic here + // For example, check session, JWT token, etc. + if (req.session && req.session.isAdmin) { + next(); + } else { + res.status(403).json({ error: "Admin access required" }); + } +}; + +// Apply admin middleware to all routes +router.use(requireAdmin); + +// Apply disk space monitoring middleware +router.use(diskSpaceMonitor.adminNotificationMiddleware()); + +// Get disk space status +router.get("/disk-space/status", diskSpaceMonitor.getStatusEndpoint()); + +// Perform manual cleanup +router.post("/disk-space/cleanup", diskSpaceMonitor.manualCleanupEndpoint()); + +// Get disk space configuration +router.get("/disk-space/config", (req, res) => { + res.json({ + success: true, + data: { + thresholds: { + warning: diskSpaceMonitor.options.warningThreshold, + critical: diskSpaceMonitor.options.criticalThreshold, + emergency: diskSpaceMonitor.options.emergencyThreshold, + }, + cleanup: { + normalCleanupDays: diskSpaceMonitor.options.normalCleanupDays, + warningCleanupDays: diskSpaceMonitor.options.warningCleanupDays, + criticalCleanupDays: diskSpaceMonitor.options.criticalCleanupDays, + emergencyCleanupDays: diskSpaceMonitor.options.emergencyCleanupDays, + }, + monitoring: { + interval: diskSpaceMonitor.options.monitoringInterval, + maxLogDirectorySize: diskSpaceMonitor.options.maxLogDirectorySize, + }, + }, + }); +}); + +// Update disk space configuration +router.put("/disk-space/config", (req, res) => { + try { + const { thresholds, cleanup, monitoring } = req.body; + + if (thresholds) { + Object.assign(diskSpaceMonitor.options, thresholds); + } + + if (cleanup) { + Object.assign(diskSpaceMonitor.options, cleanup); + } + + if (monitoring) { + Object.assign(diskSpaceMonitor.options, monitoring); + // Restart monitoring with new interval + diskSpaceMonitor.startMonitoring(); + } + + res.json({ + success: true, + message: "Configuration updated successfully", + data: diskSpaceMonitor.options, + }); + } catch (error) { + res.status(500).json({ + success: false, + error: "Failed to update configuration", + details: error.message, + }); + } +}); + +// Get log directory contents +router.get("/logs/directory", async (req, res) => { + try { + const fs = require("fs").promises; + const path = require("path"); + + const logDir = path.join(__dirname, "..", "..", "logs"); + const getDirectoryInfo = async (dir) => { + const items = await fs.readdir(dir); + const info = []; + + for (const item of items) { + const itemPath = path.join(dir, item); + const stats = await fs.stat(itemPath); + + info.push({ + name: item, + type: stats.isDirectory() ? "directory" : "file", + size: stats.size, + modified: stats.mtime, + relativePath: path.relative(logDir, itemPath), + }); + } + + return info.sort((a, b) => b.modified - a.modified); + }; + + const contents = await getDirectoryInfo(logDir); + res.json({ + success: true, + data: contents, + }); + } catch (error) { + res.status(500).json({ + success: false, + error: "Failed to get log directory contents", + details: error.message, + }); + } +}); + +module.exports = router; diff --git a/src/routes/index.js b/src/routes/index.js index 40d1d4f..56ed618 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -26,7 +26,7 @@ const faviconsPath = path.join(__dirname, "..", "..", "public", "favicons"); const faviconFile = path.resolve(faviconsPath, "favicon.ico"); -router.use(securedMiddleware, securedRoutes); +router.use("/admin", securedMiddleware, securedRoutes); router.get("/error", errorPage); // Landing page after error is logged diff --git a/src/routes/secured/logs.js b/src/routes/secured/logs.js index 06c6e34..85bbac0 100644 --- a/src/routes/secured/logs.js +++ b/src/routes/secured/logs.js @@ -15,7 +15,7 @@ const db = new Database(dbPath, { readonly: true }); router.get("/logs", (req, res) => { - res.renderWithBaseContext("pages/logs", { + res.renderWithBaseContext("admin-pages/logs", { showSidebar: false, showFooter: false, }); diff --git a/src/utils/diskSpaceMonitor.js b/src/utils/diskSpaceMonitor.js new file mode 100644 index 0000000..1413d1c --- /dev/null +++ b/src/utils/diskSpaceMonitor.js @@ -0,0 +1,290 @@ +// utils/diskSpaceMonitor.js +const fs = require("fs"); +const path = require("path"); +const { promisify } = require("util"); +const statvfs = promisify(require("statvfs")); + +class DiskSpaceMonitor { + constructor(logDir, options = {}) { + this.logDir = logDir; + this.options = { + // Disk space thresholds (in GB) + warningThreshold: options.warningThreshold || 5, + criticalThreshold: options.criticalThreshold || 2, + emergencyThreshold: options.emergencyThreshold || 1, + + // Cleanup policies + normalCleanupDays: options.normalCleanupDays || 30, + warningCleanupDays: options.warningCleanupDays || 14, + criticalCleanupDays: options.criticalCleanupDays || 7, + emergencyCleanupDays: options.emergencyCleanupDays || 3, + + // Monitoring interval (in minutes) + monitoringInterval: options.monitoringInterval || 30, + + // Maximum log directory size (in GB) + maxLogDirectorySize: options.maxLogDirectorySize || 10, + }; + + this.monitoringTimer = null; + this.lastCleanupTime = null; + this.diskSpaceStatus = { + availableGB: 0, + usedGB: 0, + logDirectorySizeGB: 0, + status: "normal", // normal, warning, critical, emergency + lastCheck: null, + autoCleanupPerformed: false, + }; + } + + async getDiskSpace() { + try { + const stats = await statvfs(this.logDir); + const blockSize = stats.f_frsize || stats.f_bsize; + const totalBytes = stats.f_blocks * blockSize; + const freeBytes = stats.f_bavail * blockSize; + const usedBytes = totalBytes - freeBytes; + + return { + totalGB: totalBytes / 1024 ** 3, + freeGB: freeBytes / 1024 ** 3, + usedGB: usedBytes / 1024 ** 3, + }; + } catch (error) { + console.error("Error getting disk space:", error); + return null; + } + } + + async getDirectorySize(dir) { + let size = 0; + + const calculateSize = async (currentDir) => { + try { + const items = await fs.promises.readdir(currentDir); + + for (const item of items) { + const itemPath = path.join(currentDir, item); + const stats = await fs.promises.stat(itemPath); + + if (stats.isDirectory()) { + await calculateSize(itemPath); + } else { + size += stats.size; + } + } + } catch (error) { + console.error(`Error calculating size for ${currentDir}:`, error); + } + }; + + await calculateSize(dir); + return size / 1024 ** 3; // Convert to GB + } + + async checkDiskSpace() { + const diskSpace = await this.getDiskSpace(); + if (!diskSpace) return this.diskSpaceStatus; + + const logDirectorySize = await this.getDirectorySize(this.logDir); + + this.diskSpaceStatus = { + availableGB: diskSpace.freeGB, + usedGB: diskSpace.usedGB, + logDirectorySizeGB: logDirectorySize, + lastCheck: new Date().toISOString(), + autoCleanupPerformed: false, + }; + + // Determine status based on available space + if (diskSpace.freeGB <= this.options.emergencyThreshold) { + this.diskSpaceStatus.status = "emergency"; + await this.performEmergencyCleanup(); + } else if (diskSpace.freeGB <= this.options.criticalThreshold) { + this.diskSpaceStatus.status = "critical"; + await this.performCleanup(this.options.criticalCleanupDays); + } else if (diskSpace.freeGB <= this.options.warningThreshold) { + this.diskSpaceStatus.status = "warning"; + await this.performCleanup(this.options.warningCleanupDays); + } else if (logDirectorySize > this.options.maxLogDirectorySize) { + this.diskSpaceStatus.status = "warning"; + await this.performCleanup(this.options.warningCleanupDays); + } else { + this.diskSpaceStatus.status = "normal"; + } + + return this.diskSpaceStatus; + } + + async performCleanup(retentionDays) { + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - retentionDays); + + let deletedFiles = 0; + let freedSpace = 0; + + // Clean up session directories + const sessionsDir = path.join(this.logDir, "sessions"); + if (fs.existsSync(sessionsDir)) { + const sessions = await fs.promises.readdir(sessionsDir); + + for (const sessionFolder of sessions) { + const sessionPath = path.join(sessionsDir, sessionFolder); + const stats = await fs.promises.stat(sessionPath); + + if (stats.isDirectory() && stats.mtime < cutoffDate) { + const sizeBeforeDelete = await this.getDirectorySize(sessionPath); + await fs.promises.rm(sessionPath, { recursive: true, force: true }); + freedSpace += sizeBeforeDelete; + deletedFiles++; + } + } + } + + // Clean up old log files in other directories + const logDirectories = [ + "info", + "error", + "warn", + "debug", + "notice", + "functions", + ]; + + for (const dir of logDirectories) { + const dirPath = path.join(this.logDir, dir); + if (fs.existsSync(dirPath)) { + const files = await fs.promises.readdir(dirPath); + + for (const file of files) { + const filePath = path.join(dirPath, file); + const stats = await fs.promises.stat(filePath); + + if (stats.isFile() && stats.mtime < cutoffDate) { + freedSpace += stats.size / 1024 ** 3; + await fs.promises.unlink(filePath); + deletedFiles++; + } + } + } + } + + this.lastCleanupTime = new Date().toISOString(); + this.diskSpaceStatus.autoCleanupPerformed = true; + + console.log( + `Cleanup completed: ${deletedFiles} files/directories deleted, ${freedSpace.toFixed(2)} GB freed` + ); + return { deletedFiles, freedSpace }; + } + + async performEmergencyCleanup() { + console.log("Performing emergency cleanup..."); + + // More aggressive cleanup - keep only last 24 hours of logs + const cutoffDate = new Date(); + cutoffDate.setHours(cutoffDate.getHours() - 24); + + const result = await this.performCleanup(1); + + // If still not enough space, remove function logs + const diskSpace = await this.getDiskSpace(); + if (diskSpace && diskSpace.freeGB <= this.options.emergencyThreshold) { + const functionsDir = path.join(this.logDir, "functions"); + if (fs.existsSync(functionsDir)) { + await fs.promises.rm(functionsDir, { recursive: true, force: true }); + await fs.promises.mkdir(functionsDir, { recursive: true }); + } + } + + return result; + } + + startMonitoring() { + this.stopMonitoring(); // Stop any existing monitoring + + this.monitoringTimer = setInterval( + async () => { + await this.checkDiskSpace(); + }, + this.options.monitoringInterval * 60 * 1000 + ); // Convert minutes to milliseconds + + // Initial check + this.checkDiskSpace(); + } + + stopMonitoring() { + if (this.monitoringTimer) { + clearInterval(this.monitoringTimer); + this.monitoringTimer = null; + } + } + + getStatus() { + return this.diskSpaceStatus; + } + + // Express middleware for admin notifications + adminNotificationMiddleware() { + return async (req, res, next) => { + if (req.path.startsWith("/admin")) { + const status = await this.checkDiskSpace(); + res.locals.diskSpaceStatus = status; + } + next(); + }; + } + + // API endpoint for status + getStatusEndpoint() { + return async (req, res) => { + try { + const status = await this.checkDiskSpace(); + res.json({ + success: true, + data: status, + thresholds: { + warning: this.options.warningThreshold, + critical: this.options.criticalThreshold, + emergency: this.options.emergencyThreshold, + }, + }); + } catch (error) { + res.status(500).json({ + success: false, + error: "Failed to get disk space status", + details: error.message, + }); + } + }; + } + + // Manual cleanup endpoint + manualCleanupEndpoint() { + return async (req, res) => { + try { + const { retentionDays } = req.body; + const days = retentionDays || this.options.normalCleanupDays; + + const result = await this.performCleanup(days); + const newStatus = await this.checkDiskSpace(); + + res.json({ + success: true, + cleanup: result, + newStatus: newStatus, + }); + } catch (error) { + res.status(500).json({ + success: false, + error: "Failed to perform cleanup", + details: error.message, + }); + } + }; + } +} + +module.exports = DiskSpaceMonitor; diff --git a/src/utils/logging.js b/src/utils/logging.js index b6839bb..f21de6d 100644 --- a/src/utils/logging.js +++ b/src/utils/logging.js @@ -11,13 +11,17 @@ const logDir = path.join(__dirname, "..", "..", "logs"); const projectRoot = path.join(__dirname, "..", ".."); +// Create session-specific directory with timestamp +const sessionTimestamp = new Date().toISOString().replace(/[:.]/g, "-"); +const sessionDir = path.join(logDir, "sessions", sessionTimestamp); + // Define log file paths const logFiles = { - session: path.join(logDir, "session.log"), + session: path.join(sessionDir, "session.log"), info: path.join(logDir, "info", "info.log"), notice: path.join(logDir, "notice", "notice.log"), error: path.join(logDir, "error", "error.log"), - warn: path.join(logDir, "warn", "warning.log"), + warn: path.join(logDir, "warn", "warn.log"), debug: path.join(logDir, "debug", "debug.log"), }; @@ -38,7 +42,6 @@ // Create write streams const logStreams = { - session: fs.createWriteStream(logFiles.session, { flags: "a" }), info: fs.createWriteStream(logFiles.info, { flags: "a" }), notice: fs.createWriteStream(logFiles.notice, { flags: "a" }), error: fs.createWriteStream(logFiles.error, { flags: "a" }), @@ -46,6 +49,22 @@ debug: fs.createWriteStream(logFiles.debug, { flags: "a" }), }; +// Session-specific daily rotate transport +const sessionTransport = new DailyRotateFile({ + dirname: sessionDir, + filename: "session-%DATE%.log", + datePattern: "YYYY-MM-DD", + zippedArchive: true, + maxFiles: "30d", // Keep session logs for 30 days + format: format.combine( + format.timestamp(), + format.printf( + ({ timestamp, level, message }) => + `[${timestamp}] [${level.toUpperCase()}] ${message}` + ) + ), +}); + // Utility function for custom function logs const dynamicCustomStreams = {}; @@ -58,6 +77,7 @@ const timestamp = new Date().toISOString(); return `[${timestamp}] ${args.join(" ")}\n`; } + const functionLog = (functionName, ...args) => { const safeFunctionName = formatFunctionName(functionName).replace( /[^a-z0-9_\-]/gi, @@ -80,13 +100,19 @@ //console.log(`[${functionName}]`, ...args) }; -// Generic log writer +// Generic log writer with session logging function writeLog(level, stream, consoleFn, ...args) { const timestamp = new Date().toISOString(); const message = args.join(" "); const logLine = `[${timestamp}] [${level}] ${message}\n`; + + // Write to specific log file stream.write(logLine); - logStreams.session.write(logLine); + + // Write to session log via winston transport + sessionTransport.write({ level: level.toLowerCase(), message, timestamp }); + + // Console output consoleFn(`[${timestamp}] [${level}]`, ...args); } @@ -107,6 +133,7 @@ ), }); } + function patchConsole() { console.log = (...args) => writeLog("INFO", logStreams.info, originalConsole.log, ...args); @@ -132,6 +159,12 @@ writeLog("ERROR", logStreams.error, console.error, ...args), debug: (...args) => writeLog("DEBUG", logStreams.debug, console.debug, ...args), + // Add session info method + sessionInfo: () => ({ + sessionId: sessionTimestamp, + sessionDir: sessionDir, + startTime: new Date().toISOString(), + }), }; const winstonLogger = createLogger({ @@ -147,6 +180,7 @@ buildTransport("warn", "warn"), buildTransport("debug", "debug"), buildTransport("notice", "notice"), + sessionTransport, // Add session transport to winston new transports.Console({ level: "debug", format: format.combine( @@ -165,14 +199,38 @@ }) ), }), - sqliteTransport, ], }); +// Log session start +winstonLogger.info(`Session started: ${sessionTimestamp}`); + +// Clean up old session directories (optional) +function cleanupOldSessions() { + const sessionsDir = path.join(logDir, "sessions"); + if (fs.existsSync(sessionsDir)) { + const sessions = fs.readdirSync(sessionsDir); + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - 30); // Keep 30 days of sessions + + sessions.forEach((sessionFolder) => { + const sessionPath = path.join(sessionsDir, sessionFolder); + const stats = fs.statSync(sessionPath); + if (stats.isDirectory() && stats.mtime < cutoffDate) { + fs.rmSync(sessionPath, { recursive: true, force: true }); + } + }); + } +} + +// Run cleanup on startup +cleanupOldSessions(); + if (process.env.NODE_ENV !== "production") { patchConsole(); } + module.exports = { manualLogger, winstonLogger, diff --git a/src/views/admin-pages/logs.handlebars b/src/views/admin-pages/logs.handlebars new file mode 100644 index 0000000..7ead8ea --- /dev/null +++ b/src/views/admin-pages/logs.handlebars @@ -0,0 +1,43 @@ +{{!-- pages/logs.hbs --}} +{{#section "styles"}} + +{{/section}} +{{#section "scripts"}} + +{{/section}} +

Log Viewer

+ +
+ {{!-- + + + --}} + + + + + + + + +
+ + + + + + +
diff --git a/src/views/pages/logs.handlebars b/src/views/pages/logs.handlebars deleted file mode 100644 index 7ead8ea..0000000 --- a/src/views/pages/logs.handlebars +++ /dev/null @@ -1,43 +0,0 @@ -{{!-- pages/logs.hbs --}} -{{#section "styles"}} - -{{/section}} -{{#section "scripts"}} - -{{/section}} -

Log Viewer

- -
- {{!-- - - - --}} - - - - - - - - -
- - - - - - -
diff --git a/yarn.lock b/yarn.lock index a848689..a79fed0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30,13 +30,6 @@ enabled "2.0.x" kuler "^2.0.0" -"@emnapi/runtime@^1.4.4": - version "1.4.4" - resolved "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.4.tgz" - integrity sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg== - dependencies: - tslib "^2.4.0" - "@faker-js/faker@^9.8.0": version "9.8.0" resolved "https://registry.npmjs.org/@faker-js/faker/-/faker-9.8.0.tgz" @@ -47,93 +40,16 @@ resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== -"@img/sharp-darwin-arm64@0.34.3": - version "0.34.3" - resolved "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz" - integrity sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg== - optionalDependencies: - "@img/sharp-libvips-darwin-arm64" "1.2.0" - -"@img/sharp-darwin-x64@0.34.3": - version "0.34.3" - resolved "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz" - integrity sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA== - optionalDependencies: - "@img/sharp-libvips-darwin-x64" "1.2.0" - -"@img/sharp-libvips-darwin-arm64@1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz" - integrity sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ== - -"@img/sharp-libvips-darwin-x64@1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz" - integrity sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg== - -"@img/sharp-libvips-linux-arm@1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz" - integrity sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw== - -"@img/sharp-libvips-linux-arm64@1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz" - integrity sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA== - -"@img/sharp-libvips-linux-ppc64@1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz" - integrity sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ== - -"@img/sharp-libvips-linux-s390x@1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz" - integrity sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw== - "@img/sharp-libvips-linux-x64@1.2.0": version "1.2.0" resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz" integrity sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg== -"@img/sharp-libvips-linuxmusl-arm64@1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz" - integrity sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q== - "@img/sharp-libvips-linuxmusl-x64@1.2.0": version "1.2.0" resolved "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz" integrity sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q== -"@img/sharp-linux-arm@0.34.3": - version "0.34.3" - resolved "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz" - integrity sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A== - optionalDependencies: - "@img/sharp-libvips-linux-arm" "1.2.0" - -"@img/sharp-linux-arm64@0.34.3": - version "0.34.3" - resolved "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz" - integrity sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA== - optionalDependencies: - "@img/sharp-libvips-linux-arm64" "1.2.0" - -"@img/sharp-linux-ppc64@0.34.3": - version "0.34.3" - resolved "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz" - integrity sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA== - optionalDependencies: - "@img/sharp-libvips-linux-ppc64" "1.2.0" - -"@img/sharp-linux-s390x@0.34.3": - version "0.34.3" - resolved "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz" - integrity sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ== - optionalDependencies: - "@img/sharp-libvips-linux-s390x" "1.2.0" - "@img/sharp-linux-x64@0.34.3": version "0.34.3" resolved "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz" @@ -141,13 +57,6 @@ optionalDependencies: "@img/sharp-libvips-linux-x64" "1.2.0" -"@img/sharp-linuxmusl-arm64@0.34.3": - version "0.34.3" - resolved "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz" - integrity sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ== - optionalDependencies: - "@img/sharp-libvips-linuxmusl-arm64" "1.2.0" - "@img/sharp-linuxmusl-x64@0.34.3": version "0.34.3" resolved "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz" @@ -155,28 +64,6 @@ optionalDependencies: "@img/sharp-libvips-linuxmusl-x64" "1.2.0" -"@img/sharp-wasm32@0.34.3": - version "0.34.3" - resolved "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz" - integrity sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg== - dependencies: - "@emnapi/runtime" "^1.4.4" - -"@img/sharp-win32-arm64@0.34.3": - version "0.34.3" - resolved "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz" - integrity sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ== - -"@img/sharp-win32-ia32@0.34.3": - version "0.34.3" - resolved "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz" - integrity sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw== - -"@img/sharp-win32-x64@0.34.3": - version "0.34.3" - resolved "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz" - integrity sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g== - "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" @@ -1574,11 +1461,6 @@ resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@~2.3.2: - version "2.3.3" - resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" @@ -2663,6 +2545,13 @@ resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz" integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== +node-disk-info@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/node-disk-info/-/node-disk-info-1.3.0.tgz" + integrity sha512-NEx858vJZ0AoBtmD/ChBIHLjFTF28xCsDIgmFl4jtGKsvlUx9DU/OrMDjvj3qp/E4hzLN0HvTg7eJx5XFQvbeg== + dependencies: + iconv-lite "^0.6.2" + node-fetch@^2.7.0: version "2.7.0" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" @@ -4029,11 +3918,6 @@ resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== -tslib@^2.4.0: - version "2.8.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" - integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== - tslib@1.9.3: version "1.9.3" resolved "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz"