Add nodes management and initial setup pages

- Implemented nodes management functionality in `nodes.php` including create, update, and delete actions.
- Added form validation and error handling for node operations.
- Created a new setup page in `setup.php` for initial administrator account creation.
- Included user feedback messages for successful operations and errors.
- Designed user interface for both nodes management and setup processes.
This commit is contained in:
2025-12-14 01:33:12 -05:00
parent cf0ec74888
commit d8b76233c0
19 changed files with 8800 additions and 0 deletions

538
pathvector-admin/index.php Normal file
View File

@@ -0,0 +1,538 @@
<?php
/**
* Pathvector Admin Dashboard
* Main Entry Point and Router
*/
// Error reporting
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
// Session configuration
session_start();
// Load configuration
require_once __DIR__ . '/config/config.php';
// Autoloader
spl_autoload_register(function ($class) {
$file = __DIR__ . '/lib/' . $class . '.php';
if (file_exists($file)) {
require_once $file;
}
});
// Initialize core classes
$db = new FlatFileDB(DATA_PATH);
$logger = new Logger($db);
$auth = new Auth($db);
// Check if installed (has at least one user)
$users = $db->getAll('users');
if (empty($users) && !isset($_GET['setup'])) {
header('Location: setup.php');
exit;
}
// Handle logout
if (isset($_GET['action']) && $_GET['action'] === 'logout') {
$auth->logout();
header('Location: login.php');
exit;
}
// Check authentication
if (!$auth->isLoggedIn()) {
header('Location: login.php');
exit;
}
// Get current user
$currentUser = $auth->getCurrentUser();
// Route handling
$page = $_GET['page'] ?? 'dashboard';
$action = $_GET['action'] ?? 'index';
// Define available pages
$pages = [
'dashboard' => ['file' => 'pages/dashboard.php', 'title' => 'Dashboard', 'icon' => 'home', 'roles' => ['admin', 'operator', 'readonly']],
'asns' => ['file' => 'pages/asns.php', 'title' => 'ASNs', 'icon' => 'organization', 'roles' => ['admin', 'operator', 'readonly']],
'nodes' => ['file' => 'pages/nodes.php', 'title' => 'Nodes', 'icon' => 'server', 'roles' => ['admin', 'operator', 'readonly']],
'peers' => ['file' => 'pages/peers.php', 'title' => 'Peers', 'icon' => 'git-branch', 'roles' => ['admin', 'operator', 'readonly']],
'templates' => ['file' => 'pages/templates.php', 'title' => 'Templates', 'icon' => 'file-code', 'roles' => ['admin', 'operator', 'readonly']],
'hosts' => ['file' => 'pages/hosts.php', 'title' => 'Hosts', 'icon' => 'terminal', 'roles' => ['admin', 'operator']],
'config' => ['file' => 'pages/config.php', 'title' => 'Configuration', 'icon' => 'file', 'roles' => ['admin', 'operator', 'readonly']],
'execute' => ['file' => 'pages/execute.php', 'title' => 'Execute', 'icon' => 'play', 'roles' => ['admin', 'operator']],
'logs' => ['file' => 'pages/logs.php', 'title' => 'Logs', 'icon' => 'history', 'roles' => ['admin', 'operator', 'readonly']],
'backups' => ['file' => 'pages/backups.php', 'title' => 'Backups', 'icon' => 'archive', 'roles' => ['admin']],
'users' => ['file' => 'pages/users.php', 'title' => 'Users', 'icon' => 'people', 'roles' => ['admin']],
'settings' => ['file' => 'pages/settings.php', 'title' => 'Settings', 'icon' => 'gear', 'roles' => ['admin']],
];
// Check if page exists
if (!isset($pages[$page])) {
$page = 'dashboard';
}
// Check page access
if (!in_array($currentUser['role'], $pages[$page]['roles'])) {
$page = 'dashboard';
}
$currentPage = $pages[$page];
// Helper function to check permission
function hasPermission(string $permission): bool {
global $auth;
return $auth->hasPermission($permission);
}
// Helper function to generate CSRF token field
function csrfField(): string {
global $auth;
return '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($auth->generateCsrfToken()) . '">';
}
// Helper function to escape output
function e($value): string {
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
// Helper function to format date
function formatDate($date): string {
if (empty($date)) return '-';
return date('Y-m-d H:i:s', strtotime($date));
}
// Helper function to format bytes
function formatBytes(int $bytes, int $precision = 2): string {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, $precision) . ' ' . $units[$pow];
}
?>
<!DOCTYPE html>
<html lang="en" data-color-mode="auto" data-light-theme="light" data-dark-theme="dark_dimmed">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= e($currentPage['title']) ?> - Pathvector Admin</title>
<link rel="stylesheet" href="https://unpkg.com/@primer/css@^20.2.4/dist/primer.css">
<link rel="stylesheet" href="assets/css/style.css">
<style>
/* Layout Styles */
.app-container {
display: flex;
min-height: 100vh;
}
.sidebar {
width: 260px;
background: var(--color-canvas-subtle);
border-right: 1px solid var(--color-border-default);
display: flex;
flex-direction: column;
position: fixed;
height: 100vh;
overflow-y: auto;
}
.sidebar-header {
padding: 16px;
border-bottom: 1px solid var(--color-border-default);
}
.sidebar-nav {
flex: 1;
padding: 8px 0;
}
.sidebar-footer {
padding: 16px;
border-top: 1px solid var(--color-border-default);
}
.nav-item {
display: flex;
align-items: center;
padding: 8px 16px;
color: var(--color-fg-default);
text-decoration: none;
border-radius: 6px;
margin: 2px 8px;
font-size: 14px;
}
.nav-item:hover {
background: var(--color-action-list-item-default-hover-bg);
text-decoration: none;
}
.nav-item.active {
background: var(--color-action-list-item-default-selected-bg);
font-weight: 600;
}
.nav-item .octicon {
margin-right: 8px;
}
.nav-section {
padding: 8px 16px 4px;
font-size: 12px;
font-weight: 600;
color: var(--color-fg-muted);
text-transform: uppercase;
}
.main-content {
flex: 1;
margin-left: 260px;
min-height: 100vh;
background: var(--color-canvas-default);
}
.page-header {
padding: 24px 32px 16px;
border-bottom: 1px solid var(--color-border-default);
background: var(--color-canvas-default);
}
.page-content {
padding: 24px 32px;
}
/* Card Styles */
.stat-card {
background: var(--color-canvas-subtle);
border: 1px solid var(--color-border-default);
border-radius: 6px;
padding: 16px;
}
.stat-value {
font-size: 32px;
font-weight: 600;
line-height: 1;
}
.stat-label {
font-size: 12px;
color: var(--color-fg-muted);
margin-top: 4px;
}
/* Table Enhancements */
.data-table {
width: 100%;
}
.data-table th {
font-weight: 600;
text-align: left;
padding: 8px 16px;
border-bottom: 1px solid var(--color-border-default);
background: var(--color-canvas-subtle);
}
.data-table td {
padding: 12px 16px;
border-bottom: 1px solid var(--color-border-muted);
}
.data-table tr:hover {
background: var(--color-action-list-item-default-hover-bg);
}
/* Status Indicators */
.status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.status-dot.success { background: var(--color-success-fg); }
.status-dot.warning { background: var(--color-attention-fg); }
.status-dot.danger { background: var(--color-danger-fg); }
.status-dot.muted { background: var(--color-fg-muted); }
/* Form Styles */
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
font-weight: 600;
margin-bottom: 4px;
font-size: 14px;
}
.form-hint {
font-size: 12px;
color: var(--color-fg-muted);
margin-top: 4px;
}
.form-input-wide {
width: 100%;
}
/* Modal Backdrop */
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 999;
display: none;
}
.modal-backdrop.active {
display: block;
}
/* Toast Notifications */
.toast-container {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 1000;
}
.toast {
padding: 12px 16px;
border-radius: 6px;
margin-top: 8px;
box-shadow: var(--color-shadow-large);
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Code Preview */
.code-preview {
background: var(--color-canvas-subtle);
border: 1px solid var(--color-border-default);
border-radius: 6px;
padding: 16px;
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
font-size: 12px;
overflow-x: auto;
white-space: pre;
}
/* Responsive */
@media (max-width: 768px) {
.sidebar {
width: 100%;
height: auto;
position: relative;
}
.main-content {
margin-left: 0;
}
.page-header,
.page-content {
padding: 16px;
}
}
</style>
</head>
<body>
<div class="app-container">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-header">
<a href="?page=dashboard" class="d-flex flex-items-center text-decoration-none">
<svg class="octicon color-fg-accent" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24" height="24">
<path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zm.75 4.75a.75.75 0 00-1.5 0v2.5h-2.5a.75.75 0 000 1.5h2.5v2.5a.75.75 0 001.5 0v-2.5h2.5a.75.75 0 000-1.5h-2.5v-2.5z"/>
</svg>
<span class="ml-2 f3 text-bold color-fg-default">Pathvector</span>
</a>
<div class="text-small color-fg-muted mt-1">BGP Administration</div>
</div>
<nav class="sidebar-nav">
<div class="nav-section">Overview</div>
<a href="?page=dashboard" class="nav-item <?= $page === 'dashboard' ? 'active' : '' ?>">
<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.156 1.835a.25.25 0 00-.312 0l-5.25 4.2a.25.25 0 00-.094.196v7.019c0 .138.112.25.25.25H5.5V8.25a.75.75 0 01.75-.75h3.5a.75.75 0 01.75.75v5.25h2.75a.25.25 0 00.25-.25V6.23a.25.25 0 00-.094-.195l-5.25-4.2zM6.906.664a1.75 1.75 0 012.188 0l5.25 4.2c.415.332.656.835.656 1.367v7.019A1.75 1.75 0 0113.25 15h-2.5a.75.75 0 01-.75-.75V9H6v5.25a.75.75 0 01-.75.75h-2.5A1.75 1.75 0 011 13.25V6.23c0-.531.24-1.034.656-1.366l5.25-4.2z"/></svg>
Dashboard
</a>
<div class="nav-section mt-3">Configuration</div>
<a href="?page=asns" class="nav-item <?= $page === 'asns' ? 'active' : '' ?>">
<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 14.25c0 .138.112.25.25.25H4v-1.25a.75.75 0 01.75-.75h2.5a.75.75 0 01.75.75v1.25h2.25a.25.25 0 00.25-.25V1.75a.25.25 0 00-.25-.25h-8.5a.25.25 0 00-.25.25v12.5zM1.75 16A1.75 1.75 0 010 14.25V1.75C0 .784.784 0 1.75 0h8.5C11.216 0 12 .784 12 1.75v12.5c0 .085-.006.168-.018.25h2.268a.25.25 0 00.25-.25V8.285a.25.25 0 00-.111-.208l-1.055-.703a.75.75 0 11.832-1.248l1.055.703c.487.325.779.871.779 1.456v5.965A1.75 1.75 0 0114.25 16h-3.5a.75.75 0 01-.197-.026c-.099.017-.2.026-.303.026h-3a.75.75 0 01-.75-.75V14h-1v1.25a.75.75 0 01-.75.75h-3zM3 3.75A.75.75 0 013.75 3h.5a.75.75 0 010 1.5h-.5A.75.75 0 013 3.75zM3.75 6a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h-.5zM3 9.75A.75.75 0 013.75 9h.5a.75.75 0 010 1.5h-.5A.75.75 0 013 9.75zM7.75 9a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h-.5zM7 6.75A.75.75 0 017.75 6h.5a.75.75 0 010 1.5h-.5A.75.75 0 017 6.75zM7.75 3a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h-.5z"/></svg>
ASNs
</a>
<a href="?page=nodes" class="nav-item <?= $page === 'nodes' ? 'active' : '' ?>">
<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.75 1A1.75 1.75 0 000 2.75v4c0 .372.116.717.314 1a1.742 1.742 0 00-.314 1v4c0 .966.784 1.75 1.75 1.75h12.5A1.75 1.75 0 0016 12.75v-4c0-.372-.116-.717-.314-1 .198-.283.314-.628.314-1v-4A1.75 1.75 0 0014.25 1H1.75zm0 7.5a.25.25 0 00-.25.25v4c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-4a.25.25 0 00-.25-.25H1.75zM1.5 2.75a.25.25 0 01.25-.25h12.5a.25.25 0 01.25.25v4a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25v-4zm5.5 2A.75.75 0 017.75 4h4.5a.75.75 0 010 1.5h-4.5A.75.75 0 017 4.75zM7.75 10a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-4.5zM3 4.75A.75.75 0 013.75 4h.5a.75.75 0 010 1.5h-.5A.75.75 0 013 4.75zM3.75 10a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h-.5z"/></svg>
Nodes
</a>
<a href="?page=peers" class="nav-item <?= $page === 'peers' ? 'active' : '' ?>">
<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M11.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122V6A2.5 2.5 0 0110 8.5H6a1 1 0 00-1 1v1.128a2.251 2.251 0 11-1.5 0V5.372a2.25 2.25 0 111.5 0v1.836A2.492 2.492 0 016 7h4a1 1 0 001-1v-.628A2.25 2.25 0 019.5 3.25zM4.25 12a.75.75 0 100 1.5.75.75 0 000-1.5zM3.5 3.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0z"/></svg>
Peers
</a>
<a href="?page=templates" class="nav-item <?= $page === 'templates' ? 'active' : '' ?>">
<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4 1.75C4 .784 4.784 0 5.75 0h5.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v8.586A1.75 1.75 0 0114.25 15h-9a.75.75 0 010-1.5h9a.25.25 0 00.25-.25V6h-2.75A1.75 1.75 0 0110 4.25V1.5H5.75a.25.25 0 00-.25.25v2.5a.75.75 0 01-1.5 0v-2.5zm7.5-.188V4.25c0 .138.112.25.25.25h2.688a.252.252 0 00-.011-.013l-2.914-2.914a.272.272 0 00-.013-.011zM5.72 6.72a.75.75 0 000 1.06l1.47 1.47-1.47 1.47a.75.75 0 101.06 1.06l2-2a.75.75 0 000-1.06l-2-2a.75.75 0 00-1.06 0zM3.28 7.78a.75.75 0 00-1.06-1.06l-2 2a.75.75 0 000 1.06l2 2a.75.75 0 001.06-1.06L1.81 9.25l1.47-1.47z"/></svg>
Templates
</a>
<?php if (hasPermission('manage_hosts')): ?>
<div class="nav-section mt-3">Operations</div>
<a href="?page=hosts" class="nav-item <?= $page === 'hosts' ? 'active' : '' ?>">
<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0114.25 15H1.75A1.75 1.75 0 010 13.25V2.75zm1.75-.25a.25.25 0 00-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25V2.75a.25.25 0 00-.25-.25H1.75zM7.25 8a.75.75 0 01-.22.53l-2.25 2.25a.75.75 0 11-1.06-1.06L5.44 8 3.72 6.28a.75.75 0 111.06-1.06l2.25 2.25c.141.14.22.331.22.53zm1.5 1.5a.75.75 0 000 1.5h3a.75.75 0 000-1.5h-3z"/></svg>
Hosts
</a>
<a href="?page=config" class="nav-item <?= $page === 'config' ? 'active' : '' ?>">
<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M3.75 1.5a.25.25 0 00-.25.25v11.5c0 .138.112.25.25.25h8.5a.25.25 0 00.25-.25V6H9.75A1.75 1.75 0 018 4.25V1.5H3.75zm5.75.56v2.19c0 .138.112.25.25.25h2.19L9.5 2.06zM2 1.75C2 .784 2.784 0 3.75 0h5.086c.464 0 .909.184 1.237.513l3.414 3.414c.329.328.513.773.513 1.237v8.086A1.75 1.75 0 0112.25 15h-8.5A1.75 1.75 0 012 13.25V1.75z"/></svg>
Config Preview
</a>
<a href="?page=execute" class="nav-item <?= $page === 'execute' ? 'active' : '' ?>">
<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zM6.379 5.227A.25.25 0 006 5.442v5.117a.25.25 0 00.379.214l4.264-2.559a.25.25 0 000-.428L6.379 5.227z"/></svg>
Execute
</a>
<?php endif; ?>
<div class="nav-section mt-3">System</div>
<a href="?page=logs" class="nav-item <?= $page === 'logs' ? 'active' : '' ?>">
<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.643 3.143L.427 1.927A.25.25 0 000 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 00.177-.427L2.715 4.215a6.5 6.5 0 11-1.18 4.458.75.75 0 10-1.493.154 8.001 8.001 0 101.6-5.684zM7.75 4a.75.75 0 01.75.75v2.992l2.028.812a.75.75 0 01-.557 1.392l-2.5-1A.75.75 0 017 8.25v-3.5A.75.75 0 017.75 4z"/></svg>
Logs
</a>
<?php if (hasPermission('manage_backups')): ?>
<a href="?page=backups" class="nav-item <?= $page === 'backups' ? 'active' : '' ?>">
<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M3.5 8a.5.5 0 01-.5-.5v-5a.5.5 0 01.5-.5h9a.5.5 0 01.5.5v5a.5.5 0 01-.5.5h-9zm-2 .5A1.5 1.5 0 003 10v4.5a.5.5 0 00.5.5h9a.5.5 0 00.5-.5V10a1.5 1.5 0 001.5-1.5v-6A1.5 1.5 0 0013 1H3a1.5 1.5 0 00-1.5 1.5v6zM4.5 11a.5.5 0 000 1h7a.5.5 0 000-1h-7z"/></svg>
Backups
</a>
<a href="?page=users" class="nav-item <?= $page === 'users' ? 'active' : '' ?>">
<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M5.5 3.5a2 2 0 100 4 2 2 0 000-4zM2 5.5a3.5 3.5 0 115.898 2.549 5.507 5.507 0 013.034 4.084.75.75 0 11-1.482.235 4.001 4.001 0 00-7.9 0 .75.75 0 01-1.482-.236A5.507 5.507 0 013.102 8.05 3.49 3.49 0 012 5.5zM11 4a.75.75 0 100 1.5 1.5 1.5 0 01.666 2.844.75.75 0 00-.416.672v.352a.75.75 0 00.574.73c1.2.289 2.162 1.2 2.522 2.372a.75.75 0 101.434-.44 5.01 5.01 0 00-2.56-3.012A3 3 0 0011 4z"/></svg>
Users
</a>
<a href="?page=settings" class="nav-item <?= $page === 'settings' ? 'active' : '' ?>">
<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.429 1.525a6.593 6.593 0 011.142 0c.036.003.108.036.137.146l.289 1.105c.147.56.55.967.997 1.189.174.086.341.183.501.29.417.278.97.423 1.53.27l1.102-.303c.11-.03.175.016.195.046.219.31.41.641.573.989.014.031.022.11-.059.19l-.815.806c-.411.406-.562.957-.53 1.456a4.588 4.588 0 010 .582c-.032.499.119 1.05.53 1.456l.815.806c.08.08.073.159.059.19a6.494 6.494 0 01-.573.99c-.02.029-.086.074-.195.045l-1.103-.303c-.559-.153-1.112-.008-1.529.27-.16.107-.327.204-.5.29-.449.222-.851.628-.998 1.189l-.289 1.105c-.029.11-.101.143-.137.146a6.613 6.613 0 01-1.142 0c-.036-.003-.108-.037-.137-.146l-.289-1.105c-.147-.56-.55-.967-.997-1.189a4.502 4.502 0 01-.501-.29c-.417-.278-.97-.423-1.53-.27l-1.102.303c-.11.03-.175-.016-.195-.046a6.492 6.492 0 01-.573-.989c-.014-.031-.022-.11.059-.19l.815-.806c.411-.406.562-.957.53-1.456a4.587 4.587 0 010-.582c.032-.499-.119-1.05-.53-1.456l-.815-.806c-.08-.08-.073-.159-.059-.19a6.44 6.44 0 01.573-.99c.02-.029.086-.074.195-.045l1.103.303c.559.153 1.112.008 1.529-.27.16-.107.327-.204.5-.29.449-.222.851-.628.998-1.189l.289-1.105c.029-.11.101-.143.137-.146zM8 0c-.236 0-.47.01-.701.03-.743.065-1.29.615-1.458 1.261l-.29 1.106c-.017.066-.078.158-.211.224a5.994 5.994 0 00-.668.386c-.123.082-.233.09-.3.071L3.27 2.776c-.644-.177-1.392.02-1.82.63a7.977 7.977 0 00-.704 1.217c-.315.675-.111 1.422.363 1.891l.815.806c.05.048.098.147.088.294a6.084 6.084 0 000 .772c.01.147-.038.246-.088.294l-.815.806c-.474.469-.678 1.216-.363 1.891.2.428.436.835.704 1.218.428.609 1.176.806 1.82.63l1.103-.303c.066-.019.176-.011.299.071.213.143.436.272.668.386.133.066.194.158.212.224l.289 1.106c.169.646.715 1.196 1.458 1.26a8.094 8.094 0 001.402 0c.743-.064 1.29-.614 1.458-1.26l.29-1.106c.017-.066.078-.158.211-.224a5.98 5.98 0 00.668-.386c.123-.082.233-.09.3-.071l1.102.302c.644.177 1.392-.02 1.82-.63.268-.382.505-.789.704-1.217.315-.675.111-1.422-.364-1.891l-.814-.806c-.05-.048-.098-.147-.088-.294a6.1 6.1 0 000-.772c-.01-.147.039-.246.088-.294l.814-.806c.475-.469.679-1.216.364-1.891a7.992 7.992 0 00-.704-1.218c-.428-.609-1.176-.806-1.82-.63l-1.103.303c-.066.019-.176.011-.299-.071a5.991 5.991 0 00-.668-.386c-.133-.066-.194-.158-.212-.224L10.16 1.29C9.99.645 9.444.095 8.701.031A8.094 8.094 0 008 0zm1.5 8a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM11 8a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
Settings
</a>
<?php endif; ?>
</nav>
<div class="sidebar-footer">
<div class="d-flex flex-items-center">
<div class="avatar avatar-2 mr-2 color-bg-accent-emphasis color-fg-on-emphasis d-flex flex-items-center flex-justify-center" style="border-radius: 50%;">
<?= strtoupper(substr($currentUser['username'], 0, 1)) ?>
</div>
<div class="flex-1 min-width-0">
<div class="text-bold text-small"><?= e($currentUser['username']) ?></div>
<div class="text-small color-fg-muted"><?= ucfirst($currentUser['role']) ?></div>
</div>
<a href="?action=logout" class="btn-octicon" title="Logout">
<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2 2.75C2 1.784 2.784 1 3.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5a.75.75 0 01-1.5 0v-5.5a.25.25 0 00-.25-.25h-8.5a.25.25 0 00-.25.25v10.5c0 .138.112.25.25.25h8.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 0112.25 15h-8.5A1.75 1.75 0 012 13.25V2.75zm10.44 4.5H6.75a.75.75 0 000 1.5h5.69l-1.97 1.97a.75.75 0 101.06 1.06l3.25-3.25a.75.75 0 000-1.06l-3.25-3.25a.75.75 0 10-1.06 1.06l1.97 1.97z"/></svg>
</a>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="main-content">
<?php
// Include the page file
$pageFile = __DIR__ . '/' . $currentPage['file'];
if (file_exists($pageFile)) {
include $pageFile;
} else {
?>
<div class="page-header">
<h1 class="h2"><?= e($currentPage['title']) ?></h1>
</div>
<div class="page-content">
<div class="flash flash-error">
<p>Page not found or not yet implemented.</p>
</div>
</div>
<?php
}
?>
</main>
</div>
<!-- Toast Container -->
<div id="toast-container" class="toast-container"></div>
<script>
// Toast notification function
function showToast(message, type = 'default') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
const typeClasses = {
'success': 'flash-success',
'error': 'flash-error',
'warning': 'flash-warn',
'default': ''
};
toast.className = 'toast flash ' + (typeClasses[type] || '');
toast.innerHTML = message;
container.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 5000);
}
// Confirm delete helper
function confirmDelete(message) {
return confirm(message || 'Are you sure you want to delete this item?');
}
// Form validation helper
function validateForm(form) {
const required = form.querySelectorAll('[required]');
let valid = true;
required.forEach(field => {
if (!field.value.trim()) {
field.classList.add('border-danger');
valid = false;
} else {
field.classList.remove('border-danger');
}
});
return valid;
}
// Auto-dismiss flash messages
document.querySelectorAll('.flash[data-dismiss]').forEach(flash => {
setTimeout(() => {
flash.style.opacity = '0';
setTimeout(() => flash.remove(), 300);
}, 5000);
});
</script>
</body>
</html>