mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 16:58:06 +00:00
The sports scoreboards read `getattr(self, 'global_config', {})` to find a
shared scroll frame rate, but nothing ever set that attribute: the loader
constructs plugins with only plugin_id/config/display_manager/cache_manager/
plugin_manager (plugin_loader.py:671), `global_config` appears nowhere in
src/, no plugin manager assigns it, and BasePlugin has no __getattr__ to
synthesize it. The lookup always returned {}, so the ten scroll_display.py
copies that thread target_fps through to ScrollHelper could never fire on any
core. There was also no global target_fps to find -- the only one in the
template is display.vegas_scroll.target_fps, which is Vegas-scoped.
Adds the missing half:
- `BasePlugin.global_config` resolves the full config via
plugin_manager.config_manager, then cache_manager.config_manager, then {}.
Same order the sports timezone helpers already use. Exceptions are swallowed
to debug so an unreadable config can never stop a plugin loading, and a
non-dict result is rejected rather than handed to callers that will .get()
it and feed the result to numeric code.
- A top-level `target_fps` (default 100), exposed on the General tab and
validated 30-200 on save to match ScrollHelper.set_target_fps -- which
clamps silently, so a rejected save reports a value that would otherwise
appear to save and then behave differently.
The property has a setter deliberately. news, stock-news, ledmatrix-stocks,
ledmatrix-elections, ledmatrix-leaderboard and nfl-draft all assign
`self.global_config = config.get('global', {})`; without a setter that raises
"property has no setter" and those six plugins stop loading. Reproduced, then
pinned with a test.
target_fps is also kept out of the `is_general_update` key list: that branch
treats a missing web_display_autostart as an unchecked box, so counting a
target_fps-only POST as a General save would silently switch autostart off.
Verified end to end: config.json -> BasePlugin.global_config ->
scroll_display's existing block -> ScrollHelper.target_fps 120 -> 100, with no
plugin-side change needed. Suite 1441 passed; the 4 failures
(test_display_dirty_tracking, test_web_api::test_get_system_status, two in
test_state_reconciliation) are pre-existing and reproduce identically on a
clean tree.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
193 lines
11 KiB
HTML
193 lines
11 KiB
HTML
{% import 'v3/partials/_macros.html' as ui %}
|
|
<div class="bg-white rounded-lg shadow p-6">
|
|
<div class="border-b border-gray-200 pb-4 mb-6">
|
|
<h2 class="text-lg font-semibold text-gray-900">General Settings</h2>
|
|
<p class="mt-1 text-sm text-gray-600">Configure general system settings and location information.</p>
|
|
</div>
|
|
|
|
{{ ui.settings_filter() }}
|
|
|
|
<form hx-post="/api/v3/config/main"
|
|
hx-ext="json-enc"
|
|
hx-headers='{"Content-Type": "application/json"}'
|
|
hx-swap="none"
|
|
hx-on:htmx:after-request="
|
|
var xhr = event.detail.xhr;
|
|
var isSuccess = xhr.status >= 200 && xhr.status < 300;
|
|
var message = '';
|
|
var status = 'success';
|
|
try {
|
|
var data = JSON.parse(xhr.responseText);
|
|
message = data.message || '';
|
|
status = data.status || status;
|
|
} catch (e) {}
|
|
if (isSuccess) {
|
|
message = message || 'Settings saved';
|
|
} else {
|
|
message = message || 'Failed to save settings';
|
|
status = 'error';
|
|
}
|
|
showNotification(message, status);
|
|
"
|
|
class="space-y-6">
|
|
|
|
<!-- Web Display Autostart -->
|
|
<div class="form-group" id="setting-general-web_display_autostart" data-setting-key="web_display_autostart">
|
|
<label class="flex items-center">
|
|
<input type="checkbox"
|
|
name="web_display_autostart"
|
|
value="true"
|
|
{% if main_config.web_display_autostart %}checked{% endif %}
|
|
class="form-control h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
|
<span class="ml-2 text-sm font-medium text-gray-900">Web Display Autostart</span>
|
|
{{ ui.help_tip('Automatically start the web interface when the device boots.\nDefault: on. Turn off if you launch the web UI manually or run headless.', 'Web Display Autostart') }}
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Timezone -->
|
|
<div class="form-group" id="setting-general-timezone" data-setting-key="timezone">
|
|
<label for="timezone" class="block text-sm font-medium text-gray-700">Timezone{{ ui.help_tip('Time zone used for clocks, schedules, and time-based content.\nChoose the zone where the display physically lives so on/off schedules fire at the correct local time.', 'Timezone') }}</label>
|
|
<div id="timezone_container" class="mt-1"></div>
|
|
</div>
|
|
|
|
<!-- Scroll frame rate (device-wide) -->
|
|
<div class="form-group" id="setting-general-target-fps" data-setting-key="target_fps">
|
|
<label for="target_fps" class="block text-sm font-medium text-gray-700">Scroll Frame Rate{{ ui.help_tip('Frames per second for scrolling content, applied across plugins that scroll.\nHigher is smoother but uses more CPU; lower frees CPU but looks steppier.\nRange 30-200. Default: 100.', 'Scroll Frame Rate') }}</label>
|
|
<input type="number"
|
|
id="target_fps"
|
|
name="target_fps"
|
|
value="{{ main_config.target_fps or 100 }}"
|
|
min="30"
|
|
max="200"
|
|
class="form-control">
|
|
</div>
|
|
<script>
|
|
(function() {
|
|
// Track if already initialized to prevent re-render
|
|
if (window.__timezoneWidgetInitialized) return;
|
|
|
|
function initTimezoneWidget() {
|
|
if (!window.LEDMatrixWidgets) { setTimeout(initTimezoneWidget, 50); return; }
|
|
var widget = window.LEDMatrixWidgets.get('timezone-selector');
|
|
if (!widget) { setTimeout(initTimezoneWidget, 50); return; }
|
|
var container = document.getElementById('timezone_container');
|
|
if (!container) return;
|
|
|
|
// Only render if container is empty (not already rendered)
|
|
if (container.children.length > 0) return;
|
|
|
|
widget.render(container, {
|
|
'x-options': { showOffset: true, placeholder: 'Select your timezone...' }
|
|
}, {{ (main_config.timezone or "America/Chicago")|tojson }}, {
|
|
fieldId: 'timezone',
|
|
name: 'timezone'
|
|
});
|
|
window.__timezoneWidgetInitialized = true;
|
|
}
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initTimezoneWidget);
|
|
} else {
|
|
setTimeout(initTimezoneWidget, 50);
|
|
}
|
|
})();
|
|
</script>
|
|
|
|
<!-- Location Information -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-3 gap-4">
|
|
<div class="form-group" id="setting-general-city" data-setting-key="location.city">
|
|
<label for="city" class="block text-sm font-medium text-gray-700">City{{ ui.help_tip('City used for weather, sunrise/sunset, and other location-based content.\nExample: Dallas.', 'City') }}</label>
|
|
<input type="text"
|
|
id="city"
|
|
name="city"
|
|
value="{{ main_config.location.city or 'Dallas' }}"
|
|
class="form-control">
|
|
</div>
|
|
|
|
<div class="form-group" id="setting-general-state" data-setting-key="location.state">
|
|
<label for="state" class="block text-sm font-medium text-gray-700">State{{ ui.help_tip('State or region for your location.\nExample: Texas. Improves location-lookup accuracy.', 'State') }}</label>
|
|
<input type="text"
|
|
id="state"
|
|
name="state"
|
|
value="{{ main_config.location.state or 'Texas' }}"
|
|
class="form-control">
|
|
</div>
|
|
|
|
<div class="form-group" id="setting-general-country" data-setting-key="location.country">
|
|
<label for="country" class="block text-sm font-medium text-gray-700">Country{{ ui.help_tip('Country code or name for your location.\nExample: US. Used with City and State for weather and geolocation.', 'Country') }}</label>
|
|
<input type="text"
|
|
id="country"
|
|
name="country"
|
|
value="{{ main_config.location.country or 'US' }}"
|
|
class="form-control">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Plugin System Settings -->
|
|
<div class="border-t border-gray-200 pt-6 mt-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Plugin System Settings</h3>
|
|
<p class="text-sm text-gray-600 mb-4">Configure the core plugin system behavior.</p>
|
|
|
|
<div class="space-y-4">
|
|
<!-- Auto Discover -->
|
|
<div class="form-group" id="setting-general-auto_discover" data-setting-key="plugin_system.auto_discover">
|
|
<label class="flex items-center">
|
|
<input type="checkbox"
|
|
name="auto_discover"
|
|
value="true"
|
|
{% if main_config.get('plugin_system', {}).get('auto_discover', True) %}checked{% endif %}
|
|
class="form-control h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
|
<span class="ml-2 text-sm font-medium text-gray-900">Auto Discover Plugins</span>
|
|
{{ ui.help_tip('Scan the plugins directory for installed plugins each time the service starts.\nDefault: on. Leave on unless you manage plugins manually.', 'Auto Discover Plugins') }}
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Auto Load Enabled -->
|
|
<div class="form-group" id="setting-general-auto_load_enabled" data-setting-key="plugin_system.auto_load_enabled">
|
|
<label class="flex items-center">
|
|
<input type="checkbox"
|
|
name="auto_load_enabled"
|
|
value="true"
|
|
{% if main_config.get('plugin_system', {}).get('auto_load_enabled', True) %}checked{% endif %}
|
|
class="form-control h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
|
<span class="ml-2 text-sm font-medium text-gray-900">Auto Load Enabled Plugins</span>
|
|
{{ ui.help_tip('Load every plugin marked enabled in the configuration at startup.\nDefault: on. Turn off to keep plugins installed but dormant.', 'Auto Load Enabled Plugins') }}
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Development Mode -->
|
|
<div class="form-group" id="setting-general-development_mode" data-setting-key="plugin_system.development_mode">
|
|
<label class="flex items-center">
|
|
<input type="checkbox"
|
|
name="development_mode"
|
|
value="true"
|
|
{% if main_config.get('plugin_system', {}).get('development_mode', False) %}checked{% endif %}
|
|
class="form-control h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
|
<span class="ml-2 text-sm font-medium text-gray-900">Development Mode</span>
|
|
{{ ui.help_tip('Enable verbose logging and developer features for plugin debugging.\nDefault: off. Keep off for normal use — it increases log volume.', 'Development Mode') }}
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Plugins Directory -->
|
|
<div class="form-group" id="setting-general-plugins_directory" data-setting-key="plugin_system.plugins_directory">
|
|
<label for="plugins_directory" class="block text-sm font-medium text-gray-700">Plugins Directory{{ ui.help_tip('Folder (relative to the project root) where plugins are stored.\nDefault: plugin-repos. Only change this if you keep plugins in a custom location.', 'Plugins Directory') }}</label>
|
|
<input type="text"
|
|
id="plugins_directory"
|
|
name="plugins_directory"
|
|
value="{{ main_config.get('plugin_system', {}).get('plugins_directory', 'plugin-repos') }}"
|
|
placeholder="plugin-repos"
|
|
class="form-control">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Submit Button -->
|
|
<div class="flex justify-end mt-6">
|
|
<button type="submit"
|
|
class="btn bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md">
|
|
<i class="fas fa-save mr-2"></i>
|
|
Save General Settings
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|