🧼 Refactors wifi and ap to use StatusItem

This commit is contained in:
Rune Harlyk
2025-03-08 14:35:55 +01:00
committed by Rune Harlyk
parent 72f3bcfd78
commit 99660b9a23
5 changed files with 955 additions and 1161 deletions
+43
View File
@@ -0,0 +1,43 @@
<script lang="ts">
type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning'
const {
icon,
title,
description = '',
variant = 'primary',
class: klass = '',
children = null
} = $props<{
icon: any
title: string
description?: string | number
variant?: Variant
class?: string
children?: () => any
}>()
const Icon = $derived(icon)
const variants: Record<Variant, [string, string]> = {
success: ['bg-success', 'text-success-content'],
error: ['bg-error', 'text-error-content'],
primary: ['bg-primary', 'text-primary-content'],
info: ['bg-info', 'text-info-content'],
warning: ['bg-warning', 'text-warning-content']
}
const variantKey: Variant = (variant as Variant) in variants ? (variant as Variant) : 'primary'
const [bgColor, textColor] = variants[variantKey]
</script>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2 {klass}">
<div class="mask mask-hexagon {bgColor} h-auto w-10 flex-none">
<Icon class="{textColor} h-auto w-full scale-75" />
</div>
<div class="grow">
<div class="font-bold">{title}</div>
<div class="text-sm opacity-75 grow">{description}</div>
</div>
{@render children?.()}
</div>
@@ -1,15 +0,0 @@
<script lang="ts">
const { icon, title, description } = $props()
const Icon = $derived(icon)
</script>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
<Icon class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">{title}</div>
<div class="text-sm opacity-75">{description}</div>
</div>
</div>
@@ -6,12 +6,10 @@
import Spinner from '$lib/components/Spinner.svelte' import Spinner from '$lib/components/Spinner.svelte'
import { slide } from 'svelte/transition' import { slide } from 'svelte/transition'
import { cubicOut } from 'svelte/easing' import { cubicOut } from 'svelte/easing'
import type { SystemInformation, Analytics } from '$lib/types/models' import type { SystemInformation, Analytics } from '$lib/types/models'
import { socket } from '$lib/stores/socket' import { socket } from '$lib/stores/socket'
import { api } from '$lib/api' import { api } from '$lib/api'
import { convertSeconds } from '$lib/utilities' import { convertSeconds } from '$lib/utilities'
import { useFeatureFlags } from '$lib/stores/featureFlags' import { useFeatureFlags } from '$lib/stores/featureFlags'
import { import {
Cancel, Cancel,
@@ -31,7 +29,7 @@
Temperature, Temperature,
Stopwatch Stopwatch
} from '$lib/components/icons' } from '$lib/components/icons'
import StatusItem from './StatusItem.svelte' import StatusItem from '$lib/components/StatusItem.svelte'
import ActionButton from './ActionButton.svelte' import ActionButton from './ActionButton.svelte'
const features = useFeatureFlags() const features = useFeatureFlags()
+104 -176
View File
@@ -1,52 +1,53 @@
<script lang="ts"> <script lang="ts">
import { preventDefault } from 'svelte/legacy'; import { preventDefault } from 'svelte/legacy'
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte'
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition'
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing'
import { PasswordInput } from '$lib/components/input'; import { PasswordInput } from '$lib/components/input'
import SettingsCard from '$lib/components/SettingsCard.svelte'; import SettingsCard from '$lib/components/SettingsCard.svelte'
import { notifications } from '$lib/components/toasts/notifications'; import { notifications } from '$lib/components/toasts/notifications'
import Spinner from '$lib/components/Spinner.svelte'; import Spinner from '$lib/components/Spinner.svelte'
import type { ApSettings, ApStatus } from '$lib/types/models'; import type { ApSettings, ApStatus } from '$lib/types/models'
import { api } from '$lib/api'; import { api } from '$lib/api'
import { useFeatureFlags } from '$lib/stores'; import { useFeatureFlags } from '$lib/stores'
import { AP, Devices, Home, MAC } from '$lib/components/icons'; import { AP, Devices, Home, MAC } from '$lib/components/icons'
import StatusItem from '$lib/components/StatusItem.svelte'
const features = useFeatureFlags(); const features = useFeatureFlags()
let apSettings: ApSettings = $state(); let apSettings: ApSettings = $state()
let apStatus: ApStatus = $state(); let apStatus: ApStatus = $state()
let formField: any = $state(); let formField: any = $state()
async function getAPStatus() { 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()) { if (result.isErr()) {
console.error('Error:', result.inner); console.error('Error:', result.inner)
return; return
} }
apStatus = result.inner; apStatus = result.inner
return apStatus; return apStatus
} }
async function getAPSettings() { 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()) { if (result.isErr()) {
console.error('Error:', result.inner); console.error('Error:', result.inner)
return; return
} }
apSettings = result.inner; apSettings = result.inner
return apSettings; return apSettings
} }
const interval = setInterval(async () => { const interval = setInterval(async () => {
getAPStatus(); getAPStatus()
}, 5000); }, 5000)
onDestroy(() => clearInterval(interval)); onDestroy(() => clearInterval(interval))
onMount(getAPSettings); onMount(getAPSettings)
let provisionMode = [ let provisionMode = [
{ {
@@ -61,13 +62,13 @@
id: 2, id: 2,
text: `Never` text: `Never`
} }
]; ]
let apStatusDescription = [ type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning'
{ bg_color: 'bg-success', text_color: 'text-success-content', description: 'Active' },
{ bg_color: 'bg-error', text_color: 'text-error-content', description: 'Inactive' }, let apStatusVariant: Variant[] = ['success', 'error', 'warning']
{ bg_color: 'bg-warning', text_color: 'text-warning-content', description: 'Lingering' }
]; let apStatusDescription = ['Active', 'Inactive', 'Lingering']
let formErrors = $state({ let formErrors = $state({
ssid: false, ssid: false,
@@ -76,79 +77,79 @@
local_ip: false, local_ip: false,
gateway_ip: false, gateway_ip: false,
subnet_mask: false subnet_mask: false
}); })
async function postAPSettings(data: ApSettings) { 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()) { if (result.isErr()) {
notifications.error('User not authorized.', 3000); notifications.error('User not authorized.', 3000)
console.error('Error:', result.inner); console.error('Error:', result.inner)
return; return
} }
notifications.success('Access Point settings updated.', 3000); notifications.success('Access Point settings updated.', 3000)
apSettings = result.inner; apSettings = result.inner
} }
function handleSubmitAP() { function handleSubmitAP() {
let valid = true; let valid = true
// Validate SSID // Validate SSID
if (apSettings.ssid.length < 3 || apSettings.ssid.length > 32) { if (apSettings.ssid.length < 3 || apSettings.ssid.length > 32) {
valid = false; valid = false
formErrors.ssid = true; formErrors.ssid = true
} else { } else {
formErrors.ssid = false; formErrors.ssid = false
} }
// Validate Channel // Validate Channel
let channel = Number(apSettings.channel); let channel = Number(apSettings.channel)
if (1 > channel || channel > 13) { if (1 > channel || channel > 13) {
valid = false; valid = false
formErrors.channel = true; formErrors.channel = true
} else { } else {
formErrors.channel = false; formErrors.channel = false
} }
// Validate max_clients // Validate max_clients
let maxClients = Number(apSettings.max_clients); let maxClients = Number(apSettings.max_clients)
if (1 > maxClients || maxClients > 8) { if (1 > maxClients || maxClients > 8) {
valid = false; valid = false
formErrors.max_clients = true; formErrors.max_clients = true
} else { } else {
formErrors.max_clients = false; formErrors.max_clients = false
} }
// RegEx for IPv4 // RegEx for IPv4
const regexExp = 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 // Validate gateway IP
if (!regexExp.test(apSettings.gateway_ip)) { if (!regexExp.test(apSettings.gateway_ip)) {
valid = false; valid = false
formErrors.gateway_ip = true; formErrors.gateway_ip = true
} else { } else {
formErrors.gateway_ip = false; formErrors.gateway_ip = false
} }
// Validate Subnet Mask // Validate Subnet Mask
if (!regexExp.test(apSettings.subnet_mask)) { if (!regexExp.test(apSettings.subnet_mask)) {
valid = false; valid = false
formErrors.subnet_mask = true; formErrors.subnet_mask = true
} else { } else {
formErrors.subnet_mask = false; formErrors.subnet_mask = false
} }
// Validate local IP // Validate local IP
if (!regexExp.test(apSettings.local_ip)) { if (!regexExp.test(apSettings.local_ip)) {
valid = false; valid = false
formErrors.local_ip = true; formErrors.local_ip = true
} else { } else {
formErrors.local_ip = false; formErrors.local_ip = false
} }
// Submit JSON to REST API // Submit JSON to REST API
if (valid) { if (valid) {
postAPSettings(apSettings); postAPSettings(apSettings)
} }
} }
</script> </script>
@@ -166,69 +167,25 @@
{:then nothing} {:then nothing}
<div <div
class="flex w-full flex-col space-y-1" class="flex w-full flex-col space-y-1"
transition:slide|local={{ duration: 300, easing: cubicOut }} transition:slide|local={{ duration: 300, easing: cubicOut }}>
> <StatusItem
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"> icon={AP}
<div title="Status"
class="mask mask-hexagon h-auto w-10 {apStatusDescription[apStatus.status] variant={apStatusVariant[apStatus.status]}
.bg_color}" description={apStatusDescription[apStatus.status]} />
>
<AP
class="h-auto w-full scale-75 {apStatusDescription[apStatus.status]
.text_color}"
/>
</div>
<div>
<div class="font-bold">Status</div>
<div class="text-sm opacity-75">
{apStatusDescription[apStatus.status].description}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"> <StatusItem icon={Home} title="IP Address" description={apStatus.ip_address} />
<div class="mask mask-hexagon bg-primary h-auto w-10">
<Home class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">IP Address</div>
<div class="text-sm opacity-75">
{apStatus.ip_address}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"> <StatusItem icon={MAC} title="MAC Address" description={apStatus.mac_address} />
<div class="mask mask-hexagon bg-primary h-auto w-10">
<MAC class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">MAC Address</div>
<div class="text-sm opacity-75">
{apStatus.mac_address}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"> <StatusItem icon={Devices} title="AP Clients" description={apStatus.station_num} />
<div class="mask mask-hexagon bg-primary h-auto w-10">
<Devices class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">AP Clients</div>
<div class="text-sm opacity-75">
{apStatus.station_num}
</div>
</div>
</div>
</div> </div>
{/await} {/await}
</div> </div>
<div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden"> <div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden">
<div <div
class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium" class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium">
>
Change AP Settings Change AP Settings
</div> </div>
{#await getAPSettings()} {#await getAPSettings()}
@@ -236,14 +193,12 @@
{:then nothing} {:then nothing}
<div <div
class="flex flex-col gap-2 p-0" class="flex flex-col gap-2 p-0"
transition:slide|local={{ duration: 300, easing: cubicOut }} transition:slide|local={{ duration: 300, easing: cubicOut }}>
>
<form <form
class="grid w-full grid-cols-1 content-center gap-x-4 p-0s sm:grid-cols-2" class="grid w-full grid-cols-1 content-center gap-x-4 p-0s sm:grid-cols-2"
onsubmit={preventDefault(handleSubmitAP)} onsubmit={preventDefault(handleSubmitAP)}
novalidate novalidate
bind:this={formField} bind:this={formField}>
>
<div> <div>
<label class="label" for="apmode"> <label class="label" for="apmode">
<span class="label-text">Provide Access Point ...</span> <span class="label-text">Provide Access Point ...</span>
@@ -251,8 +206,7 @@
<select <select
class="select select-bordered w-full" class="select select-bordered w-full"
id="apmode" id="apmode"
bind:value={apSettings.provision_mode} bind:value={apSettings.provision_mode}>
>
{#each provisionMode as mode} {#each provisionMode as mode}
<option value={mode.id}> <option value={mode.id}>
{mode.text} {mode.text}
@@ -275,13 +229,10 @@
id="ssid" id="ssid"
min="2" min="2"
max="32" max="32"
required required />
/>
<label class="label" for="ssid"> <label class="label" for="ssid">
<span <span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}" >SSID must be between 2 and 32 characters long</span>
>SSID must be between 2 and 32 characters long</span
>
</label> </label>
</div> </div>
@@ -306,14 +257,10 @@
: ''}" : ''}"
bind:value={apSettings.channel} bind:value={apSettings.channel}
id="channel" id="channel"
required required />
/>
<label class="label" for="channel"> <label class="label" for="channel">
<span <span class="label-text-alt text-error {formErrors.channel ? '' : 'hidden'}"
class="label-text-alt text-error {formErrors.channel ? '' : ( >Must be channel 1 to 13</span>
'hidden'
)}">Must be channel 1 to 13</span
>
</label> </label>
</div> </div>
@@ -332,14 +279,10 @@
: ''}" : ''}"
bind:value={apSettings.max_clients} bind:value={apSettings.max_clients}
id="clients" id="clients"
required required />
/>
<label class="label" for="clients"> <label class="label" for="clients">
<span <span class="label-text-alt text-error {formErrors.max_clients ? '' : 'hidden'}"
class="label-text-alt text-error {formErrors.max_clients ? '' : ( >Maximum 8 clients allowed</span>
'hidden'
)}">Maximum 8 clients allowed</span
>
</label> </label>
</div> </div>
@@ -349,22 +292,18 @@
</label> </label>
<input <input
type="text" type="text"
class="input input-bordered w-full {formErrors.local_ip ? class="input input-bordered w-full {formErrors.local_ip ? 'border-error border-2' : (
'border-error border-2' ''
: ''}" )}"
minlength="7" minlength="7"
maxlength="15" maxlength="15"
size="15" size="15"
bind:value={apSettings.local_ip} bind:value={apSettings.local_ip}
id="localIP" id="localIP"
required required />
/>
<label class="label" for="localIP"> <label class="label" for="localIP">
<span <span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
class="label-text-alt text-error {formErrors.local_ip ? '' : ( >Must be a valid IPv4 address</span>
'hidden'
)}">Must be a valid IPv4 address</span
>
</label> </label>
</div> </div>
@@ -374,22 +313,17 @@
</label> </label>
<input <input
type="text" type="text"
class="input input-bordered w-full {formErrors.gateway_ip ? class="input input-bordered w-full {formErrors.gateway_ip ? 'border-error border-2'
'border-error border-2'
: ''}" : ''}"
minlength="7" minlength="7"
maxlength="15" maxlength="15"
size="15" size="15"
bind:value={apSettings.gateway_ip} bind:value={apSettings.gateway_ip}
id="gateway" id="gateway"
required required />
/>
<label class="label" for="gateway"> <label class="label" for="gateway">
<span <span class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
class="label-text-alt text-error {formErrors.gateway_ip ? '' : ( >Must be a valid IPv4 address</span>
'hidden'
)}">Must be a valid IPv4 address</span
>
</label> </label>
</div> </div>
<div> <div>
@@ -398,22 +332,17 @@
</label> </label>
<input <input
type="text" type="text"
class="input input-bordered w-full {formErrors.subnet_mask ? class="input input-bordered w-full {formErrors.subnet_mask ? 'border-error border-2'
'border-error border-2'
: ''}" : ''}"
minlength="7" minlength="7"
maxlength="15" maxlength="15"
size="15" size="15"
bind:value={apSettings.subnet_mask} bind:value={apSettings.subnet_mask}
id="subnet" id="subnet"
required required />
/>
<label class="label" for="subnet"> <label class="label" for="subnet">
<span <span class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}"
class="label-text-alt text-error {formErrors.subnet_mask ? '' : ( >Must be a valid IPv4 address</span>
'hidden'
)}">Must be a valid IPv4 address</span
>
</label> </label>
</div> </div>
@@ -421,8 +350,7 @@
<input <input
type="checkbox" type="checkbox"
bind:checked={apSettings.ssid_hidden} bind:checked={apSettings.ssid_hidden}
class="checkbox checkbox-primary" class="checkbox checkbox-primary" />
/>
<span class="">Hide SSID</span> <span class="">Hide SSID</span>
</label> </label>
+174 -334
View File
@@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte'
import { modals } from 'svelte-modals'; import { modals } from 'svelte-modals'
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition'
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing'
import { notifications } from '$lib/components/toasts/notifications'; import { notifications } from '$lib/components/toasts/notifications'
import DragDropList, { VerticalDropZone, reorder, type DropEvent } from 'svelte-dnd-list'; import DragDropList, { VerticalDropZone, reorder, type DropEvent } from 'svelte-dnd-list'
import SettingsCard from '$lib/components/SettingsCard.svelte'; import SettingsCard from '$lib/components/SettingsCard.svelte'
import { PasswordInput } from '$lib/components/input'; import { PasswordInput } from '$lib/components/input'
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'
import ScanNetworks from './Scan.svelte'; import ScanNetworks from './Scan.svelte'
import Spinner from '$lib/components/Spinner.svelte'; import Spinner from '$lib/components/Spinner.svelte'
import InfoDialog from '$lib/components/InfoDialog.svelte'; import InfoDialog from '$lib/components/InfoDialog.svelte'
import type { KnownNetworkItem, WifiSettings, WifiStatus } from '$lib/types/models'; import type { KnownNetworkItem, WifiSettings, WifiStatus } from '$lib/types/models'
import { socket, useFeatureFlags } from '$lib/stores'; import { socket } from '$lib/stores'
import { api } from '$lib/api'; import { api } from '$lib/api'
import { import {
Cancel, Cancel,
Delete, Delete,
@@ -32,9 +32,8 @@
Add, Add,
Scan, Scan,
Edit Edit
} from '$lib/components/icons'; } from '$lib/components/icons'
import StatusItem from '$lib/components/StatusItem.svelte'
const features = useFeatureFlags();
let networkEditable: KnownNetworkItem = $state({ let networkEditable: KnownNetworkItem = $state({
ssid: '', ssid: '',
@@ -45,21 +44,21 @@
gateway_ip: undefined, gateway_ip: undefined,
dns_ip_1: 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 newNetwork: boolean = $state(true)
let showNetworkEditor: boolean = $state(false); let showNetworkEditor: boolean = $state(false)
let wifiStatus: WifiStatus = $state(); let wifiStatus: WifiStatus = $state()
let wifiSettings: WifiSettings = $state(); let wifiSettings: WifiSettings = $state()
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({ let formErrors = $state({
ssid: false, ssid: false,
@@ -68,155 +67,155 @@
subnet_mask: false, subnet_mask: false,
dns_1: false, dns_1: false,
dns_2: false dns_2: false
}); })
let formErrorhostname = $state(false); let formErrorhostname = $state(false)
async function getWifiStatus() { 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()) { if (result.isErr()) {
console.error(`Error occurred while fetching: `, result.inner); console.error(`Error occurred while fetching: `, result.inner)
return; return
} }
wifiStatus = result.inner; wifiStatus = result.inner
return wifiStatus; return wifiStatus
} }
async function getWifiSettings() { 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()) { if (result.isErr()) {
console.error(`Error occurred while fetching: `, result.inner); console.error(`Error occurred while fetching: `, result.inner)
return; return
} }
wifiSettings = result.inner; wifiSettings = result.inner
dndNetworkList = wifiSettings.wifi_networks; dndNetworkList = wifiSettings.wifi_networks
return wifiSettings; return wifiSettings
} }
onDestroy(() => socket.off('WiFiSettings')); onDestroy(() => socket.off('WiFiSettings'))
onMount(() => { onMount(() => {
socket.on<WifiSettings>('WiFiSettings', data => { socket.on<WifiSettings>('WiFiSettings', data => {
wifiSettings = data; wifiSettings = data
dndNetworkList = wifiSettings.wifi_networks; dndNetworkList = wifiSettings.wifi_networks
}); })
}); })
async function postWiFiSettings(data: WifiSettings) { 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()) { if (result.isErr()) {
console.error(`Error occurred while fetching: `, result.inner); console.error(`Error occurred while fetching: `, result.inner)
notifications.error('User not authorized.', 3000); notifications.error('User not authorized.', 3000)
return; return
} }
wifiSettings = result.inner; wifiSettings = result.inner
notifications.success('Wi-Fi settings updated.', 3000); notifications.success('Wi-Fi settings updated.', 3000)
} }
function validateHostName() { function validateHostName() {
if (wifiSettings.hostname.length < 3 || wifiSettings.hostname.length > 32) { if (wifiSettings.hostname.length < 3 || wifiSettings.hostname.length > 32) {
formErrorhostname = true; formErrorhostname = true
} else { } else {
formErrorhostname = false; formErrorhostname = false
// Update global wifiSettings object // Update global wifiSettings object
wifiSettings.wifi_networks = dndNetworkList; wifiSettings.wifi_networks = dndNetworkList
// Post to REST API // Post to REST API
postWiFiSettings(wifiSettings); postWiFiSettings(wifiSettings)
console.log(wifiSettings); console.log(wifiSettings)
} }
} }
function validateWiFiForm(event: SubmitEvent) { function validateWiFiForm(event: SubmitEvent) {
event.preventDefault(); event.preventDefault()
let valid = true; let valid = true
// Validate SSID // Validate SSID
if (networkEditable.ssid.length < 3 || networkEditable.ssid.length > 32) { if (networkEditable.ssid.length < 3 || networkEditable.ssid.length > 32) {
valid = false; valid = false
formErrors.ssid = true; formErrors.ssid = true
} else { } 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) { if (networkEditable.static_ip_config) {
// RegEx for IPv4 // RegEx for IPv4
const regexExp = 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 // Validate gateway IP
if (!regexExp.test(networkEditable.gateway_ip!)) { if (!regexExp.test(networkEditable.gateway_ip!)) {
valid = false; valid = false
formErrors.gateway_ip = true; formErrors.gateway_ip = true
} else { } else {
formErrors.gateway_ip = false; formErrors.gateway_ip = false
} }
// Validate Subnet Mask // Validate Subnet Mask
if (!regexExp.test(networkEditable.subnet_mask!)) { if (!regexExp.test(networkEditable.subnet_mask!)) {
valid = false; valid = false
formErrors.subnet_mask = true; formErrors.subnet_mask = true
} else { } else {
formErrors.subnet_mask = false; formErrors.subnet_mask = false
} }
// Validate local IP // Validate local IP
if (!regexExp.test(networkEditable.local_ip!)) { if (!regexExp.test(networkEditable.local_ip!)) {
valid = false; valid = false
formErrors.local_ip = true; formErrors.local_ip = true
} else { } else {
formErrors.local_ip = false; formErrors.local_ip = false
} }
// Validate DNS 1 // Validate DNS 1
if (!regexExp.test(networkEditable.dns_ip_1!)) { if (!regexExp.test(networkEditable.dns_ip_1!)) {
valid = false; valid = false
formErrors.dns_1 = true; formErrors.dns_1 = true
} else { } else {
formErrors.dns_1 = false; formErrors.dns_1 = false
} }
// Validate DNS 2 // Validate DNS 2
if (!regexExp.test(networkEditable.dns_ip_2!)) { if (!regexExp.test(networkEditable.dns_ip_2!)) {
valid = false; valid = false
formErrors.dns_2 = true; formErrors.dns_2 = true
} else { } else {
formErrors.dns_2 = false; formErrors.dns_2 = false
} }
} else { } else {
formErrors.local_ip = false; formErrors.local_ip = false
formErrors.subnet_mask = false; formErrors.subnet_mask = false
formErrors.gateway_ip = false; formErrors.gateway_ip = false
formErrors.dns_1 = false; formErrors.dns_1 = false
formErrors.dns_2 = false; formErrors.dns_2 = false
} }
// Submit JSON to REST API // Submit JSON to REST API
if (valid) { if (valid) {
if (newNetwork) { if (newNetwork) {
dndNetworkList.push(networkEditable); dndNetworkList.push(networkEditable)
} else { } else {
dndNetworkList.splice(dndNetworkList.indexOf(networkEditable), 1, networkEditable); dndNetworkList.splice(dndNetworkList.indexOf(networkEditable), 1, networkEditable)
} }
addNetwork(); addNetwork()
dndNetworkList = [...dndNetworkList]; //Trigger reactivity dndNetworkList = [...dndNetworkList] //Trigger reactivity
showNetworkEditor = false; showNetworkEditor = false
} }
} }
function scanForNetworks() { function scanForNetworks() {
modals.open(ScanNetworks, { modals.open(ScanNetworks, {
storeNetwork: (network: string) => { storeNetwork: (network: string) => {
addNetwork(); addNetwork()
networkEditable.ssid = network; networkEditable.ssid = network
showNetworkEditor = true; showNetworkEditor = true
modals.close(); modals.close()
} }
}); })
} }
function addNetwork() { function addNetwork() {
newNetwork = true; newNetwork = true
networkEditable = { networkEditable = {
ssid: '', ssid: '',
password: '', password: '',
@@ -226,13 +225,13 @@
gateway_ip: undefined, gateway_ip: undefined,
dns_ip_1: undefined, dns_ip_1: undefined,
dns_ip_2: undefined dns_ip_2: undefined
}; }
} }
function handleEdit(index: number) { function handleEdit(index: number) {
newNetwork = false; newNetwork = false
showNetworkEditor = true; showNetworkEditor = true
networkEditable = dndNetworkList[index]; networkEditable = dndNetworkList[index]
} }
function confirmDelete(index: number) { function confirmDelete(index: number) {
@@ -246,15 +245,15 @@
onConfirm: () => { onConfirm: () => {
// Check if network is currently been edited and delete as well // Check if network is currently been edited and delete as well
if (dndNetworkList[index].ssid === networkEditable.ssid) { if (dndNetworkList[index].ssid === networkEditable.ssid) {
addNetwork(); addNetwork()
} }
// Remove network from array // Remove network from array
dndNetworkList.splice(index, 1); dndNetworkList.splice(index, 1)
dndNetworkList = [...dndNetworkList]; //Trigger reactivity dndNetworkList = [...dndNetworkList] //Trigger reactivity
showNetworkEditor = false; showNetworkEditor = false
modals.close(); modals.close()
} }
}); })
} }
function checkNetworkList() { function checkNetworkList() {
@@ -265,20 +264,20 @@
'You have reached the maximum number of networks. Please delete one to add another.', 'You have reached the maximum number of networks. Please delete one to add another.',
dismiss: { label: 'OK', icon: Check }, dismiss: { label: 'OK', icon: Check },
onDismiss: () => modals.close() onDismiss: () => modals.close()
}); })
return false; return false
} else { } else {
return true; return true
} }
} }
function onDrop({ detail: { from, to } }: CustomEvent<DropEvent>) { function onDrop({ detail: { from, to } }: CustomEvent<DropEvent>) {
if (!to || from === to) { if (!to || from === to) {
return; return
} }
dndNetworkList = reorder(dndNetworkList, from.index, to.index); dndNetworkList = reorder(dndNetworkList, from.index, to.index)
console.log(dndNetworkList); console.log(dndNetworkList)
} }
</script> </script>
@@ -295,78 +294,32 @@
{:then nothing} {:then nothing}
<div <div
class="flex w-full flex-col space-y-1" class="flex w-full flex-col space-y-1"
transition:slide|local={{ duration: 300, easing: cubicOut }} transition:slide|local={{ duration: 300, easing: cubicOut }}>
> <StatusItem
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"> icon={AP}
<div title="Status"
class="mask mask-hexagon h-auto w-10 {wifiStatus.status === 3 ? variant={wifiStatus.status === 3 ? 'success' : 'error'}
'bg-success' description={wifiStatus.status === 3 ? 'Connected' : 'Inactive'} />
: 'bg-error'}"
>
<AP
class="h-auto w-full scale-75 {wifiStatus.status === 3 ?
'text-success-content'
: 'text-error-content'}"
/>
</div>
<div>
<div class="font-bold">Status</div>
<div class="text-sm opacity-75">
{wifiStatus.status === 3 ? 'Connected' : 'Inactive'}
</div>
</div>
</div>
{#if wifiStatus.status === 3} {#if wifiStatus.status === 3}
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"> <StatusItem icon={SSID} title="SSID" description={wifiStatus.ssid} />
<div class="mask mask-hexagon bg-primary h-auto w-10">
<SSID class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">SSID</div>
<div class="text-sm opacity-75">
{wifiStatus.ssid}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"> <StatusItem icon={Home} title="IP Address" description={wifiStatus.local_ip} />
<div class="mask mask-hexagon bg-primary h-auto w-10">
<Home class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">IP Address</div>
<div class="text-sm opacity-75">
{wifiStatus.local_ip}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"> <StatusItem icon={WiFi} title="RSSI" description={`${wifiStatus.rssi} dBm`}>
<div class="mask mask-hexagon bg-primary h-auto w-10">
<WiFi class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">RSSI</div>
<div class="text-sm opacity-75">
{wifiStatus.rssi} dBm
</div>
</div>
<div class="grow"></div>
<button <button
class="btn btn-circle btn-ghost btn-sm modal-button" class="btn btn-circle btn-ghost btn-sm modal-button"
onclick={() => { onclick={() => {
showWifiDetails = !showWifiDetails; showWifiDetails = !showWifiDetails
}} }}>
>
<Down <Down
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {( class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
showWifiDetails showWifiDetails
) ? ) ?
'rotate-180' 'rotate-180'
: ''}" : ''}" />
/>
</button> </button>
</div> </StatusItem>
{/if} {/if}
</div> </div>
@@ -374,67 +327,16 @@
{#if showWifiDetails} {#if showWifiDetails}
<div <div
class="flex w-full flex-col space-y-1 pt-1" class="flex w-full flex-col space-y-1 pt-1"
transition:slide|local={{ duration: 300, easing: cubicOut }} transition:slide|local={{ duration: 300, easing: cubicOut }}>
> <StatusItem icon={MAC} title="MAC Address" description={wifiStatus.mac_address} />
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10">
<MAC class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">MAC Address</div>
<div class="text-sm opacity-75">
{wifiStatus.mac_address}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"> <StatusItem icon={Channel} title="Channel" description={wifiStatus.channel} />
<div class="mask mask-hexagon bg-primary h-auto w-10">
<Channel class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">Channel</div>
<div class="text-sm opacity-75">
{wifiStatus.channel}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"> <StatusItem icon={Gateway} title="Gateway IP" description={wifiStatus.gateway_ip} />
<div class="mask mask-hexagon bg-primary h-auto w-10">
<Gateway class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">Gateway IP</div>
<div class="text-sm opacity-75">
{wifiStatus.gateway_ip}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"> <StatusItem icon={Subnet} title="Subnet Mask" description={wifiStatus.subnet_mask} />
<div class="mask mask-hexagon bg-primary h-auto w-10">
<Subnet class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">Subnet Mask</div>
<div class="text-sm opacity-75">
{wifiStatus.subnet_mask}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"> <StatusItem icon={DNS} title="DNS" description={wifiStatus.dns_ip_1} />
<div class="mask mask-hexagon bg-primary h-auto w-10">
<DNS class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">DNS</div>
<div class="text-sm opacity-75">
{wifiStatus.dns_ip_1}
</div>
</div>
</div>
</div> </div>
{/if} {/if}
{/await} {/await}
@@ -442,8 +344,7 @@
<div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden"> <div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden">
<div <div
class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium" class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium">
>
Saved Networks Saved Networks
</div> </div>
{#await getWifiSettings()} {#await getWifiSettings()}
@@ -454,67 +355,48 @@
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-16" class="btn btn-primary text-primary-content btn-md absolute -top-14 right-16"
onclick={() => { onclick={() => {
if (checkNetworkList()) { if (checkNetworkList()) {
addNetwork(); addNetwork()
showNetworkEditor = true; showNetworkEditor = true
} }
}} }}>
> <Add class="h-6 w-6" /></button>
<Add class="h-6 w-6" /></button
>
<button <button
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0" class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0"
onclick={() => { onclick={() => {
if (checkNetworkList()) { if (checkNetworkList()) {
scanForNetworks(); scanForNetworks()
showNetworkEditor = true; showNetworkEditor = true
} }
}} }}>
> <Scan class="h-6 w-6" /></button>
<Scan class="h-6 w-6" /></button
>
<div <div
class="overflow-x-auto space-y-1" class="overflow-x-auto space-y-1"
transition:slide|local={{ duration: 300, easing: cubicOut }} transition:slide|local={{ duration: 300, easing: cubicOut }}>
>
<DragDropList <DragDropList
id="networks" id="networks"
type={VerticalDropZone} type={VerticalDropZone}
itemSize={60} itemSize={60}
itemCount={dndNetworkList.length} itemCount={dndNetworkList.length}
on:drop={onDrop} on:drop={onDrop}>
>
{#snippet children({ index })} {#snippet children({ index })}
<!-- svelte-ignore a11y_click_events_have_key_events --> <StatusItem icon={Router} title={dndNetworkList[index].ssid}>
<div
class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"
>
<div class="mask mask-hexagon bg-primary h-auto w-10 shrink-0">
<Router class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">{dndNetworkList[index].ssid}</div>
</div>
<div class="grow"></div>
<div class="space-x-0 px-0 mx-0"> <div class="space-x-0 px-0 mx-0">
<button <button
class="btn btn-ghost btn-sm" class="btn btn-ghost btn-sm"
onclick={() => { onclick={() => {
handleEdit(index); handleEdit(index)
}} }}>
> <Edit class="h-6 w-6" /></button>
<Edit class="h-6 w-6" /></button
>
<button <button
class="btn btn-ghost btn-sm" class="btn btn-ghost btn-sm"
onclick={() => { onclick={() => {
confirmDelete(index); confirmDelete(index)
}} }}>
>
<Delete class="text-error h-6 w-6" /> <Delete class="text-error h-6 w-6" />
</button> </button>
</div> </div>
</div> </StatusItem>
{/snippet} {/snippet}
</DragDropList> </DragDropList>
</div> </div>
@@ -523,8 +405,7 @@
<div class="divider mb-0"></div> <div class="divider mb-0"></div>
<div <div
class="flex flex-col gap-2 p-0" class="flex flex-col gap-2 p-0"
transition:slide|local={{ duration: 300, easing: cubicOut }} transition:slide|local={{ duration: 300, easing: cubicOut }}>
>
<form class="" onsubmit={validateWiFiForm} novalidate bind:this={formField}> <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 class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2">
<div> <div>
@@ -542,24 +423,17 @@
: ''}" : ''}"
bind:value={wifiSettings.hostname} bind:value={wifiSettings.hostname}
id="channel" id="channel"
required required />
/>
<label class="label" for="channel"> <label class="label" for="channel">
<span <span class="label-text-alt text-error {formErrorhostname ? '' : 'hidden'}"
class="label-text-alt text-error {formErrorhostname ? '' : ( >Host name must be between 2 and 32 characters long</span>
'hidden'
)}">Host name must be between 2 and 32 characters long</span
>
</label> </label>
</div> </div>
<label <label class="label inline-flex cursor-pointer content-end justify-start gap-4">
class="label inline-flex cursor-pointer content-end justify-start gap-4"
>
<input <input
type="checkbox" type="checkbox"
bind:checked={wifiSettings.priority_RSSI} bind:checked={wifiSettings.priority_RSSI}
class="checkbox checkbox-primary sm:-mb-5" class="checkbox checkbox-primary sm:-mb-5" />
/>
<span class="sm:-mb-5">Connect to strongest WiFi</span> <span class="sm:-mb-5">Connect to strongest WiFi</span>
</label> </label>
</div> </div>
@@ -568,8 +442,7 @@
<div class="divider my-0"></div> <div class="divider my-0"></div>
<div <div
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2" 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 }} transition:slide|local={{ duration: 300, easing: cubicOut }}>
>
<div> <div>
<label class="label" for="ssid"> <label class="label" for="ssid">
<span class="label-text text-md">SSID</span> <span class="label-text text-md">SSID</span>
@@ -585,14 +458,10 @@
id="ssid" id="ssid"
min="2" min="2"
max="32" max="32"
required required />
/>
<label class="label" for="ssid"> <label class="label" for="ssid">
<span <span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
class="label-text-alt text-error {formErrors.ssid ? '' : ( >SSID must be between 3 and 32 characters long</span>
'hidden'
)}">SSID must be between 3 and 32 characters long</span
>
</label> </label>
</div> </div>
<div> <div>
@@ -602,21 +471,18 @@
<PasswordInput bind:value={networkEditable.password} id="pwd" /> <PasswordInput bind:value={networkEditable.password} id="pwd" />
</div> </div>
<label <label
class="label inline-flex cursor-pointer content-end justify-start gap-4 mt-2 sm:mb-4" class="label inline-flex cursor-pointer content-end justify-start gap-4 mt-2 sm:mb-4">
>
<input <input
type="checkbox" type="checkbox"
bind:checked={static_ip_config} bind:checked={static_ip_config}
class="checkbox checkbox-primary sm:-mb-5" class="checkbox checkbox-primary sm:-mb-5" />
/>
<span class="sm:-mb-5">Static IP Config?</span> <span class="sm:-mb-5">Static IP Config?</span>
</label> </label>
</div> </div>
{#if static_ip_config} {#if static_ip_config}
<div <div
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2" 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 }} transition:slide|local={{ duration: 300, easing: cubicOut }}>
>
<div> <div>
<label class="label" for="localIP"> <label class="label" for="localIP">
<span class="label-text text-md">Local IP</span> <span class="label-text text-md">Local IP</span>
@@ -631,14 +497,10 @@
size="15" size="15"
bind:value={networkEditable.local_ip} bind:value={networkEditable.local_ip}
id="localIP" id="localIP"
required required />
/>
<label class="label" for="localIP"> <label class="label" for="localIP">
<span <span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
class="label-text-alt text-error {formErrors.local_ip ? >Must be a valid IPv4 address</span>
''
: 'hidden'}">Must be a valid IPv4 address</span
>
</label> </label>
</div> </div>
@@ -655,16 +517,10 @@
maxlength="15" maxlength="15"
size="15" size="15"
bind:value={networkEditable.gateway_ip} bind:value={networkEditable.gateway_ip}
required required />
/>
<label class="label" for="gateway"> <label class="label" for="gateway">
<span <span class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
class="label-text-alt text-error {( >Must be a valid IPv4 address</span>
formErrors.gateway_ip
) ?
''
: 'hidden'}">Must be a valid IPv4 address</span
>
</label> </label>
</div> </div>
<div> <div>
@@ -680,16 +536,10 @@
maxlength="15" maxlength="15"
size="15" size="15"
bind:value={networkEditable.subnet_mask} bind:value={networkEditable.subnet_mask}
required required />
/>
<label class="label" for="subnet"> <label class="label" for="subnet">
<span <span
class="label-text-alt text-error {( class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}">
formErrors.subnet_mask
) ?
''
: 'hidden'}"
>
Must be a valid IPv4 address Must be a valid IPv4 address
</span> </span>
</label> </label>
@@ -700,20 +550,15 @@
</label> </label>
<input <input
type="text" type="text"
class="input input-bordered w-full {formErrors.dns_1 ? class="input input-bordered w-full {formErrors.dns_1 ? 'border-error border-2'
'border-error border-2'
: ''}" : ''}"
minlength="7" minlength="7"
maxlength="15" maxlength="15"
size="15" size="15"
bind:value={networkEditable.dns_ip_1} bind:value={networkEditable.dns_ip_1}
required required />
/>
<label class="label" for="gateway"> <label class="label" for="gateway">
<span <span class="label-text-alt text-error {formErrors.dns_1 ? '' : 'hidden'}">
class="label-text-alt text-error {formErrors.dns_1 ? ''
: 'hidden'}"
>
Must be a valid IPv4 address Must be a valid IPv4 address
</span> </span>
</label> </label>
@@ -724,20 +569,15 @@
</label> </label>
<input <input
type="text" type="text"
class="input input-bordered w-full {formErrors.dns_2 ? class="input input-bordered w-full {formErrors.dns_2 ? 'border-error border-2'
'border-error border-2'
: ''}" : ''}"
minlength="7" minlength="7"
maxlength="15" maxlength="15"
size="15" size="15"
bind:value={networkEditable.dns_ip_2} bind:value={networkEditable.dns_ip_2}
required required />
/>
<label class="label" for="subnet"> <label class="label" for="subnet">
<span <span class="label-text-alt text-error {formErrors.dns_2 ? '' : 'hidden'}">
class="label-text-alt text-error {formErrors.dns_2 ? ''
: 'hidden'}"
>
Must be a valid IPv4 address Must be a valid IPv4 address
</span> </span>
</label> </label>