Added more security and metrics

This commit is contained in:
2026-06-22 07:28:03 +02:00
committed by Jeroen Saey
parent 2a1f7b77ee
commit cb2d111ad7
27 changed files with 1638 additions and 884 deletions
+178
View File
@@ -1896,3 +1896,181 @@ textarea.field-input {
width: 14px;
height: 14px;
}
/* Dashboard Styles */
.dashboard-panel {
position: absolute;
right: 0;
top: 58px;
bottom: 0;
width: 400px;
background: var(--bg-1);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
z-index: 100;
box-shadow: var(--shadow);
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid var(--border);
background: var(--bg-raised);
}
.dashboard-header h2 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--text-0);
}
.close-btn {
background: transparent;
border: none;
font-size: 24px;
color: var(--text-1);
cursor: pointer;
padding: 0;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
color: var(--text-0);
}
.dashboard-content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.dashboard-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-bottom: 24px;
}
.stat-card {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 16px;
text-align: center;
transition: all 0.2s ease;
}
.stat-card:hover {
border-color: var(--accent);
background: var(--bg-3);
}
.stat-label {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
color: var(--text-2);
margin-bottom: 8px;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 24px;
font-weight: 700;
color: var(--accent);
font-family: var(--font-mono);
}
.stat-value.error-red {
color: var(--m-delete);
}
.metric-chart {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 16px;
margin-bottom: 16px;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-2);
font-size: 12px;
}
.recent-requests {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow: hidden;
}
.request-item {
padding: 12px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
transition: background 0.2s ease;
}
.request-item:hover {
background: var(--bg-3);
}
.request-item:last-child {
border-bottom: none;
}
.request-method {
font-weight: 600;
font-family: var(--font-mono);
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
}
.request-status-success {
color: var(--m-get);
}
.request-status-error {
color: var(--m-delete);
}
.request-time {
color: var(--text-2);
font-family: var(--font-mono);
}
#app-shell.with-dashboard {
grid-template-columns: var(--nav-w) 1fr var(--try-w) 400px;
}
@media (max-width: 1400px) {
.dashboard-panel {
width: 300px;
}
#app-shell.with-dashboard {
grid-template-columns: var(--nav-w) 1fr 300px;
}
}
@media (max-width: 768px) {
.dashboard-panel {
width: 100%;
right: auto;
left: 0;
}
}
+95
View File
@@ -1386,6 +1386,9 @@
const exportDoxaApiBtn = document.getElementById("exportDoxaApiBtn");
const exportOpenApiBtn = document.getElementById("exportOpenApiBtn");
const exportSwaggerBtn = document.getElementById("exportSwaggerBtn");
const dashboardBtn = document.getElementById("dashboardBtn");
const closeDashboard = document.getElementById("closeDashboard");
const dashboardPanel = document.getElementById("dashboardPanel");
importBtn?.addEventListener("click", () => importFile.click());
@@ -1443,6 +1446,10 @@
exportOpenApiBtn?.addEventListener("click", () => download("openapi.json", "openapi.json"));
exportSwaggerBtn?.addEventListener("click", () => download("swagger.json", "swagger.json"));
// Dashboard functionality
dashboardBtn?.addEventListener("click", () => openDashboard());
closeDashboard?.addEventListener("click", () => closeDashboardPanel());
const authBtn = document.getElementById("authBtn");
authBtn?.addEventListener("click", () => openAuthModal());
@@ -1578,5 +1585,93 @@
return escapeHtml(str);
}
// Dashboard functions
async function openDashboard() {
const dashboardPanel = document.getElementById("dashboardPanel");
const dashboard = document.querySelector(".dashboard-panel");
if (!dashboardPanel) return;
dashboardPanel.style.display = "block";
try {
const res = await fetch("analytics");
if (!res.ok) throw new Error("Failed to load analytics");
const analyticsData = await res.json();
updateDashboard(analyticsData);
// Refresh every 5 seconds
if (window.dashboardInterval) clearInterval(window.dashboardInterval);
window.dashboardInterval = setInterval(async () => {
const res = await fetch("analytics");
if (res.ok) {
const data = await res.json();
updateDashboard(data);
}
}, 5000);
} catch (err) {
console.error("Dashboard load error:", err);
}
}
function closeDashboardPanel() {
const dashboardPanel = document.getElementById("dashboardPanel");
if (dashboardPanel) dashboardPanel.style.display = "none";
if (window.dashboardInterval) {
clearInterval(window.dashboardInterval);
window.dashboardInterval = null;
}
}
function updateDashboard(data) {
document.getElementById("totalEndpoints").textContent = data.totalEndpoints || "0";
document.getElementById("totalRequests").textContent = data.totalRequests || "0";
document.getElementById("totalErrors").textContent = data.totalErrors || "0";
document.getElementById("avgResponse").textContent = Math.round((data.averageResponseTime || 0)) + "ms";
// Update top endpoints
const topEndpointsList = (data.topEndpoints || []).map(ep => `
<div class="request-item">
<div>
<span class="request-method">${ep.method}</span>
<code style="font-size: 11px; color: var(--text-1);">${ep.path}</code>
</div>
<div class="request-time">${ep.requestCount} req</div>
</div>
`).join("") || "<div style='padding: 20px; color: var(--text-2); text-align: center;'>No data yet</div>";
document.getElementById("topEndpointsChart").innerHTML = `
<div style="width: 100%;">
<h3 style="margin: 0 0 12px 0; font-size: 13px; font-weight: 600;">Top Endpoints</h3>
${topEndpointsList}
</div>
`;
// Update recent requests
const recentRequestsList = (data.recentRequests || []).map(req => {
const statusClass = req.statusCode >= 400 ? "request-status-error" : "request-status-success";
return `
<div class="request-item">
<div>
<span class="request-method">${req.method}</span>
<code style="font-size: 11px; color: var(--text-1);">${req.path}</code>
</div>
<div>
<span class="request-time ${statusClass}">${req.statusCode}</span>
<span class="request-time" style="margin-left: 8px;">${req.responseTimeMs}ms</span>
</div>
</div>
`;
}).join("") || "<div style='padding: 20px; color: var(--text-2); text-align: center;'>No requests yet</div>";
document.getElementById("recentRequestsTable").innerHTML = `
<div style="padding: 16px; background: var(--bg-2); border: 1px solid var(--border); border-radius: var(--radius-md);">
<h3 style="margin: 0 0 12px 0; font-size: 13px; font-weight: 600;">Recent Requests</h3>
${recentRequestsList}
</div>
`;
}
init();
})();
+34 -1
View File
@@ -33,7 +33,10 @@
</div>
<div class="topbar-actions">
<div class="action-group">
<button id="authBtn" class="action-btn icon-btn-badge" title="Authorization">
<button id="dashboardBtn" class="action-btn" title="View Analytics Dashboard">
<svg viewBox="0 0 24 24" fill="none" width="15" height="15" style="vertical-align:-2px;margin-right:4px;"><rect x="3" y="3" width="7" height="7" stroke="currentColor" stroke-width="2"/><rect x="14" y="3" width="7" height="7" stroke="currentColor" stroke-width="2"/><rect x="3" y="14" width="7" height="7" stroke="currentColor" stroke-width="2"/><rect x="14" y="14" width="7" height="7" stroke="currentColor" stroke-width="2"/></svg>Dashboard
</button>
<button id="authBtn" class="action-btn" title="Manage authentication">
<svg viewBox="0 0 24 24" fill="none" width="15" height="15" style="vertical-align:-2px;margin-right:4px;"><rect x="5" y="11" width="14" height="9" rx="2" stroke="currentColor" stroke-width="2" /><path d="M8 11V7a4 4 0 018 0v4" stroke="currentColor" stroke-width="2" stroke-linecap="round" /></svg>Auth
<span class="badge-dot" id="authBadgeDot" style="display:none;"></span>
</button>
@@ -71,6 +74,36 @@
<aside class="try-pane" id="tryPane">
<div id="tryContent" class="try-content"></div>
</aside>
<!-- Dashboard Panel (initially hidden) -->
<div id="dashboardPanel" class="dashboard-panel" style="display: none;">
<div class="dashboard-header">
<h2>API Analytics</h2>
<button id="closeDashboard" class="close-btn" title="Close Dashboard">&times;</button>
</div>
<div class="dashboard-content">
<div class="dashboard-stats">
<div class="stat-card">
<div class="stat-label">Total Endpoints</div>
<div class="stat-value" id="totalEndpoints">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Requests</div>
<div class="stat-value" id="totalRequests">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Errors</div>
<div class="stat-value error-red" id="totalErrors">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Avg Response</div>
<div class="stat-value" id="avgResponse">0ms</div>
</div>
</div>
<div id="topEndpointsChart" class="metric-chart"></div>
<div id="recentRequestsTable" class="recent-requests"></div>
</div>
</div>
</div>
</div>