Files
firefrost-services/services/arbiter-3.0/src/views/layout.ejs
Claude (Chronicler #61) 5e8201fd22 feat: Task #94 Global Restart Scheduler
Complete implementation of staggered restart scheduler for Trinity Console.

Database:
- global_restart_config: Node-wide settings (TX1 @ 04:00 UTC, NC1 @ 04:30 UTC)
- server_restart_schedules: Per-server state with sort order
- sync_logs: Audit trail for all sync operations

Backend:
- src/utils/scheduler.js: Stagger calculation with date-fns
- src/lib/ptero-sync.js: Pterodactyl API integration (create/update/delete/audit)
- src/routes/admin/scheduler.js: All CRUD + import + sync + audit routes

Frontend:
- Drag-and-drop server ordering (SortableJS)
- Per-node config cards with base time + interval
- Audit modal to detect and nuke rogue schedules
- Skip toggle for maintenance mode
- Visual sync status indicators

Features:
- Import servers from Pterodactyl discovery
- Recalculate effective times on reorder
- Rate-limited API calls (200ms delay)
- [Trinity] Daily Restart naming convention

Signed-off-by: Claude (Chronicler #61) <claude@firefrostgaming.com>
2026-04-05 09:58:52 +00:00

128 lines
6.4 KiB
Plaintext

<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %> | Trinity Console</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@1.9.11"></script>
<script>
// Configure htmx to include CSRF token in all requests
document.addEventListener('DOMContentLoaded', function() {
document.body.addEventListener('htmx:configRequest', function(evt) {
evt.detail.headers['CSRF-Token'] = '<%= csrfToken %>';
});
});
</script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
fire: '#FF6B35',
frost: '#4ECDC4',
universal: '#A855F7',
darkbg: '#1a1a1a',
darkcard: '#2d2d2d'
}
}
}
}
</script>
<style>
/* Mobile: Hide sidebar by default, show on toggle */
@media (max-width: 768px) {
#sidebar {
position: fixed;
left: -100%;
transition: left 0.3s ease;
z-index: 50;
height: 100vh;
}
#sidebar.open {
left: 0;
}
#sidebar-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
z-index: 40;
}
#sidebar-overlay.open {
display: block;
}
}
</style>
</head>
<body class="bg-gray-100 dark:bg-darkbg text-gray-900 dark:text-gray-100 font-sans antialiased transition-colors duration-200">
<!-- Mobile menu overlay -->
<div id="sidebar-overlay" onclick="document.getElementById('sidebar').classList.remove('open'); this.classList.remove('open');"></div>
<div class="flex h-screen overflow-hidden">
<aside id="sidebar" class="w-64 bg-white dark:bg-darkcard border-r border-gray-200 dark:border-gray-700 flex flex-col">
<div class="p-6 flex justify-between items-center">
<h1 class="text-2xl font-bold bg-gradient-to-r from-fire via-universal to-frost text-transparent bg-clip-text">Trinity Console</h1>
<!-- Mobile close button -->
<button onclick="document.getElementById('sidebar').classList.remove('open'); document.getElementById('sidebar-overlay').classList.remove('open');" class="md:hidden text-2xl">✕</button>
</div>
<nav class="flex-1 px-4 space-y-2">
<a href="/admin/dashboard" class="block px-4 py-2 rounded-md <%= currentPath === '/dashboard' ? 'bg-gray-200 dark:bg-gray-700' : 'hover:bg-gray-100 dark:hover:bg-gray-800' %>">
📊 Dashboard
</a>
<a href="/admin/servers" class="block px-4 py-2 rounded-md <%= currentPath.startsWith('/servers') ? 'bg-gray-200 dark:bg-gray-700' : 'hover:bg-gray-100 dark:hover:bg-gray-800' %>">
🖥️ Servers
</a>
<a href="/admin/players" class="block px-4 py-2 rounded-md <%= currentPath.startsWith('/players') ? 'bg-gray-200 dark:bg-gray-700' : 'hover:bg-gray-100 dark:hover:bg-gray-800' %>">
👥 Players
</a>
<a href="/admin/financials" class="block px-4 py-2 rounded-md <%= currentPath.startsWith('/financials') ? 'bg-gray-200 dark:bg-gray-700' : 'hover:bg-gray-100 dark:hover:bg-gray-800' %>">
💰 Financials
</a>
<a href="/admin/grace" class="block px-4 py-2 rounded-md <%= currentPath.startsWith('/grace') ? 'bg-gray-200 dark:bg-gray-700' : 'hover:bg-gray-100 dark:hover:bg-gray-800' %>">
⏳ Grace Period
</a>
<a href="/admin/audit" class="block px-4 py-2 rounded-md <%= currentPath.startsWith('/audit') ? 'bg-gray-200 dark:bg-gray-700' : 'hover:bg-gray-100 dark:hover:bg-gray-800' %>">
📋 Audit Log
</a>
<a href="/admin/roles" class="block px-4 py-2 rounded-md <%= currentPath.startsWith('/roles') ? 'bg-gray-200 dark:bg-gray-700' : 'hover:bg-gray-100 dark:hover:bg-gray-800' %>">
🔍 Role Audit
</a>
<a href="/admin/scheduler" class="block px-4 py-2 rounded-md <%= currentPath.startsWith('/scheduler') ? 'bg-gray-200 dark:bg-gray-700' : 'hover:bg-gray-100 dark:hover:bg-gray-800' %>">
⏰ Restart Scheduler
</a>
</nav>
<div class="p-4 border-t border-gray-200 dark:border-gray-700">
<div class="flex items-center gap-3">
<img src="https://cdn.discordapp.com/avatars/<%= adminUser.id %>/<%= adminUser.avatar %>.png" class="w-10 h-10 rounded-full">
<span class="font-medium"><%= adminUser.username %></span>
</div>
</div>
</aside>
<main class="flex-1 flex flex-col overflow-hidden">
<header class="bg-white dark:bg-darkcard border-b border-gray-200 dark:border-gray-700 h-16 flex items-center justify-between px-6">
<div class="flex items-center gap-4">
<!-- Mobile hamburger menu -->
<button onclick="document.getElementById('sidebar').classList.add('open'); document.getElementById('sidebar-overlay').classList.add('open');" class="md:hidden text-2xl">☰</button>
<h2 class="text-xl font-semibold"><%= title %></h2>
</div>
<div class="flex items-center gap-4">
<button onclick="document.documentElement.classList.toggle('dark')" class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700">
🌙/☀️
</button>
<span class="relative">
🔔 <span class="absolute -top-1 -right-1 bg-fire text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">0</span>
</span>
</div>
</header>
<div class="flex-1 overflow-auto p-6">
<%- body %>
</div>
</main>
</div>
</body>
</html>