🧼 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()
+336 -408
View File
@@ -1,436 +1,364 @@
<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() {
const result = await api.get<ApSettings>('/api/wifi/ap/settings')
if (result.isErr()) {
console.error('Error:', result.inner)
return
}
apSettings = result.inner
return apSettings
}
const interval = setInterval(async () => {
getAPStatus()
}, 5000)
onDestroy(() => clearInterval(interval))
onMount(getAPSettings)
let provisionMode = [
{
id: 0,
text: `Always`
},
{
id: 1,
text: `When WiFi Disconnected`
},
{
id: 2,
text: `Never`
}
]
type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning'
let apStatusVariant: Variant[] = ['success', 'error', 'warning']
let apStatusDescription = ['Active', 'Inactive', 'Lingering']
let formErrors = $state({
ssid: false,
channel: false,
max_clients: false,
local_ip: false,
gateway_ip: false,
subnet_mask: false
})
async function postAPSettings(data: ApSettings) {
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.success('Access Point settings updated.', 3000)
apSettings = result.inner
}
function handleSubmitAP() {
let valid = true
// Validate SSID
if (apSettings.ssid.length < 3 || apSettings.ssid.length > 32) {
valid = false
formErrors.ssid = true
} else {
formErrors.ssid = false
} }
async function getAPSettings() { // Validate Channel
const result = await api.get<ApSettings>('/api/wifi/ap/settings'); let channel = Number(apSettings.channel)
if (result.isErr()) { if (1 > channel || channel > 13) {
console.error('Error:', result.inner); valid = false
return; formErrors.channel = true
} } else {
apSettings = result.inner; formErrors.channel = false
return apSettings;
} }
const interval = setInterval(async () => { // Validate max_clients
getAPStatus(); let maxClients = Number(apSettings.max_clients)
}, 5000); if (1 > maxClients || maxClients > 8) {
valid = false
onDestroy(() => clearInterval(interval)); formErrors.max_clients = true
} else {
onMount(getAPSettings); formErrors.max_clients = false
let provisionMode = [
{
id: 0,
text: `Always`
},
{
id: 1,
text: `When WiFi Disconnected`
},
{
id: 2,
text: `Never`
}
];
let apStatusDescription = [
{ bg_color: 'bg-success', text_color: 'text-success-content', description: 'Active' },
{ bg_color: 'bg-error', text_color: 'text-error-content', description: 'Inactive' },
{ bg_color: 'bg-warning', text_color: 'text-warning-content', description: 'Lingering' }
];
let formErrors = $state({
ssid: false,
channel: false,
max_clients: false,
local_ip: false,
gateway_ip: false,
subnet_mask: false
});
async function postAPSettings(data: ApSettings) {
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.success('Access Point settings updated.', 3000);
apSettings = result.inner;
} }
function handleSubmitAP() { // RegEx for IPv4
let valid = true; 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/
// Validate SSID // Validate gateway IP
if (apSettings.ssid.length < 3 || apSettings.ssid.length > 32) { if (!regexExp.test(apSettings.gateway_ip)) {
valid = false; valid = false
formErrors.ssid = true; formErrors.gateway_ip = true
} else { } else {
formErrors.ssid = false; formErrors.gateway_ip = false
}
// Validate Channel
let channel = Number(apSettings.channel);
if (1 > channel || channel > 13) {
valid = false;
formErrors.channel = true;
} else {
formErrors.channel = false;
}
// Validate max_clients
let maxClients = Number(apSettings.max_clients);
if (1 > maxClients || maxClients > 8) {
valid = false;
formErrors.max_clients = true;
} else {
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/;
// Validate gateway IP
if (!regexExp.test(apSettings.gateway_ip)) {
valid = false;
formErrors.gateway_ip = true;
} else {
formErrors.gateway_ip = false;
}
// Validate Subnet Mask
if (!regexExp.test(apSettings.subnet_mask)) {
valid = false;
formErrors.subnet_mask = true;
} else {
formErrors.subnet_mask = false;
}
// Validate local IP
if (!regexExp.test(apSettings.local_ip)) {
valid = false;
formErrors.local_ip = true;
} else {
formErrors.local_ip = false;
}
// Submit JSON to REST API
if (valid) {
postAPSettings(apSettings);
}
} }
// Validate Subnet Mask
if (!regexExp.test(apSettings.subnet_mask)) {
valid = false
formErrors.subnet_mask = true
} else {
formErrors.subnet_mask = false
}
// Validate local IP
if (!regexExp.test(apSettings.local_ip)) {
valid = false
formErrors.local_ip = true
} else {
formErrors.local_ip = false
}
// Submit JSON to REST API
if (valid) {
postAPSettings(apSettings)
}
}
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
{#snippet icon()} {#snippet icon()}
<AP class="lex-shrink-0 mr-2 h-6 w-6 self-end" /> <AP class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet} {/snippet}
{#snippet title()} {#snippet title()}
<span >Access Point</span> <span>Access Point</span>
{/snippet} {/snippet}
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
{#await getAPStatus()} {#await getAPStatus()}
<Spinner /> <Spinner />
{: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"> </div>
<Devices class="text-primary-content h-auto w-full scale-75" /> {/await}
</div> </div>
<div>
<div class="font-bold">AP Clients</div> <div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden">
<div class="text-sm opacity-75"> <div
{apStatus.station_num} class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium">
</div> Change AP Settings
</div>
</div>
</div>
{/await}
</div> </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>
<div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden"> <div>
<div <label class="label" for="pwd">
class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium" <span class="label-text text-md">Password</span>
> </label>
Change AP Settings <PasswordInput bind:value={apSettings.password} id="pwd" />
</div> </div>
{#await getAPSettings()} <div>
<Spinner /> <label class="label" for="channel">
{:then nothing} <span class="label-text text-md">Preferred Channel</span>
<div </label>
class="flex flex-col gap-2 p-0" <input
transition:slide|local={{ duration: 300, easing: cubicOut }} type="number"
> min="1"
<form max="13"
class="grid w-full grid-cols-1 content-center gap-x-4 p-0s sm:grid-cols-2" class="input input-bordered invalid:border-error w-full invalid:border-2 {(
onsubmit={preventDefault(handleSubmitAP)} formErrors.channel
novalidate ) ?
bind:this={formField} 'border-error border-2'
> : ''}"
<div> bind:value={apSettings.channel}
<label class="label" for="apmode"> id="channel"
<span class="label-text">Provide Access Point ...</span> required />
</label> <label class="label" for="channel">
<select <span class="label-text-alt text-error {formErrors.channel ? '' : 'hidden'}"
class="select select-bordered w-full" >Must be channel 1 to 13</span>
id="apmode" </label>
bind:value={apSettings.provision_mode} </div>
>
{#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> <div>
<label class="label" for="pwd"> <label class="label" for="clients">
<span class="label-text text-md">Password</span> <span class="label-text text-md">Max Clients</span>
</label> </label>
<PasswordInput bind:value={apSettings.password} id="pwd" /> <input
</div> type="number"
<div> min="1"
<label class="label" for="channel"> max="8"
<span class="label-text text-md">Preferred Channel</span> class="input input-bordered invalid:border-error w-full invalid:border-2 {(
</label> formErrors.max_clients
<input ) ?
type="number" 'border-error border-2'
min="1" : ''}"
max="13" bind:value={apSettings.max_clients}
class="input input-bordered invalid:border-error w-full invalid:border-2 {( id="clients"
formErrors.channel required />
) ? <label class="label" for="clients">
'border-error border-2' <span class="label-text-alt text-error {formErrors.max_clients ? '' : 'hidden'}"
: ''}" >Maximum 8 clients allowed</span>
bind:value={apSettings.channel} </label>
id="channel" </div>
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> <div>
<label class="label" for="clients"> <label class="label" for="localIP">
<span class="label-text text-md">Max Clients</span> <span class="label-text text-md">Local IP</span>
</label> </label>
<input <input
type="number" type="text"
min="1" class="input input-bordered w-full {formErrors.local_ip ? 'border-error border-2' : (
max="8" ''
class="input input-bordered invalid:border-error w-full invalid:border-2 {( )}"
formErrors.max_clients minlength="7"
) ? maxlength="15"
'border-error border-2' size="15"
: ''}" bind:value={apSettings.local_ip}
bind:value={apSettings.max_clients} id="localIP"
id="clients" required />
required <label class="label" for="localIP">
/> <span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
<label class="label" for="clients"> >Must be a valid IPv4 address</span>
<span </label>
class="label-text-alt text-error {formErrors.max_clients ? '' : ( </div>
'hidden'
)}">Maximum 8 clients allowed</span
>
</label>
</div>
<div> <div>
<label class="label" for="localIP"> <label class="label" for="gateway">
<span class="label-text text-md">Local IP</span> <span class="label-text text-md">Gateway IP</span>
</label> </label>
<input <input
type="text" type="text"
class="input input-bordered w-full {formErrors.local_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.local_ip} id="gateway"
id="localIP" required />
required <label class="label" for="gateway">
/> <span class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
<label class="label" for="localIP"> >Must be a valid IPv4 address</span>
<span </label>
class="label-text-alt text-error {formErrors.local_ip ? '' : ( </div>
'hidden' <div>
)}">Must be a valid IPv4 address</span <label class="label" for="subnet">
> <span class="label-text text-md">Subnet Mask</span>
</label> </label>
</div> <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 my-auto cursor-pointer justify-start gap-4">
<label class="label" for="gateway"> <input
<span class="label-text text-md">Gateway IP</span> type="checkbox"
</label> bind:checked={apSettings.ssid_hidden}
<input class="checkbox checkbox-primary" />
type="text" <span class="">Hide SSID</span>
class="input input-bordered w-full {formErrors.gateway_ip ? </label>
'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"> <div class="place-self-end">
<input <button class="btn btn-primary" type="submit">Apply Settings</button>
type="checkbox" </div>
bind:checked={apSettings.ssid_hidden} </form>
class="checkbox checkbox-primary" </div>
/> {/await}
<span class="">Hide SSID</span> </div>
</label>
<div class="place-self-end">
<button class="btn btn-primary" type="submit">Apply Settings</button>
</div>
</form>
</div>
{/await}
</div>
</SettingsCard> </SettingsCard>
File diff suppressed because it is too large Load Diff