Files
LEDMatrix/templates/index_v2.html
2025-09-15 14:38:42 -04:00

3610 lines
194 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 - Enhanced</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: 1600px;
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;
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: 400px;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid #333;
}
/* Mock device frame look */
.display-preview::before {
content: "";
position: absolute;
inset: 10px;
border-radius: 12px;
box-shadow: inset 0 0 0 2px rgba(255,255,255,0.06), inset 0 0 30px rgba(255,255,255,0.05);
pointer-events: none;
}
.preview-stage {
position: relative;
display: inline-block;
}
.grid-overlay {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.led-canvas {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
pointer-events: none;
background: #000;
}
.display-image {
max-width: 100%;
max-height: 100%;
image-rendering: pixelated;
border: 1px solid #555;
border-radius: 4px;
background: #111;
}
.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);
max-width: 1200px;
margin: 0 auto;
}
.quick-controls {
background: var(--card-background);
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--shadow);
margin-bottom: 20px;
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
.tabs {
display: flex;
border-bottom: 2px solid var(--border-color);
margin-bottom: 20px;
overflow-x: auto;
flex-wrap: wrap;
}
.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;
max-height: 70vh;
overflow-y: auto;
}
.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);
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.config-section {
background: #f9f9f9;
padding: 20px;
margin-bottom: 20px;
border-radius: var(--border-radius);
border-left: 4px solid var(--secondary-color);
}
.config-section h3 {
color: var(--secondary-color);
margin-bottom: 15px;
}
.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;
}
.json-container {
position: relative;
margin-bottom: 15px;
}
.json-container textarea {
width: 100%;
min-height: 300px;
padding: 15px;
border: 2px solid var(--border-color);
border-radius: var(--border-radius);
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
line-height: 1.4;
transition: border-color 0.3s ease;
}
.json-container textarea:focus {
border-color: var(--secondary-color);
outline: none;
}
.json-container textarea.error {
border-color: var(--accent-color);
}
.json-container textarea.valid {
border-color: var(--success-color);
}
.json-status {
position: absolute;
top: 10px;
right: 10px;
padding: 4px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: bold;
color: white;
}
.json-status.valid {
background-color: var(--success-color);
}
.json-status.error {
background-color: var(--accent-color);
}
.json-status.warning {
background-color: var(--warning-color);
}
.json-validation {
margin-top: 10px;
padding: 10px;
border-radius: var(--border-radius);
font-family: monospace;
font-size: 12px;
display: none;
}
.json-validation.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.json-validation.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.json-validation.warning {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.json-actions {
margin-top: 15px;
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.description {
font-size: 12px;
color: #666;
margin-top: 5px;
font-style: italic;
}
.array-input {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.array-input input {
flex: 1;
min-width: 120px;
}
.checkbox-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
margin: 10px 0;
}
.checkbox-item {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.checkbox-item label {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
cursor: pointer;
}
@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;
}
.tabs {
flex-wrap: wrap;
}
}
.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 - Enhanced</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 and system_status.cpu_percent and system_status.cpu_percent > 80 else 'status-active' }}">
<i class="fas fa-microchip"></i>
{{ system_status.cpu_percent if system_status and system_status.cpu_percent is defined else 0 }}% CPU
</div>
<div class="status-item {{ 'status-warning' if system_status and system_status.memory_used_percent and system_status.memory_used_percent > 80 else 'status-active' }}">
<i class="fas fa-memory"></i>
{{ system_status.memory_used_percent if system_status and system_status.memory_used_percent is defined else 0 }}% RAM
</div>
<div class="status-item {{ 'status-warning' if system_status and system_status.cpu_temp and system_status.cpu_temp > 70 else 'status-active' }}">
<i class="fas fa-thermometer-half"></i>
{{ system_status.cpu_temp if system_status and system_status.cpu_temp is defined else 0 }}°C
</div>
<div class="status-item status-active">
<i class="fas fa-clock"></i>
{{ system_status.uptime }}
</div>
</div>
</div>
<!-- Quick Controls -->
<div class="quick-controls">
<h2 style="margin-bottom:12px;"><i class="fas fa-bolt"></i> Quick Controls</h2>
<div class="display-controls">
<button class="btn btn-success" onclick="runAction('start_display')"><i class="fas fa-play"></i> Start Display</button>
<button class="btn btn-danger" onclick="runAction('stop_display')"><i class="fas fa-stop"></i> Stop Display</button>
<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-info" onclick="systemAction('migrate_config')"><i class="fas fa-sync-alt"></i> Migrate Config</button>
<button class="btn btn-danger" onclick="systemAction('reboot_system')"><i class="fas fa-power-off"></i> Reboot</button>
<button class="btn btn-secondary" onclick="stopOnDemand()"><i class="fas fa-ban"></i> Stop On-Demand</button>
<span id="ondemand-status" style="margin-left:auto; font-size:12px; color:#333; background:#f3f3f3; padding:6px 10px; border-radius:8px;">On-Demand: None</span>
</div>
<div style="margin-top:12px; color:#666; font-size:12px;">Service actions may require sudo privileges on the Pi. Migrate Config adds new options with defaults while preserving your settings.</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" style="max-width: 1200px; margin: 0 auto;">
<h2><i class="fas fa-desktop"></i> Live Display Preview</h2>
<div class="display-preview" id="displayPreview">
<div id="previewStage" class="preview-stage" style="display:none;">
<div id="previewMeta" style="position:absolute; top:-28px; left:0; color:#ddd; font-size:12px; opacity:0.85;"></div>
<img id="displayImage" class="display-image" alt="LED Matrix Display">
<canvas id="ledCanvas" class="led-canvas" style="display:none;"></canvas>
<canvas id="gridOverlay" class="grid-overlay"></canvas>
</div>
<div id="displayPlaceholder" 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-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 style="display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin-left:auto;">
<label style="color:#333; background:#f3f3f3; padding:6px 10px; border-radius:8px;">
Scale
<input type="range" id="scaleRange" min="2" max="16" value="8" style="vertical-align:middle;">
<span id="scaleValue">8x</span>
</label>
<label style="color:#333; background:#f3f3f3; padding:6px 10px; border-radius:8px; display:inline-flex; align-items:center; gap:8px;">
<input type="checkbox" id="toggleGrid">
Show pixel grid
</label>
<label style="color:#333; background:#f3f3f3; padding:6px 10px; border-radius:8px; display:inline-flex; align-items:center; gap:8px;">
<input type="checkbox" id="toggleLedDots" checked>
LED dot mode
</label>
<label style="color:#333; background:#f3f3f3; padding:6px 10px; border-radius:8px; display:inline-flex; align-items:center; gap:8px;">
Dot fill
<input type="range" id="dotFillRange" min="40" max="95" value="75">
<span id="dotFillValue">75%</span>
</label>
</div>
</div>
</div>
<!-- Control Panel under the preview, wider -->
<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('general')">
<i class="fas fa-sliders-h"></i> General
</button>
<button class="tab-btn" onclick="showTab('schedule')">
<i class="fas fa-calendar"></i> Schedule
</button>
<button class="tab-btn" onclick="showTab('display')">
<i class="fas fa-cog"></i> Display
</button>
<button class="tab-btn" onclick="showTab('clock')">
<i class="fas fa-clock"></i> Clock
</button>
<button class="tab-btn" onclick="showTab('durations')">
<i class="fas fa-hourglass-half"></i> Durations
</button>
<button class="tab-btn" onclick="showTab('sports')">
<i class="fas fa-football-ball"></i> Sports
</button>
<button class="tab-btn" onclick="showTab('weather')">
<i class="fas fa-cloud-sun"></i> Weather
</button>
<button class="tab-btn" onclick="showTab('stocks')">
<i class="fas fa-chart-line"></i> Stocks
</button>
<button class="tab-btn" onclick="showTab('stocknews')">
<i class="fas fa-newspaper"></i> Stock News
</button>
<button class="tab-btn" onclick="showTab('odds')">
<i class="fas fa-ticket-alt"></i> Odds Ticker
</button>
<button class="tab-btn" onclick="showTab('leaderboard')">
<i class="fas fa-trophy"></i> Leaderboard
</button>
<button class="tab-btn" onclick="showTab('text')">
<i class="fas fa-font"></i> Text
</button>
<button class="tab-btn" onclick="showTab('features')">
<i class="fas fa-star"></i> Features
</button>
<button class="tab-btn" onclick="showTab('of_the_day')">
<i class="fas fa-calendar-day"></i> Of The Day
</button>
<button class="tab-btn" onclick="showTab('music')">
<i class="fas fa-music"></i> Music
</button>
<button class="tab-btn" onclick="showTab('calendar')">
<i class="fas fa-calendar-alt"></i> Calendar
</button>
<button class="tab-btn" onclick="showTab('youtube')">
<i class="fab fa-youtube"></i> YouTube
</button>
<button class="tab-btn" onclick="showTab('news')">
<i class="fas fa-newspaper"></i> News
</button>
<button class="tab-btn" onclick="showTab('secrets')">
<i class="fas fa-key"></i> API Keys
</button>
<button class="tab-btn" onclick="showTab('editor')">
<i class="fas fa-paint-brush"></i> Editor
</button>
<button class="tab-btn" onclick="showTab('actions')">
<i class="fas fa-tools"></i> Actions
</button>
<button class="tab-btn" onclick="showTab('raw-json')">
<i class="fas fa-code"></i> Raw JSON
</button>
<button class="tab-btn" onclick="showTab('logs')">
<i class="fas fa-file-alt"></i> Logs
</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.cpu_percent if system_status and system_status.cpu_percent is defined else 0 }}%</div>
<div class="stat-label">CPU Usage</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ system_status.memory_used_percent if system_status and system_status.memory_used_percent is defined else 0 }}%</div>
<div class="stat-label">Memory Usage</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ system_status.cpu_temp if system_status and system_status.cpu_temp is defined else 0 }}°C</div>
<div class="stat-label">CPU Temperature</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ main_config.get('display', {}).get('hardware', {}).get('brightness', 0) }}</div>
<div class="stat-label">Brightness</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ main_config.get('display', {}).get('hardware', {}).get('cols', 0) }}x{{ main_config.get('display', {}).get('hardware', {}).get('rows', 0) }}</div>
<div class="stat-label">Resolution</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ system_status.disk_used_percent if system_status and system_status.disk_used_percent is defined else 0 }}%</div>
<div class="stat-label">Disk Usage</div>
</div>
</div>
<h4>API Calls (24h window)</h4>
<div id="api-metrics" class="stat-card" style="text-align:left;">
<div>Loading API metrics...</div>
<div style="margin-top:10px; font-size:12px; color:#666;">If empty, ensure the server is running and /api/metrics is reachable.</div>
<div style="margin-top:10px;">
<button class="btn btn-primary" onclick="updateApiMetrics()"><i class="fas fa-sync"></i> Refresh API Metrics</button>
</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-info" onclick="systemAction('migrate_config')">
<i class="fas fa-sync-alt"></i> Migrate Config
</button>
<button class="btn btn-danger" onclick="systemAction('reboot_system')">
<i class="fas fa-power-off"></i> Reboot System
</button>
</div>
</div>
<!-- General Tab -->
<div id="general" class="tab-content">
<div class="config-section">
<h3>General Settings</h3>
<form id="general-form">
<div class="form-group">
<label>
<input type="checkbox" id="web_display_autostart" name="web_display_autostart" {% if safe_config_get(main_config, 'web_display_autostart', default=True) %}checked{% endif %}>
Web Display Autostart
</label>
<div class="description">Start the web interface on boot for easier access.</div>
</div>
<div class="form-group">
<label for="timezone">Timezone</label>
<input type="text" class="form-control" id="timezone" name="timezone" value="{{ safe_config_get(main_config, 'timezone', default='America/Chicago') }}" placeholder="e.g., America/Chicago">
<div class="description">IANA timezone, affects time-based features and scheduling.</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="city">City</label>
<input type="text" class="form-control" id="city" name="city" value="{{ safe_config_get(main_config, 'location', 'city', default='Dallas') }}">
</div>
<div class="form-group">
<label for="state">State</label>
<input type="text" class="form-control" id="state" name="state" value="{{ safe_config_get(main_config, 'location', 'state', default='Texas') }}">
</div>
<div class="form-group">
<label for="country">Country</label>
<input type="text" class="form-control" id="country" name="country" value="{{ safe_config_get(main_config, 'location', 'country', default='US') }}">
</div>
</div>
<button type="submit" class="btn btn-success">Save General Settings</button>
</form>
</div>
</div>
<!-- Schedule Tab -->
<div id="schedule" class="tab-content">
<div class="config-section">
<h3>Display Schedule</h3>
<p>Set the time for the display to be active. A restart is needed for changes to take effect.</p>
<form id="schedule-form">
<div class="form-group">
<label for="schedule_enabled">Enable Schedule:</label>
<div style="display: flex; align-items: center; gap: 10px;">
<label class="toggle-switch">
<input type="checkbox" id="schedule_enabled" name="schedule_enabled" {% if schedule_config.enabled %}checked{% endif %}>
<span class="slider"></span>
</label>
<span>Turn display on/off automatically</span>
</div>
</div>
<div class="form-group">
<label for="start_time">Display On Time:</label>
<input type="time" class="form-control" id="start_time" name="start_time" value="{{ schedule_config.start_time }}">
<div class="description">Time when the display should turn on</div>
</div>
<div class="form-group">
<label for="end_time">Display Off Time:</label>
<input type="time" class="form-control" id="end_time" name="end_time" value="{{ schedule_config.end_time }}">
<div class="description">Time when the display should turn off</div>
</div>
<button type="submit" class="btn btn-success">Save Schedule</button>
</form>
</div>
</div>
<!-- Display Settings Tab -->
<div id="display" class="tab-content">
<div class="config-section">
<h3>LED Matrix Hardware Settings</h3>
<form id="display-form">
<div class="form-row">
<div>
<div class="form-group">
<label for="rows">Rows:</label>
<input type="number" class="form-control" id="rows" name="rows" value="{{ safe_config_get(main_config, 'display', 'hardware', 'rows', default=32) }}" min="1" max="64">
<div class="description">Number of LED rows</div>
</div>
<div class="form-group">
<label for="cols">Columns:</label>
<input type="number" class="form-control" id="cols" name="cols" value="{{ safe_config_get(main_config, 'display', 'hardware', 'cols', default=64) }}" min="1" max="128">
<div class="description">Number of LED columns</div>
</div>
<div class="form-group">
<label for="chain_length">Chain Length:</label>
<input type="number" class="form-control" id="chain_length" name="chain_length" value="{{ safe_config_get(main_config, 'display', 'hardware', 'chain_length', default=2) }}" min="1" max="8">
<div class="description">Number of LED panels chained together</div>
</div>
<div class="form-group">
<label for="parallel">Parallel:</label>
<input type="number" class="form-control" id="parallel" name="parallel" value="{{ safe_config_get(main_config, 'display', 'hardware', 'parallel', default=1) }}" min="1" max="4">
<div class="description">Number of parallel chains</div>
</div>
<div class="form-group">
<label for="brightness">Brightness:</label>
<input type="range" class="form-control" id="brightness" name="brightness" value="{{ safe_config_get(main_config, 'display', 'hardware', 'brightness', default=95) }}" min="1" max="100" oninput="updateBrightnessDisplay(this.value)">
<div class="description">LED brightness: <span id="brightness-value">{{ safe_config_get(main_config, 'display', 'hardware', 'brightness', default=95) }}</span>%</div>
</div>
<div class="form-group">
<label for="hardware_mapping">Hardware Mapping:</label>
<select class="form-control" id="hardware_mapping" name="hardware_mapping">
<option value="adafruit-hat-pwm" {% if safe_config_get(main_config, 'display', 'hardware', 'hardware_mapping', default='adafruit-hat-pwm') == "adafruit-hat-pwm" %}selected{% endif %}>Adafruit HAT PWM</option>
<option value="adafruit-hat" {% if safe_config_get(main_config, 'display', 'hardware', 'hardware_mapping', default='adafruit-hat-pwm') == "adafruit-hat" %}selected{% endif %}>Adafruit HAT</option>
<option value="regular" {% if safe_config_get(main_config, 'display', 'hardware', 'hardware_mapping', default='adafruit-hat-pwm') == "regular" %}selected{% endif %}>Regular</option>
<option value="regular-pi1" {% if safe_config_get(main_config, 'display', 'hardware', 'hardware_mapping', default='adafruit-hat-pwm') == "regular-pi1" %}selected{% endif %}>Regular Pi1</option>
</select>
<div class="description">Hardware mapping type</div>
</div>
</div>
<div>
<div class="form-group">
<label for="gpio_slowdown">GPIO Slowdown:</label>
<input type="number" class="form-control" id="gpio_slowdown" name="gpio_slowdown" value="{{ safe_config_get(main_config, 'display', 'runtime', 'gpio_slowdown', default=3) }}" min="0" max="5">
<div class="description">GPIO slowdown factor (0-5)</div>
</div>
<div class="form-group">
<label for="scan_mode">Scan Mode:</label>
<input type="number" class="form-control" id="scan_mode" name="scan_mode" value="{{ safe_config_get(main_config, 'display', 'hardware', 'scan_mode', default=0) }}" min="0" max="1">
<div class="description">Scan mode for LED matrix (0-1)</div>
</div>
<div class="form-group">
<label for="pwm_bits">PWM Bits:</label>
<input type="number" class="form-control" id="pwm_bits" name="pwm_bits" value="{{ safe_config_get(main_config, 'display', 'hardware', 'pwm_bits', default=9) }}" min="1" max="11">
<div class="description">PWM bits for brightness control (1-11)</div>
</div>
<div class="form-group">
<label for="pwm_dither_bits">PWM Dither Bits:</label>
<input type="number" class="form-control" id="pwm_dither_bits" name="pwm_dither_bits" value="{{ safe_config_get(main_config, 'display', 'hardware', 'pwm_dither_bits', default=1) }}" min="0" max="4">
<div class="description">PWM dither bits (0-4)</div>
</div>
<div class="form-group">
<label for="pwm_lsb_nanoseconds">PWM LSB Nanoseconds:</label>
<input type="number" class="form-control" id="pwm_lsb_nanoseconds" name="pwm_lsb_nanoseconds" value="{{ safe_config_get(main_config, 'display', 'hardware', 'pwm_lsb_nanoseconds', default=130) }}" min="50" max="500">
<div class="description">PWM LSB nanoseconds (50-500)</div>
</div>
<div class="form-group">
<label for="limit_refresh_rate_hz">Limit Refresh Rate (Hz):</label>
<input type="number" class="form-control" id="limit_refresh_rate_hz" name="limit_refresh_rate_hz" value="{{ safe_config_get(main_config, 'display', 'hardware', 'limit_refresh_rate_hz', default=120) }}" min="1" max="1000">
<div class="description">Limit refresh rate in Hz (1-1000)</div>
</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>
<input type="checkbox" id="disable_hardware_pulsing" name="disable_hardware_pulsing" {% if safe_config_get(main_config, 'display', 'hardware', 'disable_hardware_pulsing', default=False) %}checked{% endif %}>
Disable Hardware Pulsing
</label>
<div class="description">Disable hardware pulsing</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="inverse_colors" name="inverse_colors" {% if safe_config_get(main_config, 'display', 'hardware', 'inverse_colors', default=False) %}checked{% endif %}>
Inverse Colors
</label>
<div class="description">Inverse color display</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="show_refresh_rate" name="show_refresh_rate" {% if safe_config_get(main_config, 'display', 'hardware', 'show_refresh_rate', default=False) %}checked{% endif %}>
Show Refresh Rate
</label>
<div class="description">Show refresh rate on display</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="use_short_date_format" name="use_short_date_format" {% if safe_config_get(main_config, 'display', 'use_short_date_format', default=True) %}checked{% endif %}>
Use Short Date Format
</label>
<div class="description">Use short date format for display</div>
</div>
</div>
<button type="submit" class="btn btn-success">Save Display Settings</button>
</form>
</div>
</div>
<!-- Clock Tab -->
<div id="clock" class="tab-content">
<div class="config-section">
<h3>Clock</h3>
<form id="clock-form">
<div class="form-group">
<label>
<input type="checkbox" id="clock_enabled" {% if safe_config_get(main_config, 'clock', 'enabled', default=True) %}checked{% endif %}>
Enable Clock
</label>
</div>
<div class="form-group">
<label for="clock_format">Format</label>
<input type="text" id="clock_format" class="form-control" value="{{ safe_config_get(main_config, 'clock', 'format', default='%I:%M %p') }}">
<div class="description">Python strftime format. Example: %I:%M %p for 12-hour time.</div>
</div>
<div class="form-group">
<label for="clock_update_interval">Update Interval (seconds)</label>
<input type="number" id="clock_update_interval" class="form-control" value="{{ safe_config_get(main_config, 'clock', 'update_interval', default=1) }}" min="1" max="60">
</div>
<button type="submit" class="btn btn-success">Save Clock Settings</button>
</form>
</div>
</div>
<!-- Durations Tab -->
<div id="durations" class="tab-content">
<div class="config-section">
<h3>Rotation Durations</h3>
<p class="description">How long each screen is shown before switching. Values in seconds.</p>
<form id="durations-form">
<div class="form-row">
{% for key, value in main_config.get('display', {}).get('display_durations', {}).items() %}
<div class="form-group">
<label for="duration_{{ key }}">{{ key | replace('_', ' ') | title }}</label>
<input type="number" class="form-control duration-input" id="duration_{{ key }}" data-name="{{ key }}" value="{{ value }}" min="5" max="600">
</div>
{% endfor %}
</div>
<button type="submit" class="btn btn-success">Save Durations</button>
</form>
</div>
</div>
<!-- Sports Tab -->
<div id="sports" class="tab-content">
<div class="config-section">
<h3>Sports Configuration</h3>
<p>Configure which sports leagues to display and their settings.</p>
<!-- Sports configuration will be populated by JavaScript -->
<div id="sports-config">
Loading sports configuration...
</div>
<div style="margin-top:10px; display:flex; gap:10px;">
<button type="button" class="btn btn-primary" onclick="refreshSportsConfig()">Refresh</button>
<button type="button" class="btn btn-success" onclick="saveSportsConfig()">Save Sports Settings</button>
</div>
</div>
</div>
<!-- Weather Tab -->
<div id="weather" class="tab-content">
<div class="config-section">
<div style="display:flex; justify-content: space-between; align-items:center;">
<h3>Weather Configuration</h3>
<div style="display:flex; gap:8px;">
<button type="button" class="btn btn-info" onclick="startOnDemand('weather_current')"><i class="fas fa-bolt"></i> On-Demand Current</button>
<button type="button" class="btn btn-info" onclick="startOnDemand('weather_hourly')"><i class="fas fa-bolt"></i> Hourly</button>
<button type="button" class="btn btn-info" onclick="startOnDemand('weather_daily')"><i class="fas fa-bolt"></i> Daily</button>
</div>
</div>
<form id="weather-form">
<div class="form-group">
<label>
<input type="checkbox" id="weather_enabled" name="weather_enabled" {% if safe_config_get(main_config, 'weather', 'enabled', default=False) %}checked{% endif %}>
Enable Weather
</label>
</div>
<div class="form-group">
<label for="weather_city">City:</label>
<input type="text" class="form-control" id="weather_city" name="weather_city" value="{{ safe_config_get(main_config, 'location', 'city', default='Dallas') }}">
<div class="description">City name for weather data</div>
</div>
<div class="form-group">
<label for="weather_state">State:</label>
<input type="text" class="form-control" id="weather_state" name="weather_state" value="{{ safe_config_get(main_config, 'location', 'state', default='Texas') }}">
<div class="description">State/province name</div>
</div>
<div class="form-group">
<label for="weather_units">Units:</label>
<select class="form-control" id="weather_units" name="weather_units">
<option value="imperial" {% if safe_config_get(main_config, 'weather', 'units', default='imperial') == "imperial" %}selected{% endif %}>Fahrenheit</option>
<option value="metric" {% if safe_config_get(main_config, 'weather', 'units', default='imperial') == "metric" %}selected{% endif %}>Celsius</option>
</select>
<div class="description">Temperature units</div>
</div>
<div class="form-group">
<label for="weather_display_format">Display Format</label>
<textarea id="weather_display_format" class="form-control" rows="2">{{ safe_config_get(main_config, 'weather', 'display_format', default='{temp}°F\n{condition}') }}</textarea>
<div class="description">Use tokens like {temp}, {condition}. Supports new lines.</div>
</div>
<div class="form-group">
<label for="weather_update_interval">Update Interval (seconds):</label>
<input type="number" class="form-control" id="weather_update_interval" name="weather_update_interval" value="{{ safe_config_get(main_config, 'weather', 'update_interval', default=1800) }}" min="300" max="3600">
<div class="description">How often to update weather data (300-3600 seconds)</div>
</div>
<button type="submit" class="btn btn-success">Save Weather Settings</button>
</form>
</div>
</div>
<!-- Stocks Tab -->
<div id="stocks" class="tab-content">
<div class="config-section">
<div style="display:flex; justify-content: space-between; align-items:center;">
<h3>Stocks & Crypto Configuration</h3>
<div style="display:flex; gap:8px;">
<button type="button" class="btn btn-info" onclick="startOnDemand('stocks')"><i class="fas fa-bolt"></i> On-Demand Stocks</button>
</div>
</div>
<form id="stocks-form">
<div class="form-group">
<label>
<input type="checkbox" id="stocks_enabled" name="stocks_enabled" {% if safe_config_get(main_config, 'stocks', 'enabled', default=False) %}checked{% endif %}>
Enable Stocks
</label>
</div>
<div class="form-group">
<label for="stocks_symbols">Stock Symbols:</label>
<input type="text" class="form-control" id="stocks_symbols" name="stocks_symbols" value="{{ safe_config_get(main_config, 'stocks', 'symbols', default=['ASTS', 'SCHD', 'INTC', 'NVDA', 'T', 'VOO', 'SMCI'])|join(', ') }}" placeholder="AAPL, GOOGL, MSFT">
<div class="description">Comma-separated stock symbols</div>
</div>
<div class="form-group">
<label for="stocks_update_interval">Update Interval (seconds):</label>
<input type="number" class="form-control" id="stocks_update_interval" name="stocks_update_interval" value="{{ safe_config_get(main_config, 'stocks', 'update_interval', default=600) }}" min="60" max="3600">
<div class="description">How often to update stock data</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="stocks_scroll_speed">Scroll Speed</label>
<input type="number" step="0.1" class="form-control" id="stocks_scroll_speed" value="{{ safe_config_get(main_config, 'stocks', 'scroll_speed', default=1) }}">
<div class="description">Horizontal scroll pixels per step.</div>
</div>
<div class="form-group">
<label for="stocks_scroll_delay">Scroll Delay (seconds)</label>
<input type="number" step="0.001" class="form-control" id="stocks_scroll_delay" value="{{ safe_config_get(main_config, 'stocks', 'scroll_delay', default=0.01) }}">
<div class="description">Delay between scroll steps.</div>
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="stocks_toggle_chart" name="stocks_toggle_chart" {% if safe_config_get(main_config, 'stocks', 'toggle_chart', default=True) %}checked{% endif %}>
Show Charts
</label>
<div class="description">Display mini charts alongside stock ticker data</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="stocks_dynamic_duration" {% if safe_config_get(main_config, 'stocks', 'dynamic_duration', default=True) %}checked{% endif %}>
Dynamic Duration
</label>
<div class="description">Adjust display duration based on content length.</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="stocks_min_duration">Min Duration (sec)</label>
<input type="number" class="form-control" id="stocks_min_duration" value="{{ safe_config_get(main_config, 'stocks', 'min_duration', default=30) }}">
</div>
<div class="form-group">
<label for="stocks_max_duration">Max Duration (sec)</label>
<input type="number" class="form-control" id="stocks_max_duration" value="{{ safe_config_get(main_config, 'stocks', 'max_duration', default=300) }}">
</div>
<div class="form-group">
<label for="stocks_duration_buffer">Duration Buffer</label>
<input type="number" step="0.01" class="form-control" id="stocks_duration_buffer" value="{{ safe_config_get(main_config, 'stocks', 'duration_buffer', default=0.1) }}">
</div>
</div>
<div class="form-group">
<label for="stocks_display_format">Display Format</label>
<input type="text" class="form-control" id="stocks_display_format" value="{{ safe_config_get(main_config, 'stocks', 'display_format', default='{symbol}: ${price} ({change}%)') }}">
<div class="description">Use tokens like {symbol}, {price}, {change}.</div>
</div>
<button type="submit" class="btn btn-success">Save Stocks Settings</button>
</form>
<h3>Cryptocurrency</h3>
<form id="crypto-form">
<div class="form-group">
<label>
<input type="checkbox" id="crypto_enabled" name="crypto_enabled" {% if safe_config_get(main_config, 'crypto', 'enabled', default=False) %}checked{% endif %}>
Enable Crypto
</label>
</div>
<div class="form-group">
<label for="crypto_symbols">Crypto Symbols:</label>
<input type="text" class="form-control" id="crypto_symbols" name="crypto_symbols" value="{{ safe_config_get(main_config, 'crypto', 'symbols', default=['BTC-USD', 'ETH-USD'])|join(', ') }}" placeholder="BTC-USD, ETH-USD">
<div class="description">Comma-separated crypto symbols (e.g., BTC-USD, ETH-USD)</div>
</div>
<div class="form-group">
<label for="crypto_update_interval">Update Interval (seconds):</label>
<input type="number" class="form-control" id="crypto_update_interval" name="crypto_update_interval" value="{{ safe_config_get(main_config, 'crypto', 'update_interval', default=600) }}" min="60" max="3600">
<div class="description">How often to update crypto data</div>
</div>
<button type="submit" class="btn btn-success">Save Crypto Settings</button>
</form>
</div>
</div>
<!-- Stock News Tab -->
<div id="stocknews" class="tab-content">
<div class="config-section">
<div style="display:flex; justify-content: space-between; align-items:center;">
<h3>Stock News</h3>
<div style="display:flex; gap:8px;">
<button type="button" class="btn btn-info" onclick="startOnDemand('stock_news')"><i class="fas fa-bolt"></i> On-Demand News</button>
</div>
</div>
<form id="stocknews-form">
<div class="form-group">
<label>
<input type="checkbox" id="stocknews_enabled" {% if safe_config_get(main_config, 'stock_news', 'enabled', default=False) %}checked{% endif %}>
Enable Stock News
</label>
</div>
<div class="form-row">
<div class="form-group">
<label for="stocknews_update_interval">Update Interval (sec)</label>
<input type="number" class="form-control" id="stocknews_update_interval" value="{{ safe_config_get(main_config, 'stock_news', 'update_interval', default=3600) }}">
</div>
<div class="form-group">
<label for="stocknews_scroll_speed">Scroll Speed</label>
<input type="number" step="0.1" class="form-control" id="stocknews_scroll_speed" value="{{ safe_config_get(main_config, 'stock_news', 'scroll_speed', default=1) }}">
</div>
<div class="form-group">
<label for="stocknews_scroll_delay">Scroll Delay (sec)</label>
<input type="number" step="0.001" class="form-control" id="stocknews_scroll_delay" value="{{ safe_config_get(main_config, 'stock_news', 'scroll_delay', default=0.01) }}">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="stocknews_max_headlines_per_symbol">Max Headlines per Symbol</label>
<input type="number" class="form-control" id="stocknews_max_headlines_per_symbol" value="{{ safe_config_get(main_config, 'stock_news', 'max_headlines_per_symbol', default=1) }}">
</div>
<div class="form-group">
<label for="stocknews_headlines_per_rotation">Headlines per Rotation</label>
<input type="number" class="form-control" id="stocknews_headlines_per_rotation" value="{{ safe_config_get(main_config, 'stock_news', 'headlines_per_rotation', default=2) }}">
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="stocknews_dynamic_duration" {% if safe_config_get(main_config, 'stock_news', 'dynamic_duration', default=True) %}checked{% endif %}>
Dynamic Duration
</label>
</div>
<div class="form-row">
<div class="form-group"><label for="stocknews_min_duration">Min Duration</label><input type="number" class="form-control" id="stocknews_min_duration" value="{{ safe_config_get(main_config, 'stock_news', 'min_duration', default=30) }}"></div>
<div class="form-group"><label for="stocknews_max_duration">Max Duration</label><input type="number" class="form-control" id="stocknews_max_duration" value="{{ safe_config_get(main_config, 'stock_news', 'max_duration', default=300) }}"></div>
<div class="form-group"><label for="stocknews_duration_buffer">Duration Buffer</label><input type="number" step="0.01" class="form-control" id="stocknews_duration_buffer" value="{{ safe_config_get(main_config, 'stock_news', 'duration_buffer', default=0.1) }}"></div>
</div>
<button type="submit" class="btn btn-success">Save Stock News</button>
</form>
</div>
</div>
<!-- Odds Ticker Tab -->
<div id="odds" class="tab-content">
<div class="config-section">
<div style="display:flex; justify-content: space-between; align-items:center;">
<h3>Odds Ticker</h3>
<div style="display:flex; gap:8px;">
<button type="button" class="btn btn-info" onclick="startOnDemand('odds_ticker')"><i class="fas fa-bolt"></i> On-Demand</button>
<button type="button" class="btn btn-secondary" onclick="stopOnDemand()"><i class="fas fa-ban"></i> Stop</button>
</div>
</div>
<form id="odds-form">
<div class="form-group">
<label>
<input type="checkbox" id="odds_enabled" {% if safe_config_get(main_config, 'odds_ticker', 'enabled', default=True) %}checked{% endif %}>
Enable Odds Ticker
</label>
</div>
<div class="form-row">
<div class="form-group"><label for="odds_update_interval">Update Interval (sec)</label><input type="number" class="form-control" id="odds_update_interval" value="{{ safe_config_get(main_config, 'odds_ticker', 'update_interval', default=3600) }}"></div>
<div class="form-group"><label for="odds_scroll_speed">Scroll Speed</label><input type="number" step="0.1" class="form-control" id="odds_scroll_speed" value="{{ safe_config_get(main_config, 'odds_ticker', 'scroll_speed', default=1) }}"></div>
<div class="form-group"><label for="odds_scroll_delay">Scroll Delay (sec)</label><input type="number" step="0.001" class="form-control" id="odds_scroll_delay" value="{{ safe_config_get(main_config, 'odds_ticker', 'scroll_delay', default=0.01) }}"></div>
</div>
<div class="form-row">
<div class="form-group"><label for="odds_games_per_favorite_team">Games per Favorite Team</label><input type="number" class="form-control" id="odds_games_per_favorite_team" value="{{ safe_config_get(main_config, 'odds_ticker', 'games_per_favorite_team', default=1) }}"></div>
<div class="form-group"><label for="odds_max_games_per_league">Max Games per League</label><input type="number" class="form-control" id="odds_max_games_per_league" value="{{ safe_config_get(main_config, 'odds_ticker', 'max_games_per_league', default=5) }}"></div>
<div class="form-group"><label for="odds_future_fetch_days">Future Fetch Days</label><input type="number" class="form-control" id="odds_future_fetch_days" value="{{ safe_config_get(main_config, 'odds_ticker', 'future_fetch_days', default=50) }}"></div>
</div>
<div class="form-row">
<div class="form-group">
<label for="odds_enabled_leagues">Enabled Leagues</label>
<input type="text" class="form-control" id="odds_enabled_leagues" value="{{ safe_config_get(main_config, 'odds_ticker', 'enabled_leagues', default=['nfl', 'mlb', 'ncaa_fb', 'milb']) | join(', ') }}">
<div class="description">Comma-separated list, e.g., nfl, mlb, ncaa_fb, milb</div>
</div>
<div class="form-group">
<label for="odds_sort_order">Sort Order</label>
<select id="odds_sort_order" class="form-control">
<option value="soonest" {% if safe_config_get(main_config, 'odds_ticker', 'sort_order', default='soonest') == 'soonest' %}selected{% endif %}>Soonest</option>
<option value="league" {% if safe_config_get(main_config, 'odds_ticker', 'sort_order', default='soonest') == 'league' %}selected{% endif %}>By League</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group"><label><input type="checkbox" id="odds_show_favorite_teams_only" {% if safe_config_get(main_config, 'odds_ticker', 'show_favorite_teams_only', default=True) %}checked{% endif %}> Show Favorite Teams Only</label></div>
<div class="form-group"><label><input type="checkbox" id="odds_show_odds_only" {% if safe_config_get(main_config, 'odds_ticker', 'show_odds_only', default=False) %}checked{% endif %}> Show Odds Only</label></div>
<div class="form-group"><label><input type="checkbox" id="odds_loop" {% if safe_config_get(main_config, 'odds_ticker', 'loop', default=True) %}checked{% endif %}> Loop</label></div>
<div class="form-group"><label><input type="checkbox" id="odds_show_channel_logos" {% if safe_config_get(main_config, 'odds_ticker', 'show_channel_logos', default=True) %}checked{% endif %}> Show Channel Logos</label></div>
</div>
<div class="form-group"><label><input type="checkbox" id="odds_dynamic_duration" {% if safe_config_get(main_config, 'odds_ticker', 'dynamic_duration', default=True) %}checked{% endif %}> Dynamic Duration</label></div>
<div class="form-row">
<div class="form-group"><label for="odds_min_duration">Min Duration</label><input type="number" class="form-control" id="odds_min_duration" value="{{ safe_config_get(main_config, 'odds_ticker', 'min_duration', default=30) }}"></div>
<div class="form-group"><label for="odds_max_duration">Max Duration</label><input type="number" class="form-control" id="odds_max_duration" value="{{ safe_config_get(main_config, 'odds_ticker', 'max_duration', default=300) }}"></div>
<div class="form-group"><label for="odds_duration_buffer">Duration Buffer</label><input type="number" step="0.01" class="form-control" id="odds_duration_buffer" value="{{ safe_config_get(main_config, 'odds_ticker', 'duration_buffer', default=0.1) }}"></div>
</div>
<button type="submit" class="btn btn-success">Save Odds Settings</button>
</form>
</div>
</div>
<!-- Leaderboard Tab -->
<div id="leaderboard" class="tab-content">
<div class="config-section">
<div style="display:flex; justify-content: space-between; align-items:center;">
<h3>Leaderboard Configuration</h3>
<div style="display:flex; gap:8px;">
<button type="button" class="btn btn-info" onclick="startOnDemand('leaderboard')"><i class="fas fa-bolt"></i> On-Demand</button>
<button type="button" class="btn btn-secondary" onclick="stopOnDemand()"><i class="fas fa-ban"></i> Stop</button>
</div>
</div>
<form id="leaderboard-form">
<div class="form-group">
<label>
<input type="checkbox" id="leaderboard_enabled" {% if safe_config_get(main_config, 'leaderboard', 'enabled', default=False) %}checked{% endif %}>
Enable Leaderboard
</label>
</div>
<div class="form-row">
<div class="form-group">
<label for="leaderboard_update_interval">Update Interval (sec)</label>
<input type="number" class="form-control" id="leaderboard_update_interval" value="{{ safe_config_get(main_config, 'leaderboard', 'update_interval', default=3600) }}">
</div>
<div class="form-group">
<label for="leaderboard_scroll_speed">Scroll Speed</label>
<input type="number" step="0.1" class="form-control" id="leaderboard_scroll_speed" value="{{ safe_config_get(main_config, 'leaderboard', 'scroll_speed', default=1) }}">
</div>
<div class="form-group">
<label for="leaderboard_scroll_delay">Scroll Delay (sec)</label>
<input type="number" step="0.001" class="form-control" id="leaderboard_scroll_delay" value="{{ safe_config_get(main_config, 'leaderboard', 'scroll_delay', default=0.01) }}">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="leaderboard_display_duration">Display Duration (sec)</label>
<input type="number" class="form-control" id="leaderboard_display_duration" value="{{ safe_config_get(main_config, 'leaderboard', 'display_duration', default=30) }}">
</div>
<div class="form-group">
<label for="leaderboard_request_timeout">Request Timeout (sec)</label>
<input type="number" class="form-control" id="leaderboard_request_timeout" value="{{ safe_config_get(main_config, 'leaderboard', 'request_timeout', default=10) }}">
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="leaderboard_loop" {% if safe_config_get(main_config, 'leaderboard', 'loop', default=True) %}checked{% endif %}>
Loop
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="leaderboard_dynamic_duration" {% if safe_config_get(main_config, 'leaderboard', 'dynamic_duration', default=True) %}checked{% endif %}>
Dynamic Duration
</label>
</div>
<div class="form-row">
<div class="form-group">
<label for="leaderboard_min_duration">Min Duration (sec)</label>
<input type="number" class="form-control" id="leaderboard_min_duration" value="{{ safe_config_get(main_config, 'leaderboard', 'min_duration', default=30) }}">
</div>
<div class="form-group">
<label for="leaderboard_max_duration">Max Duration (sec)</label>
<input type="number" class="form-control" id="leaderboard_max_duration" value="{{ safe_config_get(main_config, 'leaderboard', 'max_duration', default=300) }}">
</div>
<div class="form-group">
<label for="leaderboard_duration_buffer">Duration Buffer</label>
<input type="number" step="0.01" class="form-control" id="leaderboard_duration_buffer" value="{{ safe_config_get(main_config, 'leaderboard', 'duration_buffer', default=0.1) }}">
</div>
</div>
<h4>Enabled Sports</h4>
<div class="form-group">
<label>
<input type="checkbox" id="leaderboard_nfl_enabled" {% if safe_config_get(main_config, 'leaderboard', 'enabled_sports', 'nfl', 'enabled', default=False) %}checked{% endif %}>
NFL
</label>
<div class="form-group" style="margin-left: 20px;">
<label for="leaderboard_nfl_top_teams">Top Teams</label>
<input type="number" class="form-control" id="leaderboard_nfl_top_teams" value="{{ safe_config_get(main_config, 'leaderboard', 'enabled_sports', 'nfl', 'top_teams', default=10) }}" min="1" max="32">
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="leaderboard_nba_enabled" {% if safe_config_get(main_config, 'leaderboard', 'enabled_sports', 'nba', 'enabled', default=False) %}checked{% endif %}>
NBA
</label>
<div class="form-group" style="margin-left: 20px;">
<label for="leaderboard_nba_top_teams">Top Teams</label>
<input type="number" class="form-control" id="leaderboard_nba_top_teams" value="{{ safe_config_get(main_config, 'leaderboard', 'enabled_sports', 'nba', 'top_teams', default=10) }}" min="1" max="30">
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="leaderboard_mlb_enabled" {% if safe_config_get(main_config, 'leaderboard', 'enabled_sports', 'mlb', 'enabled', default=False) %}checked{% endif %}>
MLB
</label>
<div class="form-group" style="margin-left: 20px;">
<label for="leaderboard_mlb_top_teams">Top Teams</label>
<input type="number" class="form-control" id="leaderboard_mlb_top_teams" value="{{ safe_config_get(main_config, 'leaderboard', 'enabled_sports', 'mlb', 'top_teams', default=10) }}" min="1" max="30">
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="leaderboard_ncaa_fb_enabled" {% if safe_config_get(main_config, 'leaderboard', 'enabled_sports', 'ncaa_fb', 'enabled', default=False) %}checked{% endif %}>
NCAA Football
</label>
<div class="form-group" style="margin-left: 20px;">
<label for="leaderboard_ncaa_fb_top_teams">Top Teams</label>
<input type="number" class="form-control" id="leaderboard_ncaa_fb_top_teams" value="{{ safe_config_get(main_config, 'leaderboard', 'enabled_sports', 'ncaa_fb', 'top_teams', default=10) }}" min="1" max="25">
</div>
<div class="form-group" style="margin-left: 20px;">
<label>
<input type="checkbox" id="leaderboard_ncaa_fb_show_ranking" {% if safe_config_get(main_config, 'leaderboard', 'enabled_sports', 'ncaa_fb', 'show_ranking', default=True) %}checked{% endif %}>
Show Ranking
</label>
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="leaderboard_nhl_enabled" {% if safe_config_get(main_config, 'leaderboard', 'enabled_sports', 'nhl', 'enabled', default=False) %}checked{% endif %}>
NHL
</label>
<div class="form-group" style="margin-left: 20px;">
<label for="leaderboard_nhl_top_teams">Top Teams</label>
<input type="number" class="form-control" id="leaderboard_nhl_top_teams" value="{{ safe_config_get(main_config, 'leaderboard', 'enabled_sports', 'nhl', 'top_teams', default=10) }}" min="1" max="32">
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="leaderboard_ncaam_basketball_enabled" {% if safe_config_get(main_config, 'leaderboard', 'enabled_sports', 'ncaam_basketball', 'enabled', default=False) %}checked{% endif %}>
NCAA Men's Basketball
</label>
<div class="form-group" style="margin-left: 20px;">
<label for="leaderboard_ncaam_basketball_top_teams">Top Teams</label>
<input type="number" class="form-control" id="leaderboard_ncaam_basketball_top_teams" value="{{ safe_config_get(main_config, 'leaderboard', 'enabled_sports', 'ncaam_basketball', 'top_teams', default=10) }}" min="1" max="25">
</div>
</div>
<button type="submit" class="btn btn-success">Save Leaderboard Settings</button>
</form>
</div>
</div>
<!-- Text Display Tab -->
<div id="text" class="tab-content">
<div class="config-section">
<div style="display:flex; justify-content: space-between; align-items:center;">
<h3>Text Display</h3>
<div style="display:flex; gap:8px;">
<button type="button" class="btn btn-info" onclick="startOnDemand('text_display')"><i class="fas fa-bolt"></i> On-Demand</button>
</div>
</div>
<form id="text-form">
<div class="form-group"><label><input type="checkbox" id="text_enabled" {% if safe_config_get(main_config, 'text_display', 'enabled', default=False) %}checked{% endif %}> Enable</label></div>
<div class="form-group"><label for="text_text">Text</label><input type="text" id="text_text" class="form-control" value="{{ safe_config_get(main_config, 'text_display', 'text', default='Subscribe to ChuckBuilds') }}"></div>
<div class="form-group"><label for="text_font_path">Font Path</label><input type="text" id="text_font_path" class="form-control" value="{{ safe_config_get(main_config, 'text_display', 'font_path', default='assets/fonts/press-start-2p.ttf') }}"></div>
<div class="form-row">
<div class="form-group"><label for="text_font_size">Font Size</label><input type="number" id="text_font_size" class="form-control" value="{{ safe_config_get(main_config, 'text_display', 'font_size', default=8) }}"></div>
<div class="form-group"><label><input type="checkbox" id="text_scroll" {% if safe_config_get(main_config, 'text_display', 'scroll', default=True) %}checked{% endif %}> Scroll</label></div>
<div class="form-group"><label for="text_scroll_speed">Scroll Speed</label><input type="number" id="text_scroll_speed" class="form-control" value="{{ safe_config_get(main_config, 'text_display', 'scroll_speed', default=40) }}"></div>
<div class="form-group"><label for="text_scroll_gap_width">Scroll Gap Width</label><input type="number" id="text_scroll_gap_width" class="form-control" value="{{ safe_config_get(main_config, 'text_display', 'scroll_gap_width', default=32) }}"></div>
</div>
<div class="form-row">
<div class="form-group"><label for="text_text_color">Text Color</label><input type="color" id="text_text_color" class="form-control" data-rgb='{{ safe_config_get(main_config, 'text_display', 'text_color', default=[255, 0, 0]) | tojson }}'></div>
<div class="form-group"><label for="text_background_color">Background Color</label><input type="color" id="text_background_color" class="form-control" data-rgb='{{ safe_config_get(main_config, 'text_display', 'background_color', default=[0, 0, 0]) | tojson }}'></div>
</div>
<button type="submit" class="btn btn-success">Save Text Settings</button>
</form>
</div>
</div>
<!-- Features Tab -->
<div id="features" class="tab-content">
<div class="config-section">
<h3>Additional Features</h3>
<p>Configure additional features like clock, text display, and more.</p>
<!-- Features configuration will be populated by JavaScript -->
<div id="features-config">
Loading features configuration...
</div>
</div>
</div>
<!-- Of The Day Tab -->
<div id="of_the_day" class="tab-content">
<div class="config-section">
<div style="display:flex; justify-content: space-between; align-items:center;">
<h3>Of The Day Configuration</h3>
<div style="display:flex; gap:8px;">
<button type="button" class="btn btn-info" onclick="startOnDemand('of_the_day')"><i class="fas fa-bolt"></i> On-Demand</button>
</div>
</div>
<form id="of_the_day-form">
<div class="form-group">
<label>
<input type="checkbox" id="of_the_day_enabled" {% if safe_config_get(main_config, 'of_the_day', 'enabled', default=False) %}checked{% endif %}>
Enable Of The Day
</label>
</div>
<div class="form-row">
<div class="form-group">
<label for="of_the_day_update_interval">Update Interval (sec)</label>
<input type="number" class="form-control" id="of_the_day_update_interval" value="{{ safe_config_get(main_config, 'of_the_day', 'update_interval', default=3600) }}">
</div>
<div class="form-group">
<label for="of_the_day_display_rotate_interval">Display Rotate Interval (sec)</label>
<input type="number" class="form-control" id="of_the_day_display_rotate_interval" value="{{ safe_config_get(main_config, 'of_the_day', 'display_rotate_interval', default=20) }}">
</div>
<div class="form-group">
<label for="of_the_day_subtitle_rotate_interval">Subtitle Rotate Interval (sec)</label>
<input type="number" class="form-control" id="of_the_day_subtitle_rotate_interval" value="{{ safe_config_get(main_config, 'of_the_day', 'subtitle_rotate_interval', default=10) }}">
</div>
</div>
<div class="form-group">
<label for="of_the_day_category_order">Category Order</label>
<input type="text" class="form-control" id="of_the_day_category_order" value="{{ safe_config_get(main_config, 'of_the_day', 'category_order', default=['word_of_the_day', 'slovenian_word_of_the_day']) | join(', ') }}" placeholder="word_of_the_day, slovenian_word_of_the_day">
<div class="description">Comma-separated list of category keys in display order</div>
</div>
<h4>Categories</h4>
<div class="form-group">
<h5>Word of the Day</h5>
<div class="form-group" style="margin-left: 20px;">
<label>
<input type="checkbox" id="of_the_day_word_enabled" {% if safe_config_get(main_config, 'of_the_day', 'categories', 'word_of_the_day', 'enabled', default=True) %}checked{% endif %}>
Enable Word of the Day
</label>
</div>
<div class="form-group" style="margin-left: 20px;">
<label for="of_the_day_word_data_file">Data File</label>
<input type="text" class="form-control" id="of_the_day_word_data_file" value="{{ safe_config_get(main_config, 'of_the_day', 'categories', 'word_of_the_day', 'data_file', default='of_the_day/word_of_the_day.json') }}">
</div>
<div class="form-group" style="margin-left: 20px;">
<label for="of_the_day_word_display_name">Display Name</label>
<input type="text" class="form-control" id="of_the_day_word_display_name" value="{{ safe_config_get(main_config, 'of_the_day', 'categories', 'word_of_the_day', 'display_name', default='Word of the Day') }}">
</div>
</div>
<div class="form-group">
<h5>Slovenian Word of the Day</h5>
<div class="form-group" style="margin-left: 20px;">
<label>
<input type="checkbox" id="of_the_day_slovenian_enabled" {% if safe_config_get(main_config, 'of_the_day', 'categories', 'slovenian_word_of_the_day', 'enabled', default=True) %}checked{% endif %}>
Enable Slovenian Word of the Day
</label>
</div>
<div class="form-group" style="margin-left: 20px;">
<label for="of_the_day_slovenian_data_file">Data File</label>
<input type="text" class="form-control" id="of_the_day_slovenian_data_file" value="{{ safe_config_get(main_config, 'of_the_day', 'categories', 'slovenian_word_of_the_day', 'data_file', default='of_the_day/slovenian_word_of_the_day.json') }}">
</div>
<div class="form-group" style="margin-left: 20px;">
<label for="of_the_day_slovenian_display_name">Display Name</label>
<input type="text" class="form-control" id="of_the_day_slovenian_display_name" value="{{ safe_config_get(main_config, 'of_the_day', 'categories', 'slovenian_word_of_the_day', 'display_name', default='Slovenian Word of the Day') }}">
</div>
</div>
<button type="submit" class="btn btn-success">Save Of The Day Settings</button>
</form>
</div>
</div>
<!-- Music Tab -->
<div id="music" class="tab-content">
<div class="config-section">
<h3>Music Configuration</h3>
<form id="music-form">
<div class="form-group">
<label>
<input type="checkbox" id="music_enabled" name="music_enabled" {% if safe_config_get(main_config, 'music', 'enabled', default=False) %}checked{% endif %}>
Enable Music Display
</label>
</div>
<div class="form-group">
<label for="music_preferred_source">Preferred Source:</label>
<select class="form-control" id="music_preferred_source" name="music_preferred_source">
<option value="ytm" {% if safe_config_get(main_config, 'music', 'preferred_source', default='ytm') == "ytm" %}selected{% endif %}>YouTube Music</option>
<option value="spotify" {% if safe_config_get(main_config, 'music', 'preferred_source', default='ytm') == "spotify" %}selected{% endif %}>Spotify</option>
</select>
<div class="description">Primary music source to display</div>
</div>
<div class="form-group">
<label for="ytm_companion_url">YouTube Music Companion URL:</label>
<input type="text" class="form-control" id="ytm_companion_url" name="ytm_companion_url" value="{{ safe_config_get(main_config, 'music', 'YTM_COMPANION_URL', default='http://192.168.86.12:9863') }}">
<div class="description">URL for YouTube Music companion app</div>
</div>
<div class="form-group">
<label for="music_polling_interval">Polling Interval (seconds):</label>
<input type="number" class="form-control" id="music_polling_interval" name="music_polling_interval" value="{{ safe_config_get(main_config, 'music', 'POLLING_INTERVAL_SECONDS', default=1) }}" min="1" max="60">
<div class="description">How often to check for music updates</div>
</div>
<button type="submit" class="btn btn-success">Save Music Settings</button>
</form>
</div>
</div>
<!-- YouTube Tab -->
<div id="youtube" class="tab-content">
<div class="config-section">
<div style="display:flex; justify-content: space-between; align-items:center;">
<h3>YouTube</h3>
<div style="display:flex; gap:8px;">
<button type="button" class="btn btn-info" onclick="startOnDemand('youtube')"><i class="fas fa-bolt"></i> On-Demand</button>
</div>
</div>
<form id="youtube-form">
<div class="form-group"><label><input type="checkbox" id="youtube_enabled" {% if safe_config_get(main_config, 'youtube', 'enabled', default=False) %}checked{% endif %}> Enable YouTube</label></div>
<div class="form-group"><label for="youtube_update_interval">Update Interval (sec)</label><input type="number" id="youtube_update_interval" class="form-control" value="{{ safe_config_get(main_config, 'youtube', 'update_interval', default=3600) }}"></div>
<button type="submit" class="btn btn-success">Save YouTube Settings</button>
</form>
</div>
</div>
<!-- Calendar Tab -->
<div id="calendar" class="tab-content">
<div class="config-section">
<div style="display:flex; justify-content: space-between; align-items:center;">
<h3>Calendar Configuration</h3>
<div style="display:flex; gap:8px;">
<button type="button" class="btn btn-info" onclick="startOnDemand('calendar')"><i class="fas fa-bolt"></i> On-Demand</button>
</div>
</div>
<form id="calendar-form">
<div class="form-group">
<label>
<input type="checkbox" id="calendar_enabled" name="calendar_enabled" {% if safe_config_get(main_config, 'calendar', 'enabled', default=False) %}checked{% endif %}>
Enable Calendar
</label>
</div>
<div class="form-group">
<label for="calendar_max_events">Max Events to Show:</label>
<input type="number" class="form-control" id="calendar_max_events" name="calendar_max_events" value="{{ safe_config_get(main_config, 'calendar', 'max_events', default=3) }}" min="1" max="10">
<div class="description">Maximum number of events to display</div>
</div>
<div class="form-group">
<label for="calendar_update_interval">Update Interval (seconds):</label>
<input type="number" class="form-control" id="calendar_update_interval" name="calendar_update_interval" value="{{ safe_config_get(main_config, 'calendar', 'update_interval', default=3600) }}" min="300" max="3600">
<div class="description">How often to update calendar data</div>
</div>
<div class="form-group">
<label for="calendar_calendars">Calendars:</label>
<input type="text" class="form-control" id="calendar_calendars" name="calendar_calendars" value="{{ safe_config_get(main_config, 'calendar', 'calendars', default=['birthdays'])|join(', ') }}" placeholder="birthdays, work">
<div class="description">Comma-separated calendar names</div>
</div>
<button type="submit" class="btn btn-success">Save Calendar Settings</button>
</form>
</div>
</div>
<!-- News Tab -->
<div id="news" class="tab-content">
<div class="config-section">
<div style="display:flex; justify-content: space-between; align-items:center;">
<h3>News Manager Configuration</h3>
<div style="display:flex; gap:8px;">
<button type="button" class="btn btn-info" onclick="startOnDemand('news_manager')"><i class="fas fa-bolt"></i> On-Demand</button>
</div>
</div>
<p>Configure RSS news feeds and scrolling ticker settings</p>
<div class="form-group">
<label>
<input type="checkbox" id="news_enabled" name="news_enabled">
Enable News Manager
</label>
</div>
<div class="form-group">
<label for="headlines_per_feed">Headlines Per Feed:</label>
<input type="number" class="form-control" id="headlines_per_feed" name="headlines_per_feed" min="1" max="5" value="2">
<div class="description">Number of headlines to show from each enabled feed</div>
</div>
<div class="form-group">
<label>Available News Feeds:</label>
<div class="checkbox-grid" id="news_feeds_grid">
<!-- Feeds will be populated by JavaScript -->
</div>
</div>
<div class="form-group">
<h4>Custom RSS Feeds</h4>
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
<input type="text" class="form-control" id="custom_feed_name" placeholder="Feed Name" style="width: 200px;">
<input type="text" class="form-control" id="custom_feed_url" placeholder="RSS Feed URL" style="flex: 1;">
<button type="button" class="btn btn-primary" onclick="addCustomFeed()">Add Feed</button>
</div>
<div id="custom_feeds_list">
<!-- Custom feeds will be populated by JavaScript -->
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="rotation_enabled" name="rotation_enabled" checked>
Enable Headline Rotation
</label>
<div class="description">Rotate through different headlines to avoid repetition</div>
</div>
<div style="display: flex; gap: 10px;">
<button type="button" class="btn btn-success" onclick="saveNewsSettings()">Save News Settings</button>
<button type="button" class="btn btn-primary" onclick="refreshNewsStatus()">Refresh Status</button>
</div>
<div id="news_status" style="margin-top: 20px; padding: 15px; background: #f0f8f0; border-radius: var(--border-radius);">
<!-- Status will be populated by JavaScript -->
</div>
<h4 style="margin-top:20px;">Advanced Settings</h4>
<div class="form-row">
<div class="form-group"><label for="news_update_interval">Update Interval (sec)</label><input type="number" id="news_update_interval" class="form-control" value="{{ safe_config_get(main_config, 'news_manager', 'update_interval', default=300) }}"></div>
<div class="form-group"><label for="news_scroll_speed">Scroll Speed</label><input type="number" step="0.1" id="news_scroll_speed" class="form-control" value="{{ safe_config_get(main_config, 'news_manager', 'scroll_speed', default=1) }}"></div>
<div class="form-group"><label for="news_scroll_delay">Scroll Delay (sec)</label><input type="number" step="0.001" id="news_scroll_delay" class="form-control" value="{{ safe_config_get(main_config, 'news_manager', 'scroll_delay', default=0.01) }}"></div>
</div>
<div class="form-row">
<div class="form-group"><label for="news_rotation_threshold">Rotation Threshold</label><input type="number" id="news_rotation_threshold" class="form-control" value="{{ safe_config_get(main_config, 'news_manager', 'rotation_threshold', default=3) }}"></div>
<div class="form-group"><label><input type="checkbox" id="news_dynamic_duration" {% if safe_config_get(main_config, 'news_manager', 'dynamic_duration', default=True) %}checked{% endif %}> Dynamic Duration</label></div>
<div class="form-group"><label for="news_min_duration">Min Duration</label><input type="number" id="news_min_duration" class="form-control" value="{{ safe_config_get(main_config, 'news_manager', 'min_duration', default=30) }}"></div>
<div class="form-group"><label for="news_max_duration">Max Duration</label><input type="number" id="news_max_duration" class="form-control" value="{{ safe_config_get(main_config, 'news_manager', 'max_duration', default=300) }}"></div>
<div class="form-group"><label for="news_duration_buffer">Duration Buffer</label><input type="number" step="0.01" id="news_duration_buffer" class="form-control" value="{{ safe_config_get(main_config, 'news_manager', 'duration_buffer', default=0.1) }}"></div>
</div>
<div class="form-row">
<div class="form-group"><label for="news_font_size">Font Size</label><input type="number" id="news_font_size" class="form-control" value="{{ safe_config_get(main_config, 'news_manager', 'font_size', default=8) }}"></div>
<div class="form-group"><label for="news_font_path">Font Path</label><input type="text" id="news_font_path" class="form-control" value="{{ safe_config_get(main_config, 'news_manager', 'font_path', default='assets/fonts/PressStart2P-Regular.ttf') }}"></div>
</div>
<div class="form-row">
<div class="form-group"><label for="news_text_color">Text Color</label><input type="color" id="news_text_color" class="form-control" data-rgb='{{ safe_config_get(main_config, 'news_manager', 'text_color', default=[255, 255, 255]) | tojson }}'></div>
<div class="form-group"><label for="news_separator_color">Separator Color</label><input type="color" id="news_separator_color" class="form-control" data-rgb='{{ safe_config_get(main_config, 'news_manager', 'separator_color', default=[255, 0, 0]) | tojson }}'></div>
</div>
<button class="btn btn-success" type="button" onclick="saveNewsAdvancedSettings()">Save Advanced News Settings</button>
</div>
</div>
<!-- API Keys Tab -->
<div id="secrets" class="tab-content">
<div class="config-section">
<h3>API Keys Configuration</h3>
<p>Enter your API keys for various services. These are stored securely and not shared.</p>
<form id="secrets-form">
<h4>Weather API</h4>
<div class="form-group">
<label for="weather_api_key">OpenWeatherMap API Key:</label>
<input type="password" class="form-control" id="weather_api_key" name="weather_api_key" value="{{ (secrets_config.weather.api_key if secrets_config and secrets_config.weather and secrets_config.weather.api_key != 'YOUR_OPENWEATHERMAP_API_KEY' else '') if secrets_config else '' }}" placeholder="Enter your OpenWeatherMap API key">
<div class="description">Get your free API key from <a href="https://openweathermap.org/api" target="_blank">OpenWeatherMap</a></div>
</div>
<h4>YouTube API</h4>
<div class="form-group">
<label for="youtube_api_key">YouTube API Key:</label>
<input type="password" class="form-control" id="youtube_api_key" name="youtube_api_key" value="{{ secrets_config.youtube.api_key if secrets_config.youtube.api_key != 'YOUR_YOUTUBE_API_KEY' else '' }}" placeholder="Enter your YouTube API key">
<div class="description">Get your API key from <a href="https://console.developers.google.com/" target="_blank">Google Cloud Console</a></div>
</div>
<h4>Spotify API</h4>
<div class="form-group">
<label for="spotify_client_id">Spotify Client ID:</label>
<input type="password" class="form-control" id="spotify_client_id" name="spotify_client_id" value="{{ secrets_config.music.SPOTIFY_CLIENT_ID if secrets_config.music.SPOTIFY_CLIENT_ID != 'YOUR_SPOTIFY_CLIENT_ID_HERE' else '' }}" placeholder="Enter your Spotify Client ID">
<div class="description">Get from <a href="https://developer.spotify.com/dashboard" target="_blank">Spotify Developer Dashboard</a></div>
</div>
<div class="form-group">
<label for="spotify_client_secret">Spotify Client Secret:</label>
<input type="password" class="form-control" id="spotify_client_secret" name="spotify_client_secret" value="{{ secrets_config.music.SPOTIFY_CLIENT_SECRET if secrets_config.music.SPOTIFY_CLIENT_SECRET != 'YOUR_SPOTIFY_CLIENT_SECRET_HERE' else '' }}" placeholder="Enter your Spotify Client Secret">
<div class="description">Your Spotify Client Secret</div>
</div>
<button type="submit" class="btn btn-success">Save API Keys</button>
</form>
</div>
</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>
<!-- Actions Tab -->
<div id="actions" class="tab-content">
<div class="config-section">
<h3>System Actions</h3>
<p>Control the display service and system operations.</p>
<h4>Display Control</h4>
<div class="display-controls">
<button type="button" class="btn btn-success" onclick="runAction('start_display')">
<i class="fas fa-play"></i> Start Display
</button>
<button type="button" class="btn btn-danger" onclick="runAction('stop_display')">
<i class="fas fa-stop"></i> Stop Display
</button>
</div>
<h4>Auto-Start Settings</h4>
<div class="display-controls">
<button type="button" class="btn btn-success" onclick="runAction('enable_autostart')">
<i class="fas fa-check"></i> Enable Auto-Start
</button>
<button type="button" class="btn btn-warning" onclick="runAction('disable_autostart')">
<i class="fas fa-times"></i> Disable Auto-Start
</button>
</div>
<h4>System Operations</h4>
<div class="display-controls">
<button type="button" class="btn btn-warning" onclick="runAction('git_pull')">
<i class="fas fa-download"></i> Download Latest Update
</button>
<button type="button" class="btn btn-danger" onclick="runAction('reboot_system')" onclick="return confirm('Are you sure you want to reboot?')">
<i class="fas fa-power-off"></i> Reboot System
</button>
<button type="button" class="btn btn-danger" onclick="confirmShutdown()">
<i class="fas fa-power-off"></i> Shutdown System
</button>
</div>
<h4>Action Output</h4>
<div style="margin-top: 20px;">
<pre id="action_output" style="background: #f5f5f5; padding: 15px; border-radius: var(--border-radius); min-height: 100px; white-space: pre-wrap;">No action run yet.</pre>
</div>
</div>
</div>
<!-- Raw JSON Tab -->
<div id="raw-json" class="tab-content">
<div class="config-section">
<h3>Raw Configuration JSON</h3>
<p>View, edit, and save the complete configuration files directly. <strong>⚠️ Warning:</strong> Be careful when editing raw JSON - invalid syntax will prevent saving.</p>
<h4>Main Configuration (config.json)</h4>
<div class="form-group">
<label>Configuration Path:</label>
<span>{{ main_config_path }}</span>
</div>
<div class="json-container">
<textarea id="main-config-json">{{ main_config_json }}</textarea>
<div id="main-config-status" class="json-status valid">VALID</div>
<div id="main-config-validation" class="json-validation"></div>
</div>
<div class="json-actions">
<button type="button" class="btn btn-primary" onclick="formatJson('main-config-json')">Format JSON</button>
<button type="button" class="btn btn-warning" onclick="validateJson('main-config-json', 'main-config-validation')">Validate JSON</button>
<button type="button" class="btn btn-success" onclick="saveRawJson('main')">Save Main Config</button>
</div>
<h4>Secrets Configuration (config_secrets.json)</h4>
<div class="form-group">
<label>Configuration Path:</label>
<span>{{ secrets_config_path }}</span>
</div>
<div class="json-container">
<textarea id="secrets-config-json">{{ secrets_config_json }}</textarea>
<div id="secrets-config-status" class="json-status valid">VALID</div>
<div id="secrets-config-validation" class="json-validation"></div>
</div>
<div class="json-actions">
<button type="button" class="btn btn-primary" onclick="formatJson('secrets-config-json')">Format JSON</button>
<button type="button" class="btn btn-warning" onclick="validateJson('secrets-config-json', 'secrets-config-validation')">Validate JSON</button>
<button type="button" class="btn btn-success" onclick="saveRawJson('secrets')">Save Secrets Config</button>
</div>
</div>
</div>
<!-- Logs Tab -->
<div id="logs" class="tab-content">
<div class="config-section">
<h3>System Logs</h3>
<p>View logs for the LED matrix service. Useful for debugging.</p>
<button class="btn btn-primary" onclick="fetchLogs()">
<i class="fas fa-refresh"></i> Refresh Logs
</button>
<pre id="log-content" style="background-color: #1e1e1e; color: #d4d4d4; padding: 15px; border-radius: var(--border-radius); max-height: 600px; overflow-y: auto; white-space: pre-wrap; word-wrap: break-word; margin-top: 15px;"></pre>
</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>
<!-- Server-provided data for JS (avoids inline Jinja in JS) -->
<script id="serverData" type="application/json">{{ {'main_config': main_config_data, 'editor_mode': editor_mode} | tojson }}</script>
<script>
// Global variables
let socket;
const __serverDataEl = document.getElementById('serverData');
const __serverData = __serverDataEl ? JSON.parse(__serverDataEl.textContent) : { main_config: {}, editor_mode: false };
let currentConfig = __serverData.main_config || {};
let editorMode = !!__serverData.editor_mode;
let currentElements = [];
let selectedElement = null;
// Function to refresh the current config from the server
async function refreshCurrentConfig() {
try {
const response = await fetch('/api/config/main');
if (response.ok) {
const configData = await response.json();
currentConfig = configData;
}
} catch (error) {
console.warn('Failed to refresh current config:', error);
}
}
async function refreshOnDemandStatus(){
try{
const res = await fetch('/api/ondemand/status');
const data = await res.json();
if(data && data.on_demand){
const s = data.on_demand;
const el = document.getElementById('ondemand-status');
if(el){ el.textContent = `On-Demand: ${s.running && s.mode ? s.mode : 'None'}`; }
}
}catch(e){ /* ignore */ }
}
async function startOnDemand(mode){
try{
const res = await fetch('/api/ondemand/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode })
});
const data = await res.json();
showNotification(data.message || 'Requested on-demand start', data.status || 'success');
refreshOnDemandStatus();
}catch(err){
showNotification('Error starting on-demand: ' + err, 'error');
}
}
async function stopOnDemand(){
try{
const res = await fetch('/api/ondemand/stop', { method: 'POST' });
const data = await res.json();
showNotification(data.message || 'On-Demand stopped', data.status || 'success');
refreshOnDemandStatus();
}catch(err){
showNotification('Error stopping on-demand: ' + err, 'error');
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
initializeSocket();
initializeEditor();
updateSystemStats();
loadNewsManagerData();
updateApiMetrics();
refreshOnDemandStatus();
// UI controls for grid & scale
const scaleRange = document.getElementById('scaleRange');
const scaleValue = document.getElementById('scaleValue');
const toggleGrid = document.getElementById('toggleGrid');
const gridCanvas = document.getElementById('gridOverlay');
if (scaleRange && scaleValue) {
scaleRange.addEventListener('input', () => {
scaleValue.textContent = `${scaleRange.value}x`;
// Repaint grid at new scale using latest known dimensions
fetch('/api/display/current')
.then(r => r.json())
.then(data => {
if (!data || !data.width || !data.height) return;
// Resize image and canvas
const img = document.getElementById('displayImage');
const scale = parseInt(scaleRange.value || '8');
img.style.width = `${data.width * scale}px`;
img.style.height = `${data.height * scale}px`;
gridCanvas.width = data.width * scale;
gridCanvas.height = data.height * scale;
drawGrid(gridCanvas, data.width, data.height, scale);
renderLedDots();
})
.catch(() => {});
});
}
if (toggleGrid && gridCanvas) {
toggleGrid.addEventListener('change', () => {
gridCanvas.style.display = toggleGrid.checked ? 'block' : 'none';
if (toggleGrid.checked) {
// Redraw grid with current size
fetch('/api/display/current')
.then(r => r.json())
.then(data => {
if (!data || !data.width || !data.height) return;
const scale = parseInt((document.getElementById('scaleRange')?.value) || '8');
drawGrid(gridCanvas, data.width, data.height, scale);
})
.catch(() => {});
}
});
// default hidden
gridCanvas.style.display = 'none';
}
// LED dot mode controls
const toggleLedDots = document.getElementById('toggleLedDots');
const dotFillRange = document.getElementById('dotFillRange');
const dotFillValue = document.getElementById('dotFillValue');
if (dotFillRange && dotFillValue) {
dotFillRange.addEventListener('input', () => {
dotFillValue.textContent = `${dotFillRange.value}%`;
renderLedDots();
});
}
if (toggleLedDots) {
toggleLedDots.addEventListener('change', renderLedDots);
// Ensure dot mode is rendered on load if enabled by default
if (toggleLedDots.checked) {
setTimeout(renderLedDots, 200);
}
}
// Update stats every 30 seconds
setInterval(updateSystemStats, 30000);
setInterval(updateApiMetrics, 60000);
});
// Socket.IO connection
function initializeSocket() {
socket = io({
path: '/socket.io',
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1000,
reconnectionDelayMax: 10000
});
socket.on('connect', function() {
updateConnectionStatus(true);
showNotification('Connected to LED Matrix', 'success');
stopPreviewPolling();
});
socket.on('disconnect', function() {
updateConnectionStatus(false);
showNotification('Disconnected from LED Matrix', 'error');
// Try to reconnect with exponential backoff
let attempt = 0;
const retry = () => {
attempt++;
const delay = Math.min(30000, 1000 * Math.pow(2, attempt));
setTimeout(() => {
if (socket.connected) return;
socket.connect();
}, delay);
};
retry();
startPreviewPolling();
});
socket.on('connect_error', function(_err){
updateConnectionStatus(false);
startPreviewPolling();
});
socket.on('display_update', function(data) {
updateDisplayPreview(data);
});
}
async function updateApiMetrics(){
try {
const res = await fetch('/api/metrics');
const data = await res.json();
if (data.status !== 'success') return;
const el = document.getElementById('api-metrics');
const w = Math.round((data.window_seconds || 86400) / 3600);
const f = data.forecast || {};
const u = data.used || {};
el.innerHTML = `
<div><strong>Window:</strong> ${w} hours</div>
<div><strong>Weather:</strong> ${u.weather || 0} used / ${f.weather || 0} forecast</div>
<div><strong>Stocks:</strong> ${u.stocks || 0} used / ${f.stocks || 0} forecast</div>
<div><strong>Sports:</strong> ${u.sports || 0} used / ${f.sports || 0} forecast</div>
<div><strong>News:</strong> ${u.news || 0} used / ${f.news || 0} forecast</div>
<div><strong>Odds:</strong> ${u.odds || 0} used / ${f.odds || 0} forecast</div>
<div><strong>Music:</strong> ${u.music || 0} used / ${f.music || 0} forecast</div>
<div><strong>YouTube:</strong> ${u.youtube || 0} used / ${f.youtube || 0} forecast</div>
`;
} catch (e) {
// ignore
}
}
// Fallback polling when websocket is disconnected
let __previewPollTimer = null;
function startPreviewPolling(){
if (__previewPollTimer) return;
__previewPollTimer = setInterval(fetchCurrentDisplayOnce, 1000);
}
function stopPreviewPolling(){
if (__previewPollTimer) clearInterval(__previewPollTimer);
__previewPollTimer = null;
}
async function fetchCurrentDisplayOnce(){
try {
const res = await fetch('/api/display/current');
const data = await res.json();
if (data && data.image) updateDisplayPreview(data);
} catch (_) {}
}
// Draw pixel grid lines on top of scaled image
function drawGrid(canvas, logicalWidth, logicalHeight, scale) {
const show = document.getElementById('toggleGrid')?.checked;
if (!canvas || !show) return;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = 'rgba(255,255,255,0.12)';
ctx.lineWidth = 1;
// Vertical lines
for (let x = 0; x <= logicalWidth; x++) {
const px = Math.floor(x * scale) + 0.5;
ctx.beginPath();
ctx.moveTo(px, 0);
ctx.lineTo(px, logicalHeight * scale);
ctx.stroke();
}
// Horizontal lines
for (let y = 0; y <= logicalHeight; y++) {
const py = Math.floor(y * scale) + 0.5;
ctx.beginPath();
ctx.moveTo(0, py);
ctx.lineTo(logicalWidth * scale, py);
ctx.stroke();
}
}
// 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 with better scaling and error handling
function updateDisplayPreview(data) {
const preview = document.getElementById('displayPreview');
const stage = document.getElementById('previewStage');
const img = document.getElementById('displayImage');
const canvas = document.getElementById('gridOverlay');
const ledCanvas = document.getElementById('ledCanvas');
const placeholder = document.getElementById('displayPlaceholder');
if (data.image) {
// Show stage
placeholder.style.display = 'none';
stage.style.display = 'inline-block';
// Current scale from slider
const scale = parseInt(document.getElementById('scaleRange').value || '8');
// Update image and meta label
img.style.imageRendering = 'pixelated';
img.onload = () => {
// Ensure LED dot overlay samples after image is ready
renderLedDots();
};
img.src = `data:image/png;base64,${data.image}`;
const meta = document.getElementById('previewMeta');
if (meta) {
meta.textContent = `${data.width || 128} x ${data.height || 32} @ ${scale}x`;
}
// Once image loads, size the canvas to match
const width = (data.width || 128) * scale;
const height = (data.height || 32) * scale;
img.style.width = width + 'px';
img.style.height = height + 'px';
ledCanvas.width = width;
ledCanvas.height = height;
canvas.width = width;
canvas.height = height;
drawGrid(canvas, data.width || 128, data.height || 32, scale);
renderLedDots();
} else {
stage.style.display = 'none';
placeholder.style.display = 'block';
placeholder.innerHTML = `<div style="color: #666; font-size: 1.2rem;">
<i class="fas fa-exclamation-triangle"></i>
No display data available
</div>`;
}
}
function renderLedDots(){
const ledCanvas = document.getElementById('ledCanvas');
const img = document.getElementById('displayImage');
const toggle = document.getElementById('toggleLedDots');
if (!ledCanvas || !img || !toggle) return;
const show = toggle.checked;
ledCanvas.style.display = show ? 'block' : 'none';
if (!show) return;
const scale = parseInt(document.getElementById('scaleRange').value || '8');
const fillPct = parseInt(document.getElementById('dotFillRange').value || '75');
const dotRadius = Math.max(1, Math.floor((scale * fillPct) / 200)); // radius in px
const ctx = ledCanvas.getContext('2d');
ctx.clearRect(0, 0, ledCanvas.width, ledCanvas.height);
// Clear previous overlay; do not forcibly black-out if sampling fails
ctx.clearRect(0, 0, ledCanvas.width, ledCanvas.height);
// Create an offscreen canvas to sample pixel colors
const off = document.createElement('canvas');
const logicalWidth = Math.floor(ledCanvas.width / scale);
const logicalHeight = Math.floor(ledCanvas.height / scale);
off.width = logicalWidth;
off.height = logicalHeight;
const offCtx = off.getContext('2d');
// Draw the current image scaled down to logical LEDs to sample colors
try {
offCtx.drawImage(img, 0, 0, logicalWidth, logicalHeight);
} catch (_) { /* draw failures ignored */ }
// Draw circular dots for each LED pixel
let drawn = 0;
for (let y = 0; y < logicalHeight; y++) {
for (let x = 0; x < logicalWidth; x++) {
const pixel = offCtx.getImageData(x, y, 1, 1).data;
const r = pixel[0], g = pixel[1], b = pixel[2], a = pixel[3];
// Skip fully black to reduce overdraw
if (a === 0 || (r|g|b) === 0) continue;
ctx.fillStyle = `rgb(${r},${g},${b})`;
const cx = Math.floor(x * scale + scale / 2);
const cy = Math.floor(y * scale + scale / 2);
ctx.beginPath();
ctx.arc(cx, cy, dotRadius, 0, Math.PI * 2);
ctx.fill();
drawn++;
}
}
// If nothing was drawn (e.g., image not ready), hide overlay to show base image
if (drawn === 0) {
ledCanvas.style.display = 'none';
}
}
// 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
if (event && event.target) {
event.target.classList.add('active');
} else {
// Fallback: match tabName to button by data
const btns = document.querySelectorAll('.tab-btn');
btns.forEach(btn => {
if (btn.getAttribute('onclick') && btn.getAttribute('onclick').includes(`'${tabName}'`)) {
btn.classList.add('active');
}
});
}
// Load specific data when tabs are opened
if (tabName === 'news') {
loadNewsManagerData();
} else if (tabName === 'sports') {
refreshSportsConfig();
} else if (tabName === 'logs') {
fetchLogs();
} else if (tabName === 'raw-json') {
setTimeout(() => {
validateJson('main-config-json', 'main-config-validation');
validateJson('secrets-config-json', 'secrets-config-validation');
}, 100);
}
refreshOnDemandStatus();
}
// Display control functions
async function startDisplay() {
// Use system service like Web UI v1 for reliability
await runAction('start_display');
}
async function stopDisplay() {
await runAction('stop_display');
}
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');
}
}
// System action functions
async function systemAction(action) {
if (action === 'reboot_system' && !confirm('Are you sure you want to reboot the system?')) {
return;
}
if (action === 'migrate_config' && !confirm('This will migrate your configuration to add any new options with default values. A backup will be created automatically. Continue?')) {
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 scaleInput = document.getElementById('scaleRange');
const scale = scaleInput ? parseInt(scaleInput.value || '8') : 8;
const x = Math.floor((e.clientX - rect.left) / scale);
const y = Math.floor((e.clientY - rect.top) / scale);
addElement(elementType, x, y);
});
}
function addElement(type, x, y) {
const element = {
id: Date.now(),
type: type,
x: x,
y: y,
properties: getDefaultProperties(type, x, y)
};
currentElements.push(element);
updatePreview();
selectElement(element);
}
function getDefaultProperties(type, baseX, baseY) {
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: (baseX || 0) + 20,
y2: baseY || 0,
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');
}
}
async function loadLayout(){
try {
const res = await fetch('/api/editor/layouts');
const data = await res.json();
if (data.status === 'success' && data.data && data.data.elements) {
currentElements = data.data.elements;
selectedElement = null;
updatePreview();
updatePropertiesPanel();
showNotification('Layout loaded', 'success');
} else {
showNotification('No saved layout found', 'warning');
}
} catch (err) {
showNotification('Error loading layout: ' + err, '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();
// Update stats in the overview tab if they exist
const cpuUsage = document.querySelector('.stat-card .stat-value');
if (cpuUsage) {
document.querySelectorAll('.stat-card .stat-value')[0].textContent = stats.cpu_percent + '%';
document.querySelectorAll('.stat-card .stat-value')[1].textContent = stats.memory_used_percent + '%';
document.querySelectorAll('.stat-card .stat-value')[2].textContent = stats.cpu_temp + '°C';
document.querySelectorAll('.stat-card .stat-value')[5].textContent = stats.disk_used_percent + '%';
}
refreshOnDemandStatus();
} catch (error) {
console.error('Error updating system stats:', error);
}
}
function updateBrightnessDisplay(value) {
document.getElementById('brightness-value').textContent = value;
}
// Form submission handlers
document.getElementById('schedule-form').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
try {
const response = await fetch('/save_schedule', {
method: 'POST',
body: formData
});
const result = await response.json();
showNotification(result.message, result.status);
} catch (error) {
showNotification('Error saving schedule: ' + error.message, 'error');
}
});
document.getElementById('display-form').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
try {
const response = await fetch('/save_config', {
method: 'POST',
body: formData
});
const result = await response.json();
showNotification(result.message, result.status);
} catch (error) {
showNotification('Error saving display settings: ' + error.message, 'error');
}
});
// General form submit
document.getElementById('general-form').addEventListener('submit', async function(e) {
e.preventDefault();
const payload = {
web_display_autostart: document.getElementById('web_display_autostart').checked,
timezone: document.getElementById('timezone').value,
location: {
city: document.getElementById('city').value,
state: document.getElementById('state').value,
country: document.getElementById('country').value
}
};
await saveConfigJson(payload);
});
// Clock form submit
document.getElementById('clock-form').addEventListener('submit', async function(e) {
e.preventDefault();
const payload = {
clock: {
enabled: document.getElementById('clock_enabled').checked,
format: document.getElementById('clock_format').value,
update_interval: parseInt(document.getElementById('clock_update_interval').value)
}
};
await saveConfigJson(payload);
});
// Durations form submit
document.getElementById('durations-form').addEventListener('submit', async function(e) {
e.preventDefault();
const inputs = document.querySelectorAll('.duration-input');
const durations = {};
inputs.forEach(inp => {
durations[inp.dataset.name] = parseInt(inp.value);
});
const payload = { display: { display_durations: durations } };
await saveConfigJson(payload);
});
// Weather form submit augmentation
(function augmentWeatherForm(){
const form = document.getElementById('weather-form');
form.addEventListener('submit', async function(e){
e.preventDefault();
const payload = {
weather: {
enabled: document.getElementById('weather_enabled').checked,
update_interval: parseInt(document.getElementById('weather_update_interval').value),
units: document.getElementById('weather_units').value,
display_format: document.getElementById('weather_display_format').value
},
location: {
city: document.getElementById('weather_city').value,
state: document.getElementById('weather_state').value
}
};
await saveConfigJson(payload);
});
})();
// Stocks form submit augmentation
(function augmentStocksForm(){
const form = document.getElementById('stocks-form');
form.addEventListener('submit', async function(e){
e.preventDefault();
const symbols = document.getElementById('stocks_symbols').value.split(',').map(s => s.trim()).filter(Boolean);
const payload = {
stocks: {
enabled: document.getElementById('stocks_enabled').checked,
update_interval: parseInt(document.getElementById('stocks_update_interval').value),
scroll_speed: parseFloat(document.getElementById('stocks_scroll_speed').value),
scroll_delay: parseFloat(document.getElementById('stocks_scroll_delay').value),
toggle_chart: document.getElementById('stocks_toggle_chart').checked,
dynamic_duration: document.getElementById('stocks_dynamic_duration').checked,
min_duration: parseInt(document.getElementById('stocks_min_duration').value),
max_duration: parseInt(document.getElementById('stocks_max_duration').value),
duration_buffer: parseFloat(document.getElementById('stocks_duration_buffer').value),
symbols: symbols,
display_format: document.getElementById('stocks_display_format').value
}
};
await saveConfigJson(payload);
});
})();
// Crypto form submit
(function augmentCryptoForm(){
const form = document.getElementById('crypto-form');
form.addEventListener('submit', async function(e){
e.preventDefault();
const symbols = document.getElementById('crypto_symbols').value.split(',').map(s => s.trim()).filter(Boolean);
const payload = {
crypto: {
enabled: document.getElementById('crypto_enabled').checked,
update_interval: parseInt(document.getElementById('crypto_update_interval').value),
symbols: symbols
}
};
await saveConfigJson(payload);
});
})();
// Stock news form submit
(function augmentStockNewsForm(){
const form = document.getElementById('stocknews-form');
form.addEventListener('submit', async function(e){
e.preventDefault();
const payload = {
stock_news: {
enabled: document.getElementById('stocknews_enabled').checked,
update_interval: parseInt(document.getElementById('stocknews_update_interval').value),
scroll_speed: parseFloat(document.getElementById('stocknews_scroll_speed').value),
scroll_delay: parseFloat(document.getElementById('stocknews_scroll_delay').value),
max_headlines_per_symbol: parseInt(document.getElementById('stocknews_max_headlines_per_symbol').value),
headlines_per_rotation: parseInt(document.getElementById('stocknews_headlines_per_rotation').value),
dynamic_duration: document.getElementById('stocknews_dynamic_duration').checked,
min_duration: parseInt(document.getElementById('stocknews_min_duration').value),
max_duration: parseInt(document.getElementById('stocknews_max_duration').value),
duration_buffer: parseFloat(document.getElementById('stocknews_duration_buffer').value)
}
};
await saveConfigJson(payload);
});
})();
// Odds form submit
(function augmentOddsForm(){
const form = document.getElementById('odds-form');
form.addEventListener('submit', async function(e){
e.preventDefault();
const leagues = document.getElementById('odds_enabled_leagues').value.split(',').map(s => s.trim()).filter(Boolean);
const payload = {
odds_ticker: {
enabled: document.getElementById('odds_enabled').checked,
update_interval: parseInt(document.getElementById('odds_update_interval').value),
scroll_speed: parseFloat(document.getElementById('odds_scroll_speed').value),
scroll_delay: parseFloat(document.getElementById('odds_scroll_delay').value),
games_per_favorite_team: parseInt(document.getElementById('odds_games_per_favorite_team').value),
max_games_per_league: parseInt(document.getElementById('odds_max_games_per_league').value),
future_fetch_days: parseInt(document.getElementById('odds_future_fetch_days').value),
enabled_leagues: leagues,
sort_order: document.getElementById('odds_sort_order').value,
show_favorite_teams_only: document.getElementById('odds_show_favorite_teams_only').checked,
show_odds_only: document.getElementById('odds_show_odds_only').checked,
loop: document.getElementById('odds_loop').checked,
show_channel_logos: document.getElementById('odds_show_channel_logos').checked,
dynamic_duration: document.getElementById('odds_dynamic_duration').checked,
min_duration: parseInt(document.getElementById('odds_min_duration').value),
max_duration: parseInt(document.getElementById('odds_max_duration').value),
duration_buffer: parseFloat(document.getElementById('odds_duration_buffer').value)
}
};
await saveConfigJson(payload);
});
})();
// Leaderboard form submit
(function augmentLeaderboardForm(){
const form = document.getElementById('leaderboard-form');
form.addEventListener('submit', async function(e){
e.preventDefault();
const payload = {
leaderboard: {
enabled: document.getElementById('leaderboard_enabled').checked,
update_interval: parseInt(document.getElementById('leaderboard_update_interval').value),
scroll_speed: parseFloat(document.getElementById('leaderboard_scroll_speed').value),
scroll_delay: parseFloat(document.getElementById('leaderboard_scroll_delay').value),
display_duration: parseInt(document.getElementById('leaderboard_display_duration').value),
loop: document.getElementById('leaderboard_loop').checked,
request_timeout: parseInt(document.getElementById('leaderboard_request_timeout').value),
dynamic_duration: document.getElementById('leaderboard_dynamic_duration').checked,
min_duration: parseInt(document.getElementById('leaderboard_min_duration').value),
max_duration: parseInt(document.getElementById('leaderboard_max_duration').value),
duration_buffer: parseFloat(document.getElementById('leaderboard_duration_buffer').value),
enabled_sports: {
nfl: {
enabled: document.getElementById('leaderboard_nfl_enabled').checked,
top_teams: parseInt(document.getElementById('leaderboard_nfl_top_teams').value)
},
nba: {
enabled: document.getElementById('leaderboard_nba_enabled').checked,
top_teams: parseInt(document.getElementById('leaderboard_nba_top_teams').value)
},
mlb: {
enabled: document.getElementById('leaderboard_mlb_enabled').checked,
top_teams: parseInt(document.getElementById('leaderboard_mlb_top_teams').value)
},
ncaa_fb: {
enabled: document.getElementById('leaderboard_ncaa_fb_enabled').checked,
top_teams: parseInt(document.getElementById('leaderboard_ncaa_fb_top_teams').value),
show_ranking: document.getElementById('leaderboard_ncaa_fb_show_ranking').checked
},
nhl: {
enabled: document.getElementById('leaderboard_nhl_enabled').checked,
top_teams: parseInt(document.getElementById('leaderboard_nhl_top_teams').value)
},
ncaam_basketball: {
enabled: document.getElementById('leaderboard_ncaam_basketball_enabled').checked,
top_teams: parseInt(document.getElementById('leaderboard_ncaam_basketball_top_teams').value)
}
}
}
};
await saveConfigJson(payload);
});
})();
// Of The Day form submit
(function augmentOfTheDayForm(){
const form = document.getElementById('of_the_day-form');
form.addEventListener('submit', async function(e){
e.preventDefault();
const categoryOrder = document.getElementById('of_the_day_category_order').value.split(',').map(s => s.trim()).filter(Boolean);
const payload = {
of_the_day: {
enabled: document.getElementById('of_the_day_enabled').checked,
update_interval: parseInt(document.getElementById('of_the_day_update_interval').value),
display_rotate_interval: parseInt(document.getElementById('of_the_day_display_rotate_interval').value),
subtitle_rotate_interval: parseInt(document.getElementById('of_the_day_subtitle_rotate_interval').value),
category_order: categoryOrder,
categories: {
word_of_the_day: {
enabled: document.getElementById('of_the_day_word_enabled').checked,
data_file: document.getElementById('of_the_day_word_data_file').value,
display_name: document.getElementById('of_the_day_word_display_name').value
},
slovenian_word_of_the_day: {
enabled: document.getElementById('of_the_day_slovenian_enabled').checked,
data_file: document.getElementById('of_the_day_slovenian_data_file').value,
display_name: document.getElementById('of_the_day_slovenian_display_name').value
}
}
}
};
await saveConfigJson(payload);
});
})();
// Text form submit
(function augmentTextForm(){
const form = document.getElementById('text-form');
const initColor = (input) => {
try { const rgb = JSON.parse(input.dataset.rgb || '[255,255,255]'); input.value = rgbToHex(rgb); } catch {}
};
initColor(document.getElementById('text_text_color'));
initColor(document.getElementById('text_background_color'));
form.addEventListener('submit', async function(e){
e.preventDefault();
const payload = {
text_display: {
enabled: document.getElementById('text_enabled').checked,
text: document.getElementById('text_text').value,
font_path: document.getElementById('text_font_path').value,
font_size: parseInt(document.getElementById('text_font_size').value),
scroll: document.getElementById('text_scroll').checked,
scroll_speed: parseInt(document.getElementById('text_scroll_speed').value),
scroll_gap_width: parseInt(document.getElementById('text_scroll_gap_width').value),
text_color: hexToRgbArray(document.getElementById('text_text_color').value),
background_color: hexToRgbArray(document.getElementById('text_background_color').value)
}
};
await saveConfigJson(payload);
});
})();
// YouTube form submit
(function augmentYouTubeForm(){
const form = document.getElementById('youtube-form');
form.addEventListener('submit', async function(e){
e.preventDefault();
const payload = {
youtube: {
enabled: document.getElementById('youtube_enabled').checked,
update_interval: parseInt(document.getElementById('youtube_update_interval').value)
}
};
await saveConfigJson(payload);
});
})();
// Music form submit
(function augmentMusicForm(){
const form = document.getElementById('music-form');
form.addEventListener('submit', async function(e){
e.preventDefault();
const payload = {
music: {
enabled: document.getElementById('music_enabled').checked,
preferred_source: document.getElementById('music_preferred_source').value,
YTM_COMPANION_URL: document.getElementById('ytm_companion_url').value,
POLLING_INTERVAL_SECONDS: parseInt(document.getElementById('music_polling_interval').value)
}
};
await saveConfigJson(payload);
});
})();
// Calendar form submit
(function augmentCalendarForm(){
const form = document.getElementById('calendar-form');
form.addEventListener('submit', async function(e){
e.preventDefault();
const calendars = document.getElementById('calendar_calendars').value.split(',').map(s => s.trim()).filter(Boolean);
const payload = {
calendar: {
enabled: document.getElementById('calendar_enabled').checked,
max_events: parseInt(document.getElementById('calendar_max_events').value),
update_interval: parseInt(document.getElementById('calendar_update_interval').value),
calendars: calendars
}
};
await saveConfigJson(payload);
});
})();
// News advanced save
async function saveNewsAdvancedSettings(){
const payload = {
news_manager: {
update_interval: parseInt(document.getElementById('news_update_interval').value),
scroll_speed: parseFloat(document.getElementById('news_scroll_speed').value),
scroll_delay: parseFloat(document.getElementById('news_scroll_delay').value),
rotation_threshold: parseInt(document.getElementById('news_rotation_threshold').value),
dynamic_duration: document.getElementById('news_dynamic_duration').checked,
min_duration: parseInt(document.getElementById('news_min_duration').value),
max_duration: parseInt(document.getElementById('news_max_duration').value),
duration_buffer: parseFloat(document.getElementById('news_duration_buffer').value),
font_size: parseInt(document.getElementById('news_font_size').value),
font_path: document.getElementById('news_font_path').value,
text_color: hexToRgbArray(document.getElementById('news_text_color').value),
separator_color: hexToRgbArray(document.getElementById('news_separator_color').value)
}
};
await saveConfigJson(payload);
}
// Helpers for RGB <-> hex
function hexToRgbArray(hex){
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return m ? [parseInt(m[1],16), parseInt(m[2],16), parseInt(m[3],16)] : [255,255,255];
}
function rgbToHex(arr){
const [r,g,b] = arr || [255,255,255];
const toHex = v => ('0' + Math.max(0, Math.min(255, v)).toString(16)).slice(-2);
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
// Generic JSON save using existing endpoint
async function saveConfigJson(fragment){
try {
const response = await fetch('/save_config', {
method: 'POST',
body: makeFormData({
config_type: 'main',
config_data: JSON.stringify(fragment)
})
});
const result = await response.json();
showNotification(result.message || 'Saved', result.status || 'success');
} catch (error) {
showNotification('Error saving configuration: ' + error, 'error');
}
}
function makeFormData(obj){
const fd = new FormData();
Object.entries(obj).forEach(([k,v]) => fd.append(k, v));
return fd;
}
// JSON validation and formatting functions
function formatJson(elementId) {
const textarea = document.getElementById(elementId);
const jsonText = textarea.value;
try {
const parsed = JSON.parse(jsonText);
const formatted = JSON.stringify(parsed, null, 4);
textarea.value = formatted;
textarea.classList.remove('error');
textarea.classList.add('valid');
showNotification('JSON formatted successfully!', 'success');
} catch (error) {
showNotification(`Cannot format invalid JSON: ${error.message}`, 'error');
textarea.classList.remove('valid');
textarea.classList.add('error');
}
}
function validateJson(textareaId, validationId) {
const textarea = document.getElementById(textareaId);
const validationDiv = document.getElementById(validationId);
const jsonText = textarea.value;
validationDiv.innerHTML = '';
validationDiv.className = 'json-validation';
validationDiv.style.display = 'block';
const statusId = validationId.replace('-validation', '-status');
const statusElement = document.getElementById(statusId);
try {
const parsed = JSON.parse(jsonText);
validationDiv.className = 'json-validation success';
if (statusElement) {
statusElement.textContent = 'VALID';
statusElement.className = 'json-status valid';
}
validationDiv.innerHTML = `
<div><strong>✅ JSON is valid!</strong></div>
<div>✓ Valid JSON syntax<br>✓ Proper structure<br>✓ No obvious issues detected</div>
`;
} catch (error) {
validationDiv.className = 'json-validation error';
if (statusElement) {
statusElement.textContent = 'INVALID';
statusElement.className = 'json-status error';
}
validationDiv.innerHTML = `
<div><strong>❌ Invalid JSON syntax</strong></div>
<div><strong>Error:</strong> ${error.message}</div>
`;
}
}
async function saveRawJson(configType) {
const textareaId = configType === 'main' ? 'main-config-json' : 'secrets-config-json';
const textarea = document.getElementById(textareaId);
const jsonText = textarea.value;
try {
JSON.parse(jsonText);
} catch (error) {
showNotification(`Invalid JSON format: ${error.message}`, 'error');
return;
}
const configName = configType === 'main' ? 'Main Configuration' : 'Secrets Configuration';
if (!confirm(`Are you sure you want to save changes to the ${configName}?`)) {
return;
}
try {
const response = await fetch('/save_raw_json', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
config_type: configType,
config_data: jsonText
})
});
const data = await response.json();
showNotification(data.message, data.status);
} catch (error) {
showNotification(`Error saving configuration: ${error}`, 'error');
}
}
// Action functions
async function runAction(actionName) {
const outputElement = document.getElementById('action_output');
outputElement.textContent = `Running ${actionName.replace(/_/g, ' ')}...`;
try {
const response = await fetch('/run_action', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: actionName })
});
const data = await response.json();
let outputText = `Status: ${data.status}\nMessage: ${data.message}\n`;
if (data.stdout) outputText += `\n--- STDOUT ---\n${data.stdout}`;
if (data.stderr) outputText += `\n--- STDERR ---\n${data.stderr}`;
outputElement.textContent = outputText;
showNotification(data.message, data.status);
} catch (error) {
outputElement.textContent = `Error: ${error}`;
showNotification(`Error running action: ${error}`, 'error');
}
}
function confirmShutdown(){
if (!confirm('Are you sure you want to shut down the system? This will power off the Raspberry Pi.')) return;
runAction('shutdown_system');
}
async function fetchLogs() {
const logContent = document.getElementById('log-content');
logContent.textContent = 'Loading logs...';
try {
const response = await fetch('/get_logs');
const data = await response.json();
if (data.status === 'success') {
logContent.textContent = data.logs;
} else {
logContent.textContent = `Error loading logs: ${data.message}`;
}
} catch (error) {
logContent.textContent = `Error loading logs: ${error}`;
}
}
// News Manager Functions
let newsManagerData = {};
async function loadNewsManagerData() {
try {
const response = await fetch('/news_manager/status');
const data = await response.json();
if (data.status === 'success') {
newsManagerData = data.data;
updateNewsManagerUI();
} else {
console.error('Error loading news manager data:', data.message);
}
} catch (error) {
console.error('Error loading news manager data:', error);
}
}
function updateNewsManagerUI() {
document.getElementById('news_enabled').checked = newsManagerData.enabled || false;
document.getElementById('headlines_per_feed').value = newsManagerData.headlines_per_feed || 2;
document.getElementById('rotation_enabled').checked = newsManagerData.rotation_enabled !== false;
// Populate available feeds
const feedsGrid = document.getElementById('news_feeds_grid');
feedsGrid.innerHTML = '';
if (newsManagerData.available_feeds) {
newsManagerData.available_feeds.forEach(feed => {
const isEnabled = newsManagerData.enabled_feeds.includes(feed);
const feedDiv = document.createElement('div');
feedDiv.className = 'checkbox-item';
feedDiv.innerHTML = `
<label>
<input type="checkbox" name="news_feed" value="${feed}" ${isEnabled ? 'checked' : ''}>
${feed}
</label>
`;
feedsGrid.appendChild(feedDiv);
});
}
updateCustomFeedsList();
updateNewsStatus();
}
function updateCustomFeedsList() {
const customFeedsList = document.getElementById('custom_feeds_list');
customFeedsList.innerHTML = '';
if (newsManagerData.custom_feeds) {
Object.entries(newsManagerData.custom_feeds).forEach(([name, url]) => {
const feedDiv = document.createElement('div');
feedDiv.style.cssText = 'margin: 10px 0; padding: 10px; border: 1px solid #ccc; border-radius: 4px; background: white;';
feedDiv.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center;">
<div><strong>${name}</strong>: ${url}</div>
<button type="button" onclick="removeCustomFeed('${name}')"
style="background: #ff4444; color: white; border: none; padding: 4px 8px; border-radius: 3px; cursor: pointer;">Remove</button>
</div>
`;
customFeedsList.appendChild(feedDiv);
});
}
}
function updateNewsStatus() {
const statusDiv = document.getElementById('news_status');
const enabledFeeds = newsManagerData.enabled_feeds || [];
statusDiv.innerHTML = `
<h4>Current Status</h4>
<p><strong>Enabled:</strong> ${newsManagerData.enabled ? 'Yes' : 'No'}</p>
<p><strong>Active Feeds:</strong> ${enabledFeeds.join(', ') || 'None'}</p>
<p><strong>Headlines per Feed:</strong> ${newsManagerData.headlines_per_feed || 2}</p>
<p><strong>Total Custom Feeds:</strong> ${Object.keys(newsManagerData.custom_feeds || {}).length}</p>
<p><strong>Rotation Enabled:</strong> ${newsManagerData.rotation_enabled !== false ? 'Yes' : 'No'}</p>
`;
}
async function saveNewsSettings() {
const enabledFeeds = Array.from(document.querySelectorAll('input[name="news_feed"]:checked'))
.map(input => input.value);
const headlinesPerFeed = parseInt(document.getElementById('headlines_per_feed').value);
const enabled = document.getElementById('news_enabled').checked;
try {
await fetch('/news_manager/toggle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: enabled })
});
const response = await fetch('/news_manager/update_feeds', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
enabled_feeds: enabledFeeds,
headlines_per_feed: headlinesPerFeed
})
});
const data = await response.json();
showNotification(data.message, data.status);
if (data.status === 'success') {
loadNewsManagerData();
}
} catch (error) {
showNotification('Error saving news settings: ' + error, 'error');
}
}
async function addCustomFeed() {
const name = document.getElementById('custom_feed_name').value.trim();
const url = document.getElementById('custom_feed_url').value.trim();
if (!name || !url) {
showNotification('Please enter both feed name and URL', 'error');
return;
}
try {
const response = await fetch('/news_manager/add_custom_feed', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name, url: url })
});
const data = await response.json();
showNotification(data.message, data.status);
if (data.status === 'success') {
document.getElementById('custom_feed_name').value = '';
document.getElementById('custom_feed_url').value = '';
loadNewsManagerData();
}
} catch (error) {
showNotification('Error adding custom feed: ' + error, 'error');
}
}
async function removeCustomFeed(name) {
if (!confirm(`Are you sure you want to remove the feed "${name}"?`)) {
return;
}
try {
const response = await fetch('/news_manager/remove_custom_feed', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name })
});
const data = await response.json();
showNotification(data.message, data.status);
if (data.status === 'success') {
loadNewsManagerData();
}
} catch (error) {
showNotification('Error removing custom feed: ' + error, 'error');
}
}
function refreshNewsStatus() {
loadNewsManagerData();
showNotification('News status refreshed', 'success');
}
// Sports configuration
async function refreshSportsConfig(){
try {
// Refresh the current config to ensure we have the latest data
await refreshCurrentConfig();
const res = await fetch('/api/system/status');
const stats = await res.json();
// Build a minimal sports UI off current config
const cfg = currentConfig;
const leaguePrefixes = {
'nfl_scoreboard': 'nfl',
'mlb': 'mlb',
'milb': 'milb',
'nhl_scoreboard': 'nhl',
'nba_scoreboard': 'nba',
'ncaa_fb_scoreboard': 'ncaa_fb',
'ncaa_baseball_scoreboard': 'ncaa_baseball',
'ncaam_basketball_scoreboard': 'ncaam_basketball',
'soccer_scoreboard': 'soccer'
};
const leagues = [
{ key: 'nfl_scoreboard', label: 'NFL' },
{ key: 'mlb', label: 'MLB' },
{ key: 'milb', label: 'MiLB' },
{ key: 'nhl_scoreboard', label: 'NHL' },
{ key: 'nba_scoreboard', label: 'NBA' },
{ key: 'ncaa_fb_scoreboard', label: 'NCAA FB' },
{ key: 'ncaa_baseball_scoreboard', label: 'NCAA Baseball' },
{ key: 'ncaam_basketball_scoreboard', label: 'NCAAM Basketball' },
{ key: 'soccer_scoreboard', label: 'Soccer' }
];
const container = document.getElementById('sports-config');
const html = leagues.map(l => {
const sec = cfg[l.key] || {};
const p = leaguePrefixes[l.key] || l.key;
const fav = (sec.favorite_teams || []).join(', ');
const recentToShow = sec.recent_games_to_show ?? 1;
const upcomingToShow = sec.upcoming_games_to_show ?? 1;
const liveUpd = sec.live_update_interval ?? 30;
const recentUpd = sec.recent_update_interval ?? 3600;
const upcomingUpd = sec.upcoming_update_interval ?? 3600;
return `
<div style="border:1px solid #ddd; border-radius:6px; padding:12px; margin:10px 0;">
<div style="display:flex; justify-content: space-between; align-items:center; margin-bottom:8px;">
<label style="display:flex; align-items:center; gap:8px; margin:0;">
<input type="checkbox" data-league="${l.key}" class="sp-enabled" ${sec.enabled ? 'checked' : ''}>
<strong>${l.label}</strong>
</label>
<div style="display:flex; gap:6px;">
<button type="button" class="btn btn-info" onclick="startOnDemand('${p}_live')"><i class="fas fa-bolt"></i> Live</button>
<button type="button" class="btn btn-info" onclick="startOnDemand('${p}_recent')"><i class="fas fa-bolt"></i> Recent</button>
<button type="button" class="btn btn-info" onclick="startOnDemand('${p}_upcoming')"><i class="fas fa-bolt"></i> Upcoming</button>
<button type="button" class="btn btn-secondary" onclick="stopOnDemand()"><i class="fas fa-ban"></i> Stop</button>
</div>
</div>
<div class="form-row" style="margin-top:10px;">
<div class="form-group">
<label>Live Priority</label>
<input type="checkbox" data-league="${l.key}" class="sp-live-priority" ${sec.live_priority ? 'checked' : ''}>
</div>
<div class="form-group">
<label>Show Odds</label>
<input type="checkbox" data-league="${l.key}" class="sp-show-odds" ${sec.show_odds ? 'checked' : ''}>
</div>
<div class="form-group">
<label>Favorites Only</label>
<input type="checkbox" data-league="${l.key}" class="sp-favorites-only" ${sec.show_favorite_teams_only ? 'checked' : ''}>
</div>
<div class="form-group">
<label>Favorite Teams</label>
<input type="text" data-league="${l.key}" class="form-control sp-favorites" value="${fav}">
<div class="description">Comma-separated abbreviations</div>
</div>
</div>
<div class="form-row" style="margin-top:10px;">
<div class="form-group">
<label>Live Update Interval (sec)</label>
<input type="number" min="10" class="form-control sp-live-update" data-league="${l.key}" value="${liveUpd}">
</div>
<div class="form-group">
<label>Recent Update Interval (sec)</label>
<input type="number" min="60" class="form-control sp-recent-update" data-league="${l.key}" value="${recentUpd}">
</div>
<div class="form-group">
<label>Upcoming Update Interval (sec)</label>
<input type="number" min="60" class="form-control sp-upcoming-update" data-league="${l.key}" value="${upcomingUpd}">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Recent Games to Show</label>
<input type="number" min="0" class="form-control sp-recent-count" data-league="${l.key}" value="${recentToShow}">
</div>
<div class="form-group">
<label>Upcoming Games to Show</label>
<input type="number" min="0" class="form-control sp-upcoming-count" data-league="${l.key}" value="${upcomingToShow}">
</div>
</div>
</div>
`;
}).join('');
container.innerHTML = html || 'No sports configuration found.';
} catch (err) {
document.getElementById('sports-config').textContent = 'Failed to load sports configuration';
}
}
async function saveSportsConfig(){
try {
const leagues = document.querySelectorAll('.sp-enabled');
const fragment = {};
leagues.forEach(chk => {
const key = chk.getAttribute('data-league');
const enabled = chk.checked;
const livePriority = document.querySelector(`.sp-live-priority[data-league="${key}"]`)?.checked || false;
const showOdds = document.querySelector(`.sp-show-odds[data-league="${key}"]`)?.checked || false;
const favoritesOnly = document.querySelector(`.sp-favorites-only[data-league="${key}"]`)?.checked || false;
const favs = document.querySelector(`.sp-favorites[data-league="${key}"]`)?.value || '';
const favorite_teams = favs.split(',').map(s => s.trim()).filter(Boolean);
const liveUpd = parseInt(document.querySelector(`.sp-live-update[data-league="${key}"]`)?.value || '30');
const recentUpd = parseInt(document.querySelector(`.sp-recent-update[data-league="${key}"]`)?.value || '3600');
const upcomingUpd = parseInt(document.querySelector(`.sp-upcoming-update[data-league="${key}"]`)?.value || '3600');
const recentCount = parseInt(document.querySelector(`.sp-recent-count[data-league="${key}"]`)?.value || '1');
const upcomingCount = parseInt(document.querySelector(`.sp-upcoming-count[data-league="${key}"]`)?.value || '1');
fragment[key] = {
enabled,
live_priority: livePriority,
show_odds: showOdds,
show_favorite_teams_only: favoritesOnly,
favorite_teams,
live_update_interval: liveUpd,
recent_update_interval: recentUpd,
upcoming_update_interval: upcomingUpd,
recent_games_to_show: recentCount,
upcoming_games_to_show: upcomingCount
};
});
await saveConfigJson(fragment);
} catch (err) {
showNotification('Error saving sports configuration: ' + err, 'error');
return;
}
showNotification('Sports configuration saved', 'success');
}
</script>
</body>
</html>