Fixes build warning and errors

This commit is contained in:
Rune Harlyk
2025-07-10 21:32:28 +02:00
committed by Rune Harlyk
parent c8ee64d7f4
commit d529eaa201
22 changed files with 2053 additions and 2116 deletions
+248 -247
View File
@@ -1,74 +1,71 @@
<script lang="ts">
import { preventDefault } from 'svelte/legacy'
import { preventDefault } from 'svelte/legacy';
import { onMount, onDestroy } from 'svelte'
import { slide } from 'svelte/transition'
import { cubicOut } from 'svelte/easing'
import { PasswordInput } from '$lib/components/input'
import SettingsCard from '$lib/components/SettingsCard.svelte'
import { notifications } from '$lib/components/toasts/notifications'
import Spinner from '$lib/components/Spinner.svelte'
import type { ApSettings, ApStatus } from '$lib/types/models'
import { api } from '$lib/api'
import { useFeatureFlags } from '$lib/stores'
import { AP, Devices, Home, MAC } from '$lib/components/icons'
import StatusItem from '$lib/components/StatusItem.svelte'
import { onMount, onDestroy } from 'svelte';
import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import { PasswordInput } from '$lib/components/input';
import SettingsCard from '$lib/components/SettingsCard.svelte';
import { notifications } from '$lib/components/toasts/notifications';
import Spinner from '$lib/components/Spinner.svelte';
import type { ApSettings, ApStatus } from '$lib/types/models';
import { api } from '$lib/api';
import { AP, Devices, Home, MAC } from '$lib/components/icons';
import StatusItem from '$lib/components/StatusItem.svelte';
const features = useFeatureFlags()
let apSettings: ApSettings | null = $state(null);
let apStatus: ApStatus | null = $state(null);
let apSettings: ApSettings = $state()
let apStatus: ApStatus = $state()
let formField: any = $state()
let formField: any = $state();
async function getAPStatus() {
const result = await api.get<ApStatus>('/api/wifi/ap/status')
const result = await api.get<ApStatus>('/api/wifi/ap/status');
if (result.isErr()) {
console.error('Error:', result.inner)
return
console.error('Error:', result.inner);
return;
}
apStatus = result.inner
return apStatus
apStatus = result.inner;
return apStatus;
}
async function getAPSettings() {
const result = await api.get<ApSettings>('/api/wifi/ap/settings')
const result = await api.get<ApSettings>('/api/wifi/ap/settings');
if (result.isErr()) {
console.error('Error:', result.inner)
return
console.error('Error:', result.inner);
return;
}
apSettings = result.inner
return apSettings
apSettings = result.inner;
return apSettings;
}
const interval = setInterval(async () => {
getAPStatus()
}, 5000)
getAPStatus();
}, 5000);
onDestroy(() => clearInterval(interval))
onDestroy(() => clearInterval(interval));
onMount(getAPSettings)
onMount(getAPSettings);
let provisionMode = [
{
id: 0,
text: `Always`
text: `Always`,
},
{
id: 1,
text: `When WiFi Disconnected`
text: `When WiFi Disconnected`,
},
{
id: 2,
text: `Never`
}
]
text: `Never`,
},
];
type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning'
type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning';
let apStatusVariant: Variant[] = ['success', 'error', 'warning']
let apStatusVariant: Variant[] = ['success', 'error', 'warning'];
let apStatusDescription = ['Active', 'Inactive', 'Lingering']
let apStatusDescription = ['Active', 'Inactive', 'Lingering'];
let formErrors = $state({
ssid: false,
@@ -76,80 +73,81 @@
max_clients: false,
local_ip: false,
gateway_ip: false,
subnet_mask: false
})
subnet_mask: false,
});
async function postAPSettings(data: ApSettings) {
const result = await api.post<ApSettings>('/api/wifi/ap/settings', data)
const result = await api.post<ApSettings>('/api/wifi/ap/settings', data);
if (result.isErr()) {
notifications.error('User not authorized.', 3000)
console.error('Error:', result.inner)
return
notifications.error('User not authorized.', 3000);
console.error('Error:', result.inner);
return;
}
notifications.success('Access Point settings updated.', 3000)
apSettings = result.inner
notifications.success('Access Point settings updated.', 3000);
apSettings = result.inner;
}
function handleSubmitAP() {
let valid = true
if (!apSettings) return;
let valid = true;
// Validate SSID
if (apSettings.ssid.length < 3 || apSettings.ssid.length > 32) {
valid = false
formErrors.ssid = true
valid = false;
formErrors.ssid = true;
} else {
formErrors.ssid = false
formErrors.ssid = false;
}
// Validate Channel
let channel = Number(apSettings.channel)
let channel = Number(apSettings.channel);
if (1 > channel || channel > 13) {
valid = false
formErrors.channel = true
valid = false;
formErrors.channel = true;
} else {
formErrors.channel = false
formErrors.channel = false;
}
// Validate max_clients
let maxClients = Number(apSettings.max_clients)
let maxClients = Number(apSettings.max_clients);
if (1 > maxClients || maxClients > 8) {
valid = false
formErrors.max_clients = true
valid = false;
formErrors.max_clients = true;
} else {
formErrors.max_clients = false
formErrors.max_clients = false;
}
// RegEx for IPv4
const regexExp =
/\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/
/\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/;
// Validate gateway IP
if (!regexExp.test(apSettings.gateway_ip)) {
valid = false
formErrors.gateway_ip = true
valid = false;
formErrors.gateway_ip = true;
} else {
formErrors.gateway_ip = false
formErrors.gateway_ip = false;
}
// Validate Subnet Mask
if (!regexExp.test(apSettings.subnet_mask)) {
valid = false
formErrors.subnet_mask = true
valid = false;
formErrors.subnet_mask = true;
} else {
formErrors.subnet_mask = false
formErrors.subnet_mask = false;
}
// Validate local IP
if (!regexExp.test(apSettings.local_ip)) {
valid = false
formErrors.local_ip = true
valid = false;
formErrors.local_ip = true;
} else {
formErrors.local_ip = false
formErrors.local_ip = false;
}
// Submit JSON to REST API
if (valid) {
postAPSettings(apSettings)
postAPSettings(apSettings);
}
}
</script>
@@ -164,22 +162,24 @@
<div class="w-full overflow-x-auto">
{#await getAPStatus()}
<Spinner />
{:then nothing}
<div
class="flex w-full flex-col space-y-1"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<StatusItem
icon={AP}
title="Status"
variant={apStatusVariant[apStatus.status]}
description={apStatusDescription[apStatus.status]} />
{:then}
{#if apStatus}
<div
class="flex w-full flex-col space-y-1"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<StatusItem
icon={AP}
title="Status"
variant={apStatusVariant[apStatus.status]}
description={apStatusDescription[apStatus.status]} />
<StatusItem icon={Home} title="IP Address" description={apStatus.ip_address} />
<StatusItem icon={Home} title="IP Address" description={apStatus.ip_address} />
<StatusItem icon={MAC} title="MAC Address" description={apStatus.mac_address} />
<StatusItem icon={MAC} title="MAC Address" description={apStatus.mac_address} />
<StatusItem icon={Devices} title="AP Clients" description={apStatus.station_num} />
</div>
<StatusItem icon={Devices} title="AP Clients" description={apStatus.station_num} />
</div>
{/if}
{/await}
</div>
@@ -190,175 +190,176 @@
</div>
{#await getAPSettings()}
<Spinner />
{:then nothing}
<div
class="flex flex-col gap-2 p-0"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<form
class="grid w-full grid-cols-1 content-center gap-x-4 p-0s sm:grid-cols-2"
onsubmit={preventDefault(handleSubmitAP)}
novalidate
bind:this={formField}>
<div>
<label class="label" for="apmode">
<span class="label-text">Provide Access Point ...</span>
</label>
<select
class="select select-bordered w-full"
id="apmode"
bind:value={apSettings.provision_mode}>
{#each provisionMode as mode}
<option value={mode.id}>
{mode.text}
</option>
{/each}
</select>
</div>
<div>
<label class="label" for="ssid">
<span class="label-text text-md">SSID</span>
</label>
<input
type="text"
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
formErrors.ssid
) ?
'border-error border-2'
: ''}"
bind:value={apSettings.ssid}
id="ssid"
min="2"
max="32"
required />
<label class="label" for="ssid">
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
>SSID must be between 2 and 32 characters long</span>
</label>
</div>
{:then}
{#if apSettings}
<div
class="flex flex-col gap-2 p-0"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<form
class="grid w-full grid-cols-1 content-center gap-x-4 p-0s sm:grid-cols-2"
onsubmit={preventDefault(handleSubmitAP)}
novalidate
bind:this={formField}>
<div>
<label class="label" for="apmode">
<span class="label-text">Provide Access Point ...</span>
</label>
<select
class="select select-bordered w-full"
id="apmode"
bind:value={apSettings.provision_mode}>
{#each provisionMode as mode}
<option value={mode.id}>
{mode.text}
</option>
{/each}
</select>
</div>
<div>
<label class="label" for="ssid">
<span class="label-text text-md">SSID</span>
</label>
<input
type="text"
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
formErrors.ssid
) ?
'border-error border-2'
: ''}"
bind:value={apSettings.ssid}
id="ssid"
min="2"
max="32"
required />
<label class="label" for="ssid">
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
>SSID must be between 2 and 32 characters long</span>
</label>
</div>
<div>
<label class="label" for="pwd">
<span class="label-text text-md">Password</span>
</label>
<PasswordInput bind:value={apSettings.password} id="pwd" />
</div>
<div>
<label class="label" for="channel">
<span class="label-text text-md">Preferred Channel</span>
</label>
<input
type="number"
min="1"
max="13"
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
formErrors.channel
) ?
'border-error border-2'
: ''}"
bind:value={apSettings.channel}
id="channel"
required />
<label class="label" for="channel">
<span class="label-text-alt text-error {formErrors.channel ? '' : 'hidden'}"
>Must be channel 1 to 13</span>
</label>
</div>
<div>
<label class="label" for="pwd">
<span class="label-text text-md">Password</span>
</label>
<PasswordInput bind:value={apSettings.password} id="pwd" />
</div>
<div>
<label class="label" for="channel">
<span class="label-text text-md">Preferred Channel</span>
</label>
<input
type="number"
min="1"
max="13"
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
formErrors.channel
) ?
'border-error border-2'
: ''}"
bind:value={apSettings.channel}
id="channel"
required />
<label class="label" for="channel">
<span class="label-text-alt text-error {formErrors.channel ? '' : 'hidden'}"
>Must be channel 1 to 13</span>
</label>
</div>
<div>
<label class="label" for="clients">
<span class="label-text text-md">Max Clients</span>
</label>
<input
type="number"
min="1"
max="8"
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
formErrors.max_clients
) ?
'border-error border-2'
: ''}"
bind:value={apSettings.max_clients}
id="clients"
required />
<label class="label" for="clients">
<span class="label-text-alt text-error {formErrors.max_clients ? '' : 'hidden'}"
>Maximum 8 clients allowed</span>
</label>
</div>
<div>
<label class="label" for="clients">
<span class="label-text text-md">Max Clients</span>
</label>
<input
type="number"
min="1"
max="8"
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
formErrors.max_clients
) ?
'border-error border-2'
: ''}"
bind:value={apSettings.max_clients}
id="clients"
required />
<label class="label" for="clients">
<span class="label-text-alt text-error {formErrors.max_clients ? '' : 'hidden'}"
>Maximum 8 clients allowed</span>
</label>
</div>
<div>
<label class="label" for="localIP">
<span class="label-text text-md">Local IP</span>
</label>
<input
type="text"
class="input input-bordered w-full {formErrors.local_ip ? 'border-error border-2' : (
''
)}"
minlength="7"
maxlength="15"
size="15"
bind:value={apSettings.local_ip}
id="localIP"
required />
<label class="label" for="localIP">
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
>Must be a valid IPv4 address</span>
</label>
</div>
<div>
<label class="label" for="localIP">
<span class="label-text text-md">Local IP</span>
</label>
<input
type="text"
class="input input-bordered w-full {formErrors.local_ip ? 'border-error border-2'
: ''}"
minlength="7"
maxlength="15"
size="15"
bind:value={apSettings.local_ip}
id="localIP"
required />
<label class="label" for="localIP">
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
>Must be a valid IPv4 address</span>
</label>
</div>
<div>
<label class="label" for="gateway">
<span class="label-text text-md">Gateway IP</span>
</label>
<input
type="text"
class="input input-bordered w-full {formErrors.gateway_ip ? 'border-error border-2'
: ''}"
minlength="7"
maxlength="15"
size="15"
bind:value={apSettings.gateway_ip}
id="gateway"
required />
<label class="label" for="gateway">
<span class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
>Must be a valid IPv4 address</span>
</label>
</div>
<div>
<label class="label" for="subnet">
<span class="label-text text-md">Subnet Mask</span>
</label>
<input
type="text"
class="input input-bordered w-full {formErrors.subnet_mask ? 'border-error border-2'
: ''}"
minlength="7"
maxlength="15"
size="15"
bind:value={apSettings.subnet_mask}
id="subnet"
required />
<label class="label" for="subnet">
<span class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}"
>Must be a valid IPv4 address</span>
</label>
</div>
<div>
<label class="label" for="gateway">
<span class="label-text text-md">Gateway IP</span>
</label>
<input
type="text"
class="input input-bordered w-full {formErrors.gateway_ip ? 'border-error border-2'
: ''}"
minlength="7"
maxlength="15"
size="15"
bind:value={apSettings.gateway_ip}
id="gateway"
required />
<label class="label" for="gateway">
<span class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
>Must be a valid IPv4 address</span>
</label>
</div>
<div>
<label class="label" for="subnet">
<span class="label-text text-md">Subnet Mask</span>
</label>
<input
type="text"
class="input input-bordered w-full {formErrors.subnet_mask ? 'border-error border-2'
: ''}"
minlength="7"
maxlength="15"
size="15"
bind:value={apSettings.subnet_mask}
id="subnet"
required />
<label class="label" for="subnet">
<span class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}"
>Must be a valid IPv4 address</span>
</label>
</div>
<label class="label my-auto cursor-pointer justify-start gap-4">
<input
type="checkbox"
bind:checked={apSettings.ssid_hidden}
class="checkbox checkbox-primary" />
<span class="">Hide SSID</span>
</label>
<label class="label my-auto cursor-pointer justify-start gap-4">
<input
type="checkbox"
bind:checked={apSettings.ssid_hidden}
class="checkbox checkbox-primary" />
<span class="">Hide SSID</span>
</label>
<div class="place-self-end">
<button class="btn btn-primary" type="submit">Apply Settings</button>
</div>
</form>
</div>
<div class="place-self-end">
<button class="btn btn-primary" type="submit">Apply Settings</button>
</div>
</form>
</div>
{/if}
{/await}
</div>
</SettingsCard>
+115 -131
View File
@@ -1,147 +1,131 @@
<script lang="ts">
import { focusTrap } from 'svelte-focus-trap';
import { fly } from 'svelte/transition';
import { onMount, onDestroy } from 'svelte';
import RssiIndicator from '$lib/components/statusbar/RSSIIndicator.svelte';
import type { NetworkItem, NetworkList } from '$lib/types/models';
import { api } from '$lib/api';
import { AP, Network, Reload, Cancel } from '$lib/components/icons';
import { modals, exitBeforeEnter } from 'svelte-modals';
import { focusTrap } from 'svelte-focus-trap';
import { fly } from 'svelte/transition';
import { onMount, onDestroy } from 'svelte';
import RssiIndicator from '$lib/components/statusbar/RSSIIndicator.svelte';
import type { NetworkItem, NetworkList } from '$lib/types/models';
import { api } from '$lib/api';
import { AP, Network, Reload, Cancel } from '$lib/components/icons';
import { modals, exitBeforeEnter, type ModalProps } from 'svelte-modals';
// provided by <Modals />
interface Props {
isOpen: boolean;
storeNetwork: any;
let { isOpen, storeNetwork }: ModalProps = $props();
const encryptionType = [
'Open',
'WEP',
'WPA PSK',
'WPA2 PSK',
'WPA WPA2 PSK',
'WPA2 Enterprise',
'WPA3 PSK',
'WPA2 WPA3 PSK',
'WAPI PSK',
];
let listOfNetworks: NetworkItem[] = $state([]);
let scanActive = $state(false);
let pollingId: ReturnType<typeof setTimeout> | number;
async function scanNetworks() {
scanActive = true;
await api.get('/api/wifi/scan');
if ((await pollingResults()) == false) {
pollingId = setInterval(() => pollingResults(), 1000);
}
return;
}
let { isOpen, storeNetwork }: Props = $props();
const encryptionType = [
'Open',
'WEP',
'WPA PSK',
'WPA2 PSK',
'WPA WPA2 PSK',
'WPA2 Enterprise',
'WPA3 PSK',
'WPA2 WPA3 PSK',
'WAPI PSK'
];
let listOfNetworks: NetworkItem[] = $state([]);
let scanActive = $state(false);
let pollingId: number;
async function scanNetworks() {
scanActive = true;
await api.get('/api/wifi/scan');
if ((await pollingResults()) == false) {
pollingId = setInterval(() => pollingResults(), 1000);
}
return;
async function pollingResults() {
const result = await api.get<NetworkList>('/api/wifi/networks');
if (result.isErr()) {
console.error(`Error occurred while fetching: `, result.inner);
return false;
}
async function pollingResults() {
const result = await api.get<NetworkList>('/api/wifi/networks');
if (result.isErr()) {
console.error(`Error occurred while fetching: `, result.inner);
return false;
}
let response = result.inner;
listOfNetworks = response.networks;
scanActive = false;
if (listOfNetworks.length) {
clearInterval(pollingId);
pollingId = 0;
}
return listOfNetworks.length;
let response = result.inner;
listOfNetworks = response.networks;
scanActive = false;
if (listOfNetworks.length) {
clearInterval(pollingId);
pollingId = 0;
}
return listOfNetworks.length;
}
onMount(() => {
scanNetworks();
});
onMount(() => {
scanNetworks();
});
onDestroy(() => {
if (pollingId) {
clearInterval(pollingId);
pollingId = 0;
}
});
onDestroy(() => {
if (pollingId) {
clearInterval(pollingId);
pollingId = 0;
}
});
</script>
{#if isOpen}
<div
role="dialog"
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }}
use:exitBeforeEnter
use:focusTrap>
<div
role="dialog"
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }}
use:exitBeforeEnter
use:focusTrap
>
<div
class="bg-base-100 rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
>
<h2 class="text-base-content text-start text-2xl font-bold">Scan Networks</h2>
<div class="divider my-2"></div>
<div class="overflow-y-auto">
{#if scanActive}<div
class="bg-base-100 flex flex-col items-center justify-center p-6"
>
<AP class="text-secondary h-32 w-32 shrink animate-ping stroke-2" />
<p class="mt-8 text-2xl">Scanning ...</p>
class="bg-base-100 rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg">
<h2 class="text-base-content text-start text-2xl font-bold">Scan Networks</h2>
<div class="divider my-2"></div>
<div class="overflow-y-auto">
{#if scanActive}<div class="bg-base-100 flex flex-col items-center justify-center p-6">
<AP class="text-secondary h-32 w-32 shrink animate-ping stroke-2" />
<p class="mt-8 text-2xl">Scanning ...</p>
</div>
{:else}
<ul class="menu">
{#each listOfNetworks as network, i}
<li>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="bg-base-200 rounded-btn my-1 flex items-center space-x-3 hover:scale-[1.02] active:scale-[0.98]"
onclick={() => {
storeNetwork(network.ssid);
}}
role="button"
tabindex="0">
<div class="mask mask-hexagon bg-primary h-auto w-10 shrink-0">
<Network class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">{network.ssid}</div>
<div class="text-sm opacity-75">
Security: {encryptionType[network.encryption_type]}, Channel: {network.channel}
</div>
{:else}
<ul class="menu">
{#each listOfNetworks as network, i}
<li>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="bg-base-200 rounded-btn my-1 flex items-center space-x-3 hover:scale-[1.02] active:scale-[0.98]"
onclick={() => {
storeNetwork(network.ssid);
}}
role="button"
tabindex="0"
>
<div class="mask mask-hexagon bg-primary h-auto w-10 shrink-0">
<Network
class="text-primary-content h-auto w-full scale-75"
/>
</div>
<div>
<div class="font-bold">{network.ssid}</div>
<div class="text-sm opacity-75">
Security: {encryptionType[network.encryption_type]},
Channel: {network.channel}
</div>
</div>
<div class="grow"></div>
<RssiIndicator showDBm={true} rssi={network.rssi} />
</div>
</li>
{/each}
</ul>
{/if}
</div>
<div class="divider my-2"></div>
<div class="flex flex-wrap justify-end gap-2">
<button
class="btn btn-primary inline-flex flex-none items-center"
disabled={scanActive}
onclick={scanNetworks}
>
<Reload class="mr-2 h-5 w-5" /><span>Scan again</span>
</button>
</div>
<div class="grow"></div>
<RssiIndicator showDBm={true} rssi={network.rssi} />
</div>
</li>
{/each}
</ul>
{/if}
</div>
<div class="divider my-2"></div>
<div class="flex flex-wrap justify-end gap-2">
<button
class="btn btn-primary inline-flex flex-none items-center"
disabled={scanActive}
onclick={scanNetworks}>
<Reload class="mr-2 h-5 w-5" /><span>Scan again</span>
</button>
<div class="grow"></div>
<button
class="btn btn-warning text-warning-content inline-flex flex-none items-center"
onclick={() => modals.close()}
>
<Cancel class="mr-2 h-5 w-5" /><span>Cancel</span>
</button>
</div>
</div>
<div class="grow"></div>
<button
class="btn btn-warning text-warning-content inline-flex flex-none items-center"
onclick={() => modals.close()}>
<Cancel class="mr-2 h-5 w-5" /><span>Cancel</span>
</button>
</div>
</div>
</div>
{/if}
+375 -369
View File
@@ -1,19 +1,19 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte'
import { modals } from 'svelte-modals'
import { slide } from 'svelte/transition'
import { cubicOut } from 'svelte/easing'
import { notifications } from '$lib/components/toasts/notifications'
import DragDropList, { VerticalDropZone, reorder, type DropEvent } from 'svelte-dnd-list'
import SettingsCard from '$lib/components/SettingsCard.svelte'
import { PasswordInput } from '$lib/components/input'
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'
import ScanNetworks from './Scan.svelte'
import Spinner from '$lib/components/Spinner.svelte'
import InfoDialog from '$lib/components/InfoDialog.svelte'
import type { KnownNetworkItem, WifiSettings, WifiStatus } from '$lib/types/models'
import { socket } from '$lib/stores'
import { api } from '$lib/api'
import { onMount, onDestroy } from 'svelte';
import { modals } from 'svelte-modals';
import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import { notifications } from '$lib/components/toasts/notifications';
import DragDropList, { VerticalDropZone, reorder, type DropEvent } from 'svelte-dnd-list';
import SettingsCard from '$lib/components/SettingsCard.svelte';
import { PasswordInput } from '$lib/components/input';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import ScanNetworks from './Scan.svelte';
import Spinner from '$lib/components/Spinner.svelte';
import InfoDialog from '$lib/components/InfoDialog.svelte';
import type { KnownNetworkItem, WifiSettings, WifiStatus } from '$lib/types/models';
import { socket } from '$lib/stores';
import { api } from '$lib/api';
import {
Cancel,
Delete,
@@ -31,9 +31,9 @@
DNS,
Add,
Scan,
Edit
} from '$lib/components/icons'
import StatusItem from '$lib/components/StatusItem.svelte'
Edit,
} from '$lib/components/icons';
import StatusItem from '$lib/components/StatusItem.svelte';
let networkEditable: KnownNetworkItem = $state({
ssid: '',
@@ -43,22 +43,22 @@
subnet_mask: undefined,
gateway_ip: undefined,
dns_ip_1: undefined,
dns_ip_2: undefined
})
dns_ip_2: undefined,
});
let static_ip_config = $state(false)
let static_ip_config = $state(false);
let newNetwork: boolean = $state(true)
let showNetworkEditor: boolean = $state(false)
let newNetwork: boolean = $state(true);
let showNetworkEditor: boolean = $state(false);
let wifiStatus: WifiStatus = $state()
let wifiSettings: WifiSettings = $state()
let wifiStatus: WifiStatus | null = $state(null);
let wifiSettings: WifiSettings | null = $state(null);
let dndNetworkList: KnownNetworkItem[] = $state([])
let dndNetworkList: KnownNetworkItem[] = $state([]);
let showWifiDetails = $state(false)
let showWifiDetails = $state(false);
let formField: any = $state()
let formField: any = $state();
let formErrors = $state({
ssid: false,
@@ -66,156 +66,157 @@
gateway_ip: false,
subnet_mask: false,
dns_1: false,
dns_2: false
})
dns_2: false,
});
let formErrorhostname = $state(false)
let formErrorhostname = $state(false);
async function getWifiStatus() {
const result = await api.get<WifiStatus>('/api/wifi/sta/status')
const result = await api.get<WifiStatus>('/api/wifi/sta/status');
if (result.isErr()) {
console.error(`Error occurred while fetching: `, result.inner)
return
console.error(`Error occurred while fetching: `, result.inner);
return;
}
wifiStatus = result.inner
return wifiStatus
wifiStatus = result.inner;
return wifiStatus;
}
async function getWifiSettings() {
const result = await api.get<WifiSettings>('/api/wifi/sta/settings')
const result = await api.get<WifiSettings>('/api/wifi/sta/settings');
if (result.isErr()) {
console.error(`Error occurred while fetching: `, result.inner)
return
console.error(`Error occurred while fetching: `, result.inner);
return;
}
wifiSettings = result.inner
dndNetworkList = wifiSettings.wifi_networks
return wifiSettings
wifiSettings = result.inner;
dndNetworkList = wifiSettings.wifi_networks;
return wifiSettings;
}
onDestroy(() => socket.off('WiFiSettings'))
onDestroy(() => socket.off('WiFiSettings'));
onMount(() => {
socket.on<WifiSettings>('WiFiSettings', data => {
wifiSettings = data
dndNetworkList = wifiSettings.wifi_networks
})
})
wifiSettings = data;
dndNetworkList = wifiSettings.wifi_networks;
});
});
async function postWiFiSettings(data: WifiSettings) {
const result = await api.post<WifiSettings>('/api/wifi/sta/settings', data)
const result = await api.post<WifiSettings>('/api/wifi/sta/settings', data);
if (result.isErr()) {
console.error(`Error occurred while fetching: `, result.inner)
notifications.error('User not authorized.', 3000)
return
console.error(`Error occurred while fetching: `, result.inner);
notifications.error('User not authorized.', 3000);
return;
}
wifiSettings = result.inner
notifications.success('Wi-Fi settings updated.', 3000)
wifiSettings = result.inner;
notifications.success('Wi-Fi settings updated.', 3000);
}
function validateHostName() {
if (!wifiSettings) return false;
if (wifiSettings.hostname.length < 3 || wifiSettings.hostname.length > 32) {
formErrorhostname = true
formErrorhostname = true;
} else {
formErrorhostname = false
formErrorhostname = false;
// Update global wifiSettings object
wifiSettings.wifi_networks = dndNetworkList
wifiSettings.wifi_networks = dndNetworkList;
// Post to REST API
postWiFiSettings(wifiSettings)
console.log(wifiSettings)
postWiFiSettings(wifiSettings);
console.log(wifiSettings);
}
}
function validateWiFiForm(event: SubmitEvent) {
event.preventDefault()
let valid = true
event.preventDefault();
let valid = true;
// Validate SSID
if (networkEditable.ssid.length < 3 || networkEditable.ssid.length > 32) {
valid = false
formErrors.ssid = true
valid = false;
formErrors.ssid = true;
} else {
formErrors.ssid = false
formErrors.ssid = false;
}
networkEditable.static_ip_config = static_ip_config
networkEditable.static_ip_config = static_ip_config;
if (networkEditable.static_ip_config) {
// RegEx for IPv4
const regexExp =
/\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/
/\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/;
// Validate gateway IP
if (!regexExp.test(networkEditable.gateway_ip!)) {
valid = false
formErrors.gateway_ip = true
valid = false;
formErrors.gateway_ip = true;
} else {
formErrors.gateway_ip = false
formErrors.gateway_ip = false;
}
// Validate Subnet Mask
if (!regexExp.test(networkEditable.subnet_mask!)) {
valid = false
formErrors.subnet_mask = true
valid = false;
formErrors.subnet_mask = true;
} else {
formErrors.subnet_mask = false
formErrors.subnet_mask = false;
}
// Validate local IP
if (!regexExp.test(networkEditable.local_ip!)) {
valid = false
formErrors.local_ip = true
valid = false;
formErrors.local_ip = true;
} else {
formErrors.local_ip = false
formErrors.local_ip = false;
}
// Validate DNS 1
if (!regexExp.test(networkEditable.dns_ip_1!)) {
valid = false
formErrors.dns_1 = true
valid = false;
formErrors.dns_1 = true;
} else {
formErrors.dns_1 = false
formErrors.dns_1 = false;
}
// Validate DNS 2
if (!regexExp.test(networkEditable.dns_ip_2!)) {
valid = false
formErrors.dns_2 = true
valid = false;
formErrors.dns_2 = true;
} else {
formErrors.dns_2 = false
formErrors.dns_2 = false;
}
} else {
formErrors.local_ip = false
formErrors.subnet_mask = false
formErrors.gateway_ip = false
formErrors.dns_1 = false
formErrors.dns_2 = false
formErrors.local_ip = false;
formErrors.subnet_mask = false;
formErrors.gateway_ip = false;
formErrors.dns_1 = false;
formErrors.dns_2 = false;
}
// Submit JSON to REST API
if (valid) {
if (newNetwork) {
dndNetworkList.push(networkEditable)
dndNetworkList.push(networkEditable);
} else {
dndNetworkList.splice(dndNetworkList.indexOf(networkEditable), 1, networkEditable)
dndNetworkList.splice(dndNetworkList.indexOf(networkEditable), 1, networkEditable);
}
addNetwork()
dndNetworkList = [...dndNetworkList] //Trigger reactivity
showNetworkEditor = false
addNetwork();
dndNetworkList = [...dndNetworkList]; //Trigger reactivity
showNetworkEditor = false;
}
}
function scanForNetworks() {
modals.open(ScanNetworks, {
storeNetwork: (network: string) => {
addNetwork()
networkEditable.ssid = network
showNetworkEditor = true
modals.close()
}
})
addNetwork();
networkEditable.ssid = network;
showNetworkEditor = true;
modals.close();
},
});
}
function addNetwork() {
newNetwork = true
newNetwork = true;
networkEditable = {
ssid: '',
password: '',
@@ -224,14 +225,14 @@
subnet_mask: undefined,
gateway_ip: undefined,
dns_ip_1: undefined,
dns_ip_2: undefined
}
dns_ip_2: undefined,
};
}
function handleEdit(index: number) {
newNetwork = false
showNetworkEditor = true
networkEditable = dndNetworkList[index]
newNetwork = false;
showNetworkEditor = true;
networkEditable = dndNetworkList[index];
}
function confirmDelete(index: number) {
@@ -240,20 +241,20 @@
message: 'Are you sure you want to delete this network?',
labels: {
cancel: { label: 'Cancel', icon: Cancel },
confirm: { label: 'Delete', icon: Delete }
confirm: { label: 'Delete', icon: Delete },
},
onConfirm: () => {
// Check if network is currently been edited and delete as well
if (dndNetworkList[index].ssid === networkEditable.ssid) {
addNetwork()
addNetwork();
}
// Remove network from array
dndNetworkList.splice(index, 1)
dndNetworkList = [...dndNetworkList] //Trigger reactivity
showNetworkEditor = false
modals.close()
}
})
dndNetworkList.splice(index, 1);
dndNetworkList = [...dndNetworkList]; //Trigger reactivity
showNetworkEditor = false;
modals.close();
},
});
}
function checkNetworkList() {
@@ -263,21 +264,21 @@
message:
'You have reached the maximum number of networks. Please delete one to add another.',
dismiss: { label: 'OK', icon: Check },
onDismiss: () => modals.close()
})
return false
onDismiss: () => modals.close(),
});
return false;
} else {
return true
return true;
}
}
function onDrop({ detail: { from, to } }: CustomEvent<DropEvent>) {
if (!to || from === to) {
return
return;
}
dndNetworkList = reorder(dndNetworkList, from.index, to.index)
console.log(dndNetworkList)
dndNetworkList = reorder(dndNetworkList, from.index, to.index);
console.log(dndNetworkList);
}
</script>
@@ -291,53 +292,55 @@
<div class="w-full overflow-x-auto">
{#await getWifiStatus()}
<Spinner />
{:then nothing}
<div
class="flex w-full flex-col space-y-1"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<StatusItem
icon={AP}
title="Status"
variant={wifiStatus.status === 3 ? 'success' : 'error'}
description={wifiStatus.status === 3 ? 'Connected' : 'Inactive'} />
{#if wifiStatus.status === 3}
<StatusItem icon={SSID} title="SSID" description={wifiStatus.ssid} />
<StatusItem icon={Home} title="IP Address" description={wifiStatus.local_ip} />
<StatusItem icon={WiFi} title="RSSI" description={`${wifiStatus.rssi} dBm`}>
<button
class="btn btn-circle btn-ghost btn-sm modal-button"
onclick={() => {
showWifiDetails = !showWifiDetails
}}>
<Down
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
showWifiDetails
) ?
'rotate-180'
: ''}" />
</button>
</StatusItem>
{/if}
</div>
<!-- Folds open -->
{#if showWifiDetails}
{:then}
{#if wifiStatus}
<div
class="flex w-full flex-col space-y-1 pt-1"
class="flex w-full flex-col space-y-1"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<StatusItem icon={MAC} title="MAC Address" description={wifiStatus.mac_address} />
<StatusItem
icon={AP}
title="Status"
variant={wifiStatus.status === 3 ? 'success' : 'error'}
description={wifiStatus.status === 3 ? 'Connected' : 'Inactive'} />
<StatusItem icon={Channel} title="Channel" description={wifiStatus.channel} />
{#if wifiStatus.status === 3}
<StatusItem icon={SSID} title="SSID" description={wifiStatus.ssid} />
<StatusItem icon={Gateway} title="Gateway IP" description={wifiStatus.gateway_ip} />
<StatusItem icon={Home} title="IP Address" description={wifiStatus.local_ip} />
<StatusItem icon={Subnet} title="Subnet Mask" description={wifiStatus.subnet_mask} />
<StatusItem icon={DNS} title="DNS" description={wifiStatus.dns_ip_1} />
<StatusItem icon={WiFi} title="RSSI" description={`${wifiStatus.rssi} dBm`}>
<button
class="btn btn-circle btn-ghost btn-sm modal-button"
onclick={() => {
showWifiDetails = !showWifiDetails;
}}>
<Down
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
showWifiDetails
) ?
'rotate-180'
: ''}" />
</button>
</StatusItem>
{/if}
</div>
<!-- Folds open -->
{#if showWifiDetails}
<div
class="flex w-full flex-col space-y-1 pt-1"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<StatusItem icon={MAC} title="MAC Address" description={wifiStatus.mac_address} />
<StatusItem icon={Channel} title="Channel" description={wifiStatus.channel} />
<StatusItem icon={Gateway} title="Gateway IP" description={wifiStatus.gateway_ip} />
<StatusItem icon={Subnet} title="Subnet Mask" description={wifiStatus.subnet_mask} />
<StatusItem icon={DNS} title="DNS" description={wifiStatus.dns_ip_1} />
</div>
{/if}
{/if}
{/await}
</div>
@@ -349,254 +352,257 @@
</div>
{#await getWifiSettings()}
<Spinner />
{:then nothing}
<div class="relative w-full overflow-visible">
<button
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-16"
onclick={() => {
if (checkNetworkList()) {
addNetwork()
showNetworkEditor = true
}
}}>
<Add class="h-6 w-6" /></button>
<button
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0"
onclick={() => {
if (checkNetworkList()) {
scanForNetworks()
showNetworkEditor = true
}
}}>
<Scan class="h-6 w-6" /></button>
{:then}
{#if wifiSettings}
<div class="relative w-full overflow-visible">
<button
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-16"
onclick={() => {
if (checkNetworkList()) {
addNetwork();
showNetworkEditor = true;
}
}}>
<Add class="h-6 w-6" /></button>
<button
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0"
onclick={() => {
if (checkNetworkList()) {
scanForNetworks();
showNetworkEditor = true;
}
}}>
<Scan class="h-6 w-6" /></button>
<div
class="overflow-x-auto space-y-1"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<DragDropList
id="networks"
type={VerticalDropZone}
itemSize={60}
itemCount={dndNetworkList.length}
on:drop={onDrop}>
{#snippet children({ index })}
<StatusItem icon={Router} title={dndNetworkList[index].ssid}>
<div class="space-x-0 px-0 mx-0">
<button
class="btn btn-ghost btn-sm"
onclick={() => {
handleEdit(index)
}}>
<Edit class="h-6 w-6" /></button>
<button
class="btn btn-ghost btn-sm"
onclick={() => {
confirmDelete(index)
}}>
<Delete class="text-error h-6 w-6" />
</button>
</div>
</StatusItem>
{/snippet}
</DragDropList>
</div>
</div>
<div class="divider mb-0"></div>
<div
class="flex flex-col gap-2 p-0"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<form class="" onsubmit={validateWiFiForm} novalidate bind:this={formField}>
<div class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2">
<div>
<label class="label" for="channel">
<span class="label-text text-md">Host Name</span>
</label>
<input
type="text"
min="1"
max="32"
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
formErrorhostname
) ?
'border-error border-2'
: ''}"
bind:value={wifiSettings.hostname}
id="channel"
required />
<label class="label" for="channel">
<span class="label-text-alt text-error {formErrorhostname ? '' : 'hidden'}"
>Host name must be between 2 and 32 characters long</span>
</label>
</div>
<label class="label inline-flex cursor-pointer content-end justify-start gap-4">
<input
type="checkbox"
bind:checked={wifiSettings.priority_RSSI}
class="checkbox checkbox-primary sm:-mb-5" />
<span class="sm:-mb-5">Connect to strongest WiFi</span>
</label>
<div
class="overflow-x-auto space-y-1"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<DragDropList
id="networks"
type={VerticalDropZone}
itemSize={60}
itemCount={dndNetworkList.length}
on:drop={onDrop}>
{#snippet children({ index }: { index: number })}
<StatusItem icon={Router} title={dndNetworkList[index].ssid}>
<div class="space-x-0 px-0 mx-0">
<button
class="btn btn-ghost btn-sm"
onclick={() => {
handleEdit(index);
}}>
<Edit class="h-6 w-6" /></button>
<button
class="btn btn-ghost btn-sm"
onclick={() => {
confirmDelete(index);
}}>
<Delete class="text-error h-6 w-6" />
</button>
</div>
</StatusItem>
{/snippet}
</DragDropList>
</div>
</div>
{#if showNetworkEditor}
<div class="divider my-0"></div>
<div
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<div class="divider mb-0"></div>
<div
class="flex flex-col gap-2 p-0"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<form class="" onsubmit={validateWiFiForm} novalidate bind:this={formField}>
<div class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2">
<div>
<label class="label" for="ssid">
<span class="label-text text-md">SSID</span>
<label class="label" for="channel">
<span class="label-text text-md">Host Name</span>
</label>
<input
type="text"
min="1"
max="32"
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
formErrors.ssid
formErrorhostname
) ?
'border-error border-2'
: ''}"
bind:value={networkEditable.ssid}
id="ssid"
min="2"
max="32"
bind:value={wifiSettings.hostname}
id="channel"
required />
<label class="label" for="ssid">
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
>SSID must be between 3 and 32 characters long</span>
<label class="label" for="channel">
<span class="label-text-alt text-error {formErrorhostname ? '' : 'hidden'}"
>Host name must be between 2 and 32 characters long</span>
</label>
</div>
<div>
<label class="label" for="pwd">
<span class="label-text text-md">Password</span>
</label>
<PasswordInput bind:value={networkEditable.password} id="pwd" />
</div>
<label
class="label inline-flex cursor-pointer content-end justify-start gap-4 mt-2 sm:mb-4">
<label class="label inline-flex cursor-pointer content-end justify-start gap-4">
<input
type="checkbox"
bind:checked={static_ip_config}
bind:checked={wifiSettings.priority_RSSI}
class="checkbox checkbox-primary sm:-mb-5" />
<span class="sm:-mb-5">Static IP Config?</span>
<span class="sm:-mb-5">Connect to strongest WiFi</span>
</label>
</div>
{#if static_ip_config}
{#if showNetworkEditor}
<div class="divider my-0"></div>
<div
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<div>
<label class="label" for="localIP">
<span class="label-text text-md">Local IP</span>
<label class="label" for="ssid">
<span class="label-text text-md">SSID</span>
</label>
<input
type="text"
class="input input-bordered w-full {formErrors.local_ip ?
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
formErrors.ssid
) ?
'border-error border-2'
: ''}"
minlength="7"
maxlength="15"
size="15"
bind:value={networkEditable.local_ip}
id="localIP"
bind:value={networkEditable.ssid}
id="ssid"
min="2"
max="32"
required />
<label class="label" for="localIP">
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
>Must be a valid IPv4 address</span>
</label>
</div>
<div>
<label class="label" for="gateway">
<span class="label-text text-md">Gateway IP</span>
</label>
<input
type="text"
class="input input-bordered w-full {formErrors.gateway_ip ?
'border-error border-2'
: ''}"
minlength="7"
maxlength="15"
size="15"
bind:value={networkEditable.gateway_ip}
required />
<label class="label" for="gateway">
<span class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
>Must be a valid IPv4 address</span>
<label class="label" for="ssid">
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
>SSID must be between 3 and 32 characters long</span>
</label>
</div>
<div>
<label class="label" for="subnet">
<span class="label-text text-md">Subnet Mask</span>
</label>
<input
type="text"
class="input input-bordered w-full {formErrors.subnet_mask ?
'border-error border-2'
: ''}"
minlength="7"
maxlength="15"
size="15"
bind:value={networkEditable.subnet_mask}
required />
<label class="label" for="subnet">
<span
class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}">
Must be a valid IPv4 address
</span>
<label class="label" for="pwd">
<span class="label-text text-md">Password</span>
</label>
<PasswordInput bind:value={networkEditable.password} id="pwd" />
</div>
<div>
<label class="label" for="gateway">
<span class="label-text text-md">DNS 1</span>
</label>
<label
class="label inline-flex cursor-pointer content-end justify-start gap-4 mt-2 sm:mb-4">
<input
type="text"
class="input input-bordered w-full {formErrors.dns_1 ? 'border-error border-2'
: ''}"
minlength="7"
maxlength="15"
size="15"
bind:value={networkEditable.dns_ip_1}
required />
<label class="label" for="gateway">
<span class="label-text-alt text-error {formErrors.dns_1 ? '' : 'hidden'}">
Must be a valid IPv4 address
</span>
</label>
</div>
<div>
<label class="label" for="subnet">
<span class="label-text text-md">DNS 2</span>
</label>
<input
type="text"
class="input input-bordered w-full {formErrors.dns_2 ? 'border-error border-2'
: ''}"
minlength="7"
maxlength="15"
size="15"
bind:value={networkEditable.dns_ip_2}
required />
<label class="label" for="subnet">
<span class="label-text-alt text-error {formErrors.dns_2 ? '' : 'hidden'}">
Must be a valid IPv4 address
</span>
</label>
</div>
type="checkbox"
bind:checked={static_ip_config}
class="checkbox checkbox-primary sm:-mb-5" />
<span class="sm:-mb-5">Static IP Config?</span>
</label>
</div>
{/if}
{/if}
{#if static_ip_config}
<div
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<div>
<label class="label" for="localIP">
<span class="label-text text-md">Local IP</span>
</label>
<input
type="text"
class="input input-bordered w-full {formErrors.local_ip ?
'border-error border-2'
: ''}"
minlength="7"
maxlength="15"
size="15"
bind:value={networkEditable.local_ip}
id="localIP"
required />
<label class="label" for="localIP">
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
>Must be a valid IPv4 address</span>
</label>
</div>
<div class="divider mb-2 mt-0"></div>
<div class="mx-4 flex flex-wrap justify-end gap-2">
<button class="btn btn-primary" type="submit" disabled={!showNetworkEditor}>
{newNetwork ? 'Add Network' : 'Update Network'}
</button>
<button class="btn btn-primary" type="button" onclick={validateHostName}>
Apply Settings
</button>
</div>
</form>
</div>
<div>
<label class="label" for="gateway">
<span class="label-text text-md">Gateway IP</span>
</label>
<input
type="text"
class="input input-bordered w-full {formErrors.gateway_ip ?
'border-error border-2'
: ''}"
minlength="7"
maxlength="15"
size="15"
bind:value={networkEditable.gateway_ip}
required />
<label class="label" for="gateway">
<span
class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
>Must be a valid IPv4 address</span>
</label>
</div>
<div>
<label class="label" for="subnet">
<span class="label-text text-md">Subnet Mask</span>
</label>
<input
type="text"
class="input input-bordered w-full {formErrors.subnet_mask ?
'border-error border-2'
: ''}"
minlength="7"
maxlength="15"
size="15"
bind:value={networkEditable.subnet_mask}
required />
<label class="label" for="subnet">
<span
class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}">
Must be a valid IPv4 address
</span>
</label>
</div>
<div>
<label class="label" for="gateway">
<span class="label-text text-md">DNS 1</span>
</label>
<input
type="text"
class="input input-bordered w-full {formErrors.dns_1 ? 'border-error border-2'
: ''}"
minlength="7"
maxlength="15"
size="15"
bind:value={networkEditable.dns_ip_1}
required />
<label class="label" for="gateway">
<span class="label-text-alt text-error {formErrors.dns_1 ? '' : 'hidden'}">
Must be a valid IPv4 address
</span>
</label>
</div>
<div>
<label class="label" for="subnet">
<span class="label-text text-md">DNS 2</span>
</label>
<input
type="text"
class="input input-bordered w-full {formErrors.dns_2 ? 'border-error border-2'
: ''}"
minlength="7"
maxlength="15"
size="15"
bind:value={networkEditable.dns_ip_2}
required />
<label class="label" for="subnet">
<span class="label-text-alt text-error {formErrors.dns_2 ? '' : 'hidden'}">
Must be a valid IPv4 address
</span>
</label>
</div>
</div>
{/if}
{/if}
<div class="divider mb-2 mt-0"></div>
<div class="mx-4 flex flex-wrap justify-end gap-2">
<button class="btn btn-primary" type="submit" disabled={!showNetworkEditor}>
{newNetwork ? 'Add Network' : 'Update Network'}
</button>
<button class="btn btn-primary" type="button" onclick={validateHostName}>
Apply Settings
</button>
</div>
</form>
</div>
{/if}
{/await}
</div>
</SettingsCard>