updated webinterface with modals for station/ap config. landing page. network overview if config is present

This commit is contained in:
SebastianOtt 2026-05-22 20:38:07 +02:00
parent 7a3e2830a0
commit fb9f9284a4
4 changed files with 43 additions and 141 deletions

View file

@ -1,8 +1,3 @@
---
import IconEye from "../icons/IconEye.astro";
import IconEyeClosed from "../icons/IconEyeClosed.astro";
---
<dialog id="ap-config-modal" class="modal modal-middle">
<div class="modal-box w-11/12 sm:w-6/12 max-w-5xl flex flex-col">
<h1 class="text-lg font-bold">Access Point Configuration</h1>
@ -29,12 +24,7 @@ import IconEyeClosed from "../icons/IconEyeClosed.astro";
<input type="password" class="w-full border-none outline-none bg-transparent" id="ap-password" placeholder="Leave empty for open network" />
</label>
<button type="button" id="toggle-ap-password-btn" class="absolute right-3 top-1/2 -translate-y-1/2 btn btn-ghost btn-xs p-1 min-h-0 h-7 hover:bg-primary hover:text-primary-content">
<span id="ap-eye-icon-open">
<IconEye size={20} class="opacity-70 hover:opacity-100" />
</span>
<span id="ap-eye-icon-closed" class="hidden">
<IconEyeClosed size={20} class="opacity-70 hover:opacity-100" />
</span>
<span id="ap-eye-text" class="text-xs opacity-70">show</span>
</button>
</div>
<p id="ap-password-error" class="text-sm text-error hidden"></p>
@ -83,7 +73,8 @@ import IconEyeClosed from "../icons/IconEyeClosed.astro";
$current_network_configuration,
send_new_network_configuration_to_alpha,
ConnectionMode,
} from "../backend_components/NetworkManagerStore";
type NetworkConfigNVS,
} from "./NetworkManagerStore";
const modal = document.getElementById("ap-config-modal") as HTMLDialogElement;
const apSSID = document.getElementById("ap-ssid") as HTMLInputElement;
@ -94,8 +85,7 @@ import IconEyeClosed from "../icons/IconEyeClosed.astro";
const apPasswordError = document.getElementById("ap-password-error");
const apChannel = document.getElementById("ap-channel") as HTMLSelectElement;
const toggleApPasswordBtn = document.getElementById("toggle-ap-password-btn");
const apEyeOpen = document.getElementById("ap-eye-icon-open");
const apEyeClosed = document.getElementById("ap-eye-icon-closed");
const apEyeText = document.getElementById("ap-eye-text");
function resetModal() {
if (apSSID) apSSID.value = "";
@ -105,14 +95,15 @@ import IconEyeClosed from "../icons/IconEyeClosed.astro";
apSSIDError?.classList.add("hidden");
apPasswordWrap?.classList.remove("input-error");
apPasswordError?.classList.add("hidden");
apEyeOpen?.classList.remove("hidden");
apEyeClosed?.classList.add("hidden");
if (apEyeText) apEyeText.textContent = "show";
}
function prefillFromConfig() {
const cfg = $current_network_configuration.get();
if (apSSID) apSSID.value = cfg.SSID_AP || "";
if (apPassword) apPassword.value = cfg.Password_AP || "";
// Password is intentionally blanked by firmware on read — leave the field empty so the
// user is forced to re-enter it. Updating it without re-entering would risk wiping it.
if (apPassword) apPassword.value = "";
if (apChannel) apChannel.value = String(cfg.AP_Channel ?? 0);
}
@ -132,8 +123,7 @@ import IconEyeClosed from "../icons/IconEyeClosed.astro";
if (!apPassword) return;
const isVisible = apPassword.type === "text";
apPassword.type = isVisible ? "password" : "text";
apEyeOpen?.classList.toggle("hidden", !isVisible);
apEyeClosed?.classList.toggle("hidden", isVisible);
if (apEyeText) apEyeText.textContent = isVisible ? "show" : "hide";
});
apSSID?.addEventListener("input", () => {
@ -175,24 +165,10 @@ import IconEyeClosed from "../icons/IconEyeClosed.astro";
if (!valid) return;
const base = $current_network_configuration.get();
// Derive the APSTA connection mode: keep ETH DHCP/static, switch WiFi to APSTA
let apstaMode: ConnectionMode;
switch (base.Connection_Mode) {
case ConnectionMode.CONNECTION_MODE_ETH_STATIC_NO_WIFI:
case ConnectionMode.CONNECTION_MODE_ETH_STATIC_APSTA:
case ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_DHCP:
case ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_STATIC:
apstaMode = ConnectionMode.CONNECTION_MODE_ETH_STATIC_APSTA;
break;
default:
apstaMode = ConnectionMode.CONNECTION_MODE_ETH_DHCP_APSTA;
}
const updatedConfig = {
// No ethernet in our case — APSTA is always just APSTA mode, no eth-static-vs-dhcp variants.
const updatedConfig: NetworkConfigNVS = {
...base,
Connection_Mode: apstaMode,
WiFi_Enabled: true,
Connection_Mode: ConnectionMode.CONNECTION_MODE_APSTA,
SSID_AP: apSSID.value,
Password_AP: apPassword?.value ?? "",
AP_Channel: parseInt(apChannel?.value ?? "0", 10),

View file

@ -1,5 +1,7 @@
---
import { IconPlant } from "@tabler/icons-preact";
import StationConfigModal from "./StationConfigModal.astro";
import AccessPointConfigModal from "./AccessPointConfigModal.astro";
---
<div class="space-y-6">
@ -163,26 +165,14 @@ import { IconPlant } from "@tabler/icons-preact";
</button>
</div>
<!-- Inline APSTA form (shown when "Access Point" is clicked) -->
<div id="apsta-form" class="hidden mt-4 space-y-3 p-4 rounded-lg bg-base-200/50">
<h4 class="font-semibold text-sm">Configure Access Point</h4>
<label class="form-control">
<span class="label-text text-xs">AP SSID</span>
<input type="text" id="ov-ap-ssid" maxlength="31" class="input input-bordered input-sm" />
</label>
<label class="form-control">
<span class="label-text text-xs">AP Password (leave blank for open)</span>
<input type="text" id="ov-ap-password" maxlength="31" class="input input-bordered input-sm font-mono" />
</label>
<div class="flex justify-end gap-2">
<button id="ov-ap-cancel" class="btn btn-ghost btn-sm">Cancel</button>
<button id="ov-ap-apply" class="btn btn-primary btn-sm">Apply AP Mode</button>
</div>
</div>
</div>
</div>
</div>
<!-- Mode-switch modals — opened by the "WiFi Station" / "Access Point" buttons above. -->
<StationConfigModal />
<AccessPointConfigModal />
<!-- Network-error dialog (raised when network_config_failed arrives) -->
<dialog id="overview-network-error-modal" class="modal modal-middle">
<div class="modal-box w-11/12 sm:w-5/12 max-w-lg">
@ -262,11 +252,6 @@ import { IconPlant } from "@tabler/icons-preact";
const modeStationBtn = document.getElementById("overview-mode-station");
const modeApstaBtn = document.getElementById("overview-mode-apsta");
const apstaForm = document.getElementById("apsta-form");
const ovApSsid = document.getElementById("ov-ap-ssid") as HTMLInputElement | null;
const ovApPassword = document.getElementById("ov-ap-password") as HTMLInputElement | null;
const ovApCancel = document.getElementById("ov-ap-cancel");
const ovApApply = document.getElementById("ov-ap-apply") as HTMLButtonElement | null;
const forgetBtn = document.getElementById("overview-forget-btn");
const errorModal = document.getElementById("overview-network-error-modal") as HTMLDialogElement | null;
@ -483,42 +468,16 @@ import { IconPlant } from "@tabler/icons-preact";
});
// ---------- Mode switch ----------
// Both buttons just open their respective modals. The modal's own Save handler
// builds the NetworkConfigNVS and calls send_new_network_configuration_to_alpha.
modeStationBtn?.addEventListener("click", () => {
// "Switch to Station" = re-run the wizard. Easiest path is to nuke SSID_Station
// locally so index.astro shows the wizard. We don't push to the device yet —
// the wizard's Apply step does that.
const baseCfg = $current_network_configuration.get();
$current_network_configuration.set({ ...baseCfg, SSID_Station: "" });
const m = document.getElementById("station-config-modal") as HTMLDialogElement | null;
m?.showModal();
});
modeApstaBtn?.addEventListener("click", () => {
// Open inline AP config form, prefilled with current AP values
const cfg = $current_network_configuration.get();
if (ovApSsid) ovApSsid.value = cfg.SSID_AP || "";
if (ovApPassword) ovApPassword.value = cfg.Password_AP || "";
apstaForm?.classList.remove("hidden");
apstaForm?.scrollIntoView({ behavior: "smooth", block: "nearest" });
});
ovApCancel?.addEventListener("click", () => {
apstaForm?.classList.add("hidden");
});
ovApApply?.addEventListener("click", () => {
const ssid = ovApSsid?.value.trim() ?? "";
if (!ssid) {
// Minimal inline feedback: focus the field
ovApSsid?.focus();
return;
}
const baseCfg = $current_network_configuration.get();
send_new_network_configuration_to_alpha({
...baseCfg,
Connection_Mode: ConnectionMode.CONNECTION_MODE_APSTA,
SSID_AP: ssid,
Password_AP: ovApPassword?.value ?? "",
});
apstaForm?.classList.add("hidden");
const m = document.getElementById("ap-config-modal") as HTMLDialogElement | null;
m?.showModal();
});
// ---------- Re-setup ----------

View file

@ -1,6 +1,5 @@
---
import IconEye from "../icons/IconEye.astro";
import IconEyeClosed from "../icons/IconEyeClosed.astro";
// Icon refs use inline SVG (formerly imported IconEye / IconEyeClosed from a deleted folder).
---
<dialog id="station-config-modal" class="modal modal-middle">
@ -54,12 +53,7 @@ import IconEyeClosed from "../icons/IconEyeClosed.astro";
<div class="relative">
<input type="password" class="input w-full focus-within:bg-primary/10 focus-within:border-none focus-within:outline-primary pr-10" id="station-password" placeholder="WiFi password" />
<button type="button" id="toggle-station-password-btn" class="absolute right-3 top-1/2 -translate-y-1/2 btn btn-ghost btn-xs p-1 min-h-0 h-7 hover:bg-primary hover:text-primary-content">
<span id="station-eye-icon-open">
<IconEye size={20} class="opacity-70 hover:opacity-100" />
</span>
<span id="station-eye-icon-closed" class="hidden">
<IconEyeClosed size={20} class="opacity-70 hover:opacity-100" />
</span>
<span id="station-eye-text" class="text-xs opacity-70">show</span>
</button>
</div>
<p id="station-password-error" class="mt-2 text-sm text-error hidden"></p>
@ -156,19 +150,6 @@ import IconEyeClosed from "../icons/IconEyeClosed.astro";
<li>
The WiFi credentials must be valid to establish a working connection. Otherwise, the device will revert to the previous valid WiFi configuration, if there was one.
</li>
<li>The device remains reachable via Ethernet (if connected) or USB at any time.</li>
</ul>
</div>
</div>
<div id="ethernet-warning" class="hidden alert alert-info py-2">
<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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div class="flex flex-col gap-1">
<span class="font-semibold">Ethernet Detected</span>
<ul class="list-disc list-inside text-sm space-y-0.5">
<li>Ethernet and WiFi can be active simultaneously. The device will be reachable through both interfaces.</li>
</ul>
</div>
</div>
@ -200,10 +181,9 @@ import IconEyeClosed from "../icons/IconEyeClosed.astro";
type WiFiNetwork,
type NetworkConfigNVS,
ConnectionMode,
AntennaSelection,
$current_network_configuration,
send_new_network_configuration_to_alpha,
} from "../backend_components/NetworkManagerStore";
} from "./NetworkManagerStore";
import {
is_valid_ip_format,
@ -211,7 +191,7 @@ import IconEyeClosed from "../icons/IconEyeClosed.astro";
isPracticalNetmask,
isInValidHostRange,
isInSameSubnet,
} from "../helper_scripts/IP_Subnetmask_validation";
} from "./helper_scripts/IP_Subnetmask_validation";
const stationSSID = document.getElementById("station-ssid") as HTMLInputElement;
const stationSsidError = document.getElementById("station-ssid-error");
@ -222,15 +202,13 @@ import IconEyeClosed from "../icons/IconEyeClosed.astro";
const scanList = document.getElementById("network-scan-list");
const refreshBtnRow = document.getElementById("refresh-btn-row");
const toggleStationPasswordBtn = document.getElementById("toggle-station-password-btn") as HTMLButtonElement | null;
const stationEyeOpen = document.getElementById("station-eye-icon-open");
const stationEyeClosed = document.getElementById("station-eye-icon-closed");
const stationEyeText = document.getElementById("station-eye-text");
toggleStationPasswordBtn?.addEventListener("click", () => {
if (!stationPassword) return;
const isVisible = stationPassword.type === "text";
stationPassword.type = isVisible ? "password" : "text";
stationEyeOpen?.classList.toggle("hidden", !isVisible);
stationEyeClosed?.classList.toggle("hidden", isVisible);
if (stationEyeText) stationEyeText.textContent = isVisible ? "show" : "hide";
});
const staticIpToggle = document.getElementById("static-ip-toggle-modal") as HTMLInputElement;
@ -542,8 +520,7 @@ import IconEyeClosed from "../icons/IconEyeClosed.astro";
scanList?.classList.remove("hidden");
selectedNetworkForConfirm = null;
if (stationPassword) stationPassword.type = "password";
stationEyeOpen?.classList.remove("hidden");
stationEyeClosed?.classList.add("hidden");
if (stationEyeText) stationEyeText.textContent = "show";
performScan();
}
});
@ -594,8 +571,7 @@ import IconEyeClosed from "../icons/IconEyeClosed.astro";
scanList?.classList.remove("hidden");
selectedNetworkForConfirm = null;
if (stationPassword) stationPassword.type = "password";
stationEyeOpen?.classList.remove("hidden");
stationEyeClosed?.classList.add("hidden");
if (stationEyeText) stationEyeText.textContent = "show";
clear_scanned_wifi_networks();
renderNetworkList([]);
});
@ -713,32 +689,20 @@ import IconEyeClosed from "../icons/IconEyeClosed.astro";
let pendingConfig: NetworkConfigNVS | null = null;
saveStationBtn?.addEventListener("click", () => {
const _currCfg = $current_network_configuration.get();
const _newStaDHCP = !staticIpToggle?.checked; // toggle checked = static IP, unchecked = DHCP
const _staMode = _currCfg.DHCP_Mode_Ethernet
? (_newStaDHCP ? ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_DHCP : ConnectionMode.CONNECTION_MODE_ETH_DHCP_STA_STATIC)
: (_newStaDHCP ? ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_DHCP : ConnectionMode.CONNECTION_MODE_ETH_STATIC_STA_STATIC);
const _cfg = $current_network_configuration.get();
const _newStaDHCP = !staticIpToggle?.checked; // toggle checked = static IP, unchecked = DHCP
// No ethernet variants anymore — STA is either DHCP or STATIC, period.
const _staMode = _newStaDHCP ? ConnectionMode.CONNECTION_MODE_STA_DHCP : ConnectionMode.CONNECTION_MODE_STA_STATIC;
let temp_station_network_config: NetworkConfigNVS = {
Connection_Mode: _staMode,
Antenna_Power: _cfg.Antenna_Power,
Antenna_Selection: _cfg.Antenna_Selection,
DHCP_Mode_Station: _cfg.DHCP_Mode_Station,
DHCP_Mode_Ethernet: _cfg.DHCP_Mode_Ethernet,
DHCP_Mode_Station: _newStaDHCP,
AP_Channel: _cfg.AP_Channel,
WiFi_Enabled: _cfg.WiFi_Enabled,
IP_Address_Ethernet: _cfg.IP_Address_Ethernet,
IP_Address_Station: _cfg.IP_Address_Station,
Gateway_IP_Address_Ethernet: _cfg.Gateway_IP_Address_Ethernet,
Gateway_IP_Address_Station: _cfg.Gateway_IP_Address_Station,
Netmask_Ethernet: _cfg.Netmask_Ethernet,
Netmask_Station: _cfg.Netmask_Station,
Static_Cfg_IP_Address_Ethernet: _cfg.Static_Cfg_IP_Address_Ethernet,
Static_Cfg_IP_Address_Station: _cfg.Static_Cfg_IP_Address_Station,
Static_Cfg_IP_Address_Station: "",
Static_Cfg_Gateway_IP_Address_Station: "",
Static_Cfg_Gateway_IP_Address_Ethernet: _cfg.Static_Cfg_Gateway_IP_Address_Ethernet,
Static_Cfg_Netmask_Station: "",
Static_Cfg_Netmask_Ethernet: _cfg.Static_Cfg_Netmask_Ethernet,
SSID_Station: "",
Password_Station: "",
SSID_AP: _cfg.SSID_AP,
@ -903,7 +867,7 @@ import IconEyeClosed from "../icons/IconEyeClosed.astro";
temp_station_network_config.SSID_Station = stationSSID.value;
temp_station_network_config.Password_Station = stationPassword.value;
temp_station_network_config.DHCP_Mode_Station = false;
temp_station_network_config.WiFi_Enabled = true;
// WiFi is always on for this board — no WiFi_Enabled field needed.
}
} else {
// enable DHCP mode
@ -913,7 +877,6 @@ import IconEyeClosed from "../icons/IconEyeClosed.astro";
temp_station_network_config.SSID_Station = stationSSID.value;
temp_station_network_config.Password_Station = stationPassword.value;
temp_station_network_config.DHCP_Mode_Station = true;
temp_station_network_config.WiFi_Enabled = true;
}
// Store the config and show confirmation view