littlefs/src/components/NetworkPage.astro
SebastianOtt 7a3e2830a0 updated hydrobuddy web interface
landing page + 1st configuration + overview when configured (hoffentlich lol)
ltack kram rausgehauen (Testbench code)
2026-05-22 19:55:19 +02:00

1823 lines
82 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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>