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:
89
services/arbiter-3.0/src/routes/admin/about.js
Normal file
89
services/arbiter-3.0/src/routes/admin/about.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
232
services/arbiter-3.0/src/views/admin/about/index.ejs
Normal file
232
services/arbiter-3.0/src/views/admin/about/index.ejs
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user