feat: Add comprehensive documentation suite and reorganize project structure
- Created complete documentation in docs/ directory - Added PROJECT_OVERVIEW.md with feature highlights and getting started guide - Added ARCHITECTURE.md with system design and technical details - Added SECURITY.md with comprehensive security implementation guide - Added DEVELOPMENT.md with development workflows and best practices - Added DEPLOYMENT.md with production deployment instructions - Added API.md with complete REST API documentation - Added CONTRIBUTING.md with contribution guidelines - Added CHANGELOG.md with version history and migration notes - Reorganized all documentation files into docs/ directory for better organization - Updated README.md with proper documentation links and quick navigation - Enhanced project structure with professional documentation standards
This commit is contained in:
346
admin.php
Normal file
346
admin.php
Normal file
@@ -0,0 +1,346 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/admin/includes/data_providers.php';
|
||||
require_once __DIR__ . '/admin/includes/layout.php';
|
||||
|
||||
$pdo = admin_pdo();
|
||||
|
||||
if (isset($_GET['action'])) {
|
||||
header('Content-Type: application/json');
|
||||
$action = $_GET['action'];
|
||||
|
||||
switch ($action) {
|
||||
case 'stats':
|
||||
echo json_encode([
|
||||
'users' => admin_fetch_user_stats($pdo),
|
||||
'videos' => admin_fetch_video_stats($pdo),
|
||||
'streams' => admin_fetch_stream_stats($pdo),
|
||||
'totalViews' => admin_fetch_total_views($pdo),
|
||||
'health' => admin_fetch_system_health($pdo),
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'activity':
|
||||
echo json_encode(['activity' => admin_fetch_recent_activity($pdo, 12)]);
|
||||
break;
|
||||
|
||||
case 'pending':
|
||||
echo json_encode(['pending' => admin_fetch_pending_videos($pdo, 10)]);
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Unknown action']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
$userStats = admin_fetch_user_stats($pdo);
|
||||
$videoStats = admin_fetch_video_stats($pdo);
|
||||
$streamStats = admin_fetch_stream_stats($pdo);
|
||||
$totalViews = admin_fetch_total_views($pdo);
|
||||
$health = admin_fetch_system_health($pdo);
|
||||
$recentActivity = admin_fetch_recent_activity($pdo);
|
||||
$pendingContent = admin_fetch_pending_videos($pdo);
|
||||
|
||||
$quickActions = [
|
||||
['label' => 'User Management', 'description' => 'Review members, roles, and status', 'icon' => '👥', 'url' => '/admin_users.php'],
|
||||
['label' => 'Content Moderation', 'description' => 'Approve, reject, or feature uploads', 'icon' => '🎬', 'url' => '/admin_content_management.php'],
|
||||
['label' => 'Token Economy', 'description' => 'Monitor purchases and balances', 'icon' => '💰', 'url' => '/admin_token_dashboard.php'],
|
||||
['label' => 'Background Jobs', 'description' => 'Track queue throughput', 'icon' => '⚙', 'url' => '/f_modules/m_backend/queue_management.php'],
|
||||
['label' => 'System Logs', 'description' => 'Audit platform health and errors', 'icon' => '📝', 'url' => '/f_modules/m_backend/log_viewer.php'],
|
||||
['label' => 'Security Center', 'description' => 'Manage bans, fingerprints, and IPs', 'icon' => '🔐', 'url' => '/f_modules/m_backend/ip_management.php'],
|
||||
];
|
||||
|
||||
admin_page_start('Platform Dashboard', 'dashboard');
|
||||
?>
|
||||
|
||||
<section class="stats-grid">
|
||||
<article class="stat-card">
|
||||
<div class="stat-card__icon">👥</div>
|
||||
<div class="stat-card__label">Total Users</div>
|
||||
<div class="stat-card__value" id="metric-users-total"><?= admin_format_number($userStats['total']) ?></div>
|
||||
<div class="stat-card__meta">
|
||||
<span><strong id="metric-users-today"><?= admin_format_number($userStats['today']) ?></strong> today</span>
|
||||
<span><strong id="metric-users-active"><?= admin_format_number($userStats['active']) ?></strong> active</span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<div class="stat-card__icon">🎬</div>
|
||||
<div class="stat-card__label">Video Library</div>
|
||||
<div class="stat-card__value" id="metric-videos-total"><?= admin_format_number($videoStats['total']) ?></div>
|
||||
<div class="stat-card__meta">
|
||||
<span><strong id="metric-videos-approved"><?= admin_format_number($videoStats['approved']) ?></strong> approved</span>
|
||||
<span><strong id="metric-videos-pending"><?= admin_format_number($videoStats['pending']) ?></strong> awaiting review</span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<div class="stat-card__icon">📡</div>
|
||||
<div class="stat-card__label">Live Streams</div>
|
||||
<div class="stat-card__value" id="metric-streams-total"><?= admin_format_number($streamStats['total']) ?></div>
|
||||
<div class="stat-card__meta">
|
||||
<span><strong id="metric-streams-active"><?= admin_format_number($streamStats['active']) ?></strong> live now</span>
|
||||
<span><strong id="metric-streams-today"><?= admin_format_number($streamStats['today']) ?></strong> started today</span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<div class="stat-card__icon">📈</div>
|
||||
<div class="stat-card__label">Total Views</div>
|
||||
<div class="stat-card__value" id="metric-views-total"><?= admin_format_number($totalViews) ?></div>
|
||||
<div class="stat-card__meta">
|
||||
<span>Platform-wide engagement this week</span>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<div class="grid grid--two">
|
||||
<section class="card">
|
||||
<div class="card__header">
|
||||
<h2>Recent Activity</h2>
|
||||
<div class="card__header-actions">
|
||||
<span class="spinner" id="activity-spinner" hidden></span>
|
||||
<button class="admin-button admin-button--ghost" id="refresh-button" type="button">Refresh Data</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="activity-list" class="timeline">
|
||||
<?php if (empty($recentActivity)): ?>
|
||||
<div class="empty-state">No recent activity recorded.</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($recentActivity as $activity): ?>
|
||||
<article class="timeline__item">
|
||||
<div class="timeline__icon">🧭</div>
|
||||
<div class="timeline__content">
|
||||
<div class="timeline__title"><?= admin_escape($activity['request_uri']) ?></div>
|
||||
<div class="timeline__meta">
|
||||
<?= admin_escape($activity['ip_address']) ?> · <?= admin_format_datetime($activity['timestamp']) ?>
|
||||
</div>
|
||||
<div class="timeline__details"><?= admin_escape($activity['user_agent'] ?? 'Unknown client') ?></div>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="card__header">
|
||||
<h2>System Health</h2>
|
||||
</div>
|
||||
<div id="health-list" class="health-grid">
|
||||
<?php foreach ($health as $key => $status): ?>
|
||||
<?php
|
||||
$state = match ($status['status']) {
|
||||
'healthy' => 'success',
|
||||
'warning' => 'warning',
|
||||
'error' => 'danger',
|
||||
default => 'muted',
|
||||
};
|
||||
$iconEntity = [
|
||||
'success' => '✅',
|
||||
'warning' => '⚠',
|
||||
'danger' => '❌',
|
||||
'muted' => 'ℹ',
|
||||
][$state];
|
||||
?>
|
||||
<div class="health-card health-card--<?= $state ?>">
|
||||
<div class="health-card__icon"><?= $iconEntity ?></div>
|
||||
<div>
|
||||
<div class="health-card__title"><?= admin_escape(ucfirst($key)) ?></div>
|
||||
<div class="health-card__details"><?= admin_escape($status['details'] ?? 'No data') ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
<div class="card__header">
|
||||
<h2>Pending Content Review</h2>
|
||||
<a class="admin-button admin-button--ghost" href="/admin_content_management.php">Moderation center</a>
|
||||
</div>
|
||||
<div id="pending-list">
|
||||
<?php if (empty($pendingContent)): ?>
|
||||
<div class="empty-state">All submissions are currently reviewed.</div>
|
||||
<?php else: ?>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>User</th>
|
||||
<th>Uploaded</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($pendingContent as $item): ?>
|
||||
<tr>
|
||||
<td><?= admin_escape($item['file_title']) ?></td>
|
||||
<td>#<?= admin_escape((string) $item['usr_id']) ?></td>
|
||||
<td><?= admin_format_datetime($item['upload_date']) ?></td>
|
||||
<td>
|
||||
<span class="badge badge--warning">
|
||||
<?= admin_escape(ucfirst($item['processing_status'] ?? 'pending')) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a class="admin-button admin-button--ghost" href="/f_modules/m_backend/files.php?search=<?= urlencode($item['file_key']) ?>">Review</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="card__header">
|
||||
<h2>Quick Actions</h2>
|
||||
</div>
|
||||
<div class="quick-actions">
|
||||
<?php foreach ($quickActions as $action): ?>
|
||||
<a class="quick-actions__item" href="<?= admin_escape($action['url']) ?>">
|
||||
<div class="quick-actions__icon"><?= $action['icon'] ?></div>
|
||||
<div class="quick-actions__content">
|
||||
<div class="quick-actions__title"><?= admin_escape($action['label']) ?></div>
|
||||
<div class="quick-actions__desc"><?= admin_escape($action['description']) ?></div>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php
|
||||
$script = <<<'JS'
|
||||
const metricMap = {
|
||||
'metric-users-total': data => data.users.total,
|
||||
'metric-users-today': data => data.users.today,
|
||||
'metric-users-active': data => data.users.active,
|
||||
'metric-videos-total': data => data.videos.total,
|
||||
'metric-videos-approved': data => data.videos.approved,
|
||||
'metric-videos-pending': data => data.videos.pending,
|
||||
'metric-streams-total': data => data.streams.total,
|
||||
'metric-streams-active': data => data.streams.active,
|
||||
'metric-streams-today': data => data.streams.today,
|
||||
'metric-views-total': data => data.totalViews,
|
||||
};
|
||||
|
||||
const healthList = document.getElementById('health-list');
|
||||
const activityList = document.getElementById('activity-list');
|
||||
const pendingList = document.getElementById('pending-list');
|
||||
const refreshButton = document.getElementById('refresh-button');
|
||||
const activitySpinner = document.getElementById('activity-spinner');
|
||||
|
||||
function renderHealth(health) {
|
||||
if (!healthList) return;
|
||||
const fragments = [];
|
||||
const icons = { success: '\u2705', warning: '\u26A0', danger: '\u274C', muted: '\u2139' };
|
||||
|
||||
for (const [key, status] of Object.entries(health)) {
|
||||
const state = status.status === 'healthy' ? 'success'
|
||||
: status.status === 'warning' ? 'warning'
|
||||
: status.status === 'error' ? 'danger'
|
||||
: 'muted';
|
||||
|
||||
fragments.push(`
|
||||
<div class="health-card health-card--${state}">
|
||||
<div class="health-card__icon">${icons[state]}</div>
|
||||
<div>
|
||||
<div class="health-card__title">${key.charAt(0).toUpperCase() + key.slice(1)}</div>
|
||||
<div class="health-card__details">${status.details ?? 'No data'}</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
healthList.innerHTML = fragments.join('');
|
||||
}
|
||||
|
||||
function renderActivity(items) {
|
||||
if (!activityList) return;
|
||||
if (!items || !items.length) {
|
||||
activityList.innerHTML = '<div class="empty-state">No recent activity recorded.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
activityList.innerHTML = items.map(item => `
|
||||
<article class="timeline__item">
|
||||
<div class="timeline__icon">🧭</div>
|
||||
<div class="timeline__content">
|
||||
<div class="timeline__title">${item.request_uri ?? '/'}</div>
|
||||
<div class="timeline__meta">${item.ip_address ?? 'Unknown IP'} · ${item.timestamp ?? ''}</div>
|
||||
<div class="timeline__details">${item.user_agent ?? 'Unknown client'}</div>
|
||||
</div>
|
||||
</article>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderPending(items) {
|
||||
if (!pendingList) return;
|
||||
if (!items || !items.length) {
|
||||
pendingList.innerHTML = '<div class="empty-state">All submissions are currently reviewed.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
pendingList.innerHTML = `
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>User</th>
|
||||
<th>Uploaded</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${items.map(item => `
|
||||
<tr>
|
||||
<td>${item.file_title ?? 'Untitled'}</td>
|
||||
<td>#${item.usr_id ?? 'N/A'}</td>
|
||||
<td>${item.upload_date ?? ''}</td>
|
||||
<td><span class="badge badge--warning">${(item.processing_status ?? 'pending').toUpperCase()}</span></td>
|
||||
<td><a class="admin-button admin-button--ghost" href="/f_modules/m_backend/files.php?search=${encodeURIComponent(item.file_key ?? '')}">Review</a></td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
async function refreshData() {
|
||||
if (activitySpinner) activitySpinner.hidden = false;
|
||||
try {
|
||||
const [statsRes, activityRes, pendingRes] = await Promise.all([
|
||||
fetch('?action=stats').then(r => r.json()),
|
||||
fetch('?action=activity').then(r => r.json()),
|
||||
fetch('?action=pending').then(r => r.json()),
|
||||
]);
|
||||
|
||||
Object.entries(metricMap).forEach(([id, getter]) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
const value = getter(statsRes);
|
||||
el.textContent = Number.isFinite(value) ? value.toLocaleString() : value;
|
||||
}
|
||||
});
|
||||
|
||||
renderHealth(statsRes.health);
|
||||
renderActivity(activityRes.activity);
|
||||
renderPending(pendingRes.pending);
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh dashboard data', error);
|
||||
} finally {
|
||||
if (activitySpinner) activitySpinner.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (refreshButton) {
|
||||
refreshButton.addEventListener('click', refreshData);
|
||||
}
|
||||
|
||||
setInterval(refreshData, 30000);
|
||||
JS;
|
||||
|
||||
admin_page_end($script);
|
||||
Reference in New Issue
Block a user