All checks were successful
Rust CI / build-test (push) Successful in 1m22s
- Implemented `/api/stats` endpoint to return JSON metrics including active assets, total uploads, storage usage, and recent activity. - Created `stats.html` page to display real-time statistics with auto-refresh functionality. - Enhanced asset logging to include uploader IP and detailed event information for uploads and deletions. - Updated asset model to store uploader IP for audit purposes. - Improved logging functionality to ensure log directory exists before writing. - Refactored asset creation and management to support new features and logging.
245 lines
6.8 KiB
HTML
245 lines
6.8 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Black Hole Share - Statistics</title>
|
|
<link rel="stylesheet" href="/style.css" />
|
|
<style>
|
|
.stats-container {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.stat-card {
|
|
background-color: var(--bg-secondary);
|
|
border: 2px solid var(--border-color);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
text-align: center;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.stat-card:hover {
|
|
border-color: var(--border-hover);
|
|
box-shadow: 0 4px 15px rgba(0, 255, 153, 0.2);
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 2.5em;
|
|
font-weight: bold;
|
|
color: var(--accent-cyan);
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.stat-label {
|
|
color: var(--text-secondary);
|
|
font-size: 0.9em;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
.stat-card.highlight .stat-value {
|
|
color: var(--accent-green);
|
|
}
|
|
|
|
.recent-activity {
|
|
margin-top: 30px;
|
|
background-color: var(--bg-secondary);
|
|
border: 2px solid var(--border-color);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
}
|
|
|
|
.recent-activity h2 {
|
|
color: var(--accent-cyan);
|
|
margin: 0 0 15px 0;
|
|
font-size: 1.2em;
|
|
}
|
|
|
|
.activity-list {
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
font-family: "JetBrains Mono", monospace;
|
|
font-size: 0.85em;
|
|
}
|
|
|
|
.activity-item {
|
|
padding: 8px 0;
|
|
border-bottom: 1px solid var(--inactive-gray);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.activity-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.activity-action {
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-size: 0.8em;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.activity-action.upload {
|
|
background-color: rgba(0, 255, 153, 0.2);
|
|
color: var(--accent-green);
|
|
}
|
|
|
|
.activity-action.delete {
|
|
background-color: rgba(255, 102, 102, 0.2);
|
|
color: #ff6666;
|
|
}
|
|
|
|
.activity-time {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.activity-details {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.refresh-btn {
|
|
background-color: var(--border-color);
|
|
color: var(--bg-tertiary);
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-weight: bold;
|
|
margin-top: 20px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.refresh-btn:hover {
|
|
background-color: var(--border-hover);
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
color: var(--text-secondary);
|
|
padding: 40px;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body class="view-page">
|
|
<h1><a href="/" class="home-link">Black Hole Share</a> - Statistics</h1>
|
|
|
|
<div id="statsContent" class="loading">
|
|
<p>Loading statistics...</p>
|
|
</div>
|
|
|
|
<footer class="powered-by">
|
|
<span
|
|
>Powered by: <img src="/logo.png" alt="ICSBox" class="footer-logo"
|
|
/></span>
|
|
</footer>
|
|
|
|
<script>
|
|
async function loadStats() {
|
|
try {
|
|
const response = await fetch("/api/stats");
|
|
if (!response.ok) {
|
|
throw new Error("Failed to load stats");
|
|
}
|
|
const stats = await response.json();
|
|
renderStats(stats);
|
|
} catch (error) {
|
|
document.getElementById("statsContent").innerHTML = `
|
|
<p class="error">Failed to load statistics: ${error.message}</p>
|
|
`;
|
|
}
|
|
}
|
|
|
|
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 parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
}
|
|
|
|
function formatTime(timestamp) {
|
|
const date = new Date(timestamp);
|
|
return date.toLocaleString();
|
|
}
|
|
|
|
function renderStats(stats) {
|
|
const html = `
|
|
<div class="stats-container">
|
|
<div class="stat-card highlight">
|
|
<div class="stat-label">Active Assets</div>
|
|
<div class="stat-value">${stats.active_assets}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Total Uploads</div>
|
|
<div class="stat-value">${stats.total_uploads}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Total Deleted</div>
|
|
<div class="stat-value">${stats.total_deleted}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Storage Used</div>
|
|
<div class="stat-value">${formatBytes(stats.storage_bytes)}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Images</div>
|
|
<div class="stat-value">${stats.image_count}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Text</div>
|
|
<div class="stat-value">${stats.text_count}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Avg Response</div>
|
|
<div class="stat-value">${stats.avg_response_ms.toFixed(2)} ms</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Total Requests</div>
|
|
<div class="stat-value">${stats.total_requests}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="recent-activity">
|
|
<h2>Recent Activity</h2>
|
|
<div class="activity-list">
|
|
${
|
|
stats.recent_activity.length === 0
|
|
? '<p style="color: var(--text-secondary);">No recent activity</p>'
|
|
: stats.recent_activity
|
|
.map(
|
|
(item) => `
|
|
<div class="activity-item">
|
|
<span class="activity-action ${item.action}">${
|
|
item.action
|
|
}</span>
|
|
<span class="activity-details">${item.mime} (${formatBytes(
|
|
item.size_bytes
|
|
)})</span>
|
|
<span class="activity-time">${item.timestamp}</span>
|
|
</div>
|
|
`
|
|
)
|
|
.join("")
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<button class="refresh-btn" onclick="loadStats()">Refresh</button>
|
|
`;
|
|
document.getElementById("statsContent").innerHTML = html;
|
|
}
|
|
|
|
loadStats();
|
|
// Auto-refresh every 30 seconds
|
|
setInterval(loadStats, 30000);
|
|
</script>
|
|
</body>
|
|
</html>
|