- Implemented VisualAITester class for automated UI/UX testing - Configured environment variables for API keys and paths - Developed methods for browser setup, login, screenshot capture, and image encoding - Integrated OpenAI Vision API for visual quality analysis with detailed feedback - Created tests for various application pages including homepage, project flow, geometry form, and more - Generated comprehensive HTML report summarizing test results and AI analysis - Added functionality to copy AI prompts for automated issue resolution
589 lines
13 KiB
JavaScript
589 lines
13 KiB
JavaScript
/**
|
|
* Admin Logs Routes
|
|
* Endpoint til at se og søge i system logs
|
|
*/
|
|
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
const db = require('../src/db/connection');
|
|
const { asyncHandler } = require('../src/middleware/errorHandler');
|
|
const logger = require('../src/utils/logger');
|
|
|
|
/**
|
|
* GET /api/admin/logs
|
|
* Hent system logs med filtrering og paginering
|
|
*/
|
|
router.get('/', asyncHandler(async (req, res) => {
|
|
const {
|
|
level,
|
|
context,
|
|
category,
|
|
startDate,
|
|
endDate,
|
|
limit = 100,
|
|
offset = 0,
|
|
resolved,
|
|
errorCode
|
|
} = req.query;
|
|
|
|
// Build WHERE clause dynamically
|
|
const conditions = [];
|
|
const params = [];
|
|
|
|
if (level) {
|
|
conditions.push('level = ?');
|
|
params.push(level);
|
|
}
|
|
|
|
if (context) {
|
|
conditions.push('context LIKE ?');
|
|
params.push(`%${context}%`);
|
|
}
|
|
|
|
if (category) {
|
|
conditions.push('category = ?');
|
|
params.push(category);
|
|
}
|
|
|
|
if (errorCode) {
|
|
conditions.push('error_code = ?');
|
|
params.push(errorCode);
|
|
}
|
|
|
|
if (startDate) {
|
|
conditions.push('created_at >= ?');
|
|
params.push(startDate);
|
|
}
|
|
|
|
if (endDate) {
|
|
conditions.push('created_at <= ?');
|
|
params.push(endDate);
|
|
}
|
|
|
|
if (resolved !== undefined) {
|
|
conditions.push('resolved = ?');
|
|
params.push(resolved === 'true' ? 1 : 0);
|
|
}
|
|
|
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
|
|
// Get total count
|
|
const [countResult] = await db.query(
|
|
`SELECT COUNT(*) as total FROM system_logs ${whereClause}`,
|
|
params
|
|
);
|
|
|
|
// Get logs
|
|
const [logs] = await db.query(
|
|
`SELECT
|
|
id, created_at, level, context, category, message, error_code,
|
|
request_method, request_url, user_id, user_email, resolved, resolved_at
|
|
FROM system_logs
|
|
${whereClause}
|
|
ORDER BY created_at DESC
|
|
LIMIT ? OFFSET ?`,
|
|
[...params, parseInt(limit), parseInt(offset)]
|
|
);
|
|
|
|
logger.logInfo('Admin.GetLogs', 'Logs retrieved', {
|
|
category: 'ADMIN',
|
|
req,
|
|
metadata: {
|
|
count: logs.length,
|
|
total: countResult[0].total
|
|
}
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: logs,
|
|
pagination: {
|
|
total: countResult[0].total,
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset),
|
|
hasMore: parseInt(offset) + logs.length < countResult[0].total
|
|
}
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* GET /api/admin/logs/:id
|
|
* Hent fuld information om en specifik log
|
|
*/
|
|
router.get('/:id', asyncHandler(async (req, res) => {
|
|
const { id } = req.params;
|
|
|
|
const [logs] = await db.query(
|
|
'SELECT * FROM system_logs WHERE id = ?',
|
|
[id]
|
|
);
|
|
|
|
if (logs.length === 0) {
|
|
const error = new Error('Log entry ikke fundet');
|
|
error.status = 404;
|
|
throw error;
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: logs[0]
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* GET /api/admin/logs/stats/summary
|
|
* Hent statistik over logs
|
|
*/
|
|
router.get('/stats/summary', asyncHandler(async (req, res) => {
|
|
const { startDate, endDate } = req.query;
|
|
|
|
const conditions = [];
|
|
const params = [];
|
|
|
|
if (startDate) {
|
|
conditions.push('created_at >= ?');
|
|
params.push(startDate);
|
|
}
|
|
|
|
if (endDate) {
|
|
conditions.push('created_at <= ?');
|
|
params.push(endDate);
|
|
}
|
|
|
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
|
|
// Count by level
|
|
const [levelStats] = await db.query(
|
|
`SELECT level, COUNT(*) as count
|
|
FROM system_logs
|
|
${whereClause}
|
|
GROUP BY level`,
|
|
params
|
|
);
|
|
|
|
// Count by category
|
|
const [categoryStats] = await db.query(
|
|
`SELECT category, COUNT(*) as count
|
|
FROM system_logs
|
|
${whereClause}
|
|
GROUP BY category
|
|
ORDER BY count DESC
|
|
LIMIT 10`,
|
|
params
|
|
);
|
|
|
|
// Most common errors
|
|
const [topErrors] = await db.query(
|
|
`SELECT error_code, message, COUNT(*) as count
|
|
FROM system_logs
|
|
${whereClause} AND level = 'ERROR'
|
|
GROUP BY error_code, message
|
|
ORDER BY count DESC
|
|
LIMIT 10`,
|
|
params
|
|
);
|
|
|
|
// Unresolved errors
|
|
const [unresolvedCount] = await db.query(
|
|
`SELECT COUNT(*) as count
|
|
FROM system_logs
|
|
${whereClause} AND level = 'ERROR' AND resolved = FALSE`,
|
|
params
|
|
);
|
|
|
|
logger.logInfo('Admin.GetLogStats', 'Log statistics retrieved', {
|
|
category: 'ADMIN',
|
|
req
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
stats: {
|
|
byLevel: levelStats,
|
|
byCategory: categoryStats,
|
|
topErrors: topErrors,
|
|
unresolvedErrors: unresolvedCount[0].count
|
|
}
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* PATCH /api/admin/logs/:id/resolve
|
|
* Marker en log som løst
|
|
*/
|
|
router.patch('/:id/resolve', asyncHandler(async (req, res) => {
|
|
const { id } = req.params;
|
|
const { notes, resolvedBy } = req.body;
|
|
|
|
await db.query(
|
|
`UPDATE system_logs
|
|
SET resolved = TRUE, resolved_at = NOW(), resolved_by = ?, resolution_notes = ?
|
|
WHERE id = ?`,
|
|
[resolvedBy || 'system', notes || null, id]
|
|
);
|
|
|
|
logger.logInfo('Admin.ResolveLog', `Log ${id} marked as resolved`, {
|
|
category: 'ADMIN',
|
|
req,
|
|
metadata: { logId: id, resolvedBy }
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Log markeret som løst'
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* GET /api/admin/logs/performance/slow-requests
|
|
* Hent langsomme requests
|
|
*/
|
|
router.get('/performance/slow-requests', asyncHandler(async (req, res) => {
|
|
const { limit = 50, minResponseTime = 1000 } = req.query;
|
|
|
|
const [slowRequests] = await db.query(
|
|
`SELECT
|
|
method, url, status_code, response_time_ms,
|
|
user_id, created_at, metadata
|
|
FROM request_logs
|
|
WHERE response_time_ms >= ?
|
|
ORDER BY response_time_ms DESC
|
|
LIMIT ?`,
|
|
[parseInt(minResponseTime), parseInt(limit)]
|
|
);
|
|
|
|
logger.logInfo('Admin.GetSlowRequests', 'Slow requests retrieved', {
|
|
category: 'ADMIN',
|
|
req,
|
|
metadata: { count: slowRequests.length }
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: slowRequests
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* GET /api/admin/logs/performance/endpoint-stats
|
|
* Hent endpoint statistik
|
|
*/
|
|
router.get('/performance/endpoint-stats', asyncHandler(async (req, res) => {
|
|
const { startDate, endDate } = req.query;
|
|
|
|
const conditions = [];
|
|
const params = [];
|
|
|
|
if (startDate) {
|
|
conditions.push('created_at >= ?');
|
|
params.push(startDate);
|
|
}
|
|
|
|
if (endDate) {
|
|
conditions.push('created_at <= ?');
|
|
params.push(endDate);
|
|
}
|
|
|
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
|
|
const [endpointStats] = await db.query(
|
|
`SELECT
|
|
url,
|
|
method,
|
|
COUNT(*) as request_count,
|
|
AVG(response_time_ms) as avg_response_time,
|
|
MIN(response_time_ms) as min_response_time,
|
|
MAX(response_time_ms) as max_response_time,
|
|
SUM(CASE WHEN status_code >= 400 THEN 1 ELSE 0 END) as error_count
|
|
FROM request_logs
|
|
${whereClause}
|
|
GROUP BY url, method
|
|
ORDER BY request_count DESC
|
|
LIMIT 20`,
|
|
params
|
|
);
|
|
|
|
logger.logInfo('Admin.GetEndpointStats', 'Endpoint statistics retrieved', {
|
|
category: 'ADMIN',
|
|
req
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: endpointStats
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* GET /api/admin/logs/user-errors/:userId
|
|
* Hent alle fejl for en specifik bruger
|
|
*/
|
|
router.get('/user-errors/:userId', asyncHandler(async (req, res) => {
|
|
const { userId } = req.params;
|
|
const { limit = 50, offset = 0 } = req.query;
|
|
|
|
const [logs] = await db.query(
|
|
`SELECT
|
|
id, created_at, level, context, category, message, error_code,
|
|
request_method, request_url, resolved
|
|
FROM system_logs
|
|
WHERE user_id = ? AND level IN ('ERROR', 'WARNING')
|
|
ORDER BY created_at DESC
|
|
LIMIT ? OFFSET ?`,
|
|
[userId, parseInt(limit), parseInt(offset)]
|
|
);
|
|
|
|
logger.logInfo('Admin.GetUserErrors', `Fetched errors for user ${userId}`, {
|
|
category: 'ADMIN',
|
|
req,
|
|
metadata: { userId, count: logs.length }
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: logs
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* GET /api/admin/logs/http-errors
|
|
* Hent alle HTTP fejl (4xx, 5xx)
|
|
*/
|
|
router.get('/http-errors', asyncHandler(async (req, res) => {
|
|
const { statusCode, startDate, endDate, limit = 100, offset = 0 } = req.query;
|
|
|
|
const conditions = ['category IN (?, ?, ?, ?, ?)'];
|
|
const params = ['HTTP_ERROR', 'CLIENT_ERROR', 'SERVER_ERROR', 'NOT_FOUND', 'ACCESS_DENIED'];
|
|
|
|
if (statusCode) {
|
|
conditions.push('error_code = ?');
|
|
params.push(`HTTP_${statusCode}`);
|
|
}
|
|
|
|
if (startDate) {
|
|
conditions.push('created_at >= ?');
|
|
params.push(startDate);
|
|
}
|
|
|
|
if (endDate) {
|
|
conditions.push('created_at <= ?');
|
|
params.push(endDate);
|
|
}
|
|
|
|
const whereClause = `WHERE ${conditions.join(' AND ')}`;
|
|
|
|
// Get total count
|
|
const [countResult] = await db.query(
|
|
`SELECT COUNT(*) as total FROM system_logs ${whereClause}`,
|
|
params
|
|
);
|
|
|
|
// Get logs
|
|
const [logs] = await db.query(
|
|
`SELECT
|
|
id, created_at, level, context, category, message, error_code,
|
|
request_method, request_url, user_id, user_email, metadata
|
|
FROM system_logs
|
|
${whereClause}
|
|
ORDER BY created_at DESC
|
|
LIMIT ? OFFSET ?`,
|
|
[...params, parseInt(limit), parseInt(offset)]
|
|
);
|
|
|
|
logger.logInfo('Admin.GetHTTPErrors', 'HTTP errors retrieved', {
|
|
category: 'ADMIN',
|
|
req,
|
|
metadata: { count: logs.length, total: countResult[0].total }
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: logs,
|
|
pagination: {
|
|
total: countResult[0].total,
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset)
|
|
}
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* GET /api/admin/logs/session-errors
|
|
* Hent alle session-relaterede fejl
|
|
*/
|
|
router.get('/session-errors', asyncHandler(async (req, res) => {
|
|
const { startDate, endDate, limit = 100, offset = 0 } = req.query;
|
|
|
|
const conditions = ['category IN (?, ?, ?)'];
|
|
const params = ['SESSION', 'AUTHENTICATION', 'ACCESS_DENIED'];
|
|
|
|
if (startDate) {
|
|
conditions.push('created_at >= ?');
|
|
params.push(startDate);
|
|
}
|
|
|
|
if (endDate) {
|
|
conditions.push('created_at <= ?');
|
|
params.push(endDate);
|
|
}
|
|
|
|
const whereClause = `WHERE ${conditions.join(' AND ')}`;
|
|
|
|
// Get total count
|
|
const [countResult] = await db.query(
|
|
`SELECT COUNT(*) as total FROM system_logs ${whereClause}`,
|
|
params
|
|
);
|
|
|
|
// Get logs
|
|
const [logs] = await db.query(
|
|
`SELECT
|
|
id, created_at, level, context, category, message, error_code,
|
|
request_url, user_id, user_email, metadata
|
|
FROM system_logs
|
|
${whereClause}
|
|
ORDER BY created_at DESC
|
|
LIMIT ? OFFSET ?`,
|
|
[...params, parseInt(limit), parseInt(offset)]
|
|
);
|
|
|
|
logger.logInfo('Admin.GetSessionErrors', 'Session errors retrieved', {
|
|
category: 'ADMIN',
|
|
req,
|
|
metadata: { count: logs.length }
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: logs,
|
|
pagination: {
|
|
total: countResult[0].total,
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset)
|
|
}
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* GET /api/admin/logs/stats/http-errors
|
|
* Statistik over HTTP fejl koder
|
|
*/
|
|
router.get('/stats/http-errors', asyncHandler(async (req, res) => {
|
|
const { startDate, endDate } = req.query;
|
|
|
|
const conditions = ['category IN (?, ?, ?, ?, ?)'];
|
|
const params = ['HTTP_ERROR', 'CLIENT_ERROR', 'SERVER_ERROR', 'NOT_FOUND', 'ACCESS_DENIED'];
|
|
|
|
if (startDate) {
|
|
conditions.push('created_at >= ?');
|
|
params.push(startDate);
|
|
}
|
|
|
|
if (endDate) {
|
|
conditions.push('created_at <= ?');
|
|
params.push(endDate);
|
|
}
|
|
|
|
const whereClause = `WHERE ${conditions.join(' AND ')}`;
|
|
|
|
// Count by status code
|
|
const [statusCodeStats] = await db.query(
|
|
`SELECT
|
|
error_code,
|
|
category,
|
|
COUNT(*) as count,
|
|
COUNT(DISTINCT user_id) as affected_users
|
|
FROM system_logs
|
|
${whereClause}
|
|
GROUP BY error_code, category
|
|
ORDER BY count DESC`,
|
|
params
|
|
);
|
|
|
|
// Most affected URLs
|
|
const [affectedUrls] = await db.query(
|
|
`SELECT
|
|
request_url as url,
|
|
COUNT(*) as error_count,
|
|
COUNT(DISTINCT user_id) as affected_users
|
|
FROM system_logs
|
|
${whereClause} AND request_url IS NOT NULL
|
|
GROUP BY request_url
|
|
ORDER BY error_count DESC
|
|
LIMIT 20`,
|
|
params
|
|
);
|
|
|
|
logger.logInfo('Admin.GetHTTPErrorStats', 'HTTP error statistics retrieved', {
|
|
category: 'ADMIN',
|
|
req
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
stats: {
|
|
byStatusCode: statusCodeStats,
|
|
affectedUrls: affectedUrls
|
|
}
|
|
});
|
|
}));
|
|
|
|
/**
|
|
* GET /api/admin/logs/frontend-errors
|
|
* Hent fejl fra frontend
|
|
*/
|
|
router.get('/frontend-errors', asyncHandler(async (req, res) => {
|
|
const { startDate, endDate, limit = 100, offset = 0 } = req.query;
|
|
|
|
const conditions = ['category IN (?, ?, ?)'];
|
|
const params = ['FRONTEND', 'FRONTEND_NETWORK', 'FRONTEND_SESSION'];
|
|
|
|
if (startDate) {
|
|
conditions.push('created_at >= ?');
|
|
params.push(startDate);
|
|
}
|
|
|
|
if (endDate) {
|
|
conditions.push('created_at <= ?');
|
|
params.push(endDate);
|
|
}
|
|
|
|
const whereClause = `WHERE ${conditions.join(' AND ')}`;
|
|
|
|
// Get total count
|
|
const [countResult] = await db.query(
|
|
`SELECT COUNT(*) as total FROM system_logs ${whereClause}`,
|
|
params
|
|
);
|
|
|
|
// Get logs
|
|
const [logs] = await db.query(
|
|
`SELECT
|
|
id, created_at, level, context, category, message, error_code,
|
|
user_id, user_email, metadata
|
|
FROM system_logs
|
|
${whereClause}
|
|
ORDER BY created_at DESC
|
|
LIMIT ? OFFSET ?`,
|
|
[...params, parseInt(limit), parseInt(offset)]
|
|
);
|
|
|
|
logger.logInfo('Admin.GetFrontendErrors', 'Frontend errors retrieved', {
|
|
category: 'ADMIN',
|
|
req,
|
|
metadata: { count: logs.length }
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: logs,
|
|
pagination: {
|
|
total: countResult[0].total,
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset)
|
|
}
|
|
});
|
|
}));
|
|
|
|
module.exports = router;
|