mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-05 14:55:08 +00:00
* feat(web): Schedule and General become ES-module pages (stage 3) Schedule and General follow stage 2 (#727): no inline scripts or inline handlers in either partial. Their code moves to static/v3/js/pages/schedule.js and pages/general.js, started per swap-in by the page registry. - Schedule: both pickers are drawn from the saved config carried as JSON in data-* attributes. The forms' hx-on save handlers become one htmx:afterRequest listener on the page; the forms are marked data-reports-result, which app.js now treats like an hx-on after-request handler, so a save still shows one notification. - General: the timezone picker reads data-timezone. The Security section's forms and buttons are delegated data-actions; requests go through core/api.js, so the login redirect is quiet, and a change made just before a swap is still reported. - handleScheduleResponse, handleDimScheduleResponse and webLogin stay as deprecated aliases through window.LEDMatrix. - New DOM suites test_schedule_page.js and test_general_page.js; the web login unit suite imports the module; test_es_modules.py pins the pages, the aliases, and the schedule config's round trip through its attribute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(web): no unused catch bindings or computed writes in the stage 3 modules Codacy flagged two unused catch variables and dynamic-key writes in pages/schedule.js and boot.js. The schedule config is read with getAttribute, and the default days and the webLogin alias object are built with Object.fromEntries. No behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
295 lines
20 KiB
HTML
295 lines
20 KiB
HTML
{% import 'v3/partials/_macros.html' as ui %}
|
|
{# No inline script: static/v3/js/pages/general.js draws the timezone picker
|
|
and runs the Security section's forms and buttons (data-action). The page
|
|
registry (static/v3/js/core/registry.js) starts it when this root appears
|
|
and stops it when the partial is swapped away. #}
|
|
<div data-page="general">
|
|
<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="showSaveResult(event.detail.xhr, 'Settings saved', 'Failed to save settings')"
|
|
class="space-y-6">
|
|
{# Marks this post as the whole form, so an unchecked box saves as
|
|
false. Without it the save endpoint treats absent keys as unchanged
|
|
(FORM_SECTION_FIELD in api_v3/config.py). #}
|
|
<input type="hidden" name="__form_section" value="general">
|
|
|
|
<!-- 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>
|
|
|
|
<!-- Weekly Automatic Updates -->
|
|
<div class="form-group" id="setting-general-auto_update" data-setting-key="auto_update.enabled">
|
|
<label class="flex items-center">
|
|
<input type="checkbox"
|
|
name="auto_update_enabled"
|
|
value="true"
|
|
{% if (main_config.auto_update or {}).enabled %}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">Automatically check for and install updates once a week</span>
|
|
{{ ui.help_tip('Once a week, update LEDMatrix and every installed plugin that has a newer version.\nSkipped (and reported) if you have local changes, low disk space, or the rollback service is missing. After updating, the services are restarted and checked; if they do not stay healthy, the update is rolled back automatically.\nRuns overnight (2-5 AM in your timezone) when possible. Problems show as a banner on Overview. A stopped display is not started. Default: off.', 'Automatic Updates') }}
|
|
</label>
|
|
{% if auto_update_status and (main_config.auto_update or {}).enabled and auto_update_status.verifier_installed is sameas false %}
|
|
<p class="mt-1 ml-6 text-xs text-amber-700">
|
|
{% if auto_update_status.setup_status == 'failed' %}
|
|
Setting up the update health check failed: {{ auto_update_status.setup_message }}
|
|
LEDMatrix code updates are paused until it is fixed; plugin updates still run.
|
|
{% else %}
|
|
Setting up the update health check. The display service does this when it starts, so it is
|
|
ready shortly after saving (or once the display is started). LEDMatrix code updates begin after that.
|
|
{% endif %}
|
|
</p>
|
|
{% endif %}
|
|
{% if auto_update_status and auto_update_status.verifying %}
|
|
<p class="mt-1 ml-6 text-xs text-blue-700">An update was just installed and its health check is running.</p>
|
|
{% endif %}
|
|
{% if auto_update_status and (auto_update_status.last_run or auto_update_status.next_due) %}
|
|
<div class="mt-1 ml-6 text-xs text-gray-500 space-y-1">
|
|
{% if auto_update_status.last_run %}
|
|
<p>Last automatic update: {{ auto_update_status.last_run }}
|
|
<span class="{{ 'text-red-600' if auto_update_status.status == 'error' else '' }}">— {{ auto_update_status.summary }}</span></p>
|
|
{% endif %}
|
|
{% if auto_update_status.next_due %}
|
|
<p>Next check: {{ auto_update_status.next_due }} or the first overnight window after it</p>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Update channel: which code Update Code and the weekly update move to -->
|
|
{% set configured_channel = (main_config.auto_update or {}).channel if (main_config.auto_update or {}).channel in ('stable', 'beta') else 'stable' %}
|
|
<div class="form-group" id="setting-general-auto_update_channel" data-setting-key="auto_update.channel">
|
|
<label for="auto_update_channel" class="block text-sm font-medium text-gray-700">Update Channel{{ ui.help_tip('Which LEDMatrix version updates install, both Update Code on the Overview tab and the weekly automatic update.\nStable: the newest release. Recommended.\nBeta: the newest code on main, before it is released. Gets fixes sooner, and problems too.\nSwitching to Stable never installs an older version than the one running: a device that is ahead of the newest release keeps following main until a release includes its version, then moves to releases.\nDefault: Stable.', 'Update Channel') }}</label>
|
|
<select id="auto_update_channel" name="auto_update_channel" class="form-control mt-1">
|
|
<option value="stable" {% if configured_channel == 'stable' %}selected{% endif %}>Stable (releases)</option>
|
|
<option value="beta" {% if configured_channel == 'beta' %}selected{% endif %}>Beta (newest code on main)</option>
|
|
</select>
|
|
{% if update_channel_status %}
|
|
<p class="mt-1 text-xs {{ 'text-amber-700' if update_channel_status.waiting and configured_channel == 'stable' else 'text-gray-500' }}">
|
|
{{ update_channel_status.message }}
|
|
</p>
|
|
{% endif %}
|
|
<p class="mt-1 text-xs text-gray-500">Takes effect at the next update: use Update Code on the Overview tab to apply it now.</p>
|
|
</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" data-timezone="{{ main_config.timezone or 'America/Chicago' }}"></div>
|
|
</div>
|
|
|
|
<!-- Legacy scroll frame rate (device-wide). Core scrolling no longer reads it. -->
|
|
<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 (legacy){{ ui.help_tip('Kept for older plugins that read it. It no longer changes scrolling speed or smoothness.\nScrolling runs at the panel refresh rate (Display settings), and each plugin sets its own scroll speed.\nRange 30-200. Default: 100.', 'Scroll Frame Rate (legacy)') }}</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>
|
|
|
|
<!-- 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, radar, and other location-based content.\nExample: Kansas City.\nUsed as the default for the location_city setting on plugins that have one; a value saved on the plugin itself overrides it.', '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: Missouri. Improves location-lookup accuracy.\nUsed as the default for the location_state setting on plugins that have one.', '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, radar, and geolocation.\nUsed as the default for the location_country setting on plugins that have one.', '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">
|
|
<!-- plugin_system.auto_discover, auto_load_enabled and
|
|
development_mode used to be toggles here. Nothing reads
|
|
them: plugins are always discovered and every enabled
|
|
plugin is loaded. Stored values are left alone. -->
|
|
|
|
<!-- 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>
|
|
|
|
{% if web_login %}
|
|
<!-- Web login (optional; web_interface/auth.py). Off until a password is set. -->
|
|
<div id="web-login-settings" class="bg-white rounded-lg shadow p-6 mt-6">
|
|
<div class="border-b border-gray-200 pb-4 mb-6">
|
|
<h2 class="text-lg font-semibold text-gray-900"><i class="fas fa-lock mr-2" aria-hidden="true"></i>Security</h2>
|
|
<p class="mt-1 text-sm text-gray-600">
|
|
{% if web_login.enabled %}
|
|
<span class="text-green-700 font-semibold">Login is on.</span>
|
|
Browsers on your network need the password; integrations use an API token.
|
|
{% else %}
|
|
Login is off: anyone on your network can open this page. Set a password to require one.
|
|
{% endif %}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="space-y-6">
|
|
<div class="form-group" id="setting-general-web_login" data-setting-key="web_auth.password">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-2">
|
|
{# A <label> so the settings search indexes it; no `for`, so
|
|
the password inputs keep their own labels. #}
|
|
<label>{% if web_login.enabled %}Change the web interface password{% else %}Web interface password{% endif %}{{ ui.help_tip('Optional. With a password set, every page and API call needs a login or an API token.\nAlways allowed without one: this Pi itself (localhost), and the Wi-Fi setup page while the Pi is in access-point mode.\nForgot it? On the Pi run: sudo python3 ~/LEDMatrix/scripts/reset_web_password.py', 'Web interface password') }}</label>
|
|
</h3>
|
|
<form class="space-y-4" data-action="set-password" autocomplete="off">
|
|
{% if web_login.enabled %}
|
|
<div>
|
|
<label for="web-login-current" class="block text-sm font-medium text-gray-700">Current password</label>
|
|
<input type="password" id="web-login-current" name="current_password" required
|
|
autocomplete="current-password" class="form-control mt-1">
|
|
</div>
|
|
{% endif %}
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label for="web-login-new" class="block text-sm font-medium text-gray-700">New password</label>
|
|
<input type="password" id="web-login-new" name="new_password" required
|
|
minlength="{{ web_login.min_length }}" autocomplete="new-password" class="form-control mt-1">
|
|
</div>
|
|
<div>
|
|
<label for="web-login-confirm" class="block text-sm font-medium text-gray-700">Type it again</label>
|
|
<input type="password" id="web-login-confirm" name="confirm_password" required
|
|
minlength="{{ web_login.min_length }}" autocomplete="new-password" class="form-control mt-1">
|
|
</div>
|
|
</div>
|
|
<p class="text-sm text-gray-600">
|
|
At least {{ web_login.min_length }} characters.
|
|
{% if not web_login.enabled %}Write it down: if you lose it, you need SSH access to the Pi (or a browser on the Pi) to turn login off again.{% endif %}
|
|
</p>
|
|
<div class="flex justify-end">
|
|
<button type="submit" class="btn bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md">
|
|
<i class="fas fa-key mr-2" aria-hidden="true"></i>{% if web_login.enabled %}Change password{% else %}Set password and turn login on{% endif %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
{% if web_login.enabled %}
|
|
<div class="border-t border-gray-200 pt-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-2">Turn login off</h3>
|
|
<form class="flex flex-wrap items-end gap-2" data-action="disable-login" autocomplete="off">
|
|
<div class="flex-1">
|
|
<label for="web-login-disable-current" class="block text-sm font-medium text-gray-700">Current password</label>
|
|
<input type="password" id="web-login-disable-current" name="current_password" required
|
|
autocomplete="current-password" class="form-control mt-1">
|
|
</div>
|
|
<button type="submit" class="btn bg-white border border-gray-300 text-gray-700 px-4 py-2 rounded-md">
|
|
<i class="fas fa-lock-open mr-2" aria-hidden="true"></i>Turn login off
|
|
</button>
|
|
</form>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="border-t border-gray-200 pt-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-2">API tokens</h3>
|
|
<p class="text-sm text-gray-600 mb-4">
|
|
For Home Assistant, scripts, or the MQTT bridge on another machine, once login is on.
|
|
Send it as <code class="bg-gray-100 font-mono">Authorization: Bearer <token></code>.
|
|
A token is shown once, when you create it.
|
|
</p>
|
|
<div id="web-login-tokens" class="space-y-2 mb-4">
|
|
{% for token in web_login.tokens %}
|
|
<div class="flex flex-wrap items-center justify-between gap-2 border border-gray-200 rounded-md px-4 py-2" data-token-id="{{ token.id }}">
|
|
<div class="text-sm">
|
|
<span class="font-semibold text-gray-900">{{ token.name }}</span>
|
|
<span class="font-mono text-gray-600 ml-2">{{ token.prefix }}…</span>
|
|
<span class="text-gray-600 ml-2">created {{ (token.created_at or '')[:10] }}</span>
|
|
</div>
|
|
<button type="button" class="text-sm text-red-600 hover:underline"
|
|
data-action="revoke-token" data-token-name="{{ token.name }}">
|
|
Revoke
|
|
</button>
|
|
</div>
|
|
{% else %}
|
|
<p class="text-sm text-gray-600" data-empty>No tokens yet.</p>
|
|
{% endfor %}
|
|
</div>
|
|
<form class="flex flex-wrap items-end gap-2" data-action="create-token" autocomplete="off">
|
|
<div class="flex-1">
|
|
<label for="web-login-token-name" class="block text-sm font-medium text-gray-700">New token name</label>
|
|
<input type="text" id="web-login-token-name" name="name" required maxlength="60"
|
|
placeholder="Home Assistant" class="form-control mt-1">
|
|
</div>
|
|
<button type="submit" class="btn bg-white border border-gray-300 text-gray-700 px-4 py-2 rounded-md">
|
|
<i class="fas fa-plus mr-2" aria-hidden="true"></i>Create token
|
|
</button>
|
|
</form>
|
|
<div id="web-login-new-token" class="hidden mt-4 border border-amber-300 bg-amber-50 rounded-md p-4" role="status">
|
|
<p class="text-sm font-semibold text-gray-900 mb-2">Copy this token now. It is not shown again.</p>
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<code id="web-login-new-token-value" class="font-mono break-all bg-white border border-gray-200 rounded-md px-4 py-2 flex-1"></code>
|
|
<button type="button" class="btn bg-white border border-gray-300 text-gray-700 px-4 py-2 rounded-md"
|
|
data-action="copy-token">
|
|
<i class="fas fa-copy mr-2" aria-hidden="true"></i>Copy
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|