Files
miku-discord/bot/static/system.html.bak

773 lines
25 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>System Settings - Miku Bot</title>
<style>
* {
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;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
color: #667eea;
font-size: 28px;
}
.header-actions {
display: flex;
gap: 10px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.content {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
}
.card {
background: white;
border-radius: 10px;
padding: 25px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.card h2 {
color: #333;
margin-bottom: 20px;
font-size: 20px;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
}
.global-settings {
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 5px;
}
.setting-row {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 10px;
}
.setting-row label {
font-weight: 600;
color: #495057;
min-width: 120px;
}
select {
padding: 8px 12px;
border: 1px solid #ced4da;
border-radius: 5px;
font-size: 14px;
background: white;
cursor: pointer;
}
.components-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
.components-table th {
background: #667eea;
color: white;
padding: 12px;
text-align: left;
font-weight: 600;
}
.components-table td {
padding: 10px 12px;
border-bottom: 1px solid #dee2e6;
}
.components-table tr:hover {
background: #f8f9fa;
}
.level-checkboxes {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.level-checkbox {
display: flex;
align-items: center;
gap: 5px;
}
.level-checkbox input[type="checkbox"] {
cursor: pointer;
width: 18px;
height: 18px;
}
.level-checkbox label {
cursor: pointer;
user-select: none;
font-size: 13px;
}
.toggle {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.toggle 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: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: 0.4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #667eea;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.status-indicator {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 5px;
}
.status-active {
background: #28a745;
}
.status-inactive {
background: #6c757d;
}
.api-filters {
margin-top: 15px;
padding: 15px;
background: #fff3cd;
border-radius: 5px;
border-left: 4px solid #ffc107;
}
.api-filters h3 {
color: #856404;
font-size: 16px;
margin-bottom: 10px;
}
.filter-row {
margin-bottom: 10px;
}
.filter-row label {
display: block;
font-weight: 600;
margin-bottom: 5px;
color: #495057;
}
input[type="text"], input[type="number"] {
width: 100%;
padding: 8px 12px;
border: 1px solid #ced4da;
border-radius: 5px;
font-size: 14px;
}
.log-preview {
background: #212529;
color: #f8f9fa;
padding: 15px;
border-radius: 5px;
font-family: 'Courier New', monospace;
font-size: 12px;
max-height: 400px;
overflow-y: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.log-preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.log-line {
margin-bottom: 5px;
line-height: 1.5;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 25px;
border-radius: 5px;
color: white;
font-weight: 600;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
z-index: 1000;
animation: slideIn 0.3s ease-out;
}
.notification-success {
background: #28a745;
}
.notification-error {
background: #dc3545;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.loading {
text-align: center;
padding: 40px;
color: #6c757d;
}
.component-description {
font-size: 12px;
color: #6c757d;
font-style: italic;
}
@media (max-width: 1200px) {
.content {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎛️ System Settings - Logging Configuration</h1>
<div class="header-actions">
<button class="btn btn-secondary" onclick="window.location.href='/'">← Back to Dashboard</button>
<button class="btn btn-primary" onclick="saveAllSettings()">💾 Save All</button>
<button class="btn btn-danger" onclick="resetToDefaults()">🔄 Reset to Defaults</button>
</div>
</div>
<div class="content">
<div class="card">
<h2>📊 Logging Components</h2>
<p style="color: #6c757d; margin-bottom: 20px;">
Enable or disable specific log levels for each component. You can toggle any combination of levels (e.g., only INFO + ERROR, or only WARNING + DEBUG).
</p>
<table class="components-table">
<thead>
<tr>
<th>Component</th>
<th>Enabled</th>
<th>Log Levels</th>
<th>Status</th>
</tr>
</thead>
<tbody id="componentsTable">
<tr>
<td colspan="4" class="loading">Loading components...</td>
</tr>
</tbody>
</table>
<div id="apiFilters" class="api-filters" style="display: none;">
<h3>🌐 API Request Filters</h3>
<div class="filter-row">
<label>Exclude Paths (comma-separated):</label>
<input type="text" id="excludePaths" placeholder="/health, /static/*">
</div>
<div class="filter-row">
<label>Exclude Status Codes (comma-separated):</label>
<input type="text" id="excludeStatus" placeholder="200, 304">
</div>
<div class="setting-row">
<label>Log Slow Requests (>1000ms):</label>
<label class="toggle">
<input type="checkbox" id="includeSlowRequests" checked>
<span class="slider"></span>
</label>
</div>
<div class="filter-row">
<label>Slow Request Threshold (ms):</label>
<input type="number" id="slowThreshold" value="1000" min="100" step="100">
</div>
<button class="btn btn-primary" onclick="saveApiFilters()" style="margin-top: 10px;">Save API Filters</button>
</div>
</div>
<div class="card">
<h2>📜 Live Log Preview</h2>
<div class="log-preview-header">
<div>
<label>Component: </label>
<select id="previewComponent" onchange="loadLogPreview()">
<option value="bot">Bot</option>
</select>
</div>
<button class="btn btn-secondary" onclick="loadLogPreview()">🔄 Refresh</button>
</div>
<div class="log-preview" id="logPreview">
<div class="loading">Select a component to view logs...</div>
</div>
</div>
</div>
</div>
<script>
let currentConfig = null;
let componentsData = null;
// Load configuration on page load
window.addEventListener('DOMContentLoaded', () => {
loadConfiguration();
loadComponents();
});
async function loadConfiguration() {
try {
const response = await fetch('/api/log/config');
const data = await response.json();
if (data.success) {
currentConfig = data.config;
// No global level to set - we use per-component levels only
} else {
showNotification('Failed to load configuration', 'error');
}
} catch (error) {
showNotification('Error loading configuration: ' + error.message, 'error');
}
}
async function loadComponents() {
try {
const response = await fetch('/api/log/components');
const data = await response.json();
if (data.success) {
componentsData = data;
renderComponentsTable();
populatePreviewSelect();
} else {
showNotification('Failed to load components', 'error');
}
} catch (error) {
showNotification('Error loading components: ' + error.message, 'error');
}
}
function renderComponentsTable() {
const tbody = document.getElementById('componentsTable');
tbody.innerHTML = '';
for (const [name, description] of Object.entries(componentsData.components)) {
const stats = componentsData.stats[name] || {};
const enabled = stats.enabled !== undefined ? stats.enabled : true;
const enabledLevels = stats.enabled_levels || ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
// Build checkboxes for each level
const allLevels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
if (name === 'api.requests') {
allLevels.push('API');
}
const levelCheckboxes = allLevels.map(level => {
const emoji = {'DEBUG': '🔍', 'INFO': '', 'WARNING': '⚠️', 'ERROR': '❌', 'CRITICAL': '🔥', 'API': '🌐'}[level];
const checked = enabledLevels.includes(level) ? 'checked' : '';
return `
<div class="level-checkbox">
<input type="checkbox"
id="level_${name}_${level}"
${checked}
onchange="updateComponentLevels('${name}')">
<label for="level_${name}_${level}">${emoji} ${level}</label>
</div>
`;
}).join('');
const row = document.createElement('tr');
row.innerHTML = `
<td>
<strong>${name}</strong><br>
<span class="component-description">${description}</span>
</td>
<td>
<label class="toggle">
<input type="checkbox" id="enabled_${name}" ${enabled ? 'checked' : ''} onchange="updateComponentEnabled('${name}')">
<span class="slider"></span>
</label>
</td>
<td>
<div class="level-checkboxes">
${levelCheckboxes}
</div>
</td>
<td>
<span class="status-indicator ${enabled ? 'status-active' : 'status-inactive'}"></span>
${enabled ? 'Active' : 'Inactive'}
</td>
`;
tbody.appendChild(row);
// Show API filters if api.requests is selected
if (name === 'api.requests') {
document.getElementById('enabled_' + name).addEventListener('change', (e) => {
document.getElementById('apiFilters').style.display = e.target.checked ? 'block' : 'none';
});
if (enabled) {
document.getElementById('apiFilters').style.display = 'block';
loadApiFilters();
}
}
}
}
function populatePreviewSelect() {
const select = document.getElementById('previewComponent');
select.innerHTML = '';
for (const name of Object.keys(componentsData.components)) {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
select.appendChild(option);
}
loadLogPreview();
}
async function updateComponentEnabled(component) {
const enabled = document.getElementById('enabled_' + component).checked;
try {
const response = await fetch('/api/log/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
component: component,
enabled: enabled
})
});
const data = await response.json();
if (data.success) {
showNotification(`${enabled ? 'Enabled' : 'Disabled'} ${component}`, 'success');
// Update status indicator
const row = document.getElementById('enabled_' + component).closest('tr');
const statusCell = row.querySelector('td:last-child');
statusCell.innerHTML = `
<span class="status-indicator ${enabled ? 'status-active' : 'status-inactive'}"></span>
${enabled ? 'Active' : 'Inactive'}
`;
} else {
showNotification('Failed to update ' + component + ': ' + data.error, 'error');
}
} catch (error) {
showNotification('Error updating component: ' + error.message, 'error');
}
}
async function updateComponentLevels(component) {
// Collect all checked levels
const allLevels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
if (component === 'api.requests') {
allLevels.push('API');
}
const enabledLevels = allLevels.filter(level => {
const checkbox = document.getElementById(`level_${component}_${level}`);
return checkbox && checkbox.checked;
});
try {
const response = await fetch('/api/log/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
component: component,
enabled_levels: enabledLevels
})
});
const data = await response.json();
if (data.success) {
showNotification(`Updated levels for ${component}: ${enabledLevels.join(', ')}`, 'success');
} else {
showNotification('Failed to update ' + component + ': ' + data.error, 'error');
}
} catch (error) {
showNotification('Error updating component: ' + error.message, 'error');
}
}
async function updateGlobalLevel() {
// Deprecated - kept for compatibility
showNotification('Global level setting removed. Use individual component levels instead.', 'success');
}
async function loadApiFilters() {
if (!currentConfig || !currentConfig.components['api.requests']) return;
const filters = currentConfig.components['api.requests'].filters || {};
document.getElementById('excludePaths').value = (filters.exclude_paths || []).join(', ');
document.getElementById('excludeStatus').value = (filters.exclude_status || []).join(', ');
document.getElementById('includeSlowRequests').checked = filters.include_slow_requests !== false;
document.getElementById('slowThreshold').value = filters.slow_threshold_ms || 1000;
}
async function saveApiFilters() {
const excludePaths = document.getElementById('excludePaths').value
.split(',')
.map(s => s.trim())
.filter(s => s.length > 0);
const excludeStatus = document.getElementById('excludeStatus').value
.split(',')
.map(s => parseInt(s.trim()))
.filter(n => !isNaN(n));
const includeSlowRequests = document.getElementById('includeSlowRequests').checked;
const slowThreshold = parseInt(document.getElementById('slowThreshold').value);
try {
const response = await fetch('/api/log/filters', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
exclude_paths: excludePaths,
exclude_status: excludeStatus,
include_slow_requests: includeSlowRequests,
slow_threshold_ms: slowThreshold
})
});
const data = await response.json();
if (data.success) {
showNotification('API filters saved', 'success');
} else {
showNotification('Failed to save filters: ' + data.error, 'error');
}
} catch (error) {
showNotification('Error saving filters: ' + error.message, 'error');
}
}
async function saveAllSettings() {
// Reload configuration to apply all changes
try {
const response = await fetch('/api/log/reload', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showNotification('All settings saved and reloaded', 'success');
await loadConfiguration();
await loadComponents();
} else {
showNotification('Failed to reload settings: ' + data.error, 'error');
}
} catch (error) {
showNotification('Error saving settings: ' + error.message, 'error');
}
}
async function resetToDefaults() {
if (!confirm('Are you sure you want to reset all logging settings to defaults?')) {
return;
}
try {
const response = await fetch('/api/log/reset', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showNotification('Settings reset to defaults', 'success');
await loadConfiguration();
await loadComponents();
} else {
showNotification('Failed to reset settings: ' + data.error, 'error');
}
} catch (error) {
showNotification('Error resetting settings: ' + error.message, 'error');
}
}
async function loadLogPreview() {
const component = document.getElementById('previewComponent').value;
const preview = document.getElementById('logPreview');
preview.innerHTML = '<div class="loading">Loading logs...</div>';
try {
const response = await fetch(`/api/log/files/${component}?lines=50`);
const data = await response.json();
if (data.success) {
if (data.lines.length === 0) {
preview.innerHTML = '<div class="loading">No logs yet for this component</div>';
} else {
preview.innerHTML = data.lines.map(line =>
`<div class="log-line">${escapeHtml(line)}</div>`
).join('');
// Scroll to bottom
preview.scrollTop = preview.scrollHeight;
}
} else {
preview.innerHTML = `<div class="loading">Error: ${data.error}</div>`;
}
} catch (error) {
preview.innerHTML = `<div class="loading">Error loading logs: ${error.message}</div>`;
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showNotification(message, type) {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
// Auto-refresh log preview every 5 seconds
setInterval(() => {
if (document.getElementById('previewComponent').value) {
loadLogPreview();
}
}, 5000);
</script>
</body>
</html>