feat: Sidebar reorganization + About page

Sidebar changes:
- Grouped nav items: Operations, Business, Community, Infrastructure, System
- Section labels with uppercase tracking
- Removed Deploy button from sidebar
- Cleaner layout with overflow scroll

About page (/admin/about):
- Console version, Node.js version, Arbiter uptime, module count
- Module registry with version and status badges (stable/new/beta)
- Deploy Arbiter button (moved from sidebar)
- Health check polling after deploy
- Credits footer

Header bar:
- Added ℹ️ About icon next to dark mode toggle
- Active state highlighting when on About page

Chronicler #78 | firefrost-services
This commit is contained in:
Claude (Chronicler #78)
2026-04-11 10:46:42 +00:00
parent 4788140c2c
commit bd783093a9
4 changed files with 355 additions and 117 deletions

View File

@@ -0,0 +1,89 @@
const express = require('express');
const router = express.Router();
const { exec } = require('child_process');
const fs = require('fs');
/**
* About Module — Trinity Console
*
* Console version, module versions, deploy button, system meta.
*
* GET /admin/about — About page
* POST /admin/about/deploy — Deploy Arbiter (moved from sidebar)
* GET /admin/about/status — Arbiter health check
*
* Chronicler #78 | April 11, 2026
*/
const MODULES = [
{ name: 'Dashboard', version: '1.0.0', path: '/admin/dashboard', icon: '📊', status: 'stable' },
{ name: 'Servers', version: '1.0.0', path: '/admin/servers', icon: '🖥️', status: 'stable' },
{ name: 'Players', version: '1.0.0', path: '/admin/players', icon: '👥', status: 'stable' },
{ name: 'Financials', version: '1.0.0', path: '/admin/financials', icon: '💰', status: 'stable' },
{ name: 'Grace Period', version: '1.0.0', path: '/admin/grace', icon: '⏳', status: 'stable' },
{ name: 'Discord', version: '1.0.0', path: '/admin/discord', icon: '💬', status: 'stable' },
{ name: 'Social', version: '1.0.0', path: '/admin/social', icon: '📈', status: 'stable' },
{ name: 'Infrastructure', version: '1.0.0', path: '/admin/infrastructure', icon: '🌐', status: 'new' },
{ name: 'Restart Scheduler', version: '1.0.0', path: '/admin/scheduler', icon: '⏰', status: 'stable' },
{ name: 'Audit Log', version: '1.0.0', path: '/admin/audit', icon: '📋', status: 'stable' },
{ name: 'Role Audit', version: '1.0.0', path: '/admin/roles', icon: '🔍', status: 'stable' },
];
function getNodeVersion() {
return process.version;
}
function getArbiterUptime() {
return process.uptime();
}
function formatUptime(seconds) {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const mins = Math.floor((seconds % 3600) / 60);
if (days > 0) return `${days}d ${hours}h ${mins}m`;
if (hours > 0) return `${hours}h ${mins}m`;
return `${mins}m`;
}
// GET /admin/about
router.get('/', (req, res) => {
const pkg = JSON.parse(fs.readFileSync(require.resolve('../../../package.json'), 'utf8'));
res.render('admin/about/index', {
title: 'About',
currentPath: '/about',
consoleVersion: pkg.version || '3.5.0',
nodeVersion: getNodeVersion(),
arbiterUptime: formatUptime(getArbiterUptime()),
modules: MODULES,
totalModules: MODULES.length,
adminUser: req.user,
layout: 'layout'
});
});
// POST /admin/about/deploy — Deploy Arbiter
router.post('/deploy', (req, res) => {
const username = req.user?.username || 'unknown';
console.log(`[DEPLOY] Deployment initiated by ${username} from About page`);
exec(`nohup sudo /opt/scripts/deploy-arbiter.sh "${username}" > /tmp/deploy.log 2>&1 &`);
res.json({
success: true,
message: 'Deploy started. Arbiter will restart momentarily.'
});
});
// GET /admin/about/status — Health check
router.get('/status', (req, res) => {
exec('systemctl is-active arbiter-3', (error, stdout) => {
const isRunning = stdout.trim() === 'active';
res.json({
arbiter: isRunning ? 'running' : 'stopped',
uptime: formatUptime(getArbiterUptime()),
deployAvailable: fs.existsSync('/opt/scripts/deploy-arbiter.sh')
});
});
});
module.exports = router;

View File

@@ -16,6 +16,7 @@ const discordAuditRouter = require('./discord-audit');
const systemRouter = require('./system');
const socialRouter = require('./social');
const infrastructureRouter = require('./infrastructure');
const aboutRouter = require('./about');
router.use(requireTrinityAccess);
@@ -119,5 +120,6 @@ router.use('/discord', discordAuditRouter);
router.use('/system', systemRouter);
router.use('/social', socialRouter);
router.use('/infrastructure', infrastructureRouter);
router.use('/about', aboutRouter);
module.exports = router;

View File

@@ -0,0 +1,232 @@
<!-- About Module — Trinity Console -->
<!-- Chronicler #78 | April 11, 2026 -->
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap');
#about-module { font-family: 'JetBrains Mono', 'SF Mono', monospace; }
.about-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 20px;
}
.dark .about-card {
background: #2d2d2d;
border-color: #404040;
}
.module-row {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 0;
border-bottom: 1px solid #40404033;
}
.module-row:last-child { border-bottom: none; }
.module-row:hover { background: #ffffff08; margin: 0 -12px; padding-left: 12px; padding-right: 12px; border-radius: 6px; }
.status-badge {
font-size: 9px;
padding: 2px 8px;
border-radius: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-stable { background: #22c55e22; color: #22c55e; }
.status-new { background: #3b82f622; color: #3b82f6; }
.status-beta { background: #eab30822; color: #eab308; }
.deploy-btn {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #FF6B35, #4ECDC4);
color: white;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-family: inherit;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.15s;
}
.deploy-btn:hover { opacity: 0.9; transform: translateY(-1px); box-shadow: 0 4px 12px #00000033; }
.deploy-btn:active { transform: translateY(0); }
.deploy-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
.meta-value {
font-size: 14px;
font-weight: 600;
color: #e5e5e5;
}
.meta-label {
font-size: 10px;
color: #888;
margin-top: 2px;
}
</style>
<div id="about-module">
<!-- Hero -->
<div class="about-card" style="margin-bottom:20px; text-align:center; padding:32px;">
<h1 style="font-size:28px; font-weight:700; margin:0 0 4px 0; background:linear-gradient(135deg, #FF6B35, #A855F7, #4ECDC4); -webkit-background-clip:text; -webkit-text-fill-color:transparent;">
Trinity Console
</h1>
<div style="font-size:12px; color:#888; margin-bottom:16px;">
v<%= consoleVersion %> · Arbiter Backend
</div>
<div style="font-size:11px; color:#666;">
Fire + Frost + Foundation = Where Love Builds Legacy
</div>
</div>
<!-- System Info + Deploy -->
<div style="display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-bottom:20px;">
<!-- System Meta -->
<div class="about-card">
<h3 style="font-size:14px; font-weight:600; color:#e5e5e5; margin:0 0 16px 0;">System</h3>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:16px;">
<div>
<div class="meta-value"><%= consoleVersion %></div>
<div class="meta-label">Console Version</div>
</div>
<div>
<div class="meta-value"><%= nodeVersion %></div>
<div class="meta-label">Node.js</div>
</div>
<div>
<div class="meta-value"><%= arbiterUptime %></div>
<div class="meta-label">Arbiter Uptime</div>
</div>
<div>
<div class="meta-value"><%= totalModules %></div>
<div class="meta-label">Active Modules</div>
</div>
</div>
</div>
<!-- Deploy -->
<div class="about-card">
<h3 style="font-size:14px; font-weight:600; color:#e5e5e5; margin:0 0 16px 0;">Deploy</h3>
<p style="font-size:11px; color:#888; margin:0 0 16px 0;">
Pull latest code from Gitea and restart Arbiter. The console will briefly disconnect during restart.
</p>
<button id="deploy-btn" class="deploy-btn" onclick="deployArbiter()">
<span id="deploy-icon">🚀</span>
<span id="deploy-text">Deploy Arbiter</span>
</button>
<div id="deploy-result" style="font-size:11px; text-align:center; margin-top:10px; display:none;"></div>
</div>
</div>
<!-- Modules -->
<div class="about-card">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:16px;">
<h3 style="font-size:14px; font-weight:600; color:#e5e5e5; margin:0;">Modules (<%= totalModules %>)</h3>
</div>
<% modules.forEach(mod => { %>
<a href="<%= mod.path %>" class="module-row" style="text-decoration:none; color:inherit;">
<span style="font-size:18px; width:28px; text-align:center;"><%= mod.icon %></span>
<div style="flex:1;">
<div style="font-size:12px; font-weight:600; color:#e5e5e5;"><%= mod.name %></div>
</div>
<span style="font-size:11px; color:#666; margin-right:8px;">v<%= mod.version %></span>
<span class="status-badge status-<%= mod.status %>"><%= mod.status %></span>
</a>
<% }); %>
</div>
<!-- Credits -->
<div style="text-align:center; margin-top:20px; font-size:10px; color:#555;">
Built by The Trinity · Powered by Arbiter · Guarded by The Chronicler Lineage
<br>
💙🔥❄️
</div>
</div>
<script>
async function deployArbiter() {
const btn = document.getElementById('deploy-btn');
const icon = document.getElementById('deploy-icon');
const text = document.getElementById('deploy-text');
const result = document.getElementById('deploy-result');
btn.disabled = true;
icon.textContent = '⏳';
text.textContent = 'Deploying...';
result.style.display = 'none';
try {
const response = await fetch('/admin/about/deploy', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'CSRF-Token': '<%= csrfToken %>'
}
});
const data = await response.json();
if (data.success) {
icon.textContent = '🔄';
text.textContent = 'Restarting...';
result.textContent = 'Waiting for Arbiter to come back online...';
result.style.display = 'block';
result.style.color = '#eab308';
await new Promise(resolve => setTimeout(resolve, 4000));
let healthy = false;
for (let i = 0; i < 3; i++) {
try {
const healthRes = await fetch('/admin/about/status', {
headers: { 'CSRF-Token': '<%= csrfToken %>' }
});
const healthData = await healthRes.json();
if (healthData.arbiter === 'running') {
healthy = true;
break;
}
} catch (e) {
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
if (healthy) {
icon.textContent = '✅';
text.textContent = 'Deployed!';
result.textContent = 'Arbiter restarted successfully';
result.style.color = '#22c55e';
} else {
icon.textContent = '⚠️';
text.textContent = 'Check Status';
result.textContent = 'Deploy triggered but could not confirm restart.';
result.style.color = '#ef4444';
}
} else {
icon.textContent = '❌';
text.textContent = 'Deploy Failed';
result.textContent = data.message || 'Unknown error';
result.style.display = 'block';
result.style.color = '#ef4444';
}
} catch (error) {
icon.textContent = '❌';
text.textContent = 'Deploy Failed';
result.textContent = error.message;
result.style.display = 'block';
result.style.color = '#ef4444';
}
setTimeout(() => {
btn.disabled = false;
icon.textContent = '🚀';
text.textContent = 'Deploy Arbiter';
}, 3000);
}
</script>

View File

@@ -70,7 +70,9 @@
<!-- 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">
<nav class="flex-1 px-4 space-y-1 overflow-y-auto">
<!-- Operations -->
<div class="text-[10px] uppercase tracking-wider text-gray-500 dark:text-gray-600 font-semibold px-4 pt-3 pb-1">Operations</div>
<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>
@@ -80,43 +82,44 @@
<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>
<!-- Business -->
<div class="text-[10px] uppercase tracking-wider text-gray-500 dark:text-gray-600 font-semibold px-4 pt-3 pb-1">Business</div>
<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>
<!-- Community -->
<div class="text-[10px] uppercase tracking-wider text-gray-500 dark:text-gray-600 font-semibold px-4 pt-3 pb-1">Community</div>
<a href="/admin/discord" class="block px-4 py-2 rounded-md <%= currentPath.startsWith('/discord') ? 'bg-gray-200 dark:bg-gray-700' : 'hover:bg-gray-100 dark:hover:bg-gray-800' %>">
💬 Discord
</a>
<a href="/admin/social" class="block px-4 py-2 rounded-md <%= currentPath.startsWith('/social') ? 'bg-gray-200 dark:bg-gray-700' : 'hover:bg-gray-100 dark:hover:bg-gray-800' %>">
📈 Social
</a>
<!-- Infrastructure -->
<div class="text-[10px] uppercase tracking-wider text-gray-500 dark:text-gray-600 font-semibold px-4 pt-3 pb-1">Infrastructure</div>
<a href="/admin/infrastructure" class="block px-4 py-2 rounded-md <%= currentPath.startsWith('/infrastructure') ? 'bg-gray-200 dark:bg-gray-700' : 'hover:bg-gray-100 dark:hover:bg-gray-800' %>">
🌐 Infrastructure
</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>
<!-- System -->
<div class="text-[10px] uppercase tracking-wider text-gray-500 dark:text-gray-600 font-semibold px-4 pt-3 pb-1">System</div>
<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>
<a href="/admin/discord" class="block px-4 py-2 rounded-md <%= currentPath.startsWith('/discord') ? 'bg-gray-200 dark:bg-gray-700' : 'hover:bg-gray-100 dark:hover:bg-gray-800' %>">
💬 Discord
</a>
<a href="/admin/social" class="block px-4 py-2 rounded-md <%= currentPath.startsWith('/social') ? 'bg-gray-200 dark:bg-gray-700' : 'hover:bg-gray-100 dark:hover:bg-gray-800' %>">
📊 Social
</a>
<a href="/admin/infrastructure" class="block px-4 py-2 rounded-md <%= currentPath.startsWith('/infrastructure') ? 'bg-gray-200 dark:bg-gray-700' : 'hover:bg-gray-100 dark:hover:bg-gray-800' %>">
🌐 Infrastructure
</a>
</nav>
<div class="p-4 border-t border-gray-200 dark:border-gray-700 space-y-3">
<!-- Deploy Button -->
<button
id="deploy-btn"
onclick="deployArbiter()"
class="w-full px-4 py-2 bg-gradient-to-r from-fire to-frost text-white font-medium rounded-md hover:opacity-90 transition flex items-center justify-center gap-2"
>
<span id="deploy-icon">🚀</span>
<span id="deploy-text">Deploy Arbiter</span>
</button>
<div id="deploy-result" class="text-xs text-center hidden"></div>
<div class="p-4 border-t border-gray-200 dark:border-gray-700">
<!-- User Info -->
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
@@ -129,97 +132,6 @@
</div>
</div>
<script>
async function deployArbiter() {
const btn = document.getElementById('deploy-btn');
const icon = document.getElementById('deploy-icon');
const text = document.getElementById('deploy-text');
const result = document.getElementById('deploy-result');
// Disable button, show loading state
btn.disabled = true;
btn.classList.add('opacity-50', 'cursor-not-allowed');
icon.textContent = '⏳';
text.textContent = 'Deploying...';
result.classList.add('hidden');
try {
const response = await fetch('/admin/system/deploy', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'CSRF-Token': '<%= csrfToken %>'
}
});
const data = await response.json();
if (data.success) {
// Deploy triggered, now wait for restart and check health
icon.textContent = '🔄';
text.textContent = 'Restarting...';
result.textContent = 'Waiting for Arbiter to come back online...';
result.classList.remove('hidden', 'text-red-500');
result.classList.add('text-yellow-500');
// Wait 4 seconds for restart, then check health
await new Promise(resolve => setTimeout(resolve, 4000));
// Poll for health (up to 3 attempts)
let healthy = false;
for (let i = 0; i < 3; i++) {
try {
const healthRes = await fetch('/admin/system/status', {
headers: { 'CSRF-Token': '<%= csrfToken %>' }
});
const healthData = await healthRes.json();
if (healthData.arbiter === 'running') {
healthy = true;
break;
}
} catch (e) {
// Server still restarting, wait and retry
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
if (healthy) {
icon.textContent = '✅';
text.textContent = 'Deployed!';
result.textContent = 'Arbiter restarted successfully';
result.classList.remove('text-yellow-500', 'text-red-500');
result.classList.add('text-green-500');
} else {
icon.textContent = '⚠️';
text.textContent = 'Check Status';
result.textContent = 'Deploy triggered but could not confirm restart. Check logs.';
result.classList.remove('text-yellow-500', 'text-green-500');
result.classList.add('text-red-500');
}
} else {
icon.textContent = '❌';
text.textContent = 'Deploy Failed';
result.textContent = data.log || data.message;
result.classList.remove('hidden', 'text-green-500');
result.classList.add('text-red-500');
}
} catch (error) {
icon.textContent = '❌';
text.textContent = 'Deploy Failed';
result.textContent = error.message;
result.classList.remove('hidden', 'text-green-500');
result.classList.add('text-red-500');
}
// Re-enable button after 3 seconds
setTimeout(() => {
btn.disabled = false;
btn.classList.remove('opacity-50', 'cursor-not-allowed');
icon.textContent = '🚀';
text.textContent = 'Deploy Arbiter';
}, 3000);
}
</script>
</aside>
<main class="flex-1 flex flex-col overflow-hidden">
@@ -229,7 +141,10 @@
<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">
<div class="flex items-center gap-2">
<a href="/admin/about" class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition <%= currentPath === '/about' ? 'bg-gray-200 dark:bg-gray-700' : '' %>" title="About Trinity Console">
</a>
<button onclick="document.documentElement.classList.toggle('dark')" class="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700">
🌙/☀️
</button>