mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-13 18:46:37 +00:00
Turns ledmatrix.service's autostart on or off at once through the existing enable_autostart / disable_autostart system actions (sudo grants already in lib_sudoers.sh). The checkbox shows systemd's own state, read with `systemctl is-enabled`, so nothing is stored in config.json and the default stays on. Hidden when the state cannot be read; a refused change puts the box back and shows the route's message. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
311 lines
21 KiB
HTML
311 lines
21 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() }}
|
|
|
|
{# Display autostart is systemd's enable/disable of ledmatrix.service, not a
|
|
config.json key: outside the form so Save never posts it. general.js
|
|
applies it on change. Hidden when the unit's state cannot be read. #}
|
|
{% if display_autostart is not none %}
|
|
<div class="form-group mb-6" id="setting-general-display_autostart" data-setting-key="display_autostart">
|
|
<label class="flex items-center">
|
|
<input type="checkbox"
|
|
data-action="display-autostart"
|
|
{% if display_autostart %}checked{% endif %}
|
|
class="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">Start Display on Boot</span>
|
|
{{ ui.help_tip('Start the LED display automatically when the device powers on.\nApplies right away; no Save needed.\nWhen off, start it with Start Display on the Overview tab. The web interface still starts on boot.\nDefault: on.', 'Start Display on Boot') }}
|
|
</label>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<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>
|