Add graceful shutdown button and improve shutdown status display

- Add refresh/shutdown button next to status indicator that gracefully
  stops the process without cleaning my-storage
- Create /api/shutdown endpoint that triggers graceful shutdown via SIGTERM
- Update status indicator to show "Gracefully Cleaning...." when system
  is shutting down (instead of "Searching for peers...")
- Include isShuttingDown flag in /api/status endpoint response
- Use built-in ConfirmationModal for shutdown confirmation with warning styling

The shutdown button appears in the top-right corner next to the status
indicator and uses the existing confirmation modal system for user
confirmation before initiating shutdown.
This commit is contained in:
Raven Scott
2025-12-26 22:03:23 -05:00
parent 1888289067
commit 3466d17eda
6 changed files with 188 additions and 6 deletions
+26 -1
View File
@@ -14,7 +14,8 @@ async function handleStatusRoutes(req, res) {
isMaster: state.isMaster, isMaster: state.isMaster,
isConnected: !!state.dnsPass, isConnected: !!state.dnsPass,
peersCount: state.connectedPeers.size, peersCount: state.connectedPeers.size,
pid: process.pid pid: process.pid,
isShuttingDown: state.isShuttingDown || false
}; };
trackRequest('/api/status', true); trackRequest('/api/status', true);
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -28,6 +29,30 @@ async function handleStatusRoutes(req, res) {
return true; return true;
} }
if (method === 'POST' && urlPath === '/api/shutdown') {
try {
const { logInfo } = require('../../../infrastructure/logger');
// Send success response before shutting down
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
message: 'Graceful shutdown initiated. Process will exit after cleanup completes.'
}));
// Trigger graceful shutdown after sending response
setTimeout(() => {
logInfo('Admin', 'Initiating graceful shutdown (without cleaning storage)...');
process.emit('SIGTERM');
}, 500); // Small delay to ensure response is sent
} catch (err) {
const errorResponse = createErrorResponse(err, 500);
res.writeHead(errorResponse.statusCode, errorResponse.headers);
res.end(errorResponse.body);
}
return true;
}
if (method === 'GET' && urlPath === '/api/health') { if (method === 'GET' && urlPath === '/api/health') {
try { try {
const query = url.parse(req.url, true).query; const query = url.parse(req.url, true).query;
+5
View File
@@ -32,7 +32,12 @@
<div class="container mx-auto max-w-7xl"> <div class="container mx-auto max-w-7xl">
<div class="flex justify-between items-center mb-8 flex-wrap gap-4"> <div class="flex justify-between items-center mb-8 flex-wrap gap-4">
<h1 class="text-4xl font-extrabold" style="background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;">P2NS Admin Panel</h1> <h1 class="text-4xl font-extrabold" style="background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;">P2NS Admin Panel</h1>
<div class="flex items-center gap-2">
<div id="status-indicator" class="px-4 py-2 rounded-lg"></div> <div id="status-indicator" class="px-4 py-2 rounded-lg"></div>
<button id="refresh-button" onclick="handleRefresh()" class="px-3 py-2 bg-primary hover:bg-primary-hover text-white rounded-lg transition-colors" title="Gracefully stop the process">
<i class="fas fa-redo"></i>
</button>
</div>
</div> </div>
<nav class="flex justify-center mb-8 space-x-4 flex-wrap"> <nav class="flex justify-center mb-8 space-x-4 flex-wrap">
+62 -1
View File
@@ -301,7 +301,10 @@ async function updateStatus() {
const data = await res.json(); const data = await res.json();
let text; let text;
let color = 'bg-blue-600'; let color = 'bg-blue-600';
if (data.isMaster) { if (data.isShuttingDown) {
text = 'Gracefully Cleaning....';
color = 'bg-orange-500';
} else if (data.isMaster) {
text = `This is Master • Peers: ${data.peersCount}`; text = `This is Master • Peers: ${data.peersCount}`;
} else { } else {
if (data.isConnected) { if (data.isConnected) {
@@ -371,6 +374,63 @@ function stopDomainsUpdates() {
} }
} }
async function handleRefresh() {
if (window.ConfirmationModal) {
const confirmed = await window.ConfirmationModal.warning(
'Are you sure you want to gracefully stop the process? This will shut down the system without cleaning my-storage.',
{
title: 'Graceful Shutdown',
confirmText: 'Shutdown',
cancelText: 'Cancel'
}
);
if (!confirmed) {
return;
}
await performShutdown();
} else if (window.showConfirm) {
// Fallback to showConfirm if ConfirmationModal not available
window.showConfirm(
'Are you sure you want to gracefully stop the process? This will shut down the system without cleaning my-storage.',
async () => {
await performShutdown();
}
);
} else {
// Final fallback to native confirm
if (!confirm('Are you sure you want to gracefully stop the process? This will shut down the system without cleaning my-storage.')) {
return;
}
await performShutdown();
}
}
async function performShutdown() {
try {
const res = await fetch('/api/shutdown', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
if (res.ok) {
const data = await res.json();
if (window.showNotification) {
window.showNotification(data.message || 'Graceful shutdown initiated', 'info');
}
} else {
const errorText = await res.text();
if (window.showNotification) {
window.showNotification('Failed to initiate shutdown: ' + errorText, 'error');
}
}
} catch (err) {
console.error('Failed to call shutdown endpoint:', err);
if (window.showNotification) {
window.showNotification('Failed to initiate shutdown: ' + err.message, 'error');
}
}
}
// Make functions globally accessible // Make functions globally accessible
window.connectWebSocket = connectWebSocket; window.connectWebSocket = connectWebSocket;
window.startPollingFallback = startPollingFallback; window.startPollingFallback = startPollingFallback;
@@ -381,6 +441,7 @@ window.startStatusUpdates = startStatusUpdates;
window.stopStatusUpdates = stopStatusUpdates; window.stopStatusUpdates = stopStatusUpdates;
window.startDomainsUpdates = startDomainsUpdates; window.startDomainsUpdates = startDomainsUpdates;
window.stopDomainsUpdates = stopDomainsUpdates; window.stopDomainsUpdates = stopDomainsUpdates;
window.handleRefresh = handleRefresh;
// Initialize WebSocket connection // Initialize WebSocket connection
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
+6 -1
View File
@@ -781,7 +781,12 @@
</dialog> </dialog>
</div> </div>
<div id="status-indicator" class="fixed top-4 right-4 px-4 py-2 bg-blue-500 text-white rounded-lg shadow-md"></div> <div class="fixed top-4 right-4 flex items-center gap-2">
<div id="status-indicator" class="px-4 py-2 bg-blue-500 text-white rounded-lg shadow-md"></div>
<button id="refresh-button" onclick="handleRefresh()" class="px-3 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg shadow-md transition-colors" title="Gracefully stop the process">
<i class="fas fa-redo"></i>
</button>
</div>
<div id="notifications" class="fixed bottom-4 right-4 flex flex-col-reverse space-y-2" style="z-index: 99999;"></div> <div id="notifications" class="fixed bottom-4 right-4 flex flex-col-reverse space-y-2" style="z-index: 99999;"></div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/xterm.js"></script> <script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/xterm.js"></script>
+26 -1
View File
@@ -13,7 +13,8 @@ async function handleStatusRoutes(req, res) {
const status = { const status = {
isMaster: state.isMaster, isMaster: state.isMaster,
isConnected: !!state.dnsPass, isConnected: !!state.dnsPass,
peersCount: state.connectedPeers.size peersCount: state.connectedPeers.size,
isShuttingDown: state.isShuttingDown || false
}; };
trackRequest('/api/status', true); trackRequest('/api/status', true);
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -27,6 +28,30 @@ async function handleStatusRoutes(req, res) {
return true; return true;
} }
if (method === 'POST' && urlPath === '/api/shutdown') {
try {
const { logInfo } = require('../../infrastructure/logger');
// Send success response before shutting down
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
message: 'Graceful shutdown initiated. Process will exit after cleanup completes.'
}));
// Trigger graceful shutdown after sending response
setTimeout(() => {
logInfo('Admin', 'Initiating graceful shutdown (without cleaning storage)...');
process.emit('SIGTERM');
}, 500); // Small delay to ensure response is sent
} catch (err) {
const errorResponse = createErrorResponse(err, 500);
res.writeHead(errorResponse.statusCode, errorResponse.headers);
res.end(errorResponse.body);
}
return true;
}
if (method === 'GET' && urlPath === '/api/health') { if (method === 'GET' && urlPath === '/api/health') {
try { try {
const query = url.parse(req.url, true).query; const query = url.parse(req.url, true).query;
+62 -1
View File
@@ -244,7 +244,10 @@ async function updateStatus() {
const data = await res.json(); const data = await res.json();
let text; let text;
let color = 'bg-blue-600'; let color = 'bg-blue-600';
if (data.isMaster) { if (data.isShuttingDown) {
text = 'Gracefully Cleaning...';
color = 'bg-orange-500';
} else if (data.isMaster) {
text = `This is Master • Peers: ${data.peersCount}`; text = `This is Master • Peers: ${data.peersCount}`;
} else { } else {
if (data.isConnected) { if (data.isConnected) {
@@ -312,6 +315,63 @@ function stopDomainsUpdates() {
} }
} }
async function handleRefresh() {
if (window.ConfirmationModal) {
const confirmed = await window.ConfirmationModal.warning(
'Are you sure you want to gracefully stop the process? This will shut down the system without cleaning my-storage.',
{
title: 'Graceful Shutdown',
confirmText: 'Shutdown',
cancelText: 'Cancel'
}
);
if (!confirmed) {
return;
}
await performShutdown();
} else if (window.showConfirm) {
// Fallback to showConfirm if ConfirmationModal not available
window.showConfirm(
'Are you sure you want to gracefully stop the process? This will shut down the system without cleaning my-storage.',
async () => {
await performShutdown();
}
);
} else {
// Final fallback to native confirm
if (!confirm('Are you sure you want to gracefully stop the process? This will shut down the system without cleaning my-storage.')) {
return;
}
await performShutdown();
}
}
async function performShutdown() {
try {
const res = await fetch('/api/shutdown', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
if (res.ok) {
const data = await res.json();
if (window.showNotification) {
window.showNotification(data.message || 'Graceful shutdown initiated', 'info');
}
} else {
const errorText = await res.text();
if (window.showNotification) {
window.showNotification('Failed to initiate shutdown: ' + errorText, 'error');
}
}
} catch (err) {
console.error('Failed to call shutdown endpoint:', err);
if (window.showNotification) {
window.showNotification('Failed to initiate shutdown: ' + err.message, 'error');
}
}
}
// Make functions globally accessible // Make functions globally accessible
window.connectWebSocket = connectWebSocket; window.connectWebSocket = connectWebSocket;
window.startPollingFallback = startPollingFallback; window.startPollingFallback = startPollingFallback;
@@ -322,6 +382,7 @@ window.startStatusUpdates = startStatusUpdates;
window.stopStatusUpdates = stopStatusUpdates; window.stopStatusUpdates = stopStatusUpdates;
window.startDomainsUpdates = startDomainsUpdates; window.startDomainsUpdates = startDomainsUpdates;
window.stopDomainsUpdates = stopDomainsUpdates; window.stopDomainsUpdates = stopDomainsUpdates;
window.handleRefresh = handleRefresh;
// Initialize WebSocket connection // Initialize WebSocket connection
if (document.readyState === 'loading') { if (document.readyState === 'loading') {