mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 16:58:06 +00:00
Add settings tooltips and search to the web UI (#387)
* Add settings tooltips and search to the web UI Help users quickly find settings and understand how each one works. Tooltips: a new delegated controller (static/v3/js/tooltips.js) drives an accessible (i) info tooltip that appears on hover, keyboard focus, and tap. A shared `help_tip` Jinja macro (partials/_macros.html) emits the trigger; the plugin config macro and the core settings partials now surface help text through it. Per the design, the always-visible field help paragraphs are folded into the tooltip to declutter the forms, and the hardware/display settings carry authored detail (default, range, recommendation). Search: a global header search box finds settings across every settings tab — even ones not yet opened — via a lazy client-side index built by scanning the same field markup (static/v3/js/settings-search.js). Selecting a result switches tabs, waits for the field to load, then scrolls to and flashes it. A per-tab filter box hides non-matching fields on the current tab. Plugin settings get tooltips for free by reusing each field's schema `description`; every settings field also gets a stable `setting-<tab>-<key>` anchor id for search navigation. Styling uses the existing --color-* theme vars so light/dark mode both work, and honors prefers-reduced-motion. Adds Flask render smoke tests that assert each settings partial ships tooltips, anchors, and a filter box. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gZxznuxw8L92FUMBN3Nqz * Address Codacy static-analysis findings in settings JS Refactor the two new modules to clear the flagged patterns without any behavior change: - Build the search dropdown with DOM nodes + textContent instead of innerHTML string concatenation, removing the XSS sinks and the manual escapeHtml helper it needed. - Replace numeric index access (index[i], terms[j], opts[idx], currentResults[i]) with array iteration methods, NodeList.item(), and Array.prototype.at() to clear detect-object-injection. - Use === via a shared termsMatch() helper, optional-catch binding, and drop a useless initial assignment. Verified with ESLint (eslint:recommended + eslint-plugin-security) at zero findings and re-ran the headless-Chromium behavior test (tooltip, per-tab filter, global search navigation) — all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gZxznuxw8L92FUMBN3Nqz * Reduce complexity of revealAncestors in settings search Extract isNodeHidden() and revealNode() helpers so revealAncestors drops below the cyclomatic-complexity threshold. No behavior change; verified with the headless-Chromium test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gZxznuxw8L92FUMBN3Nqz * Resolve remaining Codacy findings in settings search - Validate plugin ids against a strict allowlist (mirroring the server's _SAFE_PLUGIN_ID_RE) before they can appear in a fetch path, so the request URL is never built from unvalidated input (Codacy: user-controlled URL). - Document that the fetched HTML is parsed into an inert document (scripts never run, never inserted into the live DOM) purely to read field text for the search index. - Declare block-scoped locals with const instead of var where they were nested inside conditionals (Codacy: var not at function root). No behavior change; re-verified with ESLint and the headless-Chromium test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gZxznuxw8L92FUMBN3Nqz * Serve the settings search index from the server as JSON Move index building off the client so there is no client-side HTML fetching or DOM parsing (resolves Codacy's variable-fetch and DOMParser flags on the read-only, same-origin index build). - Add GET /v3/settings/search-index (pages_v3.py): renders the settings partials server-side and extracts each field's anchor id, key, label, tooltip, and section with a small stdlib HTMLParser, then caches the result keyed on the installed-plugin set. Parsing the rendered HTML keeps anchor ids identical to the live DOM, so the index cannot drift. - settings-search.js: buildIndex() now does a single fetch of the literal endpoint + .json(); removed the per-partial fetch loop, DOMParser, scanDoc, CORE_TABS, and the plugin-id allowlist. Search, keyboard nav, navigation, and the per-tab filter are unchanged. Net: fewer requests and no client-side HTML parsing. Verified with a new endpoint test in test_web_settings_ui.py (12 pass) and the headless-Chromium test (tooltip, filter, search navigate + flash all green). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gZxznuxw8L92FUMBN3Nqz * Address CodeRabbit review on settings search/tooltips - pages_v3: include Durations tab in the search index so setting-durations-* fields are actually indexed - test_web_settings_ui: assert setting-durations-clock is present in the search-index endpoint response - settings-search.js: on index fetch failure, reset buildPromise instead of caching an empty (truthy) index so search can retry - settings-search.js: filterScope returns null (not document) when no tab container matches, and the caller guards, so the per-tab filter can't hide fields across unrelated tabs - settings-search.js: refresh the stale header comment to describe the server-side JSON index flow - app.css: cap #settings-search-results height with overflow-y so the dropdown scrolls instead of overflowing small screens Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gZxznuxw8L92FUMBN3Nqz * Fix stuck search dropdown; add plugin-tab nested-settings filter Global search: - Close the results dropdown on input blur (guarded, with a short delay) so it reliably dismisses when focus leaves — previously it could linger because the only outside-close was a document click that Alpine/HTMX handlers can swallow. - Clear the query text after navigating to a result so refocusing the box doesn't re-open stale results. - Also dismiss on htmx:afterSwap (tab changes / navigation). Per-tab filter (now on plugin tabs too): - Render the shared settings_filter box in the plugin Configuration panel. It auto-wires: the delegated input handler and filterScope already target .plugin-config-tab. - Teach applyTabFilter to reveal matches inside collapsed nested sections (render_nested_section defaults them shut), hide nested-section wrappers with no matches, and restore the original collapsed layout when cleared (only re-collapsing sections the filter itself opened). - Count a visible nested-section as content for its parent heading so the heading isn't hidden while a subsection below still has matches. Adds a plugin-config render test (filter box + nested anchors + tooltips). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gZxznuxw8L92FUMBN3Nqz * Close search dropdown via capture-phase outside-click The dropdown could stay open after clicking away because the only outside-click close was a bubble-phase document listener. The v3 UI is one Alpine app() component full of HTMX/Alpine/widget click handlers; when a click lands inside an element that calls stopPropagation(), the event never bubbles to document and the close never runs. - Replace the bubble-phase document 'click' close with a capture-phase 'pointerdown' listener scoped to #settings-search-wrap. Capture runs before any bubbling stopPropagation can swallow the event, so it always fires; pointerdown also covers touch on the Pi screen. Clicking a result stays inside the wrap, so selection is unaffected. - Guard the debounced input handler so a delayed render can't re-open the box after focus has left (type-then-click-away race). Keeps the existing blur / Escape / htmx:afterSwap closes as secondary paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gZxznuxw8L92FUMBN3Nqz * Fix settings search dropdown not visually closing .hidden has no effect in this app: app.css is a hand-picked utility subset (no Tailwind build step) and never defines .hidden { display: none }. openResults()/closeResults() only toggled the class, so the dropdown stayed rendered (display: block) even once closeResults() ran - confirmed via computed style in a headless browser. Set style.display directly, matching the fallback already used by revealNode()/collapseNode() elsewhere in this file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -882,6 +882,25 @@
|
||||
|
||||
<!-- Connection status and theme toggle -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- Global settings search -->
|
||||
<div class="relative hidden sm:block" id="settings-search-wrap">
|
||||
<input id="settings-search"
|
||||
type="text"
|
||||
role="combobox"
|
||||
aria-expanded="false"
|
||||
aria-autocomplete="list"
|
||||
aria-controls="settings-search-results"
|
||||
aria-label="Search settings"
|
||||
placeholder="Search settings…"
|
||||
autocomplete="off"
|
||||
class="form-control text-sm pl-8 pr-4 py-1.5 w-48 lg:w-64">
|
||||
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-xs" aria-hidden="true"></i>
|
||||
<div id="settings-search-results"
|
||||
role="listbox"
|
||||
aria-label="Settings search results"
|
||||
class="hidden absolute right-0 mt-1 w-80 max-h-96 overflow-y-auto"></div>
|
||||
</div>
|
||||
|
||||
<!-- Theme toggle -->
|
||||
<button id="theme-toggle"
|
||||
type="button"
|
||||
@@ -4747,7 +4766,11 @@
|
||||
|
||||
<!-- Custom v3 JavaScript -->
|
||||
<script src="{{ url_for('static', filename='v3/app.js') }}" defer></script>
|
||||
|
||||
|
||||
<!-- Settings tooltips + settings search -->
|
||||
<script src="{{ url_for('static', filename='v3/js/tooltips.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/settings-search.js') }}" defer></script>
|
||||
|
||||
<!-- Modular Plugin Management JavaScript -->
|
||||
<!-- Load utilities first -->
|
||||
<script src="{{ url_for('static', filename='v3/js/utils/error_handler.js') }}" defer></script>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{# ============================================================================ #}
|
||||
{# Shared UI macros for the v3 web interface. #}
|
||||
{# #}
|
||||
{# Import at the top of a partial with: #}
|
||||
{# {% import 'v3/partials/_macros.html' as ui %} #}
|
||||
{# #}
|
||||
{# These power the settings tooltips and the settings search feature: #}
|
||||
{# - help_tip(text, label): the (i) info icon whose hover/focus tooltip #}
|
||||
{# explains a setting. This replaces the old always-visible <p> help. #}
|
||||
{# - fg_id(tab, key): stable anchor id for a .form-group so global search #}
|
||||
{# can scroll to it (e.g. "setting-display-brightness"). #}
|
||||
{# - settings_filter(): the per-tab filter box shown under a partial title. #}
|
||||
{# ============================================================================ #}
|
||||
|
||||
{# Info (i) tooltip trigger placed next to a setting label. #}
|
||||
{# `text` supports "\n" line breaks (rendered via CSS white-space: pre-line). #}
|
||||
{# Renders nothing when `text` is empty so callers can pass through schema data. #}
|
||||
{% macro help_tip(text, label='') -%}
|
||||
{%- if text -%}
|
||||
<button type="button" class="help-tip" data-tooltip="{{ text }}"
|
||||
aria-label="{% if label %}Help for {{ label }}{% else %}More information{% endif %}">
|
||||
<i class="fas fa-circle-info" aria-hidden="true"></i>
|
||||
</button>
|
||||
{%- endif -%}
|
||||
{%- endmacro %}
|
||||
|
||||
{# Stable anchor id for a settings field's .form-group wrapper. #}
|
||||
{% macro fg_id(tab, key) -%}setting-{{ tab }}-{{ key }}{%- endmacro %}
|
||||
|
||||
{# Per-tab filter box. Place directly under a partial's <h2> title block. #}
|
||||
{# The delegated handler in settings-search.js scopes to the enclosing tab. #}
|
||||
{% macro settings_filter(placeholder='Filter these settings…') -%}
|
||||
<div class="settings-filter-wrap relative mb-6">
|
||||
<input type="text"
|
||||
class="settings-filter form-control text-sm pl-9 pr-4 py-2 w-full"
|
||||
placeholder="{{ placeholder }}"
|
||||
aria-label="Filter settings on this tab"
|
||||
autocomplete="off">
|
||||
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-sm" aria-hidden="true"></i>
|
||||
<p class="settings-filter-empty text-sm text-gray-500 mt-3 hidden">No settings match your filter.</p>
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
@@ -1,3 +1,4 @@
|
||||
{% import 'v3/partials/_macros.html' as ui %}
|
||||
<div class="space-y-6" id="backup-restore-root">
|
||||
|
||||
<!-- Security warning -->
|
||||
@@ -70,12 +71,12 @@
|
||||
|
||||
<h3 class="text-sm font-medium text-gray-900 mt-4 mb-2">Choose what to restore</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm text-gray-700">
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="opt-config" checked> <span>Main configuration</span></label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="opt-secrets" checked> <span>API keys (secrets)</span></label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="opt-wifi" checked> <span>WiFi configuration</span></label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="opt-fonts" checked> <span>User-uploaded fonts</span></label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="opt-plugin-uploads" checked> <span>Plugin image uploads</span></label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="opt-reinstall" checked> <span>Reinstall missing plugins</span></label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="opt-config" checked> <span>Main configuration</span>{{ ui.help_tip('Restore config.json — display settings, schedules, location, and plugin configuration.', 'Main configuration') }}</label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="opt-secrets" checked> <span>API keys (secrets)</span>{{ ui.help_tip('Restore config_secrets.json — your weather, sports, and other service API keys.\nLeave off if you prefer to re-enter keys by hand.', 'API keys') }}</label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="opt-wifi" checked> <span>WiFi configuration</span>{{ ui.help_tip('Restore saved WiFi network names and credentials.', 'WiFi configuration') }}</label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="opt-fonts" checked> <span>User-uploaded fonts</span>{{ ui.help_tip('Restore any custom fonts you uploaded via the Fonts tab.', 'User-uploaded fonts') }}</label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="opt-plugin-uploads" checked> <span>Plugin image uploads</span>{{ ui.help_tip('Restore images and files that plugins let you upload (logos, backgrounds, etc.).', 'Plugin image uploads') }}</label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="opt-reinstall" checked> <span>Reinstall missing plugins</span>{{ ui.help_tip('After restoring, re-download any plugins that were installed in the backup but are missing now.', 'Reinstall missing plugins') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex gap-2">
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
{% 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">Display Settings</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">Configure LED matrix hardware settings and display options.</p>
|
||||
</div>
|
||||
|
||||
{{ ui.settings_filter('Filter display settings…') }}
|
||||
|
||||
<!-- Hardware status banner: shown when display service is in fallback/simulation mode -->
|
||||
<div x-data="{ show: false, errorMsg: '' }"
|
||||
x-init="fetch('/api/v3/hardware/status').then(r => r.json()).then(d => {
|
||||
@@ -37,8 +40,8 @@
|
||||
<h3 class="text-md font-medium text-gray-900 mb-4">Hardware Configuration</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-4 gap-4 mb-4">
|
||||
<div class="form-group">
|
||||
<label for="rows" class="block text-sm font-medium text-gray-700">Rows</label>
|
||||
<div class="form-group" id="setting-display-rows" data-setting-key="display.hardware.rows">
|
||||
<label for="rows" class="block text-sm font-medium text-gray-700">Rows{{ ui.help_tip('Number of LED rows on a single panel.\nCommon: 16, 32, or 64. Default: 32. Must match your panel.', 'Rows') }}</label>
|
||||
<input type="number"
|
||||
id="rows"
|
||||
name="rows"
|
||||
@@ -46,11 +49,10 @@
|
||||
min="1"
|
||||
max="64"
|
||||
class="form-control">
|
||||
<p class="mt-1 text-sm text-gray-600">Number of LED rows</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="cols" class="block text-sm font-medium text-gray-700">Columns</label>
|
||||
<div class="form-group" id="setting-display-cols" data-setting-key="display.hardware.cols">
|
||||
<label for="cols" class="block text-sm font-medium text-gray-700">Columns{{ ui.help_tip('Number of LED columns on a single panel.\nCommon: 32 or 64. Default: 64. Must match your panel.', 'Columns') }}</label>
|
||||
<input type="number"
|
||||
id="cols"
|
||||
name="cols"
|
||||
@@ -58,11 +60,10 @@
|
||||
min="1"
|
||||
max="128"
|
||||
class="form-control">
|
||||
<p class="mt-1 text-sm text-gray-600">Number of LED columns</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="chain_length" class="block text-sm font-medium text-gray-700">Chain Length</label>
|
||||
<div class="form-group" id="setting-display-chain_length" data-setting-key="display.hardware.chain_length">
|
||||
<label for="chain_length" class="block text-sm font-medium text-gray-700">Chain Length{{ ui.help_tip('How many panels are wired end-to-end in one chain.\nDefault: 2. Example: two 64×32 panels chained make a 128×32 display.', 'Chain Length') }}</label>
|
||||
<input type="number"
|
||||
id="chain_length"
|
||||
name="chain_length"
|
||||
@@ -70,11 +71,10 @@
|
||||
min="1"
|
||||
max="24"
|
||||
class="form-control">
|
||||
<p class="mt-1 text-sm text-gray-600">Number of LED panels chained together</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="parallel" class="block text-sm font-medium text-gray-700">Parallel</label>
|
||||
<div class="form-group" id="setting-display-parallel" data-setting-key="display.hardware.parallel">
|
||||
<label for="parallel" class="block text-sm font-medium text-gray-700">Parallel{{ ui.help_tip('Number of separate chains driven in parallel from the HAT.\nDefault: 1. The Raspberry Pi supports up to 3 (some HATs allow more).', 'Parallel') }}</label>
|
||||
<input type="number"
|
||||
id="parallel"
|
||||
name="parallel"
|
||||
@@ -82,13 +82,12 @@
|
||||
min="1"
|
||||
max="4"
|
||||
class="form-control">
|
||||
<p class="mt-1 text-sm text-gray-600">Number of parallel chains</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-group">
|
||||
<label for="brightness" class="block text-sm font-medium text-gray-700">Brightness</label>
|
||||
<div class="form-group" id="setting-display-brightness" data-setting-key="display.hardware.brightness">
|
||||
<label for="brightness" class="block text-sm font-medium text-gray-700">Brightness{{ ui.help_tip('Overall LED brightness (1–100%).\nLower is dimmer, higher is brighter. Recommended: 70–90 indoors, 90–100 in bright rooms.', 'Brightness') }}</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="range"
|
||||
id="brightness"
|
||||
@@ -99,11 +98,10 @@
|
||||
class="flex-1">
|
||||
<span id="brightness-value" class="text-sm font-medium w-12">{{ main_config.display.hardware.brightness or 95 }}</span>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-600">LED brightness: <span id="brightness-display">{{ main_config.display.hardware.brightness or 95 }}</span>%</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="hardware_mapping" class="block text-sm font-medium text-gray-700">Hardware Mapping</label>
|
||||
<div class="form-group" id="setting-display-hardware_mapping" data-setting-key="display.hardware.hardware_mapping">
|
||||
<label for="hardware_mapping" class="block text-sm font-medium text-gray-700">Hardware Mapping{{ ui.help_tip('How the LED panel is wired to the Pi.\nUse "Adafruit HAT PWM" for an Adafruit HAT/Bonnet with the PWM solder mod; "Adafruit HAT" without it; "Regular" for direct GPIO wiring.', 'Hardware Mapping') }}</label>
|
||||
<select id="hardware_mapping" name="hardware_mapping" class="form-control">
|
||||
<option value="adafruit-hat-pwm" {% if main_config.display.hardware.hardware_mapping == "adafruit-hat-pwm" %}selected{% endif %}>Adafruit HAT PWM</option>
|
||||
<option value="adafruit-hat" {% if main_config.display.hardware.hardware_mapping == "adafruit-hat" %}selected{% endif %}>Adafruit HAT</option>
|
||||
@@ -114,8 +112,8 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="form-group">
|
||||
<label for="led_rgb_sequence" class="block text-sm font-medium text-gray-700">LED RGB Sequence</label>
|
||||
<div class="form-group" id="setting-display-led_rgb_sequence" data-setting-key="display.hardware.led_rgb_sequence">
|
||||
<label for="led_rgb_sequence" class="block text-sm font-medium text-gray-700">LED RGB Sequence{{ ui.help_tip('Order the panel expects color channels in.\nChange this only if reds/greens/blues look swapped. Default: RGB.', 'LED RGB Sequence') }}</label>
|
||||
<select id="led_rgb_sequence" name="led_rgb_sequence" class="form-control">
|
||||
<option value="RGB" {% if main_config.display.hardware.get('led_rgb_sequence', 'RGB') == "RGB" %}selected{% endif %}>RGB</option>
|
||||
<option value="RBG" {% if main_config.display.hardware.get('led_rgb_sequence', 'RGB') == "RBG" %}selected{% endif %}>RBG</option>
|
||||
@@ -124,11 +122,10 @@
|
||||
<option value="BRG" {% if main_config.display.hardware.get('led_rgb_sequence', 'RGB') == "BRG" %}selected{% endif %}>BRG</option>
|
||||
<option value="BGR" {% if main_config.display.hardware.get('led_rgb_sequence', 'RGB') == "BGR" %}selected{% endif %}>BGR</option>
|
||||
</select>
|
||||
<p class="mt-1 text-sm text-gray-600">Color channel order for your LED panels</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="multiplexing" class="block text-sm font-medium text-gray-700">Multiplexing</label>
|
||||
<div class="form-group" id="setting-display-multiplexing" data-setting-key="display.hardware.multiplexing">
|
||||
<label for="multiplexing" class="block text-sm font-medium text-gray-700">Multiplexing{{ ui.help_tip('Pixel-mapping scheme used by outdoor/specialty panels.\nLeave at 0 (Direct) for most indoor panels. Only change if the image is scrambled — try values until it looks right.', 'Multiplexing') }}</label>
|
||||
<select id="multiplexing" name="multiplexing" class="form-control">
|
||||
<option value="0" {% if main_config.display.hardware.get('multiplexing', 0)|int == 0 %}selected{% endif %}>0 - Direct</option>
|
||||
<option value="1" {% if main_config.display.hardware.get('multiplexing', 0)|int == 1 %}selected{% endif %}>1 - Stripe</option>
|
||||
@@ -154,21 +151,19 @@
|
||||
<option value="21" {% if main_config.display.hardware.get('multiplexing', 0)|int == 21 %}selected{% endif %}>21 - DoubleZMultiplex</option>
|
||||
<option value="22" {% if main_config.display.hardware.get('multiplexing', 0)|int == 22 %}selected{% endif %}>22 - P4Outdoor-80x40</option>
|
||||
</select>
|
||||
<p class="mt-1 text-sm text-gray-600">Multiplexing scheme for your LED panels</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="panel_type" class="block text-sm font-medium text-gray-700">Panel Type</label>
|
||||
<div class="form-group" id="setting-display-panel_type" data-setting-key="display.hardware.panel_type">
|
||||
<label for="panel_type" class="block text-sm font-medium text-gray-700">Panel Type{{ ui.help_tip('Special initialization for panels with a specific driver chip (e.g. FM6126A, FM6127).\nLeave on Standard unless your panel stays blank or shows only the first pixel.', 'Panel Type') }}</label>
|
||||
<select id="panel_type" name="panel_type" class="form-control">
|
||||
<option value="" {% if not main_config.display.hardware.get('panel_type', '') %}selected{% endif %}>Standard</option>
|
||||
<option value="FM6126A" {% if main_config.display.hardware.get('panel_type', '') == "FM6126A" %}selected{% endif %}>FM6126A</option>
|
||||
<option value="FM6127" {% if main_config.display.hardware.get('panel_type', '') == "FM6127" %}selected{% endif %}>FM6127</option>
|
||||
</select>
|
||||
<p class="mt-1 text-sm text-gray-600">Special panel chipset initialization (use Standard unless your panel requires it)</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="row_address_type" class="block text-sm font-medium text-gray-700">Row Address Type</label>
|
||||
<div class="form-group" id="setting-display-row_address_type" data-setting-key="display.hardware.row_address_type">
|
||||
<label for="row_address_type" class="block text-sm font-medium text-gray-700">Row Address Type{{ ui.help_tip('Row addressing scheme used by the panel.\nLeave at 0 (Default) unless your panel needs AB/ABC addressing — a wrong value shows a garbled or shifted image.', 'Row Address Type') }}</label>
|
||||
<select id="row_address_type" name="row_address_type" class="form-control">
|
||||
<option value="0" {% if main_config.display.hardware.get('row_address_type', 0)|int == 0 %}selected{% endif %}>0 - Default</option>
|
||||
<option value="1" {% if main_config.display.hardware.get('row_address_type', 0)|int == 1 %}selected{% endif %}>1 - AB-addressed panels</option>
|
||||
@@ -176,13 +171,12 @@
|
||||
<option value="3" {% if main_config.display.hardware.get('row_address_type', 0)|int == 3 %}selected{% endif %}>3 - ABC-addressed panels</option>
|
||||
<option value="4" {% if main_config.display.hardware.get('row_address_type', 0)|int == 4 %}selected{% endif %}>4 - ABC Shift + DE direct</option>
|
||||
</select>
|
||||
<p class="mt-1 text-sm text-gray-600">Row addressing scheme — leave at Default (0) unless your panel requires a specific type</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="form-group">
|
||||
<label for="gpio_slowdown" class="block text-sm font-medium text-gray-700">GPIO Slowdown</label>
|
||||
<div class="form-group" id="setting-display-gpio_slowdown" data-setting-key="display.runtime.gpio_slowdown">
|
||||
<label for="gpio_slowdown" class="block text-sm font-medium text-gray-700">GPIO Slowdown{{ ui.help_tip('Slows the GPIO signal so the panel keeps up.\nGuide: Pi 3 → 1–2, Pi 4 → 2–4, Pi 5 (PIO) → 1–3. Increase if the display shows garbage or flicker; in RIO mode higher values may improve performance.', 'GPIO Slowdown') }}</label>
|
||||
<input type="number"
|
||||
id="gpio_slowdown"
|
||||
name="gpio_slowdown"
|
||||
@@ -190,22 +184,20 @@
|
||||
min="0"
|
||||
max="10"
|
||||
class="form-control">
|
||||
<p class="mt-1 text-sm text-gray-600">Pi 3: 1–2 · Pi 4: 2–4 · Pi 5 PIO: 1–3. Increase if display shows garbage; in RIO mode higher values may improve performance.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group" id="setting-display-rp1_rio" data-setting-key="display.runtime.rp1_rio">
|
||||
<label for="rp1_rio" class="block text-sm font-medium text-gray-700">
|
||||
RP1 Backend <span class="text-xs text-gray-400 font-normal">(Pi 5 only)</span>
|
||||
RP1 Backend <span class="text-xs text-gray-400 font-normal">(Pi 5 only)</span>{{ ui.help_tip('Pi 5 RP1 coprocessor driver mode.\nPIO (0) is the default and uses less CPU. RIO (1) can push a higher refresh rate but inverts the GPIO Slowdown behavior. Ignored on Pi 3/4.', 'RP1 Backend') }}
|
||||
</label>
|
||||
<select id="rp1_rio" name="rp1_rio" class="form-control">
|
||||
<option value="0" {% if main_config.display.get('runtime', {}).get('rp1_rio', 0)|int == 0 %}selected{% endif %}>0 — PIO (default, low CPU)</option>
|
||||
<option value="1" {% if main_config.display.get('runtime', {}).get('rp1_rio', 0)|int == 1 %}selected{% endif %}>1 — RIO (higher throughput; slowdown inverted)</option>
|
||||
</select>
|
||||
<p class="mt-1 text-sm text-gray-600">Pi 5 RP1 coprocessor mode. Ignored on Pi 3/4.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="scan_mode" class="block text-sm font-medium text-gray-700">Scan Mode</label>
|
||||
<div class="form-group" id="setting-display-scan_mode" data-setting-key="display.hardware.scan_mode">
|
||||
<label for="scan_mode" class="block text-sm font-medium text-gray-700">Scan Mode{{ ui.help_tip('Order rows are refreshed in.\n0 = progressive (default), 1 = interlaced. Change only if you see banding or flicker on certain panels.', 'Scan Mode') }}</label>
|
||||
<input type="number"
|
||||
id="scan_mode"
|
||||
name="scan_mode"
|
||||
@@ -213,13 +205,12 @@
|
||||
min="0"
|
||||
max="1"
|
||||
class="form-control">
|
||||
<p class="mt-1 text-sm text-gray-600">Scan mode for LED matrix (0-1)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-group">
|
||||
<label for="pwm_bits" class="block text-sm font-medium text-gray-700">PWM Bits</label>
|
||||
<div class="form-group" id="setting-display-pwm_bits" data-setting-key="display.hardware.pwm_bits">
|
||||
<label for="pwm_bits" class="block text-sm font-medium text-gray-700">PWM Bits{{ ui.help_tip('Color depth per channel (1–11).\nHigher means smoother color but a lower refresh rate; lower means faster refresh with more banding. Default: 11 (this build defaults to 9).', 'PWM Bits') }}</label>
|
||||
<input type="number"
|
||||
id="pwm_bits"
|
||||
name="pwm_bits"
|
||||
@@ -227,11 +218,10 @@
|
||||
min="1"
|
||||
max="11"
|
||||
class="form-control">
|
||||
<p class="mt-1 text-sm text-gray-600">PWM bits for brightness control (1-11)</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pwm_dither_bits" class="block text-sm font-medium text-gray-700">PWM Dither Bits</label>
|
||||
<div class="form-group" id="setting-display-pwm_dither_bits" data-setting-key="display.hardware.pwm_dither_bits">
|
||||
<label for="pwm_dither_bits" class="block text-sm font-medium text-gray-700">PWM Dither Bits{{ ui.help_tip('Time-dithering to gain apparent color depth (0–4).\nDefault: 0. Raising it can smooth gradients at the cost of a slightly lower refresh rate.', 'PWM Dither Bits') }}</label>
|
||||
<input type="number"
|
||||
id="pwm_dither_bits"
|
||||
name="pwm_dither_bits"
|
||||
@@ -239,13 +229,12 @@
|
||||
min="0"
|
||||
max="4"
|
||||
class="form-control">
|
||||
<p class="mt-1 text-sm text-gray-600">PWM dither bits (0-4)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-group">
|
||||
<label for="pwm_lsb_nanoseconds" class="block text-sm font-medium text-gray-700">PWM LSB Nanoseconds</label>
|
||||
<div class="form-group" id="setting-display-pwm_lsb_nanoseconds" data-setting-key="display.hardware.pwm_lsb_nanoseconds">
|
||||
<label for="pwm_lsb_nanoseconds" class="block text-sm font-medium text-gray-700">PWM LSB Nanoseconds{{ ui.help_tip('Base time for the least-significant color bit (50–500 ns).\nDefault: 130. Raising it can reduce flicker on some panels but lowers the maximum refresh rate.', 'PWM LSB Nanoseconds') }}</label>
|
||||
<input type="number"
|
||||
id="pwm_lsb_nanoseconds"
|
||||
name="pwm_lsb_nanoseconds"
|
||||
@@ -253,11 +242,10 @@
|
||||
min="50"
|
||||
max="500"
|
||||
class="form-control">
|
||||
<p class="mt-1 text-sm text-gray-600">PWM LSB nanoseconds (50-500)</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="limit_refresh_rate_hz" class="block text-sm font-medium text-gray-700">Limit Refresh Rate (Hz)</label>
|
||||
<div class="form-group" id="setting-display-limit_refresh_rate_hz" data-setting-key="display.hardware.limit_refresh_rate_hz">
|
||||
<label for="limit_refresh_rate_hz" class="block text-sm font-medium text-gray-700">Limit Refresh Rate (Hz){{ ui.help_tip('Caps the panel refresh rate (1–1000 Hz).\nDefault: 120. A steady cap reduces flicker in camera recordings and keeps timing consistent. Set higher or to the max your panel supports for the smoothest motion.', 'Limit Refresh Rate') }}</label>
|
||||
<input type="number"
|
||||
id="limit_refresh_rate_hz"
|
||||
name="limit_refresh_rate_hz"
|
||||
@@ -265,7 +253,6 @@
|
||||
min="1"
|
||||
max="1000"
|
||||
class="form-control">
|
||||
<p class="mt-1 text-sm text-gray-600">Limit refresh rate in Hz (1-1000)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -276,7 +263,7 @@
|
||||
<p class="text-sm text-gray-600 mb-4">Show the same content on every panel in the chain — e.g. two 64×32 panels mirrored, or four panels as two identical screens. Rendered once and duplicated, so it adds no extra CPU. Takes effect after a display restart.</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="form-group">
|
||||
<div class="form-group" id="setting-display-double_sided_enabled" data-setting-key="display.double_sided.enabled">
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="checkbox"
|
||||
id="double_sided_enabled"
|
||||
@@ -285,12 +272,12 @@
|
||||
{% if main_config.display.get('double_sided', {}).get('enabled') %}checked{% endif %}
|
||||
class="form-control h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
<span class="text-sm font-medium text-gray-700">Enabled</span>
|
||||
{{ ui.help_tip('Show the same content mirrored across every panel in the chain.\nRendered once and duplicated, so it adds no extra CPU. Takes effect after a display restart.', 'Double-Sided Enabled') }}
|
||||
</label>
|
||||
<p class="mt-1 text-sm text-gray-600">Mirror one screen across all panels.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="double_sided_copies" class="block text-sm font-medium text-gray-700">Copies</label>
|
||||
<div class="form-group" id="setting-display-double_sided_copies" data-setting-key="display.double_sided.copies">
|
||||
<label for="double_sided_copies" class="block text-sm font-medium text-gray-700">Copies{{ ui.help_tip('How many identical screens to split the panel area into (2–8).\nMust divide the panel evenly — e.g. 2 for a two-sided cube.', 'Copies') }}</label>
|
||||
<input type="number"
|
||||
id="double_sided_copies"
|
||||
name="double_sided_copies"
|
||||
@@ -298,16 +285,14 @@
|
||||
min="2"
|
||||
max="8"
|
||||
class="form-control">
|
||||
<p class="mt-1 text-sm text-gray-600">Number of identical screens. Must divide the panel evenly.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="double_sided_axis" class="block text-sm font-medium text-gray-700">Split Axis</label>
|
||||
<div class="form-group" id="setting-display-double_sided_axis" data-setting-key="display.double_sided.axis">
|
||||
<label for="double_sided_axis" class="block text-sm font-medium text-gray-700">Split Axis{{ ui.help_tip('Direction the display is divided into copies.\nHorizontal splits along the chained panels (side by side); Vertical splits along parallel chains (stacked).', 'Split Axis') }}</label>
|
||||
<select id="double_sided_axis" name="double_sided_axis" class="form-control">
|
||||
<option value="horizontal" {% if main_config.display.get('double_sided', {}).get('axis', 'horizontal') == 'horizontal' %}selected{% endif %}>Horizontal — chained panels (side by side)</option>
|
||||
<option value="vertical" {% if main_config.display.get('double_sided', {}).get('axis', 'horizontal') == 'vertical' %}selected{% endif %}>Vertical — parallel chains (stacked)</option>
|
||||
</select>
|
||||
<p class="mt-1 text-sm text-gray-600">Horizontal splits the chain; vertical splits parallel outputs.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -317,7 +302,7 @@
|
||||
<h3 class="text-md font-medium text-gray-900 mb-4">Display Options</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-group">
|
||||
<div class="form-group" id="setting-display-disable_hardware_pulsing" data-setting-key="display.hardware.disable_hardware_pulsing">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="disable_hardware_pulsing"
|
||||
@@ -325,10 +310,11 @@
|
||||
{% if main_config.display.hardware.disable_hardware_pulsing %}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">Disable Hardware Pulsing</span>
|
||||
{{ ui.help_tip('Turn off hardware PWM pulsing.\nEnable this if the Pi audio is in use or you hear buzzing / see instability. Slightly increases CPU usage.', 'Disable Hardware Pulsing') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group" id="setting-display-inverse_colors" data-setting-key="display.hardware.inverse_colors">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="inverse_colors"
|
||||
@@ -336,10 +322,11 @@
|
||||
{% if main_config.display.hardware.inverse_colors %}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">Inverse Colors</span>
|
||||
{{ ui.help_tip('Invert every color the panel shows.\nDefault: off. Only needed for panels wired with inverted color logic.', 'Inverse Colors') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group" id="setting-display-show_refresh_rate" data-setting-key="display.hardware.show_refresh_rate">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="show_refresh_rate"
|
||||
@@ -347,10 +334,11 @@
|
||||
{% if main_config.display.hardware.show_refresh_rate %}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">Show Refresh Rate</span>
|
||||
{{ ui.help_tip('Overlay the live panel refresh rate on the display.\nUseful for tuning GPIO Slowdown and PWM settings; turn off for normal use.', 'Show Refresh Rate') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group" id="setting-display-use_short_date_format" data-setting-key="display.use_short_date_format">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="use_short_date_format"
|
||||
@@ -358,6 +346,7 @@
|
||||
{% if main_config.display.use_short_date_format %}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">Use Short Date Format</span>
|
||||
{{ ui.help_tip('Show dates in a compact form (e.g. 7/8 instead of July 8).\nHandy on narrow displays where space is tight.', 'Use Short Date Format') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -366,8 +355,8 @@
|
||||
<div class="mt-6 pt-4 border-t border-gray-300">
|
||||
<h4 class="text-sm font-medium text-gray-900 mb-3">Dynamic Duration</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-group">
|
||||
<label for="max_dynamic_duration_seconds" class="block text-sm font-medium text-gray-700">Max Dynamic Duration (seconds)</label>
|
||||
<div class="form-group" id="setting-display-max_dynamic_duration_seconds" data-setting-key="display.dynamic_duration.max_duration_seconds">
|
||||
<label for="max_dynamic_duration_seconds" class="block text-sm font-medium text-gray-700">Max Dynamic Duration (seconds){{ ui.help_tip('Ceiling on how long a plugin may extend its own on-screen time (30–1800s).\nDefault: 180. Plugins with live content (e.g. a game in progress) can request extra time up to this limit.', 'Max Dynamic Duration') }}</label>
|
||||
<input type="number"
|
||||
id="max_dynamic_duration_seconds"
|
||||
name="max_dynamic_duration_seconds"
|
||||
@@ -375,7 +364,6 @@
|
||||
min="30"
|
||||
max="1800"
|
||||
class="form-control">
|
||||
<p class="mt-1 text-sm text-gray-600">Maximum time plugins can extend display duration (30-1800 seconds)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -405,8 +393,8 @@
|
||||
<!-- Vegas Settings (shown when enabled) -->
|
||||
<div id="vegas_scroll_settings" class="space-y-4" style="{% if not main_config.display.get('vegas_scroll', {}).get('enabled', false) %}display: none;{% endif %}">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-group">
|
||||
<label for="vegas_scroll_speed" class="block text-sm font-medium text-gray-700">Scroll Speed (pixels/second)</label>
|
||||
<div class="form-group" id="setting-display-vegas_scroll_speed" data-setting-key="display.vegas_scroll.scroll_speed">
|
||||
<label for="vegas_scroll_speed" class="block text-sm font-medium text-gray-700">Scroll Speed (pixels/second){{ ui.help_tip('How fast the Vegas ticker scrolls (10–200 px/s).\nDefault: 50. Higher is faster but harder to read.', 'Scroll Speed') }}</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="range"
|
||||
id="vegas_scroll_speed"
|
||||
@@ -418,11 +406,10 @@
|
||||
class="flex-1">
|
||||
<span id="vegas_scroll_speed_value" class="text-sm font-medium w-12">{{ main_config.display.get('vegas_scroll', {}).get('scroll_speed', 50) }}</span>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-600">Speed of the scrolling ticker (10-200 px/s)</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="vegas_separator_width" class="block text-sm font-medium text-gray-700">Separator Width (pixels)</label>
|
||||
<div class="form-group" id="setting-display-vegas_separator_width" data-setting-key="display.vegas_scroll.separator_width">
|
||||
<label for="vegas_separator_width" class="block text-sm font-medium text-gray-700">Separator Width (pixels){{ ui.help_tip('Blank gap inserted between each plugin block in the ticker (0–128 px).\nDefault: 32. Larger values make the boundary between plugins clearer.', 'Separator Width') }}</label>
|
||||
<input type="number"
|
||||
id="vegas_separator_width"
|
||||
name="vegas_separator_width"
|
||||
@@ -430,29 +417,26 @@
|
||||
min="0"
|
||||
max="128"
|
||||
class="form-control">
|
||||
<p class="mt-1 text-sm text-gray-600">Gap between plugin content blocks (0-128 px)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-group">
|
||||
<label for="vegas_target_fps" class="block text-sm font-medium text-gray-700">Target FPS</label>
|
||||
<div class="form-group" id="setting-display-vegas_target_fps" data-setting-key="display.vegas_scroll.target_fps">
|
||||
<label for="vegas_target_fps" class="block text-sm font-medium text-gray-700">Target FPS{{ ui.help_tip('Frames per second the Vegas ticker aims to render.\nHigher = smoother scrolling but more CPU. Default: 125 (smoothest). Drop to 60/90 if the Pi runs hot.', 'Target FPS') }}</label>
|
||||
<select id="vegas_target_fps" name="vegas_target_fps" class="form-control">
|
||||
<option value="60" {% if main_config.display.get('vegas_scroll', {}).get('target_fps', 125) == 60 %}selected{% endif %}>60 FPS (Lower CPU)</option>
|
||||
<option value="90" {% if main_config.display.get('vegas_scroll', {}).get('target_fps', 125) == 90 %}selected{% endif %}>90 FPS (Balanced)</option>
|
||||
<option value="125" {% if main_config.display.get('vegas_scroll', {}).get('target_fps', 125) == 125 %}selected{% endif %}>125 FPS (Smoothest)</option>
|
||||
</select>
|
||||
<p class="mt-1 text-sm text-gray-600">Higher FPS = smoother scroll, more CPU usage</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="vegas_buffer_ahead" class="block text-sm font-medium text-gray-700">Buffer Ahead</label>
|
||||
<div class="form-group" id="setting-display-vegas_buffer_ahead" data-setting-key="display.vegas_scroll.buffer_ahead">
|
||||
<label for="vegas_buffer_ahead" class="block text-sm font-medium text-gray-700">Buffer Ahead{{ ui.help_tip('How many upcoming plugins to pre-render so the scroll never stalls.\nDefault: 2 (recommended). More uses extra memory; less saves memory but risks hitches.', 'Buffer Ahead') }}</label>
|
||||
<select id="vegas_buffer_ahead" name="vegas_buffer_ahead" class="form-control">
|
||||
<option value="1" {% if main_config.display.get('vegas_scroll', {}).get('buffer_ahead', 2) == 1 %}selected{% endif %}>1 Plugin (Less memory)</option>
|
||||
<option value="2" {% if main_config.display.get('vegas_scroll', {}).get('buffer_ahead', 2) == 2 %}selected{% endif %}>2 Plugins (Recommended)</option>
|
||||
<option value="3" {% if main_config.display.get('vegas_scroll', {}).get('buffer_ahead', 2) == 3 %}selected{% endif %}>3 Plugins (More buffer)</option>
|
||||
</select>
|
||||
<p class="mt-1 text-sm text-gray-600">How many plugins to pre-load ahead</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -485,18 +469,17 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-group">
|
||||
<label for="sync_role" class="block text-sm font-medium text-gray-700">Role</label>
|
||||
<div class="form-group" id="setting-display-sync_role" data-setting-key="sync.role">
|
||||
<label for="sync_role" class="block text-sm font-medium text-gray-700">Role{{ ui.help_tip('This unit\'s part in a two-display setup.\nStandalone = sync off. Set one Pi to Leader (drives the scroll) and the other to Follower (receives frames). Restart required after changing.', 'Sync Role') }}</label>
|
||||
<select id="sync_role" name="sync_role" class="form-control" onchange="updateSyncUI()">
|
||||
<option value="standalone" {% if main_config.get('sync', {}).get('role', 'standalone') == 'standalone' %}selected{% endif %}>Standalone (disabled)</option>
|
||||
<option value="leader" {% if main_config.get('sync', {}).get('role', 'standalone') == 'leader' %}selected{% endif %}>Leader (drives scroll)</option>
|
||||
<option value="follower" {% if main_config.get('sync', {}).get('role', 'standalone') == 'follower' %}selected{% endif %}>Follower (receives frames)</option>
|
||||
</select>
|
||||
<p class="mt-1 text-sm text-gray-600">Set Leader on one Pi, Follower on the other. Restart required after changing.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sync_port" class="block text-sm font-medium text-gray-700">UDP Port</label>
|
||||
<div class="form-group" id="setting-display-sync_port" data-setting-key="sync.port">
|
||||
<label for="sync_port" class="block text-sm font-medium text-gray-700">UDP Port{{ ui.help_tip('UDP port the two displays use to exchange frames (1024–65535).\nDefault: 5765. Must match on both Pis. If the ufw firewall is active, allow it with: sudo ufw allow ' ~ main_config.get('sync', {}).get('port', 5765) ~ '/udp', 'Sync UDP Port') }}</label>
|
||||
<input type="number"
|
||||
id="sync_port"
|
||||
name="sync_port"
|
||||
@@ -504,19 +487,14 @@
|
||||
min="1024"
|
||||
max="65535"
|
||||
class="form-control">
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
Must match on both Pis. If ufw is active:
|
||||
<code class="text-xs bg-gray-200 px-1 rounded">sudo ufw allow {{ main_config.get('sync', {}).get('port', 5765) }}/udp</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="sync_position_group" style="display:none">
|
||||
<label for="sync_follower_position" class="block text-sm font-medium text-gray-700">Position</label>
|
||||
<div class="form-group" id="setting-display-sync_follower_position" data-setting-key="sync.follower_position" style="display:none">
|
||||
<label for="sync_follower_position" class="block text-sm font-medium text-gray-700">Position{{ ui.help_tip('Which side of the leader this follower display sits on.\nSets whether this unit shows the left or right half of the extended scroll.', 'Follower Position') }}</label>
|
||||
<select id="sync_follower_position" name="sync_follower_position" class="form-control">
|
||||
<option value="left" {% if main_config.get('sync', {}).get('follower_position', 'left') == 'left' %}selected{% endif %}>Left of leader</option>
|
||||
<option value="right" {% if main_config.get('sync', {}).get('follower_position', 'left') == 'right' %}selected{% endif %}>Right of leader</option>
|
||||
</select>
|
||||
<p class="mt-1 text-sm text-gray-600">Which side of the leader display this unit sits on.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -795,7 +773,7 @@ if (typeof window.fixInvalidNumberInputs !== 'function') {
|
||||
function updateSyncUI() {
|
||||
const role = document.getElementById('sync_role').value;
|
||||
const bar = document.getElementById('sync_status_bar');
|
||||
const posGroup = document.getElementById('sync_position_group');
|
||||
const posGroup = document.getElementById('setting-display-sync_follower_position');
|
||||
if (role === 'standalone') {
|
||||
bar.classList.add('hidden');
|
||||
document.getElementById('sync_error_detail').classList.add('hidden');
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
{% 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">Display Durations</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">Configure how long each screen is shown before switching. Values in seconds.</p>
|
||||
</div>
|
||||
|
||||
{{ ui.settings_filter() }}
|
||||
|
||||
<form hx-post="/api/v3/config/main"
|
||||
hx-ext="json-enc"
|
||||
hx-headers='{"Content-Type": "application/json"}'
|
||||
@@ -15,9 +18,9 @@
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{% for key, value in main_config.display.display_durations.items() %}
|
||||
<div class="form-group">
|
||||
<div class="form-group" id="setting-durations-{{ key }}" data-setting-key="display.display_durations.{{ key }}">
|
||||
<label for="duration_{{ key }}" class="block text-sm font-medium text-gray-700">
|
||||
{{ key | replace('_', ' ') | title }}
|
||||
{{ key | replace('_', ' ') | title }}{{ ui.help_tip('How long the ' ~ (key | replace('_', ' ')) ~ ' screen stays on before rotating to the next one, in seconds.\nRange: 5–600. Currently ' ~ value ~ 's.', key | replace('_', ' ') | title) }}
|
||||
</label>
|
||||
<input type="number"
|
||||
id="duration_{{ key }}"
|
||||
@@ -26,7 +29,6 @@
|
||||
min="5"
|
||||
max="600"
|
||||
class="form-control">
|
||||
<p class="mt-1 text-sm text-gray-600">{{ value }} seconds</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
{% 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"}'
|
||||
@@ -29,7 +32,7 @@
|
||||
class="space-y-6">
|
||||
|
||||
<!-- Web Display Autostart -->
|
||||
<div class="form-group">
|
||||
<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"
|
||||
@@ -37,15 +40,14 @@
|
||||
{% 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>
|
||||
<p class="mt-1 text-sm text-gray-600">Start the web interface on boot for easier access.</p>
|
||||
</div>
|
||||
|
||||
<!-- Timezone -->
|
||||
<div class="form-group">
|
||||
<label for="timezone" class="block text-sm font-medium text-gray-700">Timezone</label>
|
||||
<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>
|
||||
<p class="mt-1 text-sm text-gray-600">Select your timezone for time-based features and scheduling.</p>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
@@ -80,8 +82,8 @@
|
||||
|
||||
<!-- 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">
|
||||
<label for="city" class="block text-sm font-medium text-gray-700">City</label>
|
||||
<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"
|
||||
@@ -89,8 +91,8 @@
|
||||
class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="state" class="block text-sm font-medium text-gray-700">State</label>
|
||||
<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"
|
||||
@@ -98,8 +100,8 @@
|
||||
class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="country" class="block text-sm font-medium text-gray-700">Country</label>
|
||||
<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"
|
||||
@@ -115,7 +117,7 @@
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Auto Discover -->
|
||||
<div class="form-group">
|
||||
<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"
|
||||
@@ -123,12 +125,12 @@
|
||||
{% 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>
|
||||
<p class="mt-1 text-sm text-gray-600">Automatically discover plugins in the plugins directory on startup.</p>
|
||||
</div>
|
||||
|
||||
<!-- Auto Load Enabled -->
|
||||
<div class="form-group">
|
||||
<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"
|
||||
@@ -136,12 +138,12 @@
|
||||
{% 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>
|
||||
<p class="mt-1 text-sm text-gray-600">Automatically load plugins that are enabled in configuration.</p>
|
||||
</div>
|
||||
|
||||
<!-- Development Mode -->
|
||||
<div class="form-group">
|
||||
<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"
|
||||
@@ -149,20 +151,19 @@
|
||||
{% 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>
|
||||
<p class="mt-1 text-gray-600 text-sm">Enable verbose logging and development features for plugin debugging.</p>
|
||||
</div>
|
||||
|
||||
<!-- Plugins Directory -->
|
||||
<div class="form-group">
|
||||
<label for="plugins_directory" class="block text-sm font-medium text-gray-700">Plugins Directory</label>
|
||||
<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">
|
||||
<p class="mt-1 text-sm text-gray-600">Directory where plugins are stored (relative to project root).</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{# Plugin Configuration Partial - Server-side rendered form #}
|
||||
{# This template is loaded via HTMX when a plugin tab is clicked #}
|
||||
|
||||
{% import 'v3/partials/_macros.html' as ui %}
|
||||
|
||||
{# ===== MACROS FOR FORM FIELD GENERATION ===== #}
|
||||
|
||||
{# Render a single form field based on schema type #}
|
||||
@@ -18,9 +20,8 @@
|
||||
{% if obj_widget == 'schedule-picker' %}
|
||||
{# Schedule picker widget - renders enable/mode/times UI #}
|
||||
{% set obj_value = value if value is not none else {} %}
|
||||
<div class="form-group mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ label }}</label>
|
||||
{% if description %}<p class="text-sm text-gray-500 mb-2">{{ description }}</p>{% endif %}
|
||||
<div class="form-group mb-4" id="setting-{{ field_id }}" data-setting-key="{{ full_key }}">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ label }}{{ ui.help_tip(description, label) }}</label>
|
||||
<div id="{{ field_id }}_container" class="schedule-picker-container mt-1"></div>
|
||||
<input type="hidden" id="{{ field_id }}_data" name="{{ full_key }}" value='{{ (obj_value|tojson|safe)|replace("'", "'") }}'>
|
||||
</div>
|
||||
@@ -46,9 +47,8 @@
|
||||
{% elif obj_widget == 'time-range' %}
|
||||
{# Time range widget - renders start/end time inputs #}
|
||||
{% set obj_value = value if value is not none else {} %}
|
||||
<div class="form-group mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ label }}</label>
|
||||
{% if description %}<p class="text-sm text-gray-500 mb-2">{{ description }}</p>{% endif %}
|
||||
<div class="form-group mb-4" id="setting-{{ field_id }}" data-setting-key="{{ full_key }}">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ label }}{{ ui.help_tip(description, label) }}</label>
|
||||
<div id="{{ field_id }}_container" class="time-range-container mt-1"></div>
|
||||
<input type="hidden" id="{{ field_id }}_data" name="{{ full_key }}" value='{{ (obj_value|tojson|safe)|replace("'", "'") }}'>
|
||||
</div>
|
||||
@@ -75,15 +75,11 @@
|
||||
{{ render_nested_section(key, prop, value, prefix, plugin_id) }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="form-group mb-4">
|
||||
<div class="form-group mb-4" id="setting-{{ field_id }}" data-setting-key="{{ full_key }}">
|
||||
<label for="{{ field_id }}" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ label }}
|
||||
{{ label }}{{ ui.help_tip(description, label) }}
|
||||
</label>
|
||||
|
||||
{% if description %}
|
||||
<p class="text-sm text-gray-500 mb-2">{{ description }}</p>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{# Boolean - check for widget first #}
|
||||
{% if field_type == 'boolean' %}
|
||||
{% set bool_widget = prop.get('x-widget') or prop.get('x_widget') %}
|
||||
@@ -1003,6 +999,7 @@
|
||||
{# Configuration Form Panel #}
|
||||
<div class="bg-gray-50 rounded-lg p-4">
|
||||
<h3 class="text-md font-medium text-gray-900 mb-3">Configuration</h3>
|
||||
{{ ui.settings_filter("Filter this plugin's settings…") }}
|
||||
<div class="space-y-4 max-h-96 overflow-y-auto pr-2">
|
||||
{% if schema and schema.properties %}
|
||||
{# Use property order if defined, otherwise use natural order #}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
{% 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">Schedule Settings</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">Configure when the LED matrix display should be active. You can set global hours or customize times for each day of the week.</p>
|
||||
</div>
|
||||
|
||||
{{ ui.settings_filter() }}
|
||||
|
||||
<form id="schedule_form"
|
||||
hx-post="/api/v3/config/schedule"
|
||||
hx-ext="json-enc"
|
||||
@@ -42,9 +45,9 @@
|
||||
class="space-y-6">
|
||||
|
||||
<!-- Dim Brightness Level -->
|
||||
<div class="bg-gray-50 rounded-lg p-4 mb-4">
|
||||
<div class="form-group bg-gray-50 rounded-lg p-4 mb-4" id="setting-schedule-dim_brightness" data-setting-key="dim_schedule.dim_brightness">
|
||||
<label for="dim_brightness" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Dim Brightness Level
|
||||
Dim Brightness Level{{ ui.help_tip('Brightness the display drops to during dim hours (0–100%).\nApplies only while the display is on. Your normal brightness is currently ' ~ normal_brightness ~ '%.', 'Dim Brightness Level') }}
|
||||
</label>
|
||||
<div class="flex items-center space-x-4">
|
||||
<input type="range"
|
||||
@@ -59,7 +62,6 @@
|
||||
{{ dim_schedule_config.dim_brightness | default(30) }}%
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500">Current normal brightness: {{ normal_brightness }}%</p>
|
||||
</div>
|
||||
|
||||
<!-- Dim Schedule Picker Widget Container -->
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% import 'v3/partials/_macros.html' as ui %}
|
||||
<div class="bg-white rounded-lg shadow p-6" x-data="wifiSetup()" x-init="init(); loadStatus()">
|
||||
<!-- Captive Portal Banner (shown when AP mode is active) -->
|
||||
<div x-show="status.ap_mode_active"
|
||||
@@ -23,6 +24,8 @@
|
||||
<p class="mt-1 text-sm text-gray-600">Configure WiFi connection for your Raspberry Pi. Access point mode will automatically activate when no WiFi connection is detected.</p>
|
||||
</div>
|
||||
|
||||
{{ ui.settings_filter('Filter WiFi settings…') }}
|
||||
|
||||
<!-- Current WiFi Status -->
|
||||
<div class="mb-6 p-4 bg-gray-50 rounded-lg">
|
||||
<h3 class="text-sm font-medium text-gray-900 mb-2">Current Status</h3>
|
||||
@@ -73,9 +76,9 @@
|
||||
<h3 class="text-sm font-medium text-gray-900 mb-4">Connect to WiFi Network</h3>
|
||||
|
||||
<!-- Network Selection -->
|
||||
<div class="form-group mb-4">
|
||||
<div class="form-group mb-4" id="setting-wifi-ssid" data-setting-key="wifi.ssid">
|
||||
<label for="wifi-ssid" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Step 1: Select Network
|
||||
Step 1: Select Network{{ ui.help_tip('Choose the WiFi network to join.\nClick Scan to list nearby networks, or type the name manually below if it is hidden.', 'Select Network') }}
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<select id="wifi-ssid"
|
||||
@@ -98,7 +101,6 @@
|
||||
<span class="ml-2">Scan</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-600">Scan for available networks or manually enter SSID below.</p>
|
||||
<!-- Show selected network -->
|
||||
<div x-show="selectedSSID" class="mt-2 p-2 bg-blue-50 border border-blue-200 rounded text-sm" x-cloak>
|
||||
<i class="fas fa-check-circle text-blue-600 mr-2"></i>
|
||||
@@ -107,9 +109,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Manual SSID Entry -->
|
||||
<div class="form-group mb-4">
|
||||
<div class="form-group mb-4" id="setting-wifi-manual_ssid" data-setting-key="wifi.manual_ssid">
|
||||
<label for="manual-ssid" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Or Enter SSID Manually
|
||||
Or Enter SSID Manually{{ ui.help_tip('Type a network name by hand when it is hidden or not shown in the scan results.\nExact spelling and capitalization matter.', 'Enter SSID Manually') }}
|
||||
</label>
|
||||
<input type="text"
|
||||
id="manual-ssid"
|
||||
@@ -120,19 +122,15 @@
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="form-group mb-4">
|
||||
<div class="form-group mb-4" id="setting-wifi-password" data-setting-key="wifi.password">
|
||||
<label for="wifi-password" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Step 2: Enter Password
|
||||
Step 2: Enter Password{{ ui.help_tip('Password for the selected network.\nLeave empty if the network is open (no password required).', 'WiFi Password') }}
|
||||
</label>
|
||||
<input type="password"
|
||||
<input type="password"
|
||||
id="wifi-password"
|
||||
x-model="password"
|
||||
placeholder="Enter password (leave empty for open networks)"
|
||||
class="form-control">
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
Enter the WiFi password. Leave empty if the network is open (no password required).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Connect Button -->
|
||||
@@ -154,14 +152,10 @@
|
||||
</p>
|
||||
|
||||
<!-- Auto-Enable Toggle -->
|
||||
<div class="mb-4 p-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div class="form-group mb-4 p-4 bg-gray-50 rounded-lg border border-gray-200" id="setting-wifi-auto_enable_ap_mode" data-setting-key="wifi.auto_enable_ap_mode">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex-1">
|
||||
<label class="text-sm font-medium text-gray-900 block mb-1">Auto-Enable AP Mode</label>
|
||||
<p class="text-xs text-gray-600">
|
||||
When enabled, AP mode will automatically activate when both WiFi and Ethernet are disconnected.
|
||||
When disabled, AP mode must be manually enabled.
|
||||
</p>
|
||||
<label class="text-sm font-medium text-gray-900 block mb-1">Auto-Enable AP Mode{{ ui.help_tip('Automatically start access-point mode when both WiFi and Ethernet are disconnected, so you can always reach the device to reconfigure it.\nDefault: on. When off, AP mode must be enabled manually.', 'Auto-Enable AP Mode') }}</label>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<button type="button"
|
||||
|
||||
Reference in New Issue
Block a user