landing page + 1st configuration + overview when configured (hoffentlich lol) ltack kram rausgehauen (Testbench code)
1823 lines
82 KiB
Text
1823 lines
82 KiB
Text
---
|
||
import StationConfigModal from "../components/modals/StationConfigModal.astro";
|
||
import AccessPointConfigModal from "../components/modals/AccessPointConfigModal.astro";
|
||
import WifiDisableWarningModal from "../components/modals/WifiDisableWarningModal.astro";
|
||
import HostnameChangedModal from "../components/modals/HostnameChangedModal.astro";
|
||
import IconEye from "../components/icons/IconEye.astro";
|
||
import IconEyeClosed from "../components/icons/IconEyeClosed.astro";
|
||
|
||
const pageTitle = "Network Configuration";
|
||
---
|
||
|
||
<div class="flex flex-col gap-4 min-w-0">
|
||
<h1 class="text-2xl font-bold break-words">{pageTitle}</h1>
|
||
|
||
<div class="flex flex-wrap gap-4 mb-8 min-w-0 *:flex-1 *:min-w-full sm:*:min-w-60">
|
||
<!-- Connection Mode -->
|
||
<div class="card bg-base-100 card-border border-base-300 shadow min-w-0">
|
||
<div class="card-body p-4 gap-1">
|
||
<h3 class="text-sm opacity-60 font-medium">Connection Mode</h3>
|
||
<div class="text-2xl font-bold text-primary leading-tight wrap-break-word" id="active-mode-display">Loading...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Ethernet -->
|
||
<div class="card bg-base-100 card-border border-base-300 shadow min-w-0">
|
||
<div class="card-body p-4 gap-1">
|
||
<h3 class="text-sm opacity-60 font-medium">Ethernet</h3>
|
||
<div class="text-2xl font-bold text-primary truncate" id="eth-ip-display">-</div>
|
||
<div class="text-xs opacity-70 truncate">GW: <span id="eth-gateway-display">-</span></div>
|
||
<div class="text-xs opacity-70 truncate">Mask: <span id="eth-netmask-display">-</span></div>
|
||
<div class="text-xs opacity-70 truncate">MAC: <span id="eth-mac-display">-</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- WiFi (SSID + signal) -->
|
||
<div class="card bg-base-100 card-border border-base-300 shadow min-w-0" id="wifi-card">
|
||
<div class="card-body p-4 gap-1">
|
||
<div class="flex items-start gap-2">
|
||
<div class="flex flex-col gap-1 min-w-0 flex-1">
|
||
<h3 class="text-sm opacity-60 font-medium" id="wifi-card-title">WiFi</h3>
|
||
<div class="text-2xl font-bold text-primary truncate" id="ssid-card-display">-</div>
|
||
<div class="text-xs opacity-70 items-center gap-1 hidden" id="password-desc">
|
||
<span id="pw-display-group" class="flex items-center gap-1">
|
||
PW: <span id="password-card-display" data-password=""></span>
|
||
<button type="button" class="btn btn-ghost btn-xs p-0 min-h-0 h-4" id="toggle-password-visibility">
|
||
<span id="eye-icon-open">
|
||
<IconEye size={14} class="opacity-70 hover:opacity-100" />
|
||
</span>
|
||
<span id="eye-icon-closed" class="hidden">
|
||
<IconEyeClosed size={14} class="opacity-70 hover:opacity-100" />
|
||
</span>
|
||
</button>
|
||
</span>
|
||
<span id="open-network-label" class="hidden">Open Network</span>
|
||
</div>
|
||
<div class="text-xs opacity-70 flex items-center gap-1" id="rssi-row">RSSI: <span id="signal-display"></span></div>
|
||
<div class="text-xs opacity-70" id="signal-desc">Not connected</div>
|
||
</div>
|
||
<div id="signal-icon" class="text-primary shrink-0 mt-1">
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
width="28"
|
||
height="28"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
>
|
||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||
<path d="M12 18l.01 0"></path>
|
||
<path d="M9.172 15.172a4 4 0 0 1 5.656 0"></path>
|
||
<path d="M6.343 12.343a8 8 0 0 1 11.314 0"></path>
|
||
<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0"></path>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- WiFi / Station IP — hidden unless STA mode -->
|
||
<div class="card bg-base-100 card-border border-base-300 shadow min-w-0" id="wifi-station-card">
|
||
<div class="card-body p-4 gap-1">
|
||
<h3 class="text-sm opacity-60 font-medium" id="wifi-ip-card-title">WiFi Station</h3>
|
||
<div class="text-2xl font-bold text-primary truncate" id="sta-ip-display">-</div>
|
||
<div class="text-xs opacity-70 truncate">GW: <span id="sta-gateway-display">-</span></div>
|
||
<div class="text-xs opacity-70 truncate">Mask: <span id="sta-netmask-display">-</span></div>
|
||
<div class="text-xs opacity-70 truncate">MAC: <span id="sta-mac-display">-</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Hostname -->
|
||
<div class="card bg-base-100 card-border border-base-300 shadow min-w-0">
|
||
<div class="card-body p-4 gap-1">
|
||
<h3 class="text-sm opacity-60 font-medium">Hostname</h3>
|
||
<div class="text-2xl font-bold text-primary truncate" id="hostname-display">-</div>
|
||
<a
|
||
class="text-xs opacity-70 truncate hover:opacity-100 hover:text-primary transition-opacity"
|
||
id="hostname-desc"
|
||
href="http://hostname.local"
|
||
target="_blank"
|
||
>
|
||
http://<span id="hostname-preview-card-hint">hostname</span>.local
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Connected Clients -->
|
||
<div class="card bg-base-100 card-border border-base-300 shadow min-w-0">
|
||
<div class="card-body p-4 gap-1">
|
||
<div class="flex items-start gap-2">
|
||
<div class="flex flex-col gap-1 min-w-0 flex-1">
|
||
<h3 class="text-sm opacity-60 font-medium">Connected Clients</h3>
|
||
<div class="text-2xl font-bold text-primary" id="clients-display">0</div>
|
||
<div class="text-xs opacity-70" id="clients-desc">No clients</div>
|
||
</div>
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
width="28"
|
||
height="28"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
class="text-primary shrink-0 mt-1"
|
||
>
|
||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
|
||
<circle cx="9" cy="7" r="4"></circle>
|
||
<path d="M22 21v-2a4 4 0 0 0-3-3.87"></path>
|
||
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="space-y-4 mb-8">
|
||
<!-- Network Configuration Card -->
|
||
<div class="card bg-base-100 card-border border-base-300">
|
||
<div class="card-body">
|
||
<div class="flex flex-col md:flex-row gap-6 md:gap-8">
|
||
<!-- Hostname Configuration -->
|
||
<div class="flex flex-col gap-2 min-w-0 flex-1 lg:px-10">
|
||
<h3 class="font-semibold text-base mb-4 flex items-center gap-2">
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
width="18"
|
||
height="18"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
class="text-primary"
|
||
>
|
||
<circle cx="12" cy="12" r="10"></circle>
|
||
<line x1="2" y1="12" x2="22" y2="12"></line>
|
||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
|
||
</svg>
|
||
Network Interface Name
|
||
</h3>
|
||
|
||
<label class="label">
|
||
<span class="label-text font-semibold text-base">Hostname</span>
|
||
<div
|
||
class="tooltip tooltip-bottom tooltip-primary"
|
||
data-tip="The hostname defines under which this device and its web interface can be reachable on your network (e.g. 'hostname.local'). Open http://hostname.local in your Browser to open this page if the device has access to the same network."
|
||
>
|
||
<button type="button" class="btn btn-ghost btn-xs p-0.5 min-h-0 h-6 opacity-60 hover:opacity-100">
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
width="16"
|
||
height="16"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
>
|
||
<circle cx="12" cy="12" r="10"></circle>
|
||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</label>
|
||
<div class="flex gap-2 w-full" id="mdns-hostname-wrapper">
|
||
<label
|
||
id="mdns-hostname-field"
|
||
class="input flex-1 flex items-center gap-2 font-mono text-sm focus-within:bg-primary/10 focus-within:border-none focus-within:outline-primary"
|
||
>
|
||
<span class="opacity-70 select-none" aria-hidden="true">http://</span>
|
||
<input
|
||
type="text"
|
||
id="mdns-hostname"
|
||
placeholder="hostname"
|
||
maxlength="31"
|
||
required
|
||
class="grow min-w-0 bg-transparent border-none outline-none"
|
||
inputmode="text"
|
||
autocomplete="off"
|
||
autocapitalize="none"
|
||
spellcheck="false"
|
||
/>
|
||
<span class="opacity-70 select-none" aria-hidden="true">.local</span>
|
||
</label>
|
||
<button type="button" class="btn btn-primary" id="apply-hostname-btn">Apply</button>
|
||
</div>
|
||
<p id="hostname-error-msg" class="mt-1 text-sm text-error hidden"></p>
|
||
<label class="label">
|
||
<span class="label-text-alt"
|
||
>Access your device at <span class="font-mono font-semibold break-all"
|
||
>http://<span id="hostname-preview-hint">hostname</span>.local</span
|
||
></span
|
||
>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="divider md:divider-horizontal justify-center"></div>
|
||
|
||
<!-- IP Configuration -->
|
||
<div class="flex flex-col gap-2 min-w-0 flex-1 lg:px-10">
|
||
<h3 class="font-semibold text-base mb-4 flex items-center gap-2">Ethernet IP Address Configuration</h3>
|
||
|
||
<label class="label flex-col items-start gap-2">
|
||
<span class="label-text font-semibold text-base" id="ip-mode-description">IP assignment</span>
|
||
</label>
|
||
<div class="join w-full">
|
||
<input
|
||
class="join-item btn flex-1"
|
||
type="radio"
|
||
name="ip-mode-ethernet"
|
||
aria-label="Automatic (DHCP)"
|
||
value="dhcp"
|
||
id="ip-mode-dhcp-ethernet-btn"
|
||
/>
|
||
<input
|
||
class="join-item btn flex-1"
|
||
type="radio"
|
||
name="ip-mode-ethernet"
|
||
aria-label="Static IP"
|
||
value="static"
|
||
id="ip-mode-static-ethernet-btn"
|
||
/>
|
||
</div>
|
||
|
||
<div id="static-ip-container" class="space-y-4 hidden">
|
||
<div class="grid grid-cols-1 gap-4">
|
||
<div class="form-control">
|
||
<label class="label">
|
||
<span class="label-text font-semibold">IP Address</span>
|
||
</label>
|
||
<label class="input w-full font-mono text-sm focus-within:bg-primary/10 focus-within:border-none focus-within:outline-primary">
|
||
<input type="text" class="w-full border-none outline-none bg-transparent" id="static-ip-ethernet" placeholder="192.168.1.2" />
|
||
</label>
|
||
<p id="static-ip-ethernet-error-page" class="mt-2 text-sm text-error hidden"></p>
|
||
</div>
|
||
|
||
<div class="form-control">
|
||
<label class="label">
|
||
<span class="label-text font-semibold">Subnet Mask</span>
|
||
</label>
|
||
<label class="input w-full font-mono text-sm focus-within:bg-primary/10 focus-within:border-none focus-within:outline-primary">
|
||
<input type="text" class="w-full border-none outline-none bg-transparent" id="subnet-mask-ethernet" placeholder="255.255.255.0" />
|
||
</label>
|
||
<p id="subnet-mask-ethernet-error-page" class="mt-2 text-sm text-error hidden"></p>
|
||
</div>
|
||
|
||
<div class="form-control">
|
||
<label class="label">
|
||
<span class="label-text font-semibold">Gateway</span>
|
||
</label>
|
||
<label class="input w-full font-mono text-sm focus-within:bg-primary/10 focus-within:border-none focus-within:outline-primary">
|
||
<input type="text" class="w-full border-none outline-none bg-transparent" id="gateway-ethernet" placeholder="192.168.1.1" />
|
||
</label>
|
||
<p id="gateway-ethernet-error-page" class="mt-2 text-sm text-error hidden"></p>
|
||
</div>
|
||
<div class="flex w-full">
|
||
<button
|
||
class="btn btn-primary w-full tooltip tooltip-bottom tooltip-info"
|
||
data-tip="This can take some time to apply and will temporarily disconnect the device from the network. Review your inputs before applying!"
|
||
id="save-static-ip-ethernet-btn">Apply Static IP Configuration</button
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- WiFi Configuration Card -->
|
||
<div class="card bg-base-100 card-border border-base-300">
|
||
<div class="card-body">
|
||
<!-- WiFi Toggle and Settings -->
|
||
<div>
|
||
<div class="flex flex-col md:flex-row gap-6 md:gap-8 mb-4">
|
||
<!-- Left Half: WiFi Configuration Headline -->
|
||
<div class="flex-1 min-w-0">
|
||
<h2 class="font-semibold text-base flex items-center gap-2">
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
width="18"
|
||
height="18"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
class="text-primary"
|
||
>
|
||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||
<path d="M12 18l.01 0"></path>
|
||
<path d="M9.172 15.172a4 4 0 0 1 5.656 0"></path>
|
||
<path d="M6.343 12.343a8 8 0 0 1 11.314 0"></path>
|
||
<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0"></path>
|
||
</svg>
|
||
WiFi Configuration
|
||
</h2>
|
||
</div>
|
||
<!-- Right Half: WiFi Station IP Address Configuration Headline -->
|
||
<div class="flex-1 min-w-0"></div>
|
||
</div>
|
||
<div class="form-control mb-6">
|
||
<label class="label cursor-pointer justify-start gap-3">
|
||
<span class="label-text font-semibold">WiFi <span id="wifi-status-text">Disabled</span></span>
|
||
<input type="checkbox" class="toggle toggle-primary" id="wifi-toggle" />
|
||
</label>
|
||
</div>
|
||
|
||
<!-- WiFi Settings (shown when WiFi is enabled) -->
|
||
<div id="wifi-settings-container" class="space-y-6 hidden">
|
||
<div id="wifi-active-controls">
|
||
<div class="flex flex-col md:flex-row gap-6">
|
||
<!-- Left Half: Antenna Selection and Transmission Power -->
|
||
<div class="flex flex-col gap-6 flex-1 min-w-0 lg:px-10">
|
||
<div class="form-control">
|
||
<label class="label pb-2">
|
||
<span class="label-text font-semibold text-base">Antenna Selection</span>
|
||
</label>
|
||
<div class="join w-full">
|
||
<input
|
||
class="join-item btn flex-1"
|
||
type="radio"
|
||
name="antenna-selection"
|
||
aria-label="Internal"
|
||
value="internal"
|
||
id="antenna-internal-radio"
|
||
/>
|
||
<input
|
||
class="join-item btn flex-1"
|
||
type="radio"
|
||
name="antenna-selection"
|
||
aria-label="External"
|
||
value="external"
|
||
id="antenna-external-radio"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-control min-w-0">
|
||
<label class="label flex-wrap gap-5 pb-2">
|
||
<span class="label-text font-semibold text-base break-words">Transmission Power</span>
|
||
<span class="label-text-alt font-mono text-xs sm:text-sm break-all" id="power-value-display">20 dBm (100.00 mW)</span>
|
||
</label>
|
||
<div class="relative">
|
||
<input
|
||
type="range"
|
||
min="2"
|
||
max="20"
|
||
value="20"
|
||
step="0.25"
|
||
class="range range-primary w-full color-primary"
|
||
id="antenna-power"
|
||
/>
|
||
<div class="relative w-full h-4 mt-2">
|
||
<span class="absolute text-xs opacity-60" style="left: 0%; transform: translateX(0%);">2</span>
|
||
<span class="absolute text-xs opacity-60" style="left: 16.67%; transform: translateX(-50%);">5</span>
|
||
<span class="absolute text-xs opacity-60" style="left: 44.44%; transform: translateX(-50%);">10</span>
|
||
<span class="absolute text-xs opacity-60" style="left: 72.22%; transform: translateX(-50%);">15</span>
|
||
<span class="absolute text-xs opacity-60" style="right: 0%; transform: translateX(0%);">20</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="divider md:divider-horizontal"></div>
|
||
|
||
<!-- Right Half: Static IP Configuration for STATION Mode -->
|
||
<div class="flex flex-col gap-2 min-w-0 flex-1 lg:px-10">
|
||
<label class="label flex-col items-start gap-2">
|
||
<span class="label-text font-semibold text-base" id="ip-mode-description-station">IP assignment</span>
|
||
</label>
|
||
|
||
<!-- Placeholder for AP mode, where no DHCP/Static IP configuration is available -->
|
||
<div class="alert alert-info mb-4 hidden" id="ap-mode-ip-alert-placeholder">
|
||
<div>
|
||
<h3 class="font-bold">Access Point mode is active</h3>
|
||
<p class="text-sm">IP Address configuration is not necessary in Access Point mode.</p>
|
||
</div>
|
||
</div>
|
||
<div class="join w-full" id="ip-assignment-buttons-station">
|
||
<input
|
||
class="join-item btn flex-1"
|
||
type="radio"
|
||
name="ip-mode-station"
|
||
aria-label="Automatic (DHCP)"
|
||
value="dhcp"
|
||
id="ip-mode-dhcp-station-btn"
|
||
/>
|
||
<input
|
||
class="join-item btn flex-1"
|
||
type="radio"
|
||
name="ip-mode-station"
|
||
aria-label="Static IP"
|
||
value="static"
|
||
id="ip-mode-static-station-btn"
|
||
/>
|
||
</div>
|
||
|
||
<div id="static-ip-container-station" class="space-y-4 hidden">
|
||
<div class="grid grid-cols-1 gap-4">
|
||
<div class="form-control">
|
||
<label class="label">
|
||
<span class="label-text font-semibold">IP Address</span>
|
||
</label>
|
||
<label
|
||
class="input w-full font-mono text-sm focus-within:bg-primary/10 focus-within:border-none focus-within:outline-primary"
|
||
>
|
||
<input
|
||
type="text"
|
||
class="w-full border-none outline-none bg-transparent"
|
||
id="static-ip-station"
|
||
placeholder="192.168.1.2"
|
||
/>
|
||
</label>
|
||
<p id="static-ip-station-error-page" class="mt-2 text-sm text-error hidden"></p>
|
||
</div>
|
||
|
||
<div class="form-control">
|
||
<label class="label">
|
||
<span class="label-text font-semibold">Subnet Mask</span>
|
||
</label>
|
||
<label
|
||
class="input w-full font-mono text-sm focus-within:bg-primary/10 focus-within:border-none focus-within:outline-primary"
|
||
>
|
||
<input
|
||
type="text"
|
||
class="w-full border-none outline-none bg-transparent"
|
||
id="subnet-mask-station"
|
||
placeholder="255.255.255.0"
|
||
/>
|
||
</label>
|
||
<p id="subnet-mask-station-error-page" class="mt-2 text-sm text-error hidden"></p>
|
||
</div>
|
||
|
||
<div class="form-control">
|
||
<label class="label">
|
||
<span class="label-text font-semibold">Gateway</span>
|
||
</label>
|
||
<label
|
||
class="input w-full font-mono text-sm focus-within:bg-primary/10 focus-within:border-none focus-within:outline-primary"
|
||
>
|
||
<input type="text" class="w-full border-none outline-none bg-transparent" id="gateway-station" placeholder="192.168.1.1" />
|
||
</label>
|
||
<p id="gateway-station-error-page" class="mt-2 text-sm text-error hidden"></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<button
|
||
class="btn btn-primary w-full mt-2 tooltip tooltip-bottom tooltip-info hidden"
|
||
data-tip="This can take some time to apply and will temporarily disconnect the device from the network. Review your inputs before applying!"
|
||
id="save-static-ip-station-btn">Apply IP Configuration</button
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Banner shown when WiFi is toggled on but no mode is active yet -->
|
||
<div id="wifi-no-mode-banner" class="alert alert-info hidden mt-8 mb-0">
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
class="h-5 w-5 shrink-0"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
>
|
||
<circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||
</svg>
|
||
<span>No active WiFi mode. Connect to a network or create an Access Point first.</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="wifi-mode-section">
|
||
<div class="divider mt-2 mb-6"></div>
|
||
|
||
<div>
|
||
<h3 class="font-semibold text-base mb-2 flex items-center gap-2 opacity-90">WiFi Mode Selection</h3>
|
||
<p class="text-sm opacity-70 mb-4">Choose a WiFi mode</p>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
<button
|
||
class="h-auto py-4 mode-card bg-base-300 hover:bg-secondary/10 transition-all rounded-lg border-2 border-transparent hover:border-secondary"
|
||
data-mode="station"
|
||
id="mode-station"
|
||
>
|
||
<div class="flex flex-col items-center gap-2">
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
width="24"
|
||
height="24"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
class="text-secondary"
|
||
>
|
||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||
<path d="M12 18l.01 0"></path>
|
||
<path d="M9.172 15.172a4 4 0 0 1 5.656 0"></path>
|
||
<path d="M6.343 12.343a8 8 0 0 1 11.314 0"></path>
|
||
<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0"></path>
|
||
</svg>
|
||
<div class="font-semibold">WiFi Station</div>
|
||
<div class="text-xs opacity-70">Connect to a WiFi network</div>
|
||
</div>
|
||
</button>
|
||
|
||
<button
|
||
class="h-auto py-4 mode-card bg-base-300 hover:bg-accent/10 transition-all rounded-lg border-2 border-transparent hover:border-accent"
|
||
data-mode="ap"
|
||
id="mode-ap"
|
||
>
|
||
<div class="flex flex-col items-center gap-2">
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
width="24"
|
||
height="24"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
class="text-accent"
|
||
><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 12l0 .01"></path><path d="M14.828 9.172a4 4 0 0 1 0 5.656"
|
||
></path><path d="M17.657 6.343a8 8 0 0 1 0 11.314"></path><path d="M9.168 14.828a4 4 0 0 1 0 -5.656"></path><path
|
||
d="M6.337 17.657a8 8 0 0 1 0 -11.314"></path></svg
|
||
>
|
||
<div class="font-semibold">Access Point</div>
|
||
<div class="text-xs opacity-70">Create a WiFi hotspot</div>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<StationConfigModal />
|
||
<AccessPointConfigModal />
|
||
<WifiDisableWarningModal />
|
||
<HostnameChangedModal />
|
||
|
||
<dialog id="network-config-error-modal" class="modal modal-middle">
|
||
<div class="modal-box w-11/12 sm:w-5/12 max-w-lg">
|
||
<h3 class="text-lg font-bold mb-1 text-error">Network Configuration Failed</h3>
|
||
<div class="divider mt-2 mb-3"></div>
|
||
<div class="space-y-3">
|
||
<div class="alert alert-error py-3">
|
||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||
<path
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
stroke-width="2"
|
||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||
</svg>
|
||
<span id="network-error-reason-text">Configuration could not be applied.</span>
|
||
</div>
|
||
<div class="bg-base-200 rounded-lg overflow-hidden">
|
||
<table class="table table-compact w-full">
|
||
<tbody>
|
||
<tr id="network-error-ssid-row" class="hidden">
|
||
<td class="font-semibold w-1/3">SSID</td>
|
||
<td id="network-error-ssid"></td>
|
||
</tr>
|
||
<tr>
|
||
<td class="font-semibold w-1/3">Reason</td>
|
||
<td id="network-error-reason"></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<p class="text-sm opacity-70">
|
||
The device has restored the previous working configuration. Check your network credentials or IP settings and try again.
|
||
</p>
|
||
</div>
|
||
<div class="modal-action">
|
||
<form method="dialog">
|
||
<button class="btn btn-primary">OK</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
<form method="dialog" class="modal-backdrop">
|
||
<button>close</button>
|
||
</form>
|
||
</dialog>
|
||
|
||
<div id="config-applying-toast" class="toast toast-bottom toast-center hidden z-50">
|
||
<div class="alert alert-info shadow-lg">
|
||
<span class="loading loading-spinner loading-sm"></span>
|
||
<span>Applying network configuration — device may briefly disconnect.</span>
|
||
</div>
|
||
</div>
|
||
|
||
<dialog id="static-ip-confirm-modal" class="modal modal-middle">
|
||
<div class="modal-box w-11/12 sm:w-5/12 max-w-2xl">
|
||
<h3 class="text-lg font-bold mb-4">Confirm IP Configuration</h3>
|
||
<div class="divider"></div>
|
||
|
||
<div class="space-y-4">
|
||
<div>
|
||
<h4 class="font-semibold mb-2">Summary</h4>
|
||
<div class="bg-base-200 rounded-lg overflow-hidden">
|
||
<table class="table table-compact">
|
||
<tbody>
|
||
<tr>
|
||
<td class="font-semibold w-1/3">Mode</td>
|
||
<td id="static-ip-summary-mode">Automatic (DHCP)</td>
|
||
</tr>
|
||
<tr id="static-ip-summary-ip-row" class="hidden">
|
||
<td class="font-semibold">IP Address</td>
|
||
<td id="static-ip-summary-ip">-</td>
|
||
</tr>
|
||
<tr id="static-ip-summary-netmask-row" class="hidden">
|
||
<td class="font-semibold">Netmask</td>
|
||
<td id="static-ip-summary-netmask">-</td>
|
||
</tr>
|
||
<tr id="static-ip-summary-gateway-row" class="hidden">
|
||
<td class="font-semibold">Gateway</td>
|
||
<td id="static-ip-summary-gateway">-</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="static-ip-warning" class="alert alert-warning py-3">
|
||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-5 w-5" fill="none" viewBox="0 0 24 24">
|
||
<path
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
stroke-width="2"
|
||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||
></path>
|
||
</svg>
|
||
<div class="text-sm">
|
||
<p class="font-semibold">Warning</p>
|
||
<ul class="list-disc list-inside space-y-1 mt-2">
|
||
<li>Saving a static IP temporarily disconnects the device from the network.</li>
|
||
<li>Verify IP, netmask, and gateway carefully before applying.</li>
|
||
<li>The device remains reachable using a USB connection and the Desktop App.</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-action">
|
||
<form method="dialog">
|
||
<button class="btn" id="cancel-static-ip-btn">Cancel</button>
|
||
</form>
|
||
<button class="btn btn-primary" id="confirm-static-ip-btn" type="button">Confirm & Apply</button>
|
||
</div>
|
||
</div>
|
||
<form method="dialog" class="modal-backdrop">
|
||
<button>close</button>
|
||
</form>
|
||
</dialog>
|
||
|
||
<script>
|
||
import {
|
||
ConnectionMode,
|
||
AntennaSelection,
|
||
$current_network_configuration,
|
||
$network_status_display,
|
||
$network_config_error,
|
||
type NetworkConfigNVS,
|
||
set_hostname,
|
||
send_new_network_configuration_to_alpha,
|
||
$hostname_input_field,
|
||
set_antenna_selection,
|
||
set_antenna_power,
|
||
} from "../components/backend_components/NetworkManagerStore";
|
||
import { HOSTNAME_REGEX, MAX_HOSTNAME_LENGTH, normalizeHostnameValue } from "../components/helper_scripts/hostname_validation";
|
||
import {
|
||
is_valid_ip_format,
|
||
isValidNetmask,
|
||
isPracticalNetmask,
|
||
isInValidHostRange,
|
||
isInSameSubnet,
|
||
} from "../components/helper_scripts/IP_Subnetmask_validation";
|
||
|
||
function getModeString(mode: number): string {
|
||
switch (mode) {
|
||
case ConnectionMode.CONNECTION_MODE_ETH_DHCP_NO_WIFI:
|
||
case ConnectionMode.CONNECTION_MODE_ETH_STATIC_NO_WIFI:
|
||
return "Ethernet only";
|
||
case ConnectionMode.CONNECTION_MODE_ETH_DHCP_APSTA:
|
||
case ConnectionMode.CONNECTION_MODE_ETH_STATIC_APSTA:
|
||
return "Ethernet + Access Point";
|
||
case ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_DHCP:
|
||
case ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_STATIC:
|
||
case ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_DHCP:
|
||
case ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_STATIC:
|
||
return "Ethernet + WiFi Station";
|
||
default:
|
||
return "None";
|
||
}
|
||
}
|
||
const wifiToggle = document.getElementById("wifi-toggle") as HTMLInputElement;
|
||
const wifiSettingsContainer = document.getElementById("wifi-settings-container");
|
||
const wifiStatusText = document.getElementById("wifi-status-text");
|
||
const hostnameInput = document.getElementById("mdns-hostname") as HTMLInputElement;
|
||
const hostnameInputField = document.getElementById("mdns-hostname-field") as HTMLElement | null;
|
||
const hostnameErrorMsg = document.getElementById("hostname-error-msg");
|
||
const applyHostnameBtn = document.getElementById("apply-hostname-btn") as HTMLButtonElement | null;
|
||
const hostnamePreviewCardHint = document.getElementById("hostname-preview-card-hint");
|
||
const hostnameDescLink = document.getElementById("hostname-desc") as HTMLAnchorElement | null;
|
||
const hostnamePreviewHint = document.getElementById("hostname-preview-hint");
|
||
const staticIpContainer = document.getElementById("static-ip-container");
|
||
const ipModeDescription = document.getElementById("ip-mode-description");
|
||
const ipModeButtons = document.querySelectorAll('input[name="ip-mode-ethernet"]') as NodeListOf<HTMLInputElement>;
|
||
const staticIpInput = document.getElementById("static-ip-ethernet") as HTMLInputElement;
|
||
const subnetMaskInput = document.getElementById("subnet-mask-ethernet") as HTMLInputElement;
|
||
const gatewayInput = document.getElementById("gateway-ethernet") as HTMLInputElement;
|
||
const staticIpError = document.getElementById("static-ip-ethernet-error-page");
|
||
const subnetMaskError = document.getElementById("subnet-mask-ethernet-error-page");
|
||
const gatewayError = document.getElementById("gateway-ethernet-error-page");
|
||
const saveStaticIpBtn = document.getElementById("save-static-ip-ethernet-btn") as HTMLButtonElement;
|
||
|
||
// Station mode IP assignment elements
|
||
const apModeIpAlertPlaceholder = document.getElementById("ap-mode-ip-alert-placeholder");
|
||
const ipAssignmentButtonsStation = document.getElementById("ip-assignment-buttons-station");
|
||
const staticIpContainerStation = document.getElementById("static-ip-container-station");
|
||
const ipModeDescriptionStation = document.getElementById("ip-mode-description-station");
|
||
const ipModeButtonsStation = document.querySelectorAll('input[name="ip-mode-station"]') as NodeListOf<HTMLInputElement>;
|
||
const staticIpInputStation = document.getElementById("static-ip-station") as HTMLInputElement;
|
||
const subnetMaskInputStation = document.getElementById("subnet-mask-station") as HTMLInputElement;
|
||
const gatewayInputStation = document.getElementById("gateway-station") as HTMLInputElement;
|
||
const staticIpErrorStation = document.getElementById("static-ip-station-error-page");
|
||
const subnetMaskErrorStation = document.getElementById("subnet-mask-station-error-page");
|
||
const gatewayErrorStation = document.getElementById("gateway-station-error-page");
|
||
const saveStaticIpBtnStation = document.getElementById("save-static-ip-station-btn") as HTMLButtonElement;
|
||
const staticIpConfirmModal = document.getElementById("static-ip-confirm-modal") as HTMLDialogElement;
|
||
const staticIpSummaryMode = document.getElementById("static-ip-summary-mode");
|
||
const staticIpSummaryIpRow = document.getElementById("static-ip-summary-ip-row");
|
||
const staticIpSummaryNetmaskRow = document.getElementById("static-ip-summary-netmask-row");
|
||
const staticIpSummaryGatewayRow = document.getElementById("static-ip-summary-gateway-row");
|
||
const staticIpSummaryIp = document.getElementById("static-ip-summary-ip");
|
||
const staticIpSummaryNetmask = document.getElementById("static-ip-summary-netmask");
|
||
const staticIpSummaryGateway = document.getElementById("static-ip-summary-gateway");
|
||
const cancelStaticIpBtn = document.getElementById("cancel-static-ip-btn");
|
||
const confirmStaticIpBtn = document.getElementById("confirm-static-ip-btn") as HTMLButtonElement | null;
|
||
|
||
// WiFi stat card elements
|
||
const wifiCard = document.getElementById("wifi-card") as HTMLDivElement | null;
|
||
const ssidCardDisplay = document.getElementById("ssid-card-display") as HTMLDivElement | null;
|
||
const passwordCardDisplay = document.getElementById("password-card-display") as HTMLSpanElement | null;
|
||
const passwordDesc = document.getElementById("password-desc") as HTMLDivElement | null;
|
||
const signalDisplay = document.getElementById("signal-display") as HTMLDivElement | null;
|
||
const signalDesc = document.getElementById("signal-desc") as HTMLDivElement | null;
|
||
|
||
const togglePasswordBtn = document.getElementById("toggle-password-visibility") as HTMLButtonElement | null;
|
||
const eyeIconOpen = document.getElementById("eye-icon-open") as HTMLSpanElement | null;
|
||
const eyeIconClosed = document.getElementById("eye-icon-closed") as HTMLSpanElement | null;
|
||
let isPasswordVisible = false;
|
||
|
||
function updatePasswordDisplay(password: string) {
|
||
if (!passwordCardDisplay) return;
|
||
passwordCardDisplay.dataset.password = password;
|
||
passwordCardDisplay.textContent = isPasswordVisible ? password || "-" : password ? "•".repeat(Math.min(password.length, 12)) : "-";
|
||
}
|
||
|
||
if (togglePasswordBtn && passwordCardDisplay && eyeIconOpen && eyeIconClosed) {
|
||
togglePasswordBtn.addEventListener("click", () => {
|
||
isPasswordVisible = !isPasswordVisible;
|
||
const pw = passwordCardDisplay.dataset.password || "";
|
||
passwordCardDisplay.textContent = isPasswordVisible ? pw || "" : pw ? "•".repeat(Math.min(pw.length, 12)) : "";
|
||
eyeIconOpen.classList.toggle("hidden", isPasswordVisible);
|
||
eyeIconClosed.classList.toggle("hidden", !isPasswordVisible);
|
||
});
|
||
}
|
||
|
||
type StaticIpValues = {
|
||
ip: string;
|
||
netmask: string;
|
||
gateway: string;
|
||
};
|
||
|
||
let currentIpAssignmentMode: "dhcp" | "static" = "dhcp";
|
||
let pendingStaticIpConfig: { mode: "dhcp" | "static"; values?: StaticIpValues } | null = null;
|
||
// Tracks what is actually applied on the device — only show confirm modal when reverting a real static config
|
||
let appliedIpModeEth: "dhcp" | "static" = "dhcp";
|
||
|
||
// Station mode static IP state
|
||
let currentIpAssignmentModeStation: "dhcp" | "static" = "dhcp";
|
||
let pendingStaticIpConfigStation: { mode: "dhcp" | "static"; values?: StaticIpValues } | null = null;
|
||
let appliedIpModeSta: "dhcp" | "static" = "dhcp";
|
||
|
||
// Tracks which interface opened the confirm modal via DHCP radio click so we can
|
||
// revert the radio if the user cancels.
|
||
let dhcpRevertPending: "eth" | "sta" | null = null;
|
||
|
||
// Live status display elements (updated by $network_status_display)
|
||
const ethIpDisplay = document.getElementById("eth-ip-display");
|
||
const ethGatewayDisplay = document.getElementById("eth-gateway-display");
|
||
const ethNetmaskDisplay = document.getElementById("eth-netmask-display");
|
||
const ethMacDisplay = document.getElementById("eth-mac-display");
|
||
const staIpDisplay = document.getElementById("sta-ip-display");
|
||
const staGatewayDisplay = document.getElementById("sta-gateway-display");
|
||
const staNetmaskDisplay = document.getElementById("sta-netmask-display");
|
||
const staMacDisplay = document.getElementById("sta-mac-display");
|
||
|
||
const wifiModeSection = document.getElementById("wifi-mode-section");
|
||
const wifiNoModeBanner = document.getElementById("wifi-no-mode-banner");
|
||
const wifiActiveControls = document.getElementById("wifi-active-controls");
|
||
|
||
function updateWifiModeState(mode: ConnectionMode) {
|
||
const wifiVisible = wifiSettingsContainer && !wifiSettingsContainer.classList.contains("hidden");
|
||
const hasModeActive =
|
||
mode === ConnectionMode.CONNECTION_MODE_ETH_DHCP_APSTA ||
|
||
mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_APSTA ||
|
||
mode === ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_DHCP ||
|
||
mode === ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_STATIC ||
|
||
mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_DHCP ||
|
||
mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_STATIC;
|
||
wifiNoModeBanner?.classList.toggle("hidden", !wifiVisible || hasModeActive);
|
||
wifiActiveControls?.classList.toggle("opacity-40", !hasModeActive);
|
||
wifiActiveControls?.classList.toggle("pointer-events-none", !hasModeActive);
|
||
}
|
||
|
||
function showWifiSettings(isEnabled: boolean) {
|
||
if (wifiToggle) wifiToggle.checked = isEnabled;
|
||
if (isEnabled) {
|
||
wifiSettingsContainer?.classList.remove("hidden");
|
||
wifiModeSection?.classList.remove("hidden");
|
||
if (wifiStatusText) wifiStatusText.textContent = "Enabled";
|
||
updateWifiModeState($current_network_configuration.get().Connection_Mode);
|
||
} else {
|
||
wifiSettingsContainer?.classList.add("hidden");
|
||
wifiModeSection?.classList.add("hidden");
|
||
if (wifiStatusText) wifiStatusText.textContent = "Disabled";
|
||
updateSignalIcon(null, false);
|
||
}
|
||
}
|
||
|
||
let powerSliderDragging = false;
|
||
|
||
$current_network_configuration.subscribe((config) => {
|
||
let activeMode = config.Connection_Mode;
|
||
const activeModeDisplay = document.getElementById("active-mode-display");
|
||
const hostnameDisplay = document.getElementById("hostname-display");
|
||
|
||
if (activeModeDisplay) activeModeDisplay.textContent = getModeString(activeMode);
|
||
if (hostnameDisplay) hostnameDisplay.textContent = config.MDNS_Hostname || "-";
|
||
|
||
// Initialize hostname input with current value
|
||
if (hostnameInput) {
|
||
const currentValue = hostnameInput.value?.trim() || "";
|
||
const configHostname = (config.MDNS_Hostname || "").trim();
|
||
if (currentValue.length === 0 && configHostname.length > 0) {
|
||
hostnameInput.value = configHostname;
|
||
|
||
validateHostnameInput(false);
|
||
}
|
||
}
|
||
if (hostnamePreviewCardHint) hostnamePreviewCardHint.textContent = config.MDNS_Hostname || "hostname";
|
||
if (hostnameDescLink) hostnameDescLink.href = `http://${config.MDNS_Hostname || "hostname"}.local`;
|
||
if (hostnamePreviewHint) hostnamePreviewHint.textContent = config.MDNS_Hostname || "hostname";
|
||
|
||
if (staticIpInput) staticIpInput.value = config.Static_Cfg_IP_Address_Ethernet || "";
|
||
if (subnetMaskInput) subnetMaskInput.value = config.Static_Cfg_Netmask_Ethernet || "";
|
||
if (gatewayInput) gatewayInput.value = config.Static_Cfg_Gateway_IP_Address_Ethernet || "";
|
||
appliedIpModeEth = config.DHCP_Mode_Ethernet ? "dhcp" : "static";
|
||
setIpAssignmentMode(appliedIpModeEth);
|
||
|
||
const _isStaticSta =
|
||
config.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_STATIC ||
|
||
config.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_STATIC;
|
||
if (_isStaticSta) {
|
||
if (staticIpInputStation) staticIpInputStation.value = config.Static_Cfg_IP_Address_Station || "";
|
||
if (subnetMaskInputStation) subnetMaskInputStation.value = config.Static_Cfg_Netmask_Station || "";
|
||
if (gatewayInputStation) gatewayInputStation.value = config.Static_Cfg_Gateway_IP_Address_Station || "";
|
||
} else {
|
||
if (staticIpInputStation) staticIpInputStation.value = "";
|
||
if (subnetMaskInputStation) subnetMaskInputStation.value = "";
|
||
if (gatewayInputStation) gatewayInputStation.value = "";
|
||
}
|
||
appliedIpModeSta = config.DHCP_Mode_Station ? "dhcp" : "static";
|
||
setIpAssignmentModeStation(appliedIpModeSta);
|
||
|
||
const _isApsta =
|
||
config.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_DHCP_APSTA ||
|
||
config.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_APSTA;
|
||
const _isSta =
|
||
config.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_DHCP ||
|
||
config.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_STATIC ||
|
||
config.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_DHCP ||
|
||
config.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_STATIC;
|
||
const _isNoWifi =
|
||
config.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_DHCP_NO_WIFI ||
|
||
config.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_NO_WIFI;
|
||
|
||
const wifiStationCard = document.getElementById("wifi-station-card");
|
||
wifiStationCard?.classList.toggle("hidden", !_isSta);
|
||
|
||
// WiFi stat card: always visible, content reflects current state
|
||
const wifiCardTitle = document.getElementById("wifi-card-title");
|
||
if (wifiCardTitle) wifiCardTitle.textContent = _isApsta ? "WiFi Access Point" : "WiFi";
|
||
if (ssidCardDisplay) {
|
||
ssidCardDisplay.textContent = _isSta ? config.SSID_Station || "-" : _isApsta ? config.SSID_AP || "-" : "-";
|
||
}
|
||
const pwDisplayGroup = document.getElementById("pw-display-group");
|
||
const openNetworkLabel = document.getElementById("open-network-label");
|
||
const rssiRow = document.getElementById("rssi-row");
|
||
if (_isApsta) {
|
||
const hasApPassword = !!config.Password_AP;
|
||
if (hasApPassword) {
|
||
isPasswordVisible = true;
|
||
if (eyeIconOpen) eyeIconOpen.classList.add("hidden");
|
||
if (eyeIconClosed) eyeIconClosed.classList.remove("hidden");
|
||
updatePasswordDisplay(config.Password_AP);
|
||
pwDisplayGroup?.classList.remove("hidden");
|
||
openNetworkLabel?.classList.add("hidden");
|
||
} else {
|
||
pwDisplayGroup?.classList.add("hidden");
|
||
openNetworkLabel?.classList.remove("hidden");
|
||
}
|
||
if (passwordDesc) {
|
||
passwordDesc.classList.remove("hidden");
|
||
passwordDesc.classList.add("flex");
|
||
}
|
||
rssiRow?.classList.add("hidden");
|
||
} else if (_isSta) {
|
||
const hasStaPassword = !!config.Password_Station;
|
||
if (hasStaPassword) {
|
||
isPasswordVisible = true;
|
||
if (eyeIconOpen) eyeIconOpen.classList.add("hidden");
|
||
if (eyeIconClosed) eyeIconClosed.classList.remove("hidden");
|
||
updatePasswordDisplay(config.Password_Station);
|
||
pwDisplayGroup?.classList.remove("hidden");
|
||
openNetworkLabel?.classList.add("hidden");
|
||
} else {
|
||
pwDisplayGroup?.classList.add("hidden");
|
||
openNetworkLabel?.classList.remove("hidden");
|
||
}
|
||
if (passwordDesc) {
|
||
passwordDesc.classList.remove("hidden");
|
||
passwordDesc.classList.add("flex");
|
||
}
|
||
rssiRow?.classList.remove("hidden");
|
||
} else {
|
||
if (passwordDesc) {
|
||
passwordDesc.classList.add("hidden");
|
||
passwordDesc.classList.remove("flex");
|
||
}
|
||
rssiRow?.classList.remove("hidden");
|
||
}
|
||
|
||
if (_isApsta) {
|
||
if (apModeIpAlertPlaceholder) apModeIpAlertPlaceholder.classList.remove("hidden");
|
||
if (ipAssignmentButtonsStation) ipAssignmentButtonsStation.classList.add("hidden");
|
||
if (ipModeDescriptionStation) ipModeDescriptionStation.classList.add("hidden");
|
||
staticIpContainerStation?.classList.add("hidden");
|
||
saveStaticIpBtnStation?.classList.add("hidden");
|
||
showWifiSettings(true);
|
||
} else if (_isSta) {
|
||
if (apModeIpAlertPlaceholder) apModeIpAlertPlaceholder.classList.add("hidden");
|
||
if (ipAssignmentButtonsStation) ipAssignmentButtonsStation.classList.remove("hidden");
|
||
if (ipModeDescriptionStation) ipModeDescriptionStation.classList.remove("hidden");
|
||
showWifiSettings(true);
|
||
}
|
||
if (_isNoWifi) {
|
||
if (apModeIpAlertPlaceholder) apModeIpAlertPlaceholder.classList.add("hidden");
|
||
showWifiSettings(false);
|
||
} else if (config.Connection_Mode === ConnectionMode.CONNECTION_MODE_UNKNOWN) {
|
||
showWifiSettings(false);
|
||
}
|
||
updateWifiModeState(config.Connection_Mode);
|
||
|
||
// Initialize antenna selection radios from config
|
||
const _antennaRadios = document.querySelectorAll('input[name="antenna-selection"]') as NodeListOf<HTMLInputElement>;
|
||
const _selectionValue = config.Antenna_Selection === AntennaSelection.ANTENNA_SELECTION_EXTERNAL ? "external" : "internal";
|
||
_antennaRadios.forEach((r) => { r.checked = r.value === _selectionValue; });
|
||
|
||
// Initialize power slider from config — skip if user is currently dragging
|
||
if (!powerSliderDragging) {
|
||
const _powerSlider = document.getElementById("antenna-power") as HTMLInputElement;
|
||
const _powerDisplay = document.getElementById("power-value-display");
|
||
if (_powerSlider && config.Antenna_Power >= 8 && config.Antenna_Power <= 84) {
|
||
const _dBm = config.Antenna_Power / 4;
|
||
_powerSlider.value = String(_dBm);
|
||
if (_powerDisplay) {
|
||
const _mW = Math.pow(10, _dBm / 10);
|
||
_powerDisplay.textContent = `${_dBm.toFixed(2)} dBm (${_mW.toFixed(2)} mW)`;
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
const wifiDisableModal = document.getElementById("wifi-disable-warning-modal") as HTMLDialogElement;
|
||
|
||
wifiDisableModal?.addEventListener("wifi-disable-confirmed", () => {
|
||
showWifiSettings(false);
|
||
const baseConfig = $current_network_configuration.get();
|
||
const isStaticEth =
|
||
baseConfig.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_NO_WIFI ||
|
||
baseConfig.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_APSTA ||
|
||
baseConfig.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_DHCP ||
|
||
baseConfig.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_STATIC;
|
||
send_new_network_configuration_to_alpha({
|
||
...baseConfig,
|
||
Connection_Mode: isStaticEth ? ConnectionMode.CONNECTION_MODE_ETH_STATIC_NO_WIFI : ConnectionMode.CONNECTION_MODE_ETH_DHCP_NO_WIFI,
|
||
WiFi_Enabled: false,
|
||
});
|
||
});
|
||
|
||
wifiToggle?.addEventListener("change", (e: Event) => {
|
||
const isEnabled = (e.target as HTMLInputElement).checked;
|
||
|
||
if (!isEnabled) {
|
||
wifiDisableModal?.showModal();
|
||
wifiToggle.checked = true; // revert until confirmed
|
||
return;
|
||
}
|
||
|
||
showWifiSettings(isEnabled);
|
||
});
|
||
|
||
$network_status_display.subscribe((status) => {
|
||
// Ethernet card
|
||
if (ethIpDisplay) ethIpDisplay.textContent = status.IP_Address_Ethernet || "-";
|
||
if (ethGatewayDisplay) ethGatewayDisplay.textContent = status.Gateway_IP_Address_Ethernet || "-";
|
||
if (ethNetmaskDisplay) ethNetmaskDisplay.textContent = status.Netmask_Ethernet || "-";
|
||
if (ethMacDisplay) ethMacDisplay.textContent = status.MAC_Address_Ethernet || "-";
|
||
|
||
// WiFi Station card
|
||
const wifiIpCardTitle = document.getElementById("wifi-ip-card-title");
|
||
const isApsta =
|
||
status.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_DHCP_APSTA ||
|
||
status.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_APSTA;
|
||
const isSta =
|
||
status.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_DHCP ||
|
||
status.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_STATIC ||
|
||
status.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_DHCP ||
|
||
status.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_STATIC;
|
||
if (wifiIpCardTitle) wifiIpCardTitle.textContent = isApsta ? "Access Point" : "WiFi Station";
|
||
if (staIpDisplay) staIpDisplay.textContent = status.IP_Address_Station || "-";
|
||
if (staGatewayDisplay) staGatewayDisplay.textContent = status.Gateway_IP_Address_Station || "-";
|
||
if (staNetmaskDisplay) staNetmaskDisplay.textContent = status.Netmask_Station || "-";
|
||
if (staMacDisplay) staMacDisplay.textContent = status.MAC_Address_WiFi || "-";
|
||
|
||
// Hostname display
|
||
const hostnameDisplay = document.getElementById("hostname-display");
|
||
if (hostnameDisplay && status.MDNS_Hostname) hostnameDisplay.textContent = status.MDNS_Hostname;
|
||
if (hostnamePreviewCardHint && status.MDNS_Hostname) hostnamePreviewCardHint.textContent = status.MDNS_Hostname;
|
||
if (hostnameDescLink && status.MDNS_Hostname) hostnameDescLink.href = `http://${status.MDNS_Hostname}.local`;
|
||
if (hostnamePreviewHint && status.MDNS_Hostname) hostnamePreviewHint.textContent = status.MDNS_Hostname;
|
||
|
||
// Connected clients
|
||
const clientsDisplay = document.getElementById("clients-display");
|
||
const clientsDesc = document.getElementById("clients-desc");
|
||
if (clientsDisplay) clientsDisplay.textContent = status.ws_connected_clients.toString();
|
||
if (clientsDesc) {
|
||
const n = status.ws_connected_clients;
|
||
clientsDesc.textContent = n === 0 ? "No clients" : n === 1 ? "1 client connected" : `${n} clients connected`;
|
||
}
|
||
|
||
// Signal strength / RSSI — hidden entirely for AP-only mode (no STA)
|
||
const rssiRowEl = document.getElementById("rssi-row");
|
||
const noWifi =
|
||
status.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_DHCP_NO_WIFI ||
|
||
status.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_NO_WIFI ||
|
||
status.Connection_Mode === ConnectionMode.CONNECTION_MODE_UNKNOWN;
|
||
rssiRowEl?.classList.toggle("hidden", isApsta && !isSta);
|
||
if (isApsta && !isSta) {
|
||
updateSignalIcon(-30, true);
|
||
if (signalDesc) signalDesc.textContent = "";
|
||
} else if (status.rssi !== null) {
|
||
if (signalDisplay) signalDisplay.textContent = `${status.rssi} dBm`;
|
||
if (signalDesc) {
|
||
const r = status.rssi;
|
||
signalDesc.textContent = r >= -50 ? "Excellent" : r >= -60 ? "Good" : r >= -70 ? "Fair" : "Weak";
|
||
}
|
||
updateSignalIcon(status.rssi, true);
|
||
} else {
|
||
if (signalDisplay) signalDisplay.textContent = "-";
|
||
if (signalDesc) {
|
||
signalDesc.textContent = isSta ? "Not connected" : "WiFi disabled";
|
||
}
|
||
updateSignalIcon(null, !noWifi);
|
||
}
|
||
});
|
||
|
||
function updateSignalIcon(rssi: number | null, wifiActive: boolean) {
|
||
const signalIcon = document.getElementById("signal-icon");
|
||
if (!signalIcon) return;
|
||
|
||
const svgAttrs = `xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"`;
|
||
const pNone = `<path stroke="none" d="M0 0h24v24H0z" fill="none"/>`;
|
||
const pDot = `<path d="M12 18l.01 0"/>`;
|
||
const pArc1 = `<path d="M9.172 15.172a4 4 0 0 1 5.656 0"/>`;
|
||
const pArc2 = `<path d="M6.343 12.343a8 8 0 0 1 11.314 0"/>`;
|
||
const pArc3 = `<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0"/>`;
|
||
|
||
if (!wifiActive) {
|
||
signalIcon.innerHTML = `<svg ${svgAttrs}>${pNone}${pDot}
|
||
<path d="M9.172 15.172a4 4 0 0 1 5.656 0"/>
|
||
<path d="M6.343 12.343a7.963 7.963 0 0 1 3.864 -2.14m4.163 .155a7.965 7.965 0 0 1 3.287 2"/>
|
||
<path d="M3.515 9.515a12 12 0 0 1 3.544 -2.455m3.101 -.92a12 12 0 0 1 10.325 3.374"/>
|
||
<path d="M3 3l18 18"/></svg>`;
|
||
return;
|
||
}
|
||
|
||
const bars = rssi === null ? 0 : rssi >= -50 ? 4 : rssi >= -60 ? 3 : rssi >= -70 ? 2 : 1;
|
||
const fg = bars >= 4 ? pDot + pArc1 + pArc2 + pArc3 : bars === 3 ? pDot + pArc1 + pArc2 : bars === 2 ? pDot + pArc1 : pDot;
|
||
const bg = pDot + pArc1 + pArc2 + pArc3;
|
||
signalIcon.innerHTML =
|
||
`<div class="relative" style="width:28px;height:28px">` +
|
||
`<svg ${svgAttrs} class="absolute inset-0 opacity-20">${pNone}${bg}</svg>` +
|
||
`<svg ${svgAttrs} class="absolute inset-0">${pNone}${fg}</svg>` +
|
||
`</div>`;
|
||
}
|
||
|
||
// Network config error modal
|
||
const networkErrorModal = document.getElementById("network-config-error-modal") as HTMLDialogElement | null;
|
||
const networkErrorReasonText = document.getElementById("network-error-reason-text");
|
||
const networkErrorSsidRow = document.getElementById("network-error-ssid-row");
|
||
const networkErrorSsid = document.getElementById("network-error-ssid");
|
||
const networkErrorReason = document.getElementById("network-error-reason");
|
||
|
||
function reasonToHuman(reason: string): string {
|
||
switch (reason) {
|
||
case "max_retries_exceeded":
|
||
return "Could not connect to WiFi using the provided configuration. The access point did not accept the connection after multiple attempts.";
|
||
case "auth_failed":
|
||
return "Could not connect to WiFi using the provided configuration. Authentication was rejected — check your WiFi password.";
|
||
default:
|
||
return reason;
|
||
}
|
||
}
|
||
|
||
$network_config_error.subscribe((err) => {
|
||
if (!err || !networkErrorModal) return;
|
||
const hasSsid = !!err.failed_ssid;
|
||
if (networkErrorSsidRow) networkErrorSsidRow.classList.toggle("hidden", !hasSsid);
|
||
if (networkErrorSsid) networkErrorSsid.textContent = err.failed_ssid || "";
|
||
if (networkErrorReason) networkErrorReason.textContent = err.reason;
|
||
if (networkErrorReasonText) networkErrorReasonText.textContent = reasonToHuman(err.reason);
|
||
networkErrorModal.showModal();
|
||
});
|
||
|
||
const antennaRadios = document.querySelectorAll('input[name="antenna-selection"]') as NodeListOf<HTMLInputElement>;
|
||
|
||
function setAntennaSelection(value: "internal" | "external") {
|
||
antennaRadios.forEach((radio) => {
|
||
radio.checked = radio.value === value;
|
||
});
|
||
}
|
||
|
||
antennaRadios.forEach((radio) => {
|
||
radio.addEventListener("change", () => {
|
||
setAntennaSelection(radio.value as "internal" | "external");
|
||
const sel =
|
||
radio.value === "external"
|
||
? AntennaSelection.ANTENNA_SELECTION_EXTERNAL
|
||
: AntennaSelection.ANTENNA_SELECTION_INTERNAL;
|
||
set_antenna_selection(sel);
|
||
});
|
||
});
|
||
|
||
const powerSlider = document.getElementById("antenna-power") as HTMLInputElement;
|
||
const powerValueDisplay = document.getElementById("power-value-display");
|
||
|
||
function dbmToMilliwatts(dbm: number): number {
|
||
return Math.pow(10, dbm / 10);
|
||
}
|
||
|
||
powerSlider?.addEventListener("pointerdown", () => { powerSliderDragging = true; });
|
||
powerSlider?.addEventListener("pointerup", () => { powerSliderDragging = false; });
|
||
|
||
powerSlider?.addEventListener("input", (e) => {
|
||
const value = parseFloat((e.target as HTMLInputElement).value);
|
||
const milliwatts = dbmToMilliwatts(value);
|
||
if (powerValueDisplay) {
|
||
powerValueDisplay.textContent = `${value.toFixed(2)} dBm (${milliwatts.toFixed(2)} mW)`;
|
||
}
|
||
});
|
||
|
||
powerSlider?.addEventListener("change", (e) => {
|
||
const value = parseFloat((e.target as HTMLInputElement).value);
|
||
set_antenna_power(Math.round(value * 4));
|
||
});
|
||
|
||
function setHostnameInputState(state: "idle" | "error" | "success", message?: string) {
|
||
if (!hostnameInputField) return;
|
||
hostnameInputField.classList.remove("input-error");
|
||
if (state === "error") {
|
||
hostnameInputField.classList.add("input-error");
|
||
}
|
||
|
||
if (hostnameErrorMsg) {
|
||
if (state === "error" && message) {
|
||
hostnameErrorMsg.textContent = message;
|
||
hostnameErrorMsg.classList.remove("hidden");
|
||
} else {
|
||
hostnameErrorMsg.classList.add("hidden");
|
||
}
|
||
}
|
||
}
|
||
|
||
function validateHostnameInput(showEmptyError: boolean = false) {
|
||
if (!hostnameInput) return false;
|
||
const trimmed = (hostnameInput.value || "").trim();
|
||
if (trimmed.length === 0) {
|
||
if (showEmptyError) {
|
||
setHostnameInputState("error", "Hostname is required.");
|
||
} else {
|
||
setHostnameInputState("idle");
|
||
}
|
||
return false;
|
||
}
|
||
if (!HOSTNAME_REGEX.test(trimmed)) {
|
||
let msg: string;
|
||
if (trimmed.startsWith("-")) {
|
||
msg = "Hostname cannot start with a hyphen.";
|
||
} else if (trimmed.endsWith("-")) {
|
||
msg = "Hostname cannot end with a hyphen.";
|
||
} else if (/[A-Z]/.test(trimmed)) {
|
||
msg = "Hostname must be lowercase — only a–z, 0–9, and hyphens.";
|
||
} else {
|
||
msg = "Only lowercase letters (a–z), digits, and hyphens (-) are allowed.";
|
||
}
|
||
setHostnameInputState("error", msg);
|
||
return false;
|
||
}
|
||
setHostnameInputState("success");
|
||
return true;
|
||
}
|
||
|
||
// Subscription writes cleaned value back into the DOM input
|
||
$hostname_input_field.subscribe((val) => {
|
||
if (hostnameInput && hostnameInput.value !== val) {
|
||
hostnameInput.value = val;
|
||
}
|
||
});
|
||
|
||
// Initialize store with current input value (fires subscription immediately)
|
||
if (hostnameInput) {
|
||
$hostname_input_field.set(normalizeHostnameValue(hostnameInput.value || ""));
|
||
}
|
||
|
||
hostnameInput?.addEventListener("input", () => {
|
||
if (!hostnameInput) return;
|
||
const raw = hostnameInput.value;
|
||
let cleaned = "";
|
||
let hadInvalid = false;
|
||
for (const ch of raw) {
|
||
if (/[A-Z]/.test(ch)) {
|
||
cleaned += ch.toLowerCase();
|
||
hadInvalid = true;
|
||
} else if (/[a-z0-9-]/.test(ch)) {
|
||
cleaned += ch;
|
||
} else {
|
||
hadInvalid = true;
|
||
}
|
||
}
|
||
if (cleaned.length > MAX_HOSTNAME_LENGTH) {
|
||
cleaned = cleaned.slice(0, MAX_HOSTNAME_LENGTH);
|
||
hadInvalid = true;
|
||
}
|
||
$hostname_input_field.set(cleaned);
|
||
if (hadInvalid) {
|
||
setHostnameInputState("error", "Only lowercase letters (a–z), digits, and hyphens (-) are allowed.");
|
||
} else {
|
||
validateHostnameInput(false);
|
||
}
|
||
});
|
||
|
||
hostnameInput?.addEventListener("blur", () => {
|
||
if (!hostnameInput) return;
|
||
$hostname_input_field.set(normalizeHostnameValue(hostnameInput.value || ""));
|
||
validateHostnameInput(true);
|
||
});
|
||
|
||
// Hostname-changed redirect modal
|
||
let _hostnameRedirectTimer: ReturnType<typeof setInterval> | null = null;
|
||
|
||
function showHostnameChangedModal(newHostname: string) {
|
||
const modal = document.getElementById("hostname-changed-modal") as HTMLDialogElement;
|
||
const link = document.getElementById("hostname-changed-link") as HTMLAnchorElement;
|
||
const nameSpan = document.getElementById("hostname-changed-new-name");
|
||
const countdown = document.getElementById("hostname-redirect-countdown");
|
||
const progress = document.getElementById("hostname-redirect-progress") as HTMLProgressElement;
|
||
const goNowBtn = document.getElementById("hostname-redirect-now-btn") as HTMLAnchorElement;
|
||
|
||
const newUrl = `http://${newHostname}.local`;
|
||
if (link) {
|
||
link.href = newUrl;
|
||
}
|
||
if (goNowBtn) {
|
||
goNowBtn.href = newUrl;
|
||
}
|
||
if (nameSpan) {
|
||
nameSpan.textContent = newHostname;
|
||
}
|
||
|
||
let remaining = 10;
|
||
if (countdown) countdown.textContent = String(remaining);
|
||
if (progress) progress.value = 100;
|
||
|
||
if (_hostnameRedirectTimer) clearInterval(_hostnameRedirectTimer);
|
||
|
||
_hostnameRedirectTimer = setInterval(() => {
|
||
remaining--;
|
||
if (countdown) countdown.textContent = String(remaining);
|
||
if (progress) progress.value = (remaining / 10) * 100;
|
||
if (remaining <= 0) {
|
||
clearInterval(_hostnameRedirectTimer!);
|
||
_hostnameRedirectTimer = null;
|
||
window.location.href = newUrl;
|
||
}
|
||
}, 1000);
|
||
|
||
modal.showModal();
|
||
}
|
||
|
||
applyHostnameBtn?.addEventListener("click", () => {
|
||
if (!hostnameInput) return;
|
||
|
||
hostnameInput.value = normalizeHostnameValue(hostnameInput.value || "");
|
||
if (!validateHostnameInput(true)) {
|
||
return;
|
||
}
|
||
|
||
const hostnameValue = (hostnameInput.value || "").trim();
|
||
|
||
if (set_hostname(hostnameValue)) {
|
||
hostnameInput.value = hostnameValue;
|
||
setHostnameInputState("success");
|
||
showHostnameChangedModal(hostnameValue);
|
||
} else {
|
||
setHostnameInputState("error");
|
||
}
|
||
});
|
||
|
||
const stationBtn = document.getElementById("mode-station");
|
||
const apBtn = document.getElementById("mode-ap");
|
||
const stationModal = document.getElementById("station-config-modal") as HTMLDialogElement;
|
||
const apModal = document.getElementById("ap-config-modal") as HTMLDialogElement;
|
||
|
||
stationBtn?.addEventListener("click", () => {
|
||
stationModal?.showModal();
|
||
});
|
||
|
||
apBtn?.addEventListener("click", () => {
|
||
apModal?.showModal();
|
||
});
|
||
|
||
staticIpInput?.addEventListener("input", () => clearFieldError(staticIpInput, staticIpError));
|
||
subnetMaskInput?.addEventListener("input", () => clearFieldError(subnetMaskInput, subnetMaskError));
|
||
gatewayInput?.addEventListener("input", () => clearFieldError(gatewayInput, gatewayError));
|
||
|
||
saveStaticIpBtn?.addEventListener("click", () => {
|
||
if (!staticIpConfirmModal) return;
|
||
|
||
if (currentIpAssignmentMode === "static") {
|
||
const validation = validateStaticIpForm();
|
||
if (!validation.valid) {
|
||
return;
|
||
}
|
||
pendingStaticIpConfig = { mode: "static", values: validation.values };
|
||
} else {
|
||
pendingStaticIpConfig = { mode: "dhcp" };
|
||
clearStaticIpErrors();
|
||
}
|
||
|
||
if (pendingStaticIpConfig) {
|
||
updateStaticIpSummary(pendingStaticIpConfig);
|
||
staticIpConfirmModal.showModal();
|
||
}
|
||
});
|
||
|
||
cancelStaticIpBtn?.addEventListener("click", () => {
|
||
pendingStaticIpConfig = null;
|
||
pendingStaticIpConfigStation = null;
|
||
});
|
||
|
||
staticIpConfirmModal?.addEventListener("close", () => {
|
||
// If the modal was opened by a DHCP radio click but the user cancelled,
|
||
// revert the radio button so the UI matches the actual (unchanged) device config.
|
||
if (dhcpRevertPending === "eth") {
|
||
setIpAssignmentMode("static");
|
||
} else if (dhcpRevertPending === "sta") {
|
||
setIpAssignmentModeStation("static");
|
||
}
|
||
dhcpRevertPending = null;
|
||
pendingStaticIpConfig = null;
|
||
pendingStaticIpConfigStation = null;
|
||
});
|
||
|
||
confirmStaticIpBtn?.addEventListener("click", () => {
|
||
const configToApply = pendingStaticIpConfig || pendingStaticIpConfigStation;
|
||
if (!configToApply) return;
|
||
|
||
const baseConfig = $current_network_configuration.get();
|
||
const updatedConfig: NetworkConfigNVS = {
|
||
...baseConfig,
|
||
};
|
||
|
||
// Determine which mode we're configuring based on which config is pending
|
||
if (pendingStaticIpConfig) {
|
||
if (configToApply.mode === "dhcp") {
|
||
updatedConfig.DHCP_Mode_Ethernet = true;
|
||
updatedConfig.Static_Cfg_IP_Address_Ethernet = "";
|
||
updatedConfig.Static_Cfg_Netmask_Ethernet = "";
|
||
updatedConfig.Static_Cfg_Gateway_IP_Address_Ethernet = "";
|
||
// Switch to DHCP-ETH variant of current mode
|
||
if (updatedConfig.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_NO_WIFI)
|
||
updatedConfig.Connection_Mode = ConnectionMode.CONNECTION_MODE_ETH_DHCP_NO_WIFI;
|
||
else if (updatedConfig.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_APSTA)
|
||
updatedConfig.Connection_Mode = ConnectionMode.CONNECTION_MODE_ETH_DHCP_APSTA;
|
||
else if (updatedConfig.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_DHCP)
|
||
updatedConfig.Connection_Mode = ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_DHCP;
|
||
else if (updatedConfig.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_STATIC)
|
||
updatedConfig.Connection_Mode = ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_STATIC;
|
||
} else if (configToApply.values) {
|
||
updatedConfig.DHCP_Mode_Ethernet = false;
|
||
updatedConfig.Static_Cfg_IP_Address_Ethernet = configToApply.values.ip;
|
||
updatedConfig.Static_Cfg_Netmask_Ethernet = configToApply.values.netmask;
|
||
updatedConfig.Static_Cfg_Gateway_IP_Address_Ethernet = configToApply.values.gateway;
|
||
// Switch to static-ETH variant of current mode
|
||
if (updatedConfig.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_DHCP_NO_WIFI)
|
||
updatedConfig.Connection_Mode = ConnectionMode.CONNECTION_MODE_ETH_STATIC_NO_WIFI;
|
||
else if (updatedConfig.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_DHCP_APSTA)
|
||
updatedConfig.Connection_Mode = ConnectionMode.CONNECTION_MODE_ETH_STATIC_APSTA;
|
||
else if (updatedConfig.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_DHCP)
|
||
updatedConfig.Connection_Mode = ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_DHCP;
|
||
else if (updatedConfig.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_STATIC)
|
||
updatedConfig.Connection_Mode = ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_STATIC;
|
||
}
|
||
} else if (pendingStaticIpConfigStation) {
|
||
if (configToApply.mode === "dhcp") {
|
||
updatedConfig.DHCP_Mode_Station = true;
|
||
updatedConfig.Static_Cfg_IP_Address_Station = "";
|
||
updatedConfig.Static_Cfg_Netmask_Station = "";
|
||
updatedConfig.Static_Cfg_Gateway_IP_Address_Station = "";
|
||
// Switch to STA_DHCP variant of current mode
|
||
if (updatedConfig.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_STATIC)
|
||
updatedConfig.Connection_Mode = ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_DHCP;
|
||
else if (updatedConfig.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_STATIC)
|
||
updatedConfig.Connection_Mode = ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_DHCP;
|
||
} else if (configToApply.values) {
|
||
updatedConfig.DHCP_Mode_Station = false;
|
||
updatedConfig.Static_Cfg_IP_Address_Station = configToApply.values.ip;
|
||
updatedConfig.Static_Cfg_Netmask_Station = configToApply.values.netmask;
|
||
updatedConfig.Static_Cfg_Gateway_IP_Address_Station = configToApply.values.gateway;
|
||
// Switch to STA_STATIC variant of current mode
|
||
if (updatedConfig.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_DHCP)
|
||
updatedConfig.Connection_Mode = ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_STATIC;
|
||
else if (updatedConfig.Connection_Mode === ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_DHCP)
|
||
updatedConfig.Connection_Mode = ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_STATIC;
|
||
}
|
||
}
|
||
|
||
const sent = send_new_network_configuration_to_alpha(updatedConfig);
|
||
if (sent) {
|
||
if (pendingStaticIpConfig) appliedIpModeEth = pendingStaticIpConfig.mode;
|
||
if (pendingStaticIpConfigStation) appliedIpModeSta = pendingStaticIpConfigStation.mode;
|
||
dhcpRevertPending = null;
|
||
pendingStaticIpConfig = null;
|
||
pendingStaticIpConfigStation = null;
|
||
staticIpConfirmModal?.close();
|
||
showApplyingIndicator();
|
||
} else {
|
||
console.error("Failed to send network configuration to Alpha device.");
|
||
}
|
||
});
|
||
|
||
function showApplyingIndicator() {
|
||
const toast = document.getElementById("config-applying-toast");
|
||
if (!toast) return;
|
||
toast.classList.remove("hidden");
|
||
const unsub = $current_network_configuration.listen(() => {
|
||
toast.classList.add("hidden");
|
||
unsub();
|
||
});
|
||
}
|
||
|
||
window.addEventListener("network-config-applying", () => showApplyingIndicator());
|
||
|
||
function clearFieldError(input: HTMLInputElement | null, errorEl: HTMLElement | null) {
|
||
if (!input || !errorEl) return;
|
||
(input.parentElement ?? input).classList.remove("input-error");
|
||
errorEl.classList.add("hidden");
|
||
errorEl.textContent = "";
|
||
}
|
||
|
||
function setFieldError(
|
||
input: HTMLInputElement | null,
|
||
errorEl: HTMLElement | null,
|
||
message: string,
|
||
firstError: { field: HTMLInputElement | null },
|
||
) {
|
||
if (!input || !errorEl) return;
|
||
(input.parentElement ?? input).classList.add("input-error");
|
||
errorEl.textContent = message;
|
||
errorEl.classList.remove("hidden");
|
||
if (!firstError.field) {
|
||
firstError.field = input;
|
||
}
|
||
}
|
||
|
||
function clearStaticIpErrors() {
|
||
clearFieldError(staticIpInput, staticIpError);
|
||
clearFieldError(subnetMaskInput, subnetMaskError);
|
||
clearFieldError(gatewayInput, gatewayError);
|
||
}
|
||
|
||
function validateStaticIpForm(): { valid: true; values: StaticIpValues } | { valid: false } {
|
||
if (!staticIpInput || !subnetMaskInput || !gatewayInput) {
|
||
return { valid: false };
|
||
}
|
||
|
||
clearStaticIpErrors();
|
||
|
||
const values: StaticIpValues = {
|
||
ip: (staticIpInput.value || "").trim(),
|
||
netmask: (subnetMaskInput.value || "").trim(),
|
||
gateway: (gatewayInput.value || "").trim(),
|
||
};
|
||
|
||
let isValid = true;
|
||
const firstError: { field: HTMLInputElement | null } = { field: null };
|
||
|
||
if (!values.netmask) {
|
||
setFieldError(subnetMaskInput, subnetMaskError, "Netmask is required", firstError);
|
||
isValid = false;
|
||
} else if (!is_valid_ip_format(values.netmask)) {
|
||
setFieldError(subnetMaskInput, subnetMaskError, "Invalid netmask format", firstError);
|
||
isValid = false;
|
||
} else if (!isValidNetmask(values.netmask)) {
|
||
setFieldError(subnetMaskInput, subnetMaskError, "Netmask must use contiguous bits", firstError);
|
||
isValid = false;
|
||
} else if (!isPracticalNetmask(values.netmask)) {
|
||
setFieldError(subnetMaskInput, subnetMaskError, "Netmask must allow host and gateway addresses", firstError);
|
||
isValid = false;
|
||
}
|
||
|
||
if (!values.ip) {
|
||
setFieldError(staticIpInput, staticIpError, "IP address is required", firstError);
|
||
isValid = false;
|
||
} else if (!is_valid_ip_format(values.ip)) {
|
||
setFieldError(staticIpInput, staticIpError, "Invalid IP address format", firstError);
|
||
isValid = false;
|
||
}
|
||
|
||
if (!values.gateway) {
|
||
setFieldError(gatewayInput, gatewayError, "Gateway is required", firstError);
|
||
isValid = false;
|
||
} else if (!is_valid_ip_format(values.gateway)) {
|
||
setFieldError(gatewayInput, gatewayError, "Invalid gateway format", firstError);
|
||
isValid = false;
|
||
}
|
||
|
||
if (!isValid) {
|
||
firstError.field?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||
return { valid: false };
|
||
}
|
||
|
||
if (!isInValidHostRange(values.ip, values.netmask)) {
|
||
setFieldError(staticIpInput, staticIpError, "IP cannot be network or broadcast address", firstError);
|
||
isValid = false;
|
||
}
|
||
|
||
if (!isInValidHostRange(values.gateway, values.netmask)) {
|
||
setFieldError(gatewayInput, gatewayError, "Gateway cannot be network or broadcast address", firstError);
|
||
isValid = false;
|
||
}
|
||
|
||
if (!isInSameSubnet(values.ip, values.gateway, values.netmask)) {
|
||
setFieldError(staticIpInput, staticIpError, "IP and gateway must be in the same subnet", firstError);
|
||
setFieldError(gatewayInput, gatewayError, "IP and gateway must be in the same subnet", firstError);
|
||
isValid = false;
|
||
}
|
||
|
||
if (values.ip === values.gateway) {
|
||
setFieldError(gatewayInput, gatewayError, "Gateway cannot match the host IP", firstError);
|
||
isValid = false;
|
||
}
|
||
|
||
if (!isValid) {
|
||
firstError.field?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||
return { valid: false };
|
||
}
|
||
|
||
return { valid: true, values };
|
||
}
|
||
|
||
function updateStaticIpSummary(config: { mode: "dhcp" | "static"; values?: StaticIpValues }) {
|
||
if (staticIpSummaryMode) {
|
||
staticIpSummaryMode.textContent = config.mode === "dhcp" ? "Automatic (DHCP)" : "Static IP";
|
||
}
|
||
const showStaticDetails = config.mode === "static" && !!config.values;
|
||
[staticIpSummaryIpRow, staticIpSummaryNetmaskRow, staticIpSummaryGatewayRow].forEach((row) => {
|
||
row?.classList.toggle("hidden", !showStaticDetails);
|
||
});
|
||
|
||
if (showStaticDetails && config.values) {
|
||
if (staticIpSummaryIp) staticIpSummaryIp.textContent = config.values.ip;
|
||
if (staticIpSummaryNetmask) staticIpSummaryNetmask.textContent = config.values.netmask;
|
||
if (staticIpSummaryGateway) staticIpSummaryGateway.textContent = config.values.gateway;
|
||
}
|
||
|
||
// Warning is only relevant for static IP — hide it for DHCP
|
||
const warning = document.getElementById("static-ip-warning");
|
||
warning?.classList.toggle("hidden", config.mode === "dhcp");
|
||
}
|
||
|
||
function setIpAssignmentMode(mode: "dhcp" | "static") {
|
||
currentIpAssignmentMode = mode;
|
||
ipModeButtons.forEach((radio) => {
|
||
radio.checked = radio.value === mode;
|
||
});
|
||
|
||
if (mode === "dhcp") {
|
||
if (ipModeDescription) ipModeDescription.textContent = "Automatic IP assignment";
|
||
staticIpContainer?.classList.add("hidden");
|
||
clearStaticIpErrors();
|
||
} else {
|
||
if (ipModeDescription) ipModeDescription.textContent = "Static IP assignment";
|
||
staticIpContainer?.classList.remove("hidden");
|
||
const cfg = $current_network_configuration.get();
|
||
if (!staticIpInput?.value) staticIpInput!.value = cfg.Static_Cfg_IP_Address_Ethernet || cfg.IP_Address_Ethernet || "";
|
||
if (!subnetMaskInput?.value) subnetMaskInput!.value = cfg.Static_Cfg_Netmask_Ethernet || cfg.Netmask_Ethernet || "";
|
||
if (!gatewayInput?.value) gatewayInput!.value = cfg.Static_Cfg_Gateway_IP_Address_Ethernet || cfg.Gateway_IP_Address_Ethernet || "";
|
||
}
|
||
}
|
||
|
||
ipModeButtons.forEach((radio) => {
|
||
radio.addEventListener("change", () => {
|
||
const newMode = radio.value === "static" ? "static" : "dhcp";
|
||
setIpAssignmentMode(newMode);
|
||
if (newMode === "dhcp" && appliedIpModeEth === "static" && staticIpConfirmModal) {
|
||
pendingStaticIpConfig = { mode: "dhcp" };
|
||
dhcpRevertPending = "eth";
|
||
updateStaticIpSummary(pendingStaticIpConfig);
|
||
staticIpConfirmModal.showModal();
|
||
}
|
||
});
|
||
});
|
||
|
||
setIpAssignmentMode("dhcp");
|
||
|
||
// Station mode static IP handlers
|
||
function clearStaticIpErrorsStation() {
|
||
clearFieldError(staticIpInputStation, staticIpErrorStation);
|
||
clearFieldError(subnetMaskInputStation, subnetMaskErrorStation);
|
||
clearFieldError(gatewayInputStation, gatewayErrorStation);
|
||
}
|
||
|
||
function validateStaticIpFormStation(): { valid: true; values: StaticIpValues } | { valid: false } {
|
||
if (!staticIpInputStation || !subnetMaskInputStation || !gatewayInputStation) {
|
||
return { valid: false };
|
||
}
|
||
|
||
clearStaticIpErrorsStation();
|
||
|
||
const values: StaticIpValues = {
|
||
ip: (staticIpInputStation.value || "").trim(),
|
||
netmask: (subnetMaskInputStation.value || "").trim(),
|
||
gateway: (gatewayInputStation.value || "").trim(),
|
||
};
|
||
|
||
let isValid = true;
|
||
const firstError: { field: HTMLInputElement | null } = { field: null };
|
||
|
||
if (!values.netmask) {
|
||
setFieldError(subnetMaskInputStation, subnetMaskErrorStation, "Netmask is required", firstError);
|
||
isValid = false;
|
||
} else if (!is_valid_ip_format(values.netmask)) {
|
||
setFieldError(subnetMaskInputStation, subnetMaskErrorStation, "Invalid netmask format", firstError);
|
||
isValid = false;
|
||
} else if (!isValidNetmask(values.netmask)) {
|
||
setFieldError(subnetMaskInputStation, subnetMaskErrorStation, "Netmask must use contiguous bits", firstError);
|
||
isValid = false;
|
||
} else if (!isPracticalNetmask(values.netmask)) {
|
||
setFieldError(subnetMaskInputStation, subnetMaskErrorStation, "Netmask must allow host and gateway addresses", firstError);
|
||
isValid = false;
|
||
}
|
||
|
||
if (!values.ip) {
|
||
setFieldError(staticIpInputStation, staticIpErrorStation, "IP address is required", firstError);
|
||
isValid = false;
|
||
} else if (!is_valid_ip_format(values.ip)) {
|
||
setFieldError(staticIpInputStation, staticIpErrorStation, "Invalid IP address format", firstError);
|
||
isValid = false;
|
||
}
|
||
|
||
if (!values.gateway) {
|
||
setFieldError(gatewayInputStation, gatewayErrorStation, "Gateway is required", firstError);
|
||
isValid = false;
|
||
} else if (!is_valid_ip_format(values.gateway)) {
|
||
setFieldError(gatewayInputStation, gatewayErrorStation, "Invalid gateway format", firstError);
|
||
isValid = false;
|
||
}
|
||
|
||
if (!isValid) {
|
||
firstError.field?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||
return { valid: false };
|
||
}
|
||
|
||
if (!isInValidHostRange(values.ip, values.netmask)) {
|
||
setFieldError(staticIpInputStation, staticIpErrorStation, "IP cannot be network or broadcast address", firstError);
|
||
isValid = false;
|
||
}
|
||
|
||
if (!isInValidHostRange(values.gateway, values.netmask)) {
|
||
setFieldError(gatewayInputStation, gatewayErrorStation, "Gateway cannot be network or broadcast address", firstError);
|
||
isValid = false;
|
||
}
|
||
|
||
if (!isInSameSubnet(values.ip, values.gateway, values.netmask)) {
|
||
setFieldError(staticIpInputStation, staticIpErrorStation, "IP and gateway must be in the same subnet", firstError);
|
||
setFieldError(gatewayInputStation, gatewayErrorStation, "IP and gateway must be in the same subnet", firstError);
|
||
isValid = false;
|
||
}
|
||
|
||
if (values.ip === values.gateway) {
|
||
setFieldError(gatewayInputStation, gatewayErrorStation, "Gateway cannot match the host IP", firstError);
|
||
isValid = false;
|
||
}
|
||
|
||
if (!isValid) {
|
||
firstError.field?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||
return { valid: false };
|
||
}
|
||
|
||
return { valid: true, values };
|
||
}
|
||
|
||
function setIpAssignmentModeStation(mode: "dhcp" | "static") {
|
||
currentIpAssignmentModeStation = mode;
|
||
ipModeButtonsStation.forEach((radio) => {
|
||
radio.checked = radio.value === mode;
|
||
});
|
||
|
||
if (mode === "dhcp") {
|
||
if (ipModeDescriptionStation) ipModeDescriptionStation.textContent = "Automatic IP assignment";
|
||
staticIpContainerStation?.classList.add("hidden");
|
||
saveStaticIpBtnStation?.classList.add("hidden");
|
||
clearStaticIpErrorsStation();
|
||
} else {
|
||
if (ipModeDescriptionStation) ipModeDescriptionStation.textContent = "Static IP assignment";
|
||
staticIpContainerStation?.classList.remove("hidden");
|
||
saveStaticIpBtnStation?.classList.remove("hidden");
|
||
const cfg = $current_network_configuration.get();
|
||
if (!staticIpInputStation?.value) staticIpInputStation!.value = cfg.Static_Cfg_IP_Address_Station || "";
|
||
if (!subnetMaskInputStation?.value) subnetMaskInputStation!.value = cfg.Static_Cfg_Netmask_Station || "";
|
||
if (!gatewayInputStation?.value) gatewayInputStation!.value = cfg.Static_Cfg_Gateway_IP_Address_Station || "";
|
||
}
|
||
}
|
||
|
||
// Station mode event listeners
|
||
ipModeButtonsStation.forEach((radio) => {
|
||
radio.addEventListener("change", () => {
|
||
const newMode = radio.value === "static" ? "static" : "dhcp";
|
||
setIpAssignmentModeStation(newMode);
|
||
if (newMode === "dhcp" && appliedIpModeSta === "static" && staticIpConfirmModal) {
|
||
pendingStaticIpConfigStation = { mode: "dhcp" };
|
||
dhcpRevertPending = "sta";
|
||
updateStaticIpSummary(pendingStaticIpConfigStation);
|
||
staticIpConfirmModal.showModal();
|
||
}
|
||
});
|
||
});
|
||
|
||
staticIpInputStation?.addEventListener("input", () => clearFieldError(staticIpInputStation, staticIpErrorStation));
|
||
subnetMaskInputStation?.addEventListener("input", () => clearFieldError(subnetMaskInputStation, subnetMaskErrorStation));
|
||
gatewayInputStation?.addEventListener("input", () => clearFieldError(gatewayInputStation, gatewayErrorStation));
|
||
|
||
saveStaticIpBtnStation?.addEventListener("click", () => {
|
||
if (!staticIpConfirmModal) return;
|
||
|
||
if (currentIpAssignmentModeStation === "static") {
|
||
const validation = validateStaticIpFormStation();
|
||
if (!validation.valid) {
|
||
return;
|
||
}
|
||
pendingStaticIpConfigStation = { mode: "static", values: validation.values };
|
||
} else {
|
||
pendingStaticIpConfigStation = { mode: "dhcp" };
|
||
clearStaticIpErrorsStation();
|
||
}
|
||
|
||
if (pendingStaticIpConfigStation) {
|
||
updateStaticIpSummary(pendingStaticIpConfigStation);
|
||
staticIpConfirmModal.showModal();
|
||
}
|
||
});
|
||
|
||
// Initialize Station mode IP assignment mode
|
||
setIpAssignmentModeStation("dhcp");
|
||
</script>
|