anicove / api /templates /shared /admin_dashboard.html
mwask's picture
servertime
0309845
Raw
History Blame Contribute Delete
20.7 kB
{% extends "shared/base.html" %}
{% block title %}Admin Dashboard - AniCove{% endblock %}
{% block meta_description %}Admin dashboard for managing AniCove.{% endblock %}
{% block extra_css %}
<style>
:root {
--ad-bg: #080808;
--ad-surface: #111;
--ad-border: #1e1e1e;
--ad-muted: #777;
--ad-accent: #3b82f6;
--ad-success: #22c55e;
--ad-warning: #f59e0b;
--ad-error: #ef4444;
}
.ad-page {
max-width: 1200px;
margin: 0 auto;
padding: 32px 20px;
}
.ad-header {
margin-bottom: 24px;
}
.ad-header h1 {
font-size: 1.5rem;
font-weight: 700;
margin: 0 0 6px 0;
color: #fff;
display: flex;
align-items: center;
gap: 10px;
}
.ad-header p {
font-size: 0.85rem;
color: var(--ad-muted);
margin: 0;
}
/* Stats grid */
.ad-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 10px;
margin-bottom: 20px;
}
.ad-stat-card {
background: var(--ad-surface);
border: 1px solid var(--ad-border);
border-radius: 10px;
padding: 16px 18px;
transition: border-color 0.2s, transform 0.2s;
position: relative;
overflow: hidden;
}
.ad-stat-card:hover {
border-color: rgba(255,255,255,0.1);
transform: translateY(-1px);
}
.ad-stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 3px;
height: 100%;
border-radius: 0 3px 3px 0;
}
.ad-stat-card.users::before { background: var(--ad-accent); }
.ad-stat-card.bugs::before { background: var(--ad-error); }
.ad-stat-card.open::before { background: var(--ad-warning); }
.ad-stat-card.resolved::before { background: var(--ad-success); }
.ad-stat-card.live::before { background: #a855f7; }
.ad-stat-card.online::before { background: #22c55e; }
.ad-stat-number {
font-size: 1.8rem;
font-weight: 700;
color: #fff;
line-height: 1.2;
margin-bottom: 2px;
}
.ad-stat-label {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--ad-muted);
}
/* Sections grid */
.ad-grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
@media (max-width: 900px) {
.ad-grid-2 { grid-template-columns: 1fr; }
}
/* Quick Admin Cards */
.ad-quick-cards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 16px;
}
.ad-quick-card {
background: var(--ad-surface);
border: 1px solid var(--ad-border);
border-radius: 10px;
padding: 16px;
display: flex;
align-items: center;
gap: 12px;
text-decoration: none;
transition: all 0.2s;
color: #ccc;
}
.ad-quick-card:hover {
border-color: rgba(255,255,255,0.1);
transform: translateY(-2px);
color: #fff;
}
.ad-quick-card-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.ad-quick-card-icon.bugs { background: rgba(239,68,68,0.12); color: #ef4444; }
.ad-quick-card-icon.announce { background: rgba(245,158,11,0.12); color: #f59e0b; }
.ad-quick-card-icon.settings { background: rgba(59,130,246,0.12); color: var(--ad-accent); }
.ad-quick-card-icon.reports { background: rgba(168,85,247,0.12); color: #a855f7; }
.ad-quick-card-body {
flex: 1;
min-width: 0;
}
.ad-quick-card-title {
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 2px;
}
.ad-quick-card-desc {
font-size: 0.72rem;
color: #555;
}
.ad-quick-card-arrow {
font-size: 1.1rem;
color: #444;
flex-shrink: 0;
}
/* Live Activity Feed */
.ad-activity-feed {
background: var(--ad-surface);
border: 1px solid var(--ad-border);
border-radius: 10px;
overflow: hidden;
}
.ad-activity-header {
padding: 14px 16px;
border-bottom: 1px solid var(--ad-border);
display: flex;
align-items: center;
justify-content: space-between;
}
.ad-activity-header h2 {
font-size: 0.95rem;
font-weight: 600;
margin: 0;
color: #fff;
display: flex;
align-items: center;
gap: 8px;
}
.ad-activity-count {
font-size: 0.78rem;
color: var(--ad-success);
display: flex;
align-items: center;
gap: 5px;
}
.ad-activity-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--ad-success);
animation: adPulse 2s ease-in-out infinite;
display: inline-block;
}
@keyframes adPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.ad-activity-list {
max-height: 400px;
overflow-y: auto;
}
.ad-activity-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
border-bottom: 1px solid rgba(255,255,255,0.03);
transition: background 0.15s;
}
.ad-activity-item:last-child {
border-bottom: none;
}
.ad-activity-item:hover {
background: rgba(255,255,255,0.02);
}
.ad-activity-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
background: #222;
}
.ad-activity-avatar-placeholder {
width: 30px;
height: 30px;
border-radius: 50%;
background: #222;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 700;
color: #fff;
flex-shrink: 0;
}
.ad-activity-info {
flex: 1;
min-width: 0;
}
.ad-activity-username {
font-size: 0.82rem;
font-weight: 500;
color: #ddd;
}
.ad-activity-page {
font-size: 0.7rem;
color: #555;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 280px;
}
.ad-activity-time {
font-size: 0.65rem;
color: #444;
white-space: nowrap;
flex-shrink: 0;
}
.ad-empty {
padding: 30px;
text-align: center;
color: #555;
font-size: 0.85rem;
}
.ad-loading {
padding: 30px;
text-align: center;
color: var(--ad-muted);
}
.ad-spinner {
display: inline-block;
width: 22px;
height: 22px;
border: 2px solid rgba(255,255,255,0.1);
border-top-color: var(--ad-accent);
border-radius: 50%;
animation: adSpin 0.6s linear infinite;
}
@keyframes adSpin {
to { transform: rotate(360deg); }
}
/* Bug bar chart */
.ad-bug-stats {
background: var(--ad-surface);
border: 1px solid var(--ad-border);
border-radius: 10px;
padding: 16px;
}
.ad-bug-stats h2 {
font-size: 0.9rem;
font-weight: 600;
margin: 0 0 12px 0;
color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
}
.ad-bug-bar-chart {
display: flex;
gap: 12px;
align-items: flex-end;
}
.ad-bug-bar {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.ad-bug-bar-fill {
width: 100%;
max-width: 60px;
border-radius: 5px 5px 0 0;
transition: height 0.5s ease;
min-height: 3px;
}
.ad-bug-bar-fill.open { background: #ef4444; }
.ad-bug-bar-fill.in_progress { background: var(--ad-accent); }
.ad-bug-bar-fill.resolved { background: var(--ad-success); }
.ad-bug-bar-count {
font-size: 0.85rem;
font-weight: 600;
color: #fff;
}
.ad-bug-bar-label {
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--ad-muted);
}
/* Toast */
.ad-toast {
position: fixed;
bottom: 24px;
right: 24px;
background: var(--ad-surface);
border: 1px solid var(--ad-border);
border-radius: 10px;
padding: 12px 20px;
color: #bbb;
font-size: 0.85rem;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
z-index: 9999;
transform: translateY(20px);
opacity: 0;
transition: all 0.3s;
pointer-events: none;
}
.ad-toast.visible {
transform: translateY(0);
opacity: 1;
}
</style>
{% endblock %}
{% block content %}
<div class="ad-page">
<div class="ad-header">
<h1>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: var(--ad-accent);">
<rect x="3" y="3" width="7" height="7"></rect>
<rect x="14" y="3" width="7" height="7"></rect>
<rect x="14" y="14" width="7" height="7"></rect>
<rect x="3" y="14" width="7" height="7"></rect>
</svg>
Admin Dashboard
</h1>
<p>
<span class="ad-activity-dot"></span>
Live overview — stats and activity update every 15s
</p>
</div>
<!-- Stats Cards -->
<div class="ad-stats-grid" id="ad-stats-grid">
<div class="ad-stat-card online">
<div class="ad-stat-number" id="stat-online-users">-</div>
<div class="ad-stat-label">Online Now</div>
</div>
<div class="ad-stat-card users">
<div class="ad-stat-number" id="stat-total-users">-</div>
<div class="ad-stat-label">Total Users</div>
</div>
<div class="ad-stat-card bugs">
<div class="ad-stat-number" id="stat-total-bugs">-</div>
<div class="ad-stat-label">Total Reports</div>
</div>
<div class="ad-stat-card open">
<div class="ad-stat-number" id="stat-open-bugs">-</div>
<div class="ad-stat-label">Open Reports</div>
</div>
<div class="ad-stat-card resolved">
<div class="ad-stat-number" id="stat-resolved-bugs">-</div>
<div class="ad-stat-label">Resolved</div>
</div>
<div class="ad-stat-card live">
<div class="ad-stat-number" id="stat-live-time">--:--:--</div>
<div class="ad-stat-label">Uptime</div>
</div>
</div>
<!-- Quick Admin Actions -->
<div class="ad-quick-cards">
<a href="/admin/bug-reports" class="ad-quick-card">
<div class="ad-quick-card-icon bugs">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path>
<line x1="4" y1="22" x2="4" y2="15"></line>
</svg>
</div>
<div class="ad-quick-card-body">
<div class="ad-quick-card-title">Bug Reports</div>
<div class="ad-quick-card-desc">View, reply, and manage all bug reports</div>
</div>
<span class="ad-quick-card-arrow"></span>
</a>
<a href="/admin/announcements" class="ad-quick-card">
<div class="ad-quick-card-icon announce">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon>
</svg>
</div>
<div class="ad-quick-card-body">
<div class="ad-quick-card-title">Announcements</div>
<div class="ad-quick-card-desc">Create and manage site-wide announcements</div>
</div>
<span class="ad-quick-card-arrow"></span>
</a>
</div>
<!-- Main Grid -->
<div class="ad-grid-2">
<!-- Live Activity Feed -->
<div class="ad-activity-feed">
<div class="ad-activity-header">
<h2>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color: var(--ad-success);">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
Live Activity
</h2>
<span class="ad-activity-count">
<span class="ad-activity-dot"></span>
<span id="ad-online-count">0</span> online
</span>
</div>
<div class="ad-activity-list" id="ad-activity-list">
<div class="ad-loading"><div class="ad-spinner"></div></div>
</div>
</div>
<!-- Bug Reports Summary -->
<div style="display: flex; flex-direction: column; gap: 12px;">
<div class="ad-bug-stats">
<h2>
<span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color: var(--ad-error); vertical-align: middle; margin-right: 5px;">
<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path>
<line x1="4" y1="22" x2="4" y2="15"></line>
</svg>
Reports
</span>
<a href="/admin/bug-reports" style="font-size:0.75rem;color:var(--ad-accent);text-decoration:none;font-weight:500;">Manage →</a>
</h2>
<div class="ad-bug-bar-chart">
<div class="ad-bug-bar">
<div class="ad-bug-bar-count" id="bug-bar-open-count">0</div>
<div class="ad-bug-bar-fill open" id="bug-bar-open" style="height: 3px;"></div>
<div class="ad-bug-bar-label">Open</div>
</div>
<div class="ad-bug-bar">
<div class="ad-bug-bar-count" id="bug-bar-progress-count">0</div>
<div class="ad-bug-bar-fill in_progress" id="bug-bar-progress" style="height: 3px;"></div>
<div class="ad-bug-bar-label">In Prog</div>
</div>
<div class="ad-bug-bar">
<div class="ad-bug-bar-count" id="bug-bar-resolved-count">0</div>
<div class="ad-bug-bar-fill resolved" id="bug-bar-resolved" style="height: 3px;"></div>
<div class="ad-bug-bar-label">Resolved</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Toast -->
<div class="ad-toast" id="ad-toast"></div>
{% endblock %}
{% block extra_js %}
<script>
(function() {
const toastEl = document.getElementById('ad-toast');
function showToast(msg) {
toastEl.textContent = msg;
toastEl.classList.add('visible');
setTimeout(() => toastEl.classList.remove('visible'), 3000);
}
function updateBar(elementId, count, maxCount) {
const el = document.getElementById(elementId);
if (!el) return;
const ratio = maxCount > 0 ? count / maxCount : 0;
el.style.height = Math.max(3, ratio * 80) + 'px';
}
function formatTime(seconds) {
const h = Math.floor(seconds / 3600).toString().padStart(2, '0');
const m = Math.floor((seconds % 3600) / 60).toString().padStart(2, '0');
const s = (seconds % 60).toString().padStart(2, '0');
return `${h}:${m}:${s}`;
}
function timeAgo(isoString) {
if (!isoString) return '';
const diff = Math.floor((Date.now() - new Date(isoString).getTime()) / 1000);
if (diff < 60) return 'just now';
if (diff < 120) return '1m ago';
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
return Math.floor(diff / 3600) + 'h ago';
}
function getPageLabel(url) {
if (!url || url === '/') return 'Home';
// Clean up the URL for display
const parts = url.split('/').filter(Boolean);
if (parts.length === 0) return 'Home';
const last = decodeURIComponent(parts[parts.length - 1])
.replace(/-/g, ' ')
.replace(/\b\w/g, c => c.toUpperCase());
return last.substring(0, 30);
}
async function loadDashboard() {
try {
const res = await fetch('/api/admin/dashboard');
const data = await res.json();
if (!data.success) {
showToast('Failed to load dashboard');
return;
}
const now = new Date();
const timeStr = now.toLocaleTimeString();
// Stats
document.getElementById('stat-total-users').textContent = data.users.total;
document.getElementById('stat-online-users').textContent = data.users.active_count || 0;
// Bug stats
const bugs = data.bugs;
document.getElementById('stat-total-bugs').textContent = bugs.total;
document.getElementById('stat-open-bugs').textContent = bugs.open;
document.getElementById('stat-resolved-bugs').textContent = bugs.resolved;
// Uptime — calculate elapsed seconds since server started
var uptimeSeconds = 0;
if (data.server_start_time) {
uptimeSeconds = Math.floor(Date.now() / 1000 - data.server_start_time);
}
document.getElementById('stat-live-time').textContent = formatTime(uptimeSeconds);
// Bar chart
const maxCount = Math.max(bugs.open, bugs.in_progress, bugs.resolved, 1);
document.getElementById('bug-bar-open-count').textContent = bugs.open;
document.getElementById('bug-bar-progress-count').textContent = bugs.in_progress;
document.getElementById('bug-bar-resolved-count').textContent = bugs.resolved;
updateBar('bug-bar-open', bugs.open, maxCount);
updateBar('bug-bar-progress', bugs.in_progress, maxCount);
updateBar('bug-bar-resolved', bugs.resolved, maxCount);
// Online count
document.getElementById('ad-online-count').textContent = data.users.active_count || 0;
// Live Activity Feed
const activityList = document.getElementById('ad-activity-list');
const activeUsers = data.users.active || [];
if (activeUsers.length === 0) {
activityList.innerHTML = '<div class="ad-empty">No users currently active.</div>';
} else {
activityList.innerHTML = activeUsers.map(u => {
var isGuest = u.is_anonymous === true;
var initial = (u.username || '?')[0].toUpperCase();
var avatarHtml;
if (isGuest) {
avatarHtml = `<div class="ad-activity-avatar-placeholder" style="background:#1a1a2e;color:#888;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
</svg>
</div>`;
} else if (u.avatar) {
avatarHtml = `<img class="ad-activity-avatar" src="${u.avatar}" alt="${u.username}" onerror="this.style.display='none'">`;
} else {
avatarHtml = `<div class="ad-activity-avatar-placeholder">${initial}</div>`;
}
var usernameHtml = isGuest
? `<span style="color:#666;">${u.username}</span>`
: `<span>${u.username}</span>`;
var pageLabel = u.page_title || getPageLabel(u.page_url);
var timeLabel = timeAgo(u.last_seen);
return `
<div class="ad-activity-item">
${avatarHtml}
<div class="ad-activity-info">
<div class="ad-activity-username">${usernameHtml}</div>
<div class="ad-activity-page" title="${u.page_url || ''}">📍 ${pageLabel}</div>
</div>
<div class="ad-activity-time">${timeLabel}</div>
</div>
`;
}).join('');
}
} catch (e) {
showToast('Network error loading dashboard.');
document.getElementById('ad-activity-list').innerHTML = '<div class="ad-empty">Network error</div>';
}
}
// Load immediately, then poll every 15 seconds
loadDashboard();
setInterval(loadDashboard, 15000);
})();
</script>
{% endblock %}