Add cancel shutdown feature and enhance system control access methods in dashboard
This commit is contained in:
@@ -227,6 +227,48 @@ namespace ResourceMonitorService.Controllers
|
|||||||
return StatusCode(500, "Internal server error");
|
return StatusCode(500, "Internal server error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("cancel-shutdown")]
|
||||||
|
public ActionResult CancelShutdown()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Execute the shutdown abort command
|
||||||
|
var processInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "shutdown",
|
||||||
|
Arguments = "/a",
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var process = Process.Start(processInfo);
|
||||||
|
process?.WaitForExit();
|
||||||
|
|
||||||
|
var exitCode = process?.ExitCode ?? -1;
|
||||||
|
|
||||||
|
string message;
|
||||||
|
if (exitCode == 0)
|
||||||
|
{
|
||||||
|
message = "Shutdown/restart canceled successfully";
|
||||||
|
_logger.LogInformation("Shutdown/restart canceled by user");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
message = "No shutdown/restart was scheduled to cancel, or cancellation failed";
|
||||||
|
_logger.LogInformation("Attempted to cancel shutdown but none was scheduled");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new { message });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error canceling shutdown command");
|
||||||
|
return StatusCode(500, "Internal server error");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SystemControlRequest
|
public class SystemControlRequest
|
||||||
|
|||||||
@@ -53,22 +53,36 @@ Access the interactive web dashboard at `http://localhost:5000` featuring:
|
|||||||
|
|
||||||
For security reasons, the system control feature is hidden by default. To access it:
|
For security reasons, the system control feature is hidden by default. To access it:
|
||||||
|
|
||||||
**Activation Methods:**
|
**Mobile Device Access Methods:**
|
||||||
|
1. **Long press** (2 seconds) on the "Resource Monitor" title in the navigation bar
|
||||||
|
2. **Triple tap** quickly on the "Resource Monitor" title
|
||||||
|
3. **Five quick taps** on the chart icon (📊) next to the title
|
||||||
|
|
||||||
|
**Desktop Access Methods:**
|
||||||
1. **Triple-click** on the "Resource Monitor" title in the navigation bar
|
1. **Triple-click** on the "Resource Monitor" title in the navigation bar
|
||||||
2. **Keyboard shortcut**: `Ctrl + Shift + P`
|
2. **Keyboard shortcut**: `Ctrl + Shift + P`
|
||||||
|
3. **Long press** (2 seconds) on the title with mouse
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
- **Shutdown**: Graceful or forced system shutdown
|
- **Shutdown**: Graceful or forced system shutdown
|
||||||
- **Restart**: System restart with optional force
|
- **Restart**: System restart with optional force
|
||||||
- **Timer Support**: Delay execution from 0 to 86400 seconds (24 hours)
|
- **Timer Support**: Delay execution from 0 to 86400 seconds (24 hours) - Default: 15 seconds
|
||||||
|
- **Cancel Function**: Cancel any pending shutdown/restart (similar to `shutdown -a`)
|
||||||
- **Safety Confirmations**: Multiple confirmation dialogs before execution
|
- **Safety Confirmations**: Multiple confirmation dialogs before execution
|
||||||
|
- **Auto-lock**: System control auto-locks after 30 seconds for security
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
1. Activate the system control button using one of the methods above
|
1. Activate the system control button using one of the methods above
|
||||||
2. Click the red "System" button that appears in the navigation
|
2. Look for the red pulsing "System" button that appears in the navigation
|
||||||
3. Set optional timer (leave empty for immediate action)
|
3. Tap/click the "System" button to open the control panel
|
||||||
4. Choose shutdown or restart
|
4. Adjust timer if needed (defaults to 15 seconds for safety)
|
||||||
5. Confirm the action
|
5. Choose shutdown, restart, or cancel any pending operation
|
||||||
|
6. Confirm the action
|
||||||
|
|
||||||
|
📱 **Mobile Tips:**
|
||||||
|
- Device will vibrate when system control is unlocked (if supported)
|
||||||
|
- Touch-friendly interface optimized for mobile screens
|
||||||
|
- All gestures work with touchscreens
|
||||||
|
|
||||||
⚠️ **Warning**: Use this feature with extreme caution as it will shut down or restart the entire system.
|
⚠️ **Warning**: Use this feature with extreme caution as it will shut down or restart the entire system.
|
||||||
- **Health Monitoring**: Comprehensive health checks and uptime tracking
|
- **Health Monitoring**: Comprehensive health checks and uptime tracking
|
||||||
|
|||||||
@@ -33,6 +33,49 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* System Control Button Animations */
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.7);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 0 0 10px rgba(220, 38, 38, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 0 0 rgba(220, 38, 38, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make title elements more touch-friendly on mobile */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
nav h1 {
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-webkit-tap-highlight-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav h1:active {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav .fas.fa-chart-line {
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav .fas.fa-chart-line:active {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Custom progress bar animations */
|
/* Custom progress bar animations */
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
transition: width 0.5s ease-in-out;
|
transition: width 0.5s ease-in-out;
|
||||||
|
|||||||
+7
-4
@@ -211,22 +211,25 @@
|
|||||||
<!-- Timer Input -->
|
<!-- Timer Input -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Timer (seconds) - Leave empty for immediate action
|
Timer (seconds) - Default: 15 seconds
|
||||||
</label>
|
</label>
|
||||||
<input type="number" id="systemTimer"
|
<input type="number" id="systemTimer"
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
placeholder="0" min="0" max="86400">
|
placeholder="15" min="0" max="86400" value="15">
|
||||||
<p class="text-xs text-gray-500 mt-1">Maximum: 24 hours (86400 seconds)</p>
|
<p class="text-xs text-gray-500 mt-1">Maximum: 24 hours (86400 seconds)</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-3 gap-3">
|
||||||
<button id="shutdownBtn" class="bg-red-600 hover:bg-red-700 text-white px-4 py-3 rounded-lg transition-colors flex items-center justify-center">
|
<button id="shutdownBtn" class="bg-red-600 hover:bg-red-700 text-white px-4 py-3 rounded-lg transition-colors flex items-center justify-center">
|
||||||
<i class="fas fa-power-off mr-2"></i>Shutdown
|
<i class="fas fa-power-off mr-2"></i>Shutdown
|
||||||
</button>
|
</button>
|
||||||
<button id="restartBtn" class="bg-orange-600 hover:bg-orange-700 text-white px-4 py-3 rounded-lg transition-colors flex items-center justify-center">
|
<button id="restartBtn" class="bg-orange-600 hover:bg-orange-700 text-white px-4 py-3 rounded-lg transition-colors flex items-center justify-center">
|
||||||
<i class="fas fa-redo mr-2"></i>Restart
|
<i class="fas fa-redo mr-2"></i>Restart
|
||||||
</button>
|
</button>
|
||||||
|
<button id="cancelBtn" class="bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-3 rounded-lg transition-colors flex items-center justify-center">
|
||||||
|
<i class="fas fa-ban mr-2"></i>Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Force Shutdown Option -->
|
<!-- Force Shutdown Option -->
|
||||||
@@ -241,7 +244,7 @@
|
|||||||
<i class="fas fa-exclamation-triangle text-yellow-600 mr-2 mt-0.5"></i>
|
<i class="fas fa-exclamation-triangle text-yellow-600 mr-2 mt-0.5"></i>
|
||||||
<div class="text-sm text-yellow-800">
|
<div class="text-sm text-yellow-800">
|
||||||
<strong>Warning:</strong> This will shut down or restart the entire system.
|
<strong>Warning:</strong> This will shut down or restart the entire system.
|
||||||
Make sure to save any unsaved work first.
|
Make sure to save any unsaved work first. Use the Cancel button to abort any pending shutdown/restart.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+153
-12
@@ -58,10 +58,28 @@ class ResourceDashboard {
|
|||||||
this.executeSystemCommand('restart');
|
this.executeSystemCommand('restart');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hidden system control activation - triple click on title
|
document.getElementById('cancelBtn').addEventListener('click', () => {
|
||||||
|
this.cancelSystemCommand();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hidden system control activation methods
|
||||||
|
this.setupHiddenSystemControlAccess();
|
||||||
|
|
||||||
|
// Close modal when clicking outside
|
||||||
|
document.getElementById('systemControlModal').addEventListener('click', (e) => {
|
||||||
|
if (e.target.id === 'systemControlModal') {
|
||||||
|
this.hideSystemControlModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupHiddenSystemControlAccess() {
|
||||||
|
// Method 1: Triple click on title (works on mobile)
|
||||||
let clickCount = 0;
|
let clickCount = 0;
|
||||||
let clickTimer = null;
|
let clickTimer = null;
|
||||||
document.querySelector('h1').addEventListener('click', () => {
|
const titleElement = document.querySelector('h1');
|
||||||
|
|
||||||
|
titleElement.addEventListener('click', () => {
|
||||||
clickCount++;
|
clickCount++;
|
||||||
if (clickCount === 1) {
|
if (clickCount === 1) {
|
||||||
clickTimer = setTimeout(() => {
|
clickTimer = setTimeout(() => {
|
||||||
@@ -74,14 +92,90 @@ class ResourceDashboard {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close modal when clicking outside
|
// Method 2: Long press on title (mobile-friendly)
|
||||||
document.getElementById('systemControlModal').addEventListener('click', (e) => {
|
let pressTimer = null;
|
||||||
if (e.target.id === 'systemControlModal') {
|
let pressStartTime = 0;
|
||||||
this.hideSystemControlModal();
|
|
||||||
|
titleElement.addEventListener('touchstart', (e) => {
|
||||||
|
pressStartTime = Date.now();
|
||||||
|
pressTimer = setTimeout(() => {
|
||||||
|
// Vibrate if supported (mobile feedback)
|
||||||
|
if (navigator.vibrate) {
|
||||||
|
navigator.vibrate(200);
|
||||||
|
}
|
||||||
|
this.toggleSystemControlButton();
|
||||||
|
}, 2000); // 2 second long press
|
||||||
|
});
|
||||||
|
|
||||||
|
titleElement.addEventListener('touchend', () => {
|
||||||
|
if (pressTimer) {
|
||||||
|
clearTimeout(pressTimer);
|
||||||
|
pressTimer = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Keyboard shortcut: Ctrl+Shift+P to toggle system control
|
titleElement.addEventListener('touchmove', () => {
|
||||||
|
if (pressTimer) {
|
||||||
|
clearTimeout(pressTimer);
|
||||||
|
pressTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Method 3: Mouse long press (desktop fallback)
|
||||||
|
titleElement.addEventListener('mousedown', (e) => {
|
||||||
|
if (e.button === 0) { // Left mouse button
|
||||||
|
pressStartTime = Date.now();
|
||||||
|
pressTimer = setTimeout(() => {
|
||||||
|
this.toggleSystemControlButton();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
titleElement.addEventListener('mouseup', () => {
|
||||||
|
if (pressTimer) {
|
||||||
|
clearTimeout(pressTimer);
|
||||||
|
pressTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
titleElement.addEventListener('mouseleave', () => {
|
||||||
|
if (pressTimer) {
|
||||||
|
clearTimeout(pressTimer);
|
||||||
|
pressTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Method 4: Secret tap sequence on the favicon
|
||||||
|
let tapSequence = [];
|
||||||
|
const favicon = document.querySelector('nav .fas.fa-chart-line');
|
||||||
|
|
||||||
|
favicon.addEventListener('click', () => {
|
||||||
|
tapSequence.push(Date.now());
|
||||||
|
|
||||||
|
// Keep only last 5 taps within 3 seconds
|
||||||
|
const now = Date.now();
|
||||||
|
tapSequence = tapSequence.filter(time => now - time < 3000);
|
||||||
|
|
||||||
|
// Check for specific pattern: 5 quick taps
|
||||||
|
if (tapSequence.length >= 5) {
|
||||||
|
const timeDiffs = [];
|
||||||
|
for (let i = 1; i < tapSequence.length; i++) {
|
||||||
|
timeDiffs.push(tapSequence[i] - tapSequence[i-1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// All taps should be within 200ms of each other
|
||||||
|
const isQuickSequence = timeDiffs.every(diff => diff < 500);
|
||||||
|
if (isQuickSequence) {
|
||||||
|
if (navigator.vibrate) {
|
||||||
|
navigator.vibrate([100, 50, 100]);
|
||||||
|
}
|
||||||
|
this.toggleSystemControlButton();
|
||||||
|
tapSequence = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Method 5: Keyboard shortcut (desktop)
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.ctrlKey && e.shiftKey && e.key === 'P') {
|
if (e.ctrlKey && e.shiftKey && e.key === 'P') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -728,16 +822,32 @@ class ResourceDashboard {
|
|||||||
const systemButton = document.getElementById('systemControl');
|
const systemButton = document.getElementById('systemControl');
|
||||||
if (systemButton.classList.contains('hidden')) {
|
if (systemButton.classList.contains('hidden')) {
|
||||||
systemButton.classList.remove('hidden');
|
systemButton.classList.remove('hidden');
|
||||||
this.showNotification('System control unlocked! Use with caution.', 'info');
|
|
||||||
|
// Add visual feedback with pulsing effect
|
||||||
|
systemButton.style.animation = 'pulse 1s ease-in-out 3';
|
||||||
|
|
||||||
|
// Show mobile-friendly notification
|
||||||
|
this.showNotification('🔓 System control unlocked! Tap the red System button to access shutdown/restart options.', 'info');
|
||||||
|
|
||||||
|
// Auto-hide after 30 seconds for security
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!systemButton.classList.contains('hidden')) {
|
||||||
|
systemButton.classList.add('hidden');
|
||||||
|
this.hideSystemControlModal();
|
||||||
|
this.showNotification('System control auto-locked for security', 'info');
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
} else {
|
} else {
|
||||||
systemButton.classList.add('hidden');
|
systemButton.classList.add('hidden');
|
||||||
|
systemButton.style.animation = '';
|
||||||
this.hideSystemControlModal();
|
this.hideSystemControlModal();
|
||||||
|
this.showNotification('System control locked', 'info');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showSystemControlModal() {
|
showSystemControlModal() {
|
||||||
document.getElementById('systemControlModal').classList.remove('hidden');
|
document.getElementById('systemControlModal').classList.remove('hidden');
|
||||||
document.getElementById('systemTimer').value = '';
|
document.getElementById('systemTimer').value = '15'; // Default 15 seconds
|
||||||
document.getElementById('forceShutdown').checked = true;
|
document.getElementById('forceShutdown').checked = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -749,8 +859,8 @@ class ResourceDashboard {
|
|||||||
const timer = document.getElementById('systemTimer').value;
|
const timer = document.getElementById('systemTimer').value;
|
||||||
const force = document.getElementById('forceShutdown').checked;
|
const force = document.getElementById('forceShutdown').checked;
|
||||||
|
|
||||||
// Validate timer input
|
// Validate timer input - default to 15 if empty
|
||||||
const timerSeconds = parseInt(timer) || 0;
|
const timerSeconds = parseInt(timer) || 15;
|
||||||
if (timerSeconds < 0 || timerSeconds > 86400) {
|
if (timerSeconds < 0 || timerSeconds > 86400) {
|
||||||
this.showNotification('Timer must be between 0 and 86400 seconds (24 hours)', 'error');
|
this.showNotification('Timer must be between 0 and 86400 seconds (24 hours)', 'error');
|
||||||
return;
|
return;
|
||||||
@@ -772,7 +882,7 @@ class ResourceDashboard {
|
|||||||
} else {
|
} else {
|
||||||
confirmMessage += ' immediately';
|
confirmMessage += ' immediately';
|
||||||
}
|
}
|
||||||
confirmMessage += '?';
|
confirmMessage += '?\n\nNote: You can cancel this action using the Cancel button.';
|
||||||
|
|
||||||
if (!confirm(confirmMessage)) {
|
if (!confirm(confirmMessage)) {
|
||||||
return;
|
return;
|
||||||
@@ -804,6 +914,11 @@ class ResourceDashboard {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.showNotification(`System ${action} initiated! Connection will be lost.`, 'info');
|
this.showNotification(`System ${action} initiated! Connection will be lost.`, 'info');
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
// Show reminder about cancel option
|
||||||
|
setTimeout(() => {
|
||||||
|
this.showNotification(`Reminder: Use the Cancel button to abort the ${action} if needed.`, 'info');
|
||||||
|
}, 2000);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const error = await response.text();
|
const error = await response.text();
|
||||||
@@ -815,6 +930,32 @@ class ResourceDashboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async cancelSystemCommand() {
|
||||||
|
if (!confirm('Are you sure you want to cancel any pending shutdown/restart?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/resource/cancel-shutdown', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
this.showNotification(result.message, 'success');
|
||||||
|
} else {
|
||||||
|
const error = await response.text();
|
||||||
|
this.showNotification(`Failed to cancel shutdown: ${error}`, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error canceling shutdown:', error);
|
||||||
|
this.showNotification('Error canceling shutdown command', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
hideLoading() {
|
hideLoading() {
|
||||||
document.getElementById('loadingOverlay').style.display = 'none';
|
document.getElementById('loadingOverlay').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user