Files
LEDMatrix/templates/index_v2.html
2025-07-27 15:32:48 +00:00

1109 lines
38 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LED Matrix Control Panel</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--primary-color: #2c3e50;
--secondary-color: #3498db;
--accent-color: #e74c3c;
--success-color: #27ae60;
--warning-color: #f39c12;
--background-color: #ecf0f1;
--card-background: #ffffff;
--text-color: #2c3e50;
--border-color: #bdc3c7;
--shadow: 0 2px 10px rgba(0,0,0,0.1);
--border-radius: 8px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: var(--text-color);
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.header {
background: var(--card-background);
border-radius: var(--border-radius);
padding: 20px;
margin-bottom: 20px;
box-shadow: var(--shadow);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
.header h1 {
color: var(--primary-color);
font-size: 2rem;
font-weight: 300;
}
.system-status {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.status-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 500;
}
.status-active {
background: rgba(39, 174, 96, 0.1);
color: var(--success-color);
}
.status-inactive {
background: rgba(231, 76, 60, 0.1);
color: var(--accent-color);
}
.status-warning {
background: rgba(243, 156, 18, 0.1);
color: var(--warning-color);
}
.main-grid {
display: grid;
grid-template-columns: 1fr 400px;
gap: 20px;
margin-bottom: 20px;
}
.display-panel {
background: var(--card-background);
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--shadow);
}
.display-preview {
background: #000;
border-radius: var(--border-radius);
padding: 20px;
text-align: center;
position: relative;
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
.display-image {
max-width: 100%;
max-height: 100%;
image-rendering: pixelated;
border: 2px solid #333;
border-radius: 4px;
}
.display-controls {
margin-top: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn {
padding: 12px 20px;
border: none;
border-radius: var(--border-radius);
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 8px;
text-decoration: none;
}
.btn-primary {
background: var(--secondary-color);
color: white;
}
.btn-primary:hover {
background: #2980b9;
transform: translateY(-2px);
}
.btn-success {
background: var(--success-color);
color: white;
}
.btn-success:hover {
background: #219a52;
transform: translateY(-2px);
}
.btn-danger {
background: var(--accent-color);
color: white;
}
.btn-danger:hover {
background: #c0392b;
transform: translateY(-2px);
}
.btn-warning {
background: var(--warning-color);
color: white;
}
.btn-warning:hover {
background: #d68910;
transform: translateY(-2px);
}
.control-panel {
background: var(--card-background);
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--shadow);
}
.tabs {
display: flex;
border-bottom: 2px solid var(--border-color);
margin-bottom: 20px;
overflow-x: auto;
}
.tab-btn {
padding: 12px 20px;
border: none;
background: none;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
color: var(--text-color);
border-bottom: 3px solid transparent;
transition: all 0.3s ease;
white-space: nowrap;
}
.tab-btn.active {
color: var(--secondary-color);
border-bottom-color: var(--secondary-color);
}
.tab-btn:hover {
background: rgba(52, 152, 219, 0.1);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--text-color);
}
.form-control {
width: 100%;
padding: 12px;
border: 2px solid var(--border-color);
border-radius: var(--border-radius);
font-size: 0.9rem;
transition: border-color 0.3s ease;
}
.form-control:focus {
outline: none;
border-color: var(--secondary-color);
}
.toggle-switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: 0.4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: 0.4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--secondary-color);
}
input:checked + .slider:before {
transform: translateX(26px);
}
.editor-mode {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: var(--border-radius);
margin-bottom: 20px;
}
.editor-toolbar {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.element-palette {
background: var(--card-background);
border-radius: var(--border-radius);
padding: 15px;
margin-bottom: 20px;
}
.palette-item {
display: inline-block;
padding: 10px 15px;
margin: 5px;
background: var(--secondary-color);
color: white;
border-radius: var(--border-radius);
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.9rem;
}
.palette-item:hover {
background: #2980b9;
transform: translateY(-2px);
}
.properties-panel {
background: var(--card-background);
border-radius: var(--border-radius);
padding: 15px;
margin-top: 20px;
}
.color-picker {
display: flex;
gap: 10px;
margin-top: 10px;
}
.color-option {
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
border: 3px solid transparent;
transition: all 0.3s ease;
}
.color-option.selected {
border-color: var(--text-color);
transform: scale(1.1);
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: var(--border-radius);
color: white;
font-weight: 500;
z-index: 1000;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
}
.notification.show {
opacity: 1;
transform: translateX(0);
}
.notification.success {
background: var(--success-color);
}
.notification.error {
background: var(--accent-color);
}
.notification.warning {
background: var(--warning-color);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
background: var(--card-background);
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--shadow);
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
color: var(--secondary-color);
}
.stat-label {
color: var(--text-color);
font-size: 0.9rem;
margin-top: 5px;
}
@media (max-width: 768px) {
.main-grid {
grid-template-columns: 1fr;
}
.header {
flex-direction: column;
gap: 15px;
}
.system-status {
justify-content: center;
}
.display-controls {
justify-content: center;
}
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255,255,255,.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.connection-status {
position: fixed;
bottom: 20px;
right: 20px;
padding: 10px 15px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
z-index: 1000;
}
.connected {
background: rgba(39, 174, 96, 0.9);
color: white;
}
.disconnected {
background: rgba(231, 76, 60, 0.9);
color: white;
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<h1><i class="fas fa-tv"></i> LED Matrix Control Panel</h1>
<div class="system-status">
<div class="status-item {{ 'status-active' if system_status.service_active else 'status-inactive' }}">
<i class="fas fa-{{ 'play' if system_status.service_active else 'stop' }}"></i>
Service {{ 'Active' if system_status.service_active else 'Inactive' }}
</div>
<div class="status-item {{ 'status-warning' if system_status.memory_used_percent > 80 else 'status-active' }}">
<i class="fas fa-memory"></i>
{{ system_status.memory_used_percent }}% RAM
</div>
<div class="status-item {{ 'status-warning' if system_status.cpu_temp > 70 else 'status-active' }}">
<i class="fas fa-thermometer-half"></i>
{{ system_status.cpu_temp }}°C
</div>
<div class="status-item status-active">
<i class="fas fa-clock"></i>
{{ system_status.uptime }}
</div>
</div>
</div>
<!-- Editor Mode Banner -->
{% if editor_mode %}
<div class="editor-mode">
<h2><i class="fas fa-edit"></i> Display Editor Mode Active</h2>
<p>Normal display operation is paused. Use the tools below to customize your display layout.</p>
</div>
{% endif %}
<!-- Main Grid -->
<div class="main-grid">
<!-- Display Panel -->
<div class="display-panel">
<h2><i class="fas fa-desktop"></i> Live Display Preview</h2>
<div class="display-preview" id="displayPreview">
<div style="color: #666; font-size: 1.2rem;">
<i class="fas fa-spinner fa-spin"></i>
Connecting to display...
</div>
</div>
<div class="display-controls">
<button class="btn btn-success" onclick="startDisplay()">
<i class="fas fa-play"></i> Start Display
</button>
<button class="btn btn-danger" onclick="stopDisplay()">
<i class="fas fa-stop"></i> Stop Display
</button>
<button class="btn btn-warning" onclick="toggleEditorMode()">
<i class="fas fa-edit"></i>
{{ 'Exit Editor' if editor_mode else 'Enter Editor' }}
</button>
<button class="btn btn-primary" onclick="takeScreenshot()">
<i class="fas fa-camera"></i> Screenshot
</button>
</div>
</div>
<!-- Control Panel -->
<div class="control-panel">
<div class="tabs">
<button class="tab-btn active" onclick="showTab('overview')">
<i class="fas fa-tachometer-alt"></i> Overview
</button>
<button class="tab-btn" onclick="showTab('config')">
<i class="fas fa-cog"></i> Config
</button>
<button class="tab-btn" onclick="showTab('editor')">
<i class="fas fa-paint-brush"></i> Editor
</button>
<button class="tab-btn" onclick="showTab('system')">
<i class="fas fa-server"></i> System
</button>
</div>
<!-- Overview Tab -->
<div id="overview" class="tab-content active">
<h3>System Overview</h3>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{{ system_status.memory_used_percent }}%</div>
<div class="stat-label">Memory Usage</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ system_status.cpu_temp }}°C</div>
<div class="stat-label">CPU Temperature</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ main_config.display.hardware.brightness }}</div>
<div class="stat-label">Brightness</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ main_config.display.hardware.cols }}x{{ main_config.display.hardware.rows }}</div>
<div class="stat-label">Resolution</div>
</div>
</div>
<h4>Quick Actions</h4>
<div class="display-controls">
<button class="btn btn-primary" onclick="systemAction('restart_service')">
<i class="fas fa-redo"></i> Restart Service
</button>
<button class="btn btn-warning" onclick="systemAction('git_pull')">
<i class="fas fa-download"></i> Update Code
</button>
<button class="btn btn-danger" onclick="systemAction('reboot_system')">
<i class="fas fa-power-off"></i> Reboot System
</button>
</div>
</div>
<!-- Configuration Tab -->
<div id="config" class="tab-content">
<h3>Display Configuration</h3>
<div class="form-group">
<label>Brightness</label>
<input type="range" class="form-control" min="1" max="100"
value="{{ main_config.display.hardware.brightness }}"
onchange="updateConfig('display.hardware.brightness', this.value)">
<small>Current: {{ main_config.display.hardware.brightness }}%</small>
</div>
<div class="form-group">
<label>Schedule</label>
<label class="toggle-switch">
<input type="checkbox" {{ 'checked' if schedule_config.enabled else '' }}
onchange="updateConfig('schedule.enabled', this.checked)">
<span class="slider"></span>
</label>
<small>Auto turn on/off display</small>
</div>
<div class="form-group">
<label>Start Time</label>
<input type="time" class="form-control"
value="{{ schedule_config.start_time or '07:00' }}"
onchange="updateConfig('schedule.start_time', this.value)">
</div>
<div class="form-group">
<label>End Time</label>
<input type="time" class="form-control"
value="{{ schedule_config.end_time or '23:00' }}"
onchange="updateConfig('schedule.end_time', this.value)">
</div>
<button class="btn btn-success" onclick="saveAllConfig()">
<i class="fas fa-save"></i> Save Configuration
</button>
</div>
<!-- Editor Tab -->
<div id="editor" class="tab-content">
<h3>Display Editor</h3>
<div class="element-palette">
<h4>Elements</h4>
<div class="palette-item" draggable="true" data-type="text">
<i class="fas fa-font"></i> Text
</div>
<div class="palette-item" draggable="true" data-type="weather_icon">
<i class="fas fa-cloud-sun"></i> Weather Icon
</div>
<div class="palette-item" draggable="true" data-type="rectangle">
<i class="fas fa-square"></i> Rectangle
</div>
<div class="palette-item" draggable="true" data-type="line">
<i class="fas fa-minus"></i> Line
</div>
</div>
<div class="editor-toolbar">
<button class="btn btn-primary" onclick="clearEditor()">
<i class="fas fa-trash"></i> Clear
</button>
<button class="btn btn-success" onclick="saveLayout()">
<i class="fas fa-save"></i> Save Layout
</button>
<button class="btn btn-warning" onclick="loadLayout()">
<i class="fas fa-folder-open"></i> Load Layout
</button>
</div>
<div class="properties-panel">
<h4>Element Properties</h4>
<div id="elementProperties">
<p>Select an element to edit its properties</p>
</div>
</div>
</div>
<!-- System Tab -->
<div id="system" class="tab-content">
<h3>System Management</h3>
<div class="form-group">
<label>System Actions</label>
<div class="display-controls">
<button class="btn btn-primary" onclick="viewLogs()">
<i class="fas fa-file-alt"></i> View Logs
</button>
<button class="btn btn-warning" onclick="systemAction('git_pull')">
<i class="fas fa-download"></i> Git Pull
</button>
<button class="btn btn-danger" onclick="systemAction('reboot_system')"
onclick="return confirm('Are you sure you want to reboot?')">
<i class="fas fa-power-off"></i> Reboot
</button>
</div>
</div>
<div class="form-group">
<label>Performance Monitoring</label>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="memoryUsage">{{ system_status.memory_used_percent }}%</div>
<div class="stat-label">Memory</div>
</div>
<div class="stat-card">
<div class="stat-value" id="cpuTemp">{{ system_status.cpu_temp }}°C</div>
<div class="stat-label">CPU Temp</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Connection Status -->
<div id="connectionStatus" class="connection-status disconnected">
<i class="fas fa-wifi"></i> Disconnected
</div>
<!-- Notification -->
<div id="notification" class="notification"></div>
<script>
// Global variables
let socket;
let currentConfig = {{ main_config | tojson }};
let editorMode = {{ editor_mode | tojson }};
let currentElements = [];
let selectedElement = null;
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
initializeSocket();
initializeEditor();
updateSystemStats();
// Update stats every 30 seconds
setInterval(updateSystemStats, 30000);
});
// Socket.IO connection
function initializeSocket() {
socket = io();
socket.on('connect', function() {
updateConnectionStatus(true);
showNotification('Connected to LED Matrix', 'success');
});
socket.on('disconnect', function() {
updateConnectionStatus(false);
showNotification('Disconnected from LED Matrix', 'error');
});
socket.on('display_update', function(data) {
updateDisplayPreview(data);
});
}
// Update connection status
function updateConnectionStatus(connected) {
const status = document.getElementById('connectionStatus');
if (connected) {
status.className = 'connection-status connected';
status.innerHTML = '<i class="fas fa-wifi"></i> Connected';
} else {
status.className = 'connection-status disconnected';
status.innerHTML = '<i class="fas fa-wifi"></i> Disconnected';
}
}
// Update display preview
function updateDisplayPreview(data) {
const preview = document.getElementById('displayPreview');
if (data.image) {
preview.innerHTML = `<img src="data:image/png;base64,${data.image}"
class="display-image"
alt="LED Matrix Display">`;
}
}
// Tab functionality
function showTab(tabName) {
// Hide all tab contents
const contents = document.querySelectorAll('.tab-content');
contents.forEach(content => content.classList.remove('active'));
// Remove active class from all tab buttons
const buttons = document.querySelectorAll('.tab-btn');
buttons.forEach(btn => btn.classList.remove('active'));
// Show selected tab content
document.getElementById(tabName).classList.add('active');
// Add active class to clicked button
event.target.classList.add('active');
}
// Display control functions
async function startDisplay() {
try {
const response = await fetch('/api/display/start', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
});
const result = await response.json();
showNotification(result.message, result.status);
} catch (error) {
showNotification('Error starting display: ' + error.message, 'error');
}
}
async function stopDisplay() {
try {
const response = await fetch('/api/display/stop', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
});
const result = await response.json();
showNotification(result.message, result.status);
} catch (error) {
showNotification('Error stopping display: ' + error.message, 'error');
}
}
async function toggleEditorMode() {
try {
const response = await fetch('/api/editor/toggle', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
});
const result = await response.json();
showNotification(result.message, result.status);
if (result.status === 'success') {
editorMode = result.editor_mode;
location.reload(); // Reload to update UI
}
} catch (error) {
showNotification('Error toggling editor mode: ' + error.message, 'error');
}
}
async function takeScreenshot() {
try {
const response = await fetch('/api/display/current');
const data = await response.json();
if (data.image) {
// Create download link
const link = document.createElement('a');
link.href = 'data:image/png;base64,' + data.image;
link.download = 'led_matrix_screenshot_' + new Date().getTime() + '.png';
link.click();
showNotification('Screenshot saved', 'success');
} else {
showNotification('No display data available', 'warning');
}
} catch (error) {
showNotification('Error taking screenshot: ' + error.message, 'error');
}
}
// Configuration functions
function updateConfig(path, value) {
const keys = path.split('.');
let obj = currentConfig;
for (let i = 0; i < keys.length - 1; i++) {
if (!obj[keys[i]]) obj[keys[i]] = {};
obj = obj[keys[i]];
}
obj[keys[keys.length - 1]] = value;
}
async function saveAllConfig() {
try {
const response = await fetch('/api/config/save', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
type: 'main',
data: currentConfig
})
});
const result = await response.json();
showNotification(result.message, result.status);
} catch (error) {
showNotification('Error saving configuration: ' + error.message, 'error');
}
}
// System action functions
async function systemAction(action) {
if (action === 'reboot_system' && !confirm('Are you sure you want to reboot the system?')) {
return;
}
try {
const response = await fetch('/api/system/action', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: action})
});
const result = await response.json();
showNotification(result.message, result.status);
} catch (error) {
showNotification('Error executing action: ' + error.message, 'error');
}
}
// Editor functions
function initializeEditor() {
// Initialize drag and drop for editor elements
const paletteItems = document.querySelectorAll('.palette-item');
paletteItems.forEach(item => {
item.addEventListener('dragstart', function(e) {
e.dataTransfer.setData('text/plain', this.dataset.type);
});
});
// Make display preview a drop zone
const preview = document.getElementById('displayPreview');
preview.addEventListener('dragover', function(e) {
e.preventDefault();
});
preview.addEventListener('drop', function(e) {
e.preventDefault();
const elementType = e.dataTransfer.getData('text/plain');
const rect = preview.getBoundingClientRect();
const x = Math.floor((e.clientX - rect.left) / 4); // Scale down from preview
const y = Math.floor((e.clientY - rect.top) / 4);
addElement(elementType, x, y);
});
}
function addElement(type, x, y) {
const element = {
id: Date.now(),
type: type,
x: x,
y: y,
properties: getDefaultProperties(type)
};
currentElements.push(element);
updatePreview();
selectElement(element);
}
function getDefaultProperties(type) {
switch (type) {
case 'text':
return {
text: 'Sample Text',
color: [255, 255, 255],
font_size: 'normal'
};
case 'weather_icon':
return {
condition: 'sunny',
size: 16
};
case 'rectangle':
return {
width: 20,
height: 10,
color: [255, 255, 255]
};
case 'line':
return {
x2: x + 20,
y2: y,
color: [255, 255, 255]
};
default:
return {};
}
}
async function updatePreview() {
if (!editorMode) return;
try {
const response = await fetch('/api/editor/preview', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
elements: currentElements
})
});
const result = await response.json();
if (result.status !== 'success') {
showNotification(result.message, 'error');
}
} catch (error) {
showNotification('Error updating preview: ' + error.message, 'error');
}
}
function selectElement(element) {
selectedElement = element;
updatePropertiesPanel();
}
function updatePropertiesPanel() {
const panel = document.getElementById('elementProperties');
if (!selectedElement) {
panel.innerHTML = '<p>Select an element to edit its properties</p>';
return;
}
let html = `<h5>${selectedElement.type.toUpperCase()} Properties</h5>`;
html += `<div class="form-group">
<label>X Position</label>
<input type="number" class="form-control" value="${selectedElement.x}"
onchange="updateElementProperty('x', parseInt(this.value))">
</div>`;
html += `<div class="form-group">
<label>Y Position</label>
<input type="number" class="form-control" value="${selectedElement.y}"
onchange="updateElementProperty('y', parseInt(this.value))">
</div>`;
// Add type-specific properties
if (selectedElement.type === 'text') {
html += `<div class="form-group">
<label>Text</label>
<input type="text" class="form-control" value="${selectedElement.properties.text}"
onchange="updateElementProperty('properties.text', this.value)">
</div>`;
}
panel.innerHTML = html;
}
function updateElementProperty(path, value) {
if (!selectedElement) return;
const keys = path.split('.');
let obj = selectedElement;
for (let i = 0; i < keys.length - 1; i++) {
if (!obj[keys[i]]) obj[keys[i]] = {};
obj = obj[keys[i]];
}
obj[keys[keys.length - 1]] = value;
updatePreview();
}
function clearEditor() {
currentElements = [];
selectedElement = null;
updatePreview();
updatePropertiesPanel();
}
async function saveLayout() {
try {
const response = await fetch('/api/config/save', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
type: 'layout',
data: {
elements: currentElements,
timestamp: Date.now()
}
})
});
const result = await response.json();
showNotification(result.message, result.status);
} catch (error) {
showNotification('Error saving layout: ' + error.message, 'error');
}
}
// Utility functions
function showNotification(message, type) {
const notification = document.getElementById('notification');
notification.textContent = message;
notification.className = `notification ${type} show`;
setTimeout(() => {
notification.classList.remove('show');
}, 3000);
}
async function updateSystemStats() {
try {
const response = await fetch('/api/system/status');
const stats = await response.json();
document.getElementById('memoryUsage').textContent = stats.memory_used_percent + '%';
document.getElementById('cpuTemp').textContent = stats.cpu_temp + '°C';
} catch (error) {
console.error('Error updating system stats:', error);
}
}
function viewLogs() {
window.open('/logs', '_blank');
}
</script>
</body>
</html>