🔐 Removes auth from frontend
This commit is contained in:
@@ -8,12 +8,9 @@
|
||||
import '../app.css';
|
||||
import Menu from '../lib/components/menu/Menu.svelte';
|
||||
import Statusbar from '../lib/components/statusbar/statusbar.svelte';
|
||||
import Login from '../lib/components/login.svelte';
|
||||
import {
|
||||
telemetry,
|
||||
analytics,
|
||||
user,
|
||||
type UserProfile,
|
||||
ModesEnum,
|
||||
kinematicData,
|
||||
mode,
|
||||
@@ -21,21 +18,16 @@
|
||||
servoAngles,
|
||||
servoAnglesOut,
|
||||
socket,
|
||||
location
|
||||
location,
|
||||
useFeatureFlags
|
||||
} from '$lib/stores';
|
||||
import type { Analytics, Battery, DownloadOTA } from '$lib/types/models';
|
||||
import { api } from '$lib/api';
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags';
|
||||
|
||||
const features = useFeatureFlags();
|
||||
|
||||
onMount(async () => {
|
||||
if ($user.bearer_token !== '') {
|
||||
await validateUser($user);
|
||||
}
|
||||
const ws_token = $features.security ? '?access_token=' + $user.bearer_token : '';
|
||||
const ws = $location ? $location : window.location.host;
|
||||
socket.init(`ws://${ws}/ws/events${ws_token}`);
|
||||
socket.init(`ws://${ws}/ws/events`);
|
||||
|
||||
addEventListeners();
|
||||
|
||||
@@ -75,14 +67,6 @@
|
||||
socket.off('otastatus', handleOAT);
|
||||
};
|
||||
|
||||
async function validateUser(userdata: UserProfile) {
|
||||
const result = await api.get('/api/verifyAuthorization');
|
||||
if (result.isErr()) {
|
||||
user.invalidate();
|
||||
console.error('Error:', result.inner);
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpen = () => {
|
||||
notifications.success('Connection to device established', 5000);
|
||||
};
|
||||
@@ -109,25 +93,21 @@
|
||||
<title>{$page.data.title}</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if $features?.security && $user.bearer_token === ''}
|
||||
<Login />
|
||||
{:else}
|
||||
<div class="drawer">
|
||||
<input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} />
|
||||
<div class="drawer-content flex flex-col">
|
||||
<!-- Status bar content here -->
|
||||
<Statusbar />
|
||||
<div class="drawer">
|
||||
<input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} />
|
||||
<div class="drawer-content flex flex-col">
|
||||
<!-- Status bar content here -->
|
||||
<Statusbar />
|
||||
|
||||
<!-- Main page content here -->
|
||||
<slot />
|
||||
</div>
|
||||
<!-- Side Navigation -->
|
||||
<div class="drawer-side z-30 shadow-lg">
|
||||
<label for="main-menu" class="drawer-overlay" />
|
||||
<Menu on:menuClicked={() => (menuOpen = false)} />
|
||||
</div>
|
||||
<!-- Main page content here -->
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Side Navigation -->
|
||||
<div class="drawer-side z-30 shadow-lg">
|
||||
<label for="main-menu" class="drawer-overlay" />
|
||||
<Menu on:menuClicked={() => (menuOpen = false)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modals>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
<script lang="ts">
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { WiFi } from '$lib/components/icons';
|
||||
import { location, socket, useFeatureFlags, user } from '$lib/stores';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { WiFi } from '$lib/components/icons';
|
||||
import { location, socket, useFeatureFlags } from '$lib/stores';
|
||||
|
||||
const features = useFeatureFlags();
|
||||
const features = useFeatureFlags();
|
||||
|
||||
const update = () => {
|
||||
const ws_token = $features.security ? '?access_token=' + $user.bearer_token : '';
|
||||
const ws = $location ? $location : window.location.host;
|
||||
socket.init(`ws://${ws}/ws/events${ws_token}`);
|
||||
};
|
||||
const update = () => {
|
||||
const ws = $location ? $location : window.location.host;
|
||||
socket.init(`ws://${ws}/ws/events`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
<WiFi slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
<span slot="title">Connection</span>
|
||||
<WiFi slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
<span slot="title">Connection</span>
|
||||
|
||||
<div class="flex">
|
||||
<label class="label w-32" for="server">Address:</label>
|
||||
<input class="input" bind:value={$location} />
|
||||
</div>
|
||||
<div class="flex">
|
||||
<label class="label w-32" for="server">Address:</label>
|
||||
<input class="input" bind:value={$location} />
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" on:click={update}>Update</button>
|
||||
<button class="btn btn-primary" on:click={update}>Update</button>
|
||||
</SettingsCard>
|
||||
|
||||
@@ -1,260 +1,254 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import Collapsible from '$lib/components/Collapsible.svelte';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import { user, useFeatureFlags } from '$lib/stores';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import { TIME_ZONES } from './timezones';
|
||||
import type { NTPSettings, NTPStatus } from '$lib/types/models';
|
||||
import { api } from '$lib/api';
|
||||
import { NTP, UTC, Stopwatch, Clock, Server } from '$lib/components/icons';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import Collapsible from '$lib/components/Collapsible.svelte';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import { useFeatureFlags } from '$lib/stores';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import { TIME_ZONES } from './timezones';
|
||||
import type { NTPSettings, NTPStatus } from '$lib/types/models';
|
||||
import { api } from '$lib/api';
|
||||
import { NTP, UTC, Stopwatch, Clock, Server } from '$lib/components/icons';
|
||||
|
||||
const features = useFeatureFlags();
|
||||
|
||||
let ntpSettings: NTPSettings;
|
||||
let ntpStatus: NTPStatus;
|
||||
let ntpSettings: NTPSettings;
|
||||
let ntpStatus: NTPStatus;
|
||||
|
||||
async function getNTPStatus() {
|
||||
async function getNTPStatus() {
|
||||
const result = await api.get<NTPStatus>('/api/ntpStatus');
|
||||
if (result.isErr()){
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return
|
||||
return;
|
||||
}
|
||||
ntpStatus = result.inner
|
||||
}
|
||||
ntpStatus = result.inner;
|
||||
}
|
||||
|
||||
async function getNTPSettings() {
|
||||
async function getNTPSettings() {
|
||||
const result = await api.get<NTPSettings>('/api/ntpSettings');
|
||||
if (result.isErr()){
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return
|
||||
return;
|
||||
}
|
||||
ntpSettings = result.inner
|
||||
}
|
||||
ntpSettings = result.inner;
|
||||
}
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
getNTPStatus();
|
||||
}, 5000);
|
||||
const interval = setInterval(async () => getNTPStatus(), 5000);
|
||||
|
||||
onDestroy(() => clearInterval(interval));
|
||||
onDestroy(() => clearInterval(interval));
|
||||
|
||||
onMount(() => {
|
||||
if (!$features.security || $user.admin) {
|
||||
getNTPSettings();
|
||||
}
|
||||
});
|
||||
onMount(getNTPSettings);
|
||||
|
||||
let formField: any;
|
||||
let formField: any;
|
||||
|
||||
let formErrors = {
|
||||
server: false
|
||||
};
|
||||
let formErrors = {
|
||||
server: false
|
||||
};
|
||||
|
||||
async function postNTPSettings(data: NTPSettings) {
|
||||
async function postNTPSettings(data: NTPSettings) {
|
||||
const result = await api.post<NTPSettings>('/api/ntpSettings', data);
|
||||
if (result.isErr()){
|
||||
if (result.isErr()) {
|
||||
notifications.error('User not authorized.', 3000);
|
||||
console.error('Error:', result.inner);
|
||||
return
|
||||
return;
|
||||
}
|
||||
ntpSettings = result.inner
|
||||
}
|
||||
ntpSettings = result.inner;
|
||||
}
|
||||
|
||||
function handleSubmitNTP() {
|
||||
let valid = true;
|
||||
function handleSubmitNTP() {
|
||||
let valid = true;
|
||||
|
||||
// Validate Server
|
||||
// RegEx for IPv4
|
||||
const regexExpIPv4 =
|
||||
/\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/;
|
||||
const regexExpURL =
|
||||
/[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/i;
|
||||
// Validate Server
|
||||
// RegEx for IPv4
|
||||
const regexExpIPv4 =
|
||||
/\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/;
|
||||
const regexExpURL =
|
||||
/[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/i;
|
||||
|
||||
if (!regexExpURL.test(ntpSettings.server) && !regexExpIPv4.test(ntpSettings.server)) {
|
||||
valid = false;
|
||||
formErrors.server = true;
|
||||
} else {
|
||||
formErrors.server = false;
|
||||
}
|
||||
if (!regexExpURL.test(ntpSettings.server) && !regexExpIPv4.test(ntpSettings.server)) {
|
||||
valid = false;
|
||||
formErrors.server = true;
|
||||
} else {
|
||||
formErrors.server = false;
|
||||
}
|
||||
|
||||
ntpSettings.tz_format = TIME_ZONES[ntpSettings.tz_label];
|
||||
ntpSettings.tz_format = TIME_ZONES[ntpSettings.tz_label];
|
||||
|
||||
// Submit JSON to REST API
|
||||
if (valid) {
|
||||
postNTPSettings(ntpSettings);
|
||||
//alert('Form Valid');
|
||||
}
|
||||
}
|
||||
// Submit JSON to REST API
|
||||
if (valid) {
|
||||
postNTPSettings(ntpSettings);
|
||||
//alert('Form Valid');
|
||||
}
|
||||
}
|
||||
|
||||
function convertSeconds(seconds: number) {
|
||||
// Calculate the number of seconds, minutes, hours, and days
|
||||
let minutes = Math.floor(seconds / 60);
|
||||
let hours = Math.floor(minutes / 60);
|
||||
let days = Math.floor(hours / 24);
|
||||
function convertSeconds(seconds: number) {
|
||||
// Calculate the number of seconds, minutes, hours, and days
|
||||
let minutes = Math.floor(seconds / 60);
|
||||
let hours = Math.floor(minutes / 60);
|
||||
let days = Math.floor(hours / 24);
|
||||
|
||||
// Calculate the remaining hours, minutes, and seconds
|
||||
hours = hours % 24;
|
||||
minutes = minutes % 60;
|
||||
seconds = seconds % 60;
|
||||
// Calculate the remaining hours, minutes, and seconds
|
||||
hours = hours % 24;
|
||||
minutes = minutes % 60;
|
||||
seconds = seconds % 60;
|
||||
|
||||
// Create the formatted string
|
||||
let result = '';
|
||||
if (days > 0) {
|
||||
result += days + ' day' + (days > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
if (hours > 0) {
|
||||
result += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
if (minutes > 0) {
|
||||
result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
result += seconds + ' second' + (seconds > 1 ? 's' : '');
|
||||
// Create the formatted string
|
||||
let result = '';
|
||||
if (days > 0) {
|
||||
result += days + ' day' + (days > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
if (hours > 0) {
|
||||
result += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
if (minutes > 0) {
|
||||
result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
result += seconds + ' second' + (seconds > 1 ? 's' : '');
|
||||
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
<Clock slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
<span slot="title">Network Time</span>
|
||||
<div class="w-full overflow-x-auto">
|
||||
{#await getNTPStatus()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div
|
||||
class="mask mask-hexagon h-auto w-10 {ntpStatus.status === 1
|
||||
? 'bg-success'
|
||||
: 'bg-error'}"
|
||||
>
|
||||
<NTP
|
||||
class="h-auto w-full scale-75 {ntpStatus.status === 1
|
||||
? 'text-success-content'
|
||||
: 'text-error-content'}"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Status</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{ntpStatus.status === 1 ? 'Active' : 'Inactive'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Clock slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
<span slot="title">Network Time</span>
|
||||
<div class="w-full overflow-x-auto">
|
||||
{#await getNTPStatus()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div
|
||||
class="mask mask-hexagon h-auto w-10 {ntpStatus.status === 1 ?
|
||||
'bg-success'
|
||||
: 'bg-error'}"
|
||||
>
|
||||
<NTP
|
||||
class="h-auto w-full scale-75 {ntpStatus.status === 1 ?
|
||||
'text-success-content'
|
||||
: 'text-error-content'}"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Status</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{ntpStatus.status === 1 ? 'Active' : 'Inactive'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<Server class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">NTP Server</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{ntpStatus.server}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<Server class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">NTP Server</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{ntpStatus.server}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<Clock class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Local Time</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{new Intl.DateTimeFormat('en-GB', {
|
||||
dateStyle: 'long',
|
||||
timeStyle: 'long'
|
||||
}).format(new Date(ntpStatus.local_time))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<Clock class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Local Time</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{new Intl.DateTimeFormat('en-GB', {
|
||||
dateStyle: 'long',
|
||||
timeStyle: 'long'
|
||||
}).format(new Date(ntpStatus.local_time))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<UTC class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">UTC Time</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{new Intl.DateTimeFormat('en-GB', {
|
||||
dateStyle: 'long',
|
||||
timeStyle: 'long',
|
||||
timeZone: 'UTC'
|
||||
}).format(new Date(ntpStatus.utc_time))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<UTC class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">UTC Time</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{new Intl.DateTimeFormat('en-GB', {
|
||||
dateStyle: 'long',
|
||||
timeStyle: 'long',
|
||||
timeZone: 'UTC'
|
||||
}).format(new Date(ntpStatus.utc_time))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<Stopwatch class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Uptime</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{convertSeconds(ntpStatus.uptime)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
<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">
|
||||
<Stopwatch class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Uptime</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{convertSeconds(ntpStatus.uptime)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
{#if !$features.security || $user.admin}
|
||||
<Collapsible open={false} on:closed={getNTPSettings}>
|
||||
<span slot="title">Change NTP Settings</span>
|
||||
<form
|
||||
class="form-control w-full"
|
||||
on:submit|preventDefault={handleSubmitNTP}
|
||||
novalidate
|
||||
bind:this={formField}
|
||||
>
|
||||
<label class="label cursor-pointer justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={ntpSettings.enabled}
|
||||
class="checkbox checkbox-primary"
|
||||
/>
|
||||
<span class="">Enable NTP</span>
|
||||
</label>
|
||||
<label class="label" for="server">
|
||||
<span class="label-text text-md">Server</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
min="3"
|
||||
max="64"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {formErrors.server
|
||||
? 'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={ntpSettings.server}
|
||||
id="server"
|
||||
required
|
||||
/>
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text-alt text-error {formErrors.server ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address or URL</span
|
||||
>
|
||||
</label>
|
||||
<label class="label" for="tz">
|
||||
<span class="label-text text-md">Pick Time Zone</span>
|
||||
</label>
|
||||
<select class="select select-bordered" bind:value={ntpSettings.tz_label} id="tz">
|
||||
{#each Object.entries(TIME_ZONES) as [tz_label, tz_format]}
|
||||
<option value={tz_label}>{tz_label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<Collapsible open={false} on:closed={getNTPSettings}>
|
||||
<span slot="title">Change NTP Settings</span>
|
||||
<form
|
||||
class="form-control w-full"
|
||||
on:submit|preventDefault={handleSubmitNTP}
|
||||
novalidate
|
||||
bind:this={formField}
|
||||
>
|
||||
<label class="label cursor-pointer justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={ntpSettings.enabled}
|
||||
class="checkbox checkbox-primary"
|
||||
/>
|
||||
<span class="">Enable NTP</span>
|
||||
</label>
|
||||
<label class="label" for="server">
|
||||
<span class="label-text text-md">Server</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
min="3"
|
||||
max="64"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.server
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={ntpSettings.server}
|
||||
id="server"
|
||||
required
|
||||
/>
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text-alt text-error {formErrors.server ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address or URL</span
|
||||
>
|
||||
</label>
|
||||
<label class="label" for="tz">
|
||||
<span class="label-text text-md">Pick Time Zone</span>
|
||||
</label>
|
||||
<select class="select select-bordered" bind:value={ntpSettings.tz_label} id="tz">
|
||||
{#each Object.entries(TIME_ZONES) as [tz_label, tz_format]}
|
||||
<option value={tz_label}>{tz_label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<div class="mt-6 place-self-end">
|
||||
<button class="btn btn-primary" type="submit">Apply Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</Collapsible>
|
||||
{/if}
|
||||
<div class="mt-6 place-self-end">
|
||||
<button class="btn btn-primary" type="submit">Apply Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</Collapsible>
|
||||
</SettingsCard>
|
||||
|
||||
@@ -1,319 +1,323 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { openModal, closeModal } from 'svelte-modals';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { page } from '$app/stores';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { openModal, closeModal } from 'svelte-modals';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
|
||||
import type { SystemInformation, Analytics } from '$lib/types/models';
|
||||
import { socket } from '$lib/stores/socket';
|
||||
import { api } from '$lib/api';
|
||||
import { convertSeconds } from '$lib/utilities';
|
||||
import type { SystemInformation, Analytics } from '$lib/types/models';
|
||||
import { socket } from '$lib/stores/socket';
|
||||
import { api } from '$lib/api';
|
||||
import { convertSeconds } from '$lib/utilities';
|
||||
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags';
|
||||
import {
|
||||
Cancel,
|
||||
Power,
|
||||
FactoryReset,
|
||||
Sleep,
|
||||
Health,
|
||||
CPU,
|
||||
SDK,
|
||||
CPP,
|
||||
Speed,
|
||||
Heap,
|
||||
Pyramid,
|
||||
Sketch,
|
||||
Flash,
|
||||
Folder,
|
||||
Temperature,
|
||||
Stopwatch
|
||||
} from '$lib/components/icons';
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags';
|
||||
import {
|
||||
Cancel,
|
||||
Power,
|
||||
FactoryReset,
|
||||
Sleep,
|
||||
Health,
|
||||
CPU,
|
||||
SDK,
|
||||
CPP,
|
||||
Speed,
|
||||
Heap,
|
||||
Pyramid,
|
||||
Sketch,
|
||||
Flash,
|
||||
Folder,
|
||||
Temperature,
|
||||
Stopwatch
|
||||
} from '$lib/components/icons';
|
||||
|
||||
const features = useFeatureFlags();
|
||||
const features = useFeatureFlags();
|
||||
|
||||
let systemInformation: SystemInformation;
|
||||
let systemInformation: SystemInformation;
|
||||
|
||||
async function getSystemStatus() {
|
||||
const result = await api.get<SystemInformation>('/api/systemStatus');
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
systemInformation = result.inner;
|
||||
return systemInformation;
|
||||
}
|
||||
async function getSystemStatus() {
|
||||
const result = await api.get<SystemInformation>('/api/systemStatus');
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
systemInformation = result.inner;
|
||||
return systemInformation;
|
||||
}
|
||||
|
||||
const postFactoryReset = async () => await api.post('/api/factoryReset');
|
||||
const postFactoryReset = async () => await api.post('/api/factoryReset');
|
||||
|
||||
const postSleep = async () => await api.post('api/sleep');
|
||||
const postSleep = async () => await api.post('api/sleep');
|
||||
|
||||
onMount(() => socket.on('analytics', handleSystemData));
|
||||
onMount(() => socket.on('analytics', handleSystemData));
|
||||
|
||||
onDestroy(() => socket.off('analytics', handleSystemData));
|
||||
onDestroy(() => socket.off('analytics', handleSystemData));
|
||||
|
||||
const handleSystemData = (data: Analytics) =>
|
||||
(systemInformation = { ...systemInformation, ...data });
|
||||
const handleSystemData = (data: Analytics) =>
|
||||
(systemInformation = { ...systemInformation, ...data });
|
||||
|
||||
const postRestart = async () => await api.post('/api/restart');
|
||||
const postRestart = async () => await api.post('/api/restart');
|
||||
|
||||
function confirmRestart() {
|
||||
openModal(ConfirmDialog, {
|
||||
title: 'Confirm Restart',
|
||||
message: 'Are you sure you want to restart the device?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Restart', icon: Power }
|
||||
},
|
||||
onConfirm: () => {
|
||||
closeModal();
|
||||
postRestart();
|
||||
}
|
||||
});
|
||||
}
|
||||
function confirmRestart() {
|
||||
openModal(ConfirmDialog, {
|
||||
title: 'Confirm Restart',
|
||||
message: 'Are you sure you want to restart the device?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Restart', icon: Power }
|
||||
},
|
||||
onConfirm: () => {
|
||||
closeModal();
|
||||
postRestart();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function confirmReset() {
|
||||
openModal(ConfirmDialog, {
|
||||
title: 'Confirm Factory Reset',
|
||||
message: 'Are you sure you want to reset the device to its factory defaults?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Factory Reset', icon: FactoryReset }
|
||||
},
|
||||
onConfirm: () => {
|
||||
closeModal();
|
||||
postFactoryReset();
|
||||
}
|
||||
});
|
||||
}
|
||||
function confirmReset() {
|
||||
openModal(ConfirmDialog, {
|
||||
title: 'Confirm Factory Reset',
|
||||
message: 'Are you sure you want to reset the device to its factory defaults?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Factory Reset', icon: FactoryReset }
|
||||
},
|
||||
onConfirm: () => {
|
||||
closeModal();
|
||||
postFactoryReset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function confirmSleep() {
|
||||
openModal(ConfirmDialog, {
|
||||
title: 'Confirm Going to Sleep',
|
||||
message: 'Are you sure you want to put the device into sleep?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Sleep', icon: Sleep }
|
||||
},
|
||||
onConfirm: () => {
|
||||
closeModal();
|
||||
postSleep();
|
||||
}
|
||||
});
|
||||
}
|
||||
function confirmSleep() {
|
||||
openModal(ConfirmDialog, {
|
||||
title: 'Confirm Going to Sleep',
|
||||
message: 'Are you sure you want to put the device into sleep?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Sleep', icon: Sleep }
|
||||
},
|
||||
onConfirm: () => {
|
||||
closeModal();
|
||||
postSleep();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
<Health slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
<span slot="title">System Status</span>
|
||||
<Health slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
<span slot="title">System Status</span>
|
||||
|
||||
<div class="w-full overflow-x-auto">
|
||||
{#await getSystemStatus()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<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">
|
||||
<CPU class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Chip</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.cpu_type} Rev {systemInformation.cpu_rev}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto">
|
||||
{#await getSystemStatus()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<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">
|
||||
<CPU class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Chip</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.cpu_type} Rev {systemInformation.cpu_rev}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<SDK class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">SDK Version</div>
|
||||
<div class="text-sm opacity-75">
|
||||
ESP-IDF {systemInformation.sdk_version} / Arduino {systemInformation.arduino_version}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<SDK class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">SDK Version</div>
|
||||
<div class="text-sm opacity-75">
|
||||
ESP-IDF {systemInformation.sdk_version} / Arduino {systemInformation.arduino_version}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<CPP class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Firmware Version</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.firmware_version}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<CPP class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Firmware Version</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.firmware_version}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<Speed class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">CPU Frequency</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.cpu_freq_mhz} MHz {systemInformation.cpu_cores == 2
|
||||
? 'Dual Core'
|
||||
: 'Single Core'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<Speed class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">CPU Frequency</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.cpu_freq_mhz} MHz {systemInformation.cpu_cores == 2 ?
|
||||
'Dual Core'
|
||||
: 'Single Core'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<Heap class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Heap (Free / Max Alloc)</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.free_heap.toLocaleString('en-US')} / {systemInformation.max_alloc_heap.toLocaleString(
|
||||
'en-US'
|
||||
)} bytes
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<Heap class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Heap (Free / Max Alloc)</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.free_heap.toLocaleString('en-US')} / {systemInformation.max_alloc_heap.toLocaleString(
|
||||
'en-US'
|
||||
)} bytes
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<Pyramid class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">PSRAM (Size / Free)</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.psram_size.toLocaleString('en-US')} / {systemInformation.psram_size.toLocaleString(
|
||||
'en-US'
|
||||
)} bytes
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<Pyramid class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">PSRAM (Size / Free)</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.psram_size.toLocaleString('en-US')} / {systemInformation.psram_size.toLocaleString(
|
||||
'en-US'
|
||||
)} bytes
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<Sketch class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Sketch (Used / Free)</div>
|
||||
<div class="flex flex-wrap justify-start gap-1 text-sm opacity-75">
|
||||
<span>
|
||||
{(
|
||||
(systemInformation.sketch_size / systemInformation.free_sketch_space) *
|
||||
100
|
||||
).toFixed(1)} % of
|
||||
{(systemInformation.free_sketch_space / 1000000).toLocaleString('en-US')} MB used
|
||||
</span>
|
||||
<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">
|
||||
<Sketch class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Sketch (Used / Free)</div>
|
||||
<div class="flex flex-wrap justify-start gap-1 text-sm opacity-75">
|
||||
<span>
|
||||
{(
|
||||
(systemInformation.sketch_size /
|
||||
systemInformation.free_sketch_space) *
|
||||
100
|
||||
).toFixed(1)} % of
|
||||
{(systemInformation.free_sketch_space / 1000000).toLocaleString(
|
||||
'en-US'
|
||||
)} MB used
|
||||
</span>
|
||||
|
||||
<span>
|
||||
({(
|
||||
(systemInformation.free_sketch_space - systemInformation.sketch_size) /
|
||||
1000000
|
||||
).toLocaleString('en-US')} MB free)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span>
|
||||
({(
|
||||
(systemInformation.free_sketch_space -
|
||||
systemInformation.sketch_size) /
|
||||
1000000
|
||||
).toLocaleString('en-US')} MB free)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<Flash class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Flash Chip (Size / Speed)</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{(systemInformation.flash_chip_size / 1000000).toLocaleString('en-US')} MB / {(
|
||||
systemInformation.flash_chip_speed / 1000000
|
||||
).toLocaleString('en-US')} MHz
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<Flash class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Flash Chip (Size / Speed)</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{(systemInformation.flash_chip_size / 1000000).toLocaleString('en-US')} MB
|
||||
/ {(systemInformation.flash_chip_speed / 1000000).toLocaleString(
|
||||
'en-US'
|
||||
)} MHz
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<Folder class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">File System (Used / Total)</div>
|
||||
<div class="flex flex-wrap justify-start gap-1 text-sm opacity-75">
|
||||
<span
|
||||
>{((systemInformation.fs_used / systemInformation.fs_total) * 100).toFixed(1)} % of {(
|
||||
systemInformation.fs_total / 1000000
|
||||
).toLocaleString('en-US')} MB used</span
|
||||
>
|
||||
<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">
|
||||
<Folder class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">File System (Used / Total)</div>
|
||||
<div class="flex flex-wrap justify-start gap-1 text-sm opacity-75">
|
||||
<span
|
||||
>{(
|
||||
(systemInformation.fs_used / systemInformation.fs_total) *
|
||||
100
|
||||
).toFixed(1)} % of {(
|
||||
systemInformation.fs_total / 1000000
|
||||
).toLocaleString('en-US')} MB used</span
|
||||
>
|
||||
|
||||
<span
|
||||
>({(
|
||||
(systemInformation.fs_total - systemInformation.fs_used) /
|
||||
1000000
|
||||
).toLocaleString('en-US')}
|
||||
MB free)</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
>({(
|
||||
(systemInformation.fs_total - systemInformation.fs_used) /
|
||||
1000000
|
||||
).toLocaleString('en-US')}
|
||||
MB free)</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<Temperature class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Core Temperature</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.core_temp == 53.33
|
||||
? 'NaN'
|
||||
: systemInformation.core_temp.toFixed(2) + ' °C'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<Temperature class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Core Temperature</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.core_temp == 53.33 ?
|
||||
'NaN'
|
||||
: systemInformation.core_temp.toFixed(2) + ' °C'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<Stopwatch class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Uptime</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{convertSeconds(systemInformation.uptime)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<Stopwatch class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Uptime</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{convertSeconds(systemInformation.uptime)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<Power class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Reset Reason</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.cpu_reset_reason}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
<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">
|
||||
<Power class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Reset Reason</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{systemInformation.cpu_reset_reason}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-wrap justify-end gap-2">
|
||||
{#if $features.sleep}
|
||||
<button class="btn btn-primary inline-flex items-center" on:click={confirmSleep}
|
||||
><Sleep class="mr-2 h-5 w-5" /><span>Sleep</span></button
|
||||
>
|
||||
{/if}
|
||||
{#if !$features.security || $user.admin}
|
||||
<button class="btn btn-primary inline-flex items-center" on:click={confirmRestart}
|
||||
><Power class="mr-2 h-5 w-5" /><span>Restart</span></button
|
||||
>
|
||||
<button class="btn btn-secondary inline-flex items-center" on:click={confirmReset}
|
||||
><FactoryReset class="mr-2 h-5 w-5" /><span>Factory Reset</span></button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-4 flex flex-wrap justify-end gap-2">
|
||||
{#if $features.sleep}
|
||||
<button class="btn btn-primary inline-flex items-center" on:click={confirmSleep}>
|
||||
<Sleep class="mr-2 h-5 w-5" /><span>Sleep</span>
|
||||
</button>
|
||||
{/if}
|
||||
<button class="btn btn-primary inline-flex items-center" on:click={confirmRestart}>
|
||||
<Power class="mr-2 h-5 w-5" /><span>Restart</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary inline-flex items-center" on:click={confirmReset}>
|
||||
<FactoryReset class="mr-2 h-5 w-5" /><span>Factory Reset</span>
|
||||
</button>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<script lang="ts">
|
||||
import UploadFirmware from './UploadFirmware.svelte';
|
||||
import GithubFirmwareManager from './GithubFirmwareManager.svelte';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { useFeatureFlags } from '$lib/stores';
|
||||
import UploadFirmware from './UploadFirmware.svelte';
|
||||
import GithubFirmwareManager from './GithubFirmwareManager.svelte';
|
||||
import { useFeatureFlags } from '$lib/stores';
|
||||
|
||||
const features = useFeatureFlags();
|
||||
</script>
|
||||
|
||||
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
|
||||
{#if $features.download_firmware && (!$features.security || $user.admin)}
|
||||
<GithubFirmwareManager />
|
||||
{/if}
|
||||
{#if $features.download_firmware}
|
||||
<GithubFirmwareManager />
|
||||
{/if}
|
||||
|
||||
{#if $features.upload_firmware && (!$features.security || $user.admin)}
|
||||
<UploadFirmware />
|
||||
{/if}
|
||||
{#if $features.upload_firmware}
|
||||
<UploadFirmware />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { openModal, closeModal } from 'svelte-modals';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import { PasswordInput } from '$lib/components/input';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import EditUser from './EditUser.svelte';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
|
||||
import { api } from '$lib/api';
|
||||
import {
|
||||
Cancel,
|
||||
Check,
|
||||
Users,
|
||||
AddUser,
|
||||
Admin,
|
||||
Edit,
|
||||
Delete,
|
||||
Warning
|
||||
} from '$lib/components/icons';
|
||||
|
||||
type userSetting = {
|
||||
username: string;
|
||||
password: string;
|
||||
admin: boolean;
|
||||
};
|
||||
|
||||
type SecuritySettings = {
|
||||
jwt_secret: string;
|
||||
users: userSetting[];
|
||||
};
|
||||
|
||||
let securitySettings: SecuritySettings;
|
||||
|
||||
async function getSecuritySettings() {
|
||||
const result = await api.get<SecuritySettings>('/api/securitySettings');
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
securitySettings = result.inner;
|
||||
}
|
||||
|
||||
async function postSecuritySettings(data: SecuritySettings) {
|
||||
const result = await api.post<SecuritySettings>('/api/securitySettings', data);
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
notifications.error('User not authorized.', 3000);
|
||||
return;
|
||||
}
|
||||
securitySettings = result.inner;
|
||||
if (await validateUser()) {
|
||||
notifications.success('Security settings updated.', 3000);
|
||||
}
|
||||
}
|
||||
|
||||
async function validateUser() {
|
||||
const result = await api.get('/api/verifyAuthorization');
|
||||
if (result.isErr()) user.invalidate();
|
||||
return result.isOk();
|
||||
}
|
||||
|
||||
function confirmDelete(index: number) {
|
||||
openModal(ConfirmDialog, {
|
||||
title: 'Confirm Delete User',
|
||||
message:
|
||||
'Are you sure you want to delete the user "' +
|
||||
securitySettings.users[index].username +
|
||||
'"?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Yes', icon: Check }
|
||||
},
|
||||
onConfirm: () => {
|
||||
securitySettings.users.splice(index, 1);
|
||||
securitySettings = securitySettings;
|
||||
closeModal();
|
||||
postSecuritySettings(securitySettings);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleEdit(index: number) {
|
||||
openModal(EditUser, {
|
||||
title: 'Edit User',
|
||||
user: { ...securitySettings.users[index] }, // Shallow Copy
|
||||
onSaveUser: (editedUser: userSetting) => {
|
||||
securitySettings.users[index] = editedUser;
|
||||
closeModal();
|
||||
postSecuritySettings(securitySettings);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleNewUser() {
|
||||
openModal(EditUser, {
|
||||
title: 'Add User',
|
||||
onSaveUser: (newUser: userSetting) => {
|
||||
securitySettings.users = [...securitySettings.users, newUser];
|
||||
closeModal();
|
||||
postSecuritySettings(securitySettings);
|
||||
}
|
||||
});
|
||||
//
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $user.admin}
|
||||
<div
|
||||
class="mx-0 my-1 flex flex-col space-y-4
|
||||
sm:mx-8 sm:my-8"
|
||||
>
|
||||
<SettingsCard collapsible={false}>
|
||||
<Users slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
<span slot="title">Manage Users</span>
|
||||
{#await getSecuritySettings()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div class="relative w-full overflow-visible">
|
||||
<button
|
||||
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0"
|
||||
on:click={handleNewUser}
|
||||
>
|
||||
<AddUser class="h-6 w-6" /></button
|
||||
>
|
||||
|
||||
<div class="overflow-x-auto" transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<table class="table w-full table-auto">
|
||||
<thead>
|
||||
<tr class="font-bold">
|
||||
<th align="left">Username</th>
|
||||
<th align="center">Admin</th>
|
||||
<th align="right" class="pr-8">Edit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each securitySettings.users as user, index}
|
||||
<tr>
|
||||
<td align="left">{user.username}</td>
|
||||
<td align="center">
|
||||
{#if user.admin}
|
||||
<Admin class="text-secondary" />
|
||||
{/if}
|
||||
</td>
|
||||
<td align="right">
|
||||
<span class="my-auto inline-flex flex-row space-x-2">
|
||||
<button
|
||||
class="btn btn-ghost btn-circle btn-xs"
|
||||
on:click={() => handleEdit(index)}
|
||||
>
|
||||
<Edit class="h-6 w-6" /></button
|
||||
>
|
||||
<button
|
||||
class="btn btn-ghost btn-circle btn-xs"
|
||||
on:click={() => confirmDelete(index)}
|
||||
>
|
||||
<Delete class="text-error h-6 w-6" />
|
||||
</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider mb-0" />
|
||||
|
||||
<span class="pb-2 text-xl font-medium">Security Settings</span>
|
||||
<div class="alert alert-warning shadow-lg">
|
||||
<Warning class="h-6 w-6 flex-shrink-0" />
|
||||
<span
|
||||
>The JWT secret is used to sign authentication tokens. If you modify the JWT Secret, all
|
||||
users will be signed out.</span
|
||||
>
|
||||
</div>
|
||||
<label class="label" for="secret">
|
||||
<span class="label-text text-md">JWT Secret</span>
|
||||
</label>
|
||||
<PasswordInput bind:value={securitySettings.jwt_secret} id="secret" />
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button class="btn btn-primary" on:click={() => postSecuritySettings(securitySettings)}
|
||||
>Apply Settings</button
|
||||
>
|
||||
</div>
|
||||
{/await}
|
||||
</SettingsCard>
|
||||
</div>
|
||||
{:else}
|
||||
{goto('/')}
|
||||
{/if}
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
return {
|
||||
title: 'Users'
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
@@ -1,103 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { closeModal } from 'svelte-modals';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { PasswordInput } from '$lib/components/input';
|
||||
import { Cancel, Save } from '$lib/components/icons';
|
||||
|
||||
// provided by <Modals />
|
||||
export let isOpen: boolean;
|
||||
|
||||
export let title: string;
|
||||
export let onSaveUser: any; // Callback on Save
|
||||
export let user = {
|
||||
username: '',
|
||||
password: '',
|
||||
admin: false
|
||||
};
|
||||
|
||||
let errorUsername = false;
|
||||
|
||||
let usernameEditable = false;
|
||||
|
||||
onMount(() => {
|
||||
if (user.username == '') {
|
||||
usernameEditable = true;
|
||||
}
|
||||
});
|
||||
|
||||
function handleSave() {
|
||||
// Validate if username is within range
|
||||
if (user.username.length < 3 || user.username.length > 32) {
|
||||
errorUsername = true;
|
||||
} else {
|
||||
errorUsername = false;
|
||||
// Callback on saving
|
||||
onSaveUser(user);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
|
||||
transition:fly={{ y: 50 }}
|
||||
on:introstart
|
||||
on:outroend
|
||||
>
|
||||
<div
|
||||
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg md:w-[28rem]"
|
||||
>
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
|
||||
<div class="divider my-2" />
|
||||
<form
|
||||
class="form-control text-base-content mb-1 w-full"
|
||||
on:submit|preventDefault={handleSave}
|
||||
novalidate
|
||||
>
|
||||
<label class="label" for="username">
|
||||
<span class="label-text text-md">Username</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
min="3"
|
||||
max="32"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2"
|
||||
bind:value={user.username}
|
||||
id="username"
|
||||
disabled={!usernameEditable}
|
||||
/>
|
||||
<label for="username" class="label"
|
||||
><span class="label-text-alt text-error {errorUsername ? '' : 'hidden'}"
|
||||
>Username must be between 3 and 32 characters long</span
|
||||
></label
|
||||
>
|
||||
<label class="label" for="pwd">
|
||||
<span class="label-text text-md">Password</span>
|
||||
</label>
|
||||
<PasswordInput bind:value={user.password} id="pwd" />
|
||||
<label class="label my-auto cursor-pointer justify-start gap-4">
|
||||
<input type="checkbox" bind:checked={user.admin} class="checkbox checkbox-primary" />
|
||||
<span class="">Is Admin?</span>
|
||||
</label>
|
||||
<div class="divider my-2" />
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
class="btn btn-neutral text-neutral-content inline-flex items-center"
|
||||
on:click={closeModal}
|
||||
type="button"
|
||||
>
|
||||
<Cancel class="mr-2 h-5 w-5" /><span>Cancel</span></button
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary text-primary-content inline-flex items-center"
|
||||
type="submit"
|
||||
><Save class="mr-2 h-5 w-5" />
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,417 +1,430 @@
|
||||
<script lang="ts">
|
||||
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 { user } from '$lib/stores/user';
|
||||
import { page } from '$app/stores';
|
||||
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 { 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';
|
||||
|
||||
const features = useFeatureFlags();
|
||||
|
||||
let apSettings: ApSettings;
|
||||
let apStatus: ApStatus;
|
||||
let apSettings: ApSettings;
|
||||
let apStatus: ApStatus;
|
||||
|
||||
let formField: any;
|
||||
let formField: any;
|
||||
|
||||
async function getAPStatus() {
|
||||
async function getAPStatus() {
|
||||
const result = await api.get<ApStatus>('/api/apStatus');
|
||||
if (result.isErr()){
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return
|
||||
return;
|
||||
}
|
||||
apStatus = result.inner
|
||||
return apStatus;
|
||||
}
|
||||
apStatus = result.inner;
|
||||
return apStatus;
|
||||
}
|
||||
|
||||
async function getAPSettings() {
|
||||
const result = await api.get<ApSettings>('/api/apSetting');
|
||||
if (result.isErr()){
|
||||
async function getAPSettings() {
|
||||
const result = await api.get<ApSettings>('/api/apSetting');
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return
|
||||
return;
|
||||
}
|
||||
apSettings = result.inner
|
||||
return apSettings;
|
||||
}
|
||||
apSettings = result.inner;
|
||||
return apSettings;
|
||||
}
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
getAPStatus();
|
||||
}, 5000);
|
||||
const interval = setInterval(async () => {
|
||||
getAPStatus();
|
||||
}, 5000);
|
||||
|
||||
onDestroy(() => clearInterval(interval));
|
||||
onDestroy(() => clearInterval(interval));
|
||||
|
||||
onMount(() => {
|
||||
if (!$features.security || $user.admin) {
|
||||
getAPSettings();
|
||||
}
|
||||
});
|
||||
onMount(getAPSettings);
|
||||
|
||||
let provisionMode = [
|
||||
{
|
||||
id: 0,
|
||||
text: `Always`
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
text: `When WiFi Disconnected`
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
text: `Never`
|
||||
}
|
||||
];
|
||||
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 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 = {
|
||||
ssid: false,
|
||||
channel: false,
|
||||
max_clients: false,
|
||||
local_ip: false,
|
||||
gateway_ip: false,
|
||||
subnet_mask: false
|
||||
};
|
||||
let formErrors = {
|
||||
ssid: false,
|
||||
channel: false,
|
||||
max_clients: false,
|
||||
local_ip: false,
|
||||
gateway_ip: false,
|
||||
subnet_mask: false
|
||||
};
|
||||
|
||||
async function postAPSettings(data: ApSettings) {
|
||||
async function postAPSettings(data: ApSettings) {
|
||||
const result = await api.post<ApSettings>('/api/apSettings', data);
|
||||
if (result.isErr()){
|
||||
if (result.isErr()) {
|
||||
notifications.error('User not authorized.', 3000);
|
||||
console.error('Error:', result.inner);
|
||||
return
|
||||
return;
|
||||
}
|
||||
notifications.success('Access Point settings updated.', 3000);
|
||||
apSettings = result.inner
|
||||
}
|
||||
apSettings = result.inner;
|
||||
}
|
||||
|
||||
function handleSubmitAP() {
|
||||
let valid = true;
|
||||
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;
|
||||
}
|
||||
// Validate SSID
|
||||
if (apSettings.ssid.length < 3 || apSettings.ssid.length > 32) {
|
||||
valid = false;
|
||||
formErrors.ssid = true;
|
||||
} else {
|
||||
formErrors.ssid = false;
|
||||
}
|
||||
|
||||
// Validate Channel
|
||||
let channel = Number(apSettings.channel);
|
||||
if (1 > channel || channel > 13) {
|
||||
valid = false;
|
||||
formErrors.channel = true;
|
||||
} else {
|
||||
formErrors.channel = 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;
|
||||
}
|
||||
// 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/;
|
||||
// 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 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 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;
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
// Submit JSON to REST API
|
||||
if (valid) {
|
||||
postAPSettings(apSettings);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
<AP slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
<span slot="title">Access Point</span>
|
||||
<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 }}
|
||||
>
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div
|
||||
class="mask mask-hexagon h-auto w-10 {apStatusDescription[apStatus.status].bg_color}"
|
||||
>
|
||||
<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>
|
||||
<AP slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
<span slot="title">Access Point</span>
|
||||
<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 }}
|
||||
>
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div
|
||||
class="mask mask-hexagon h-auto w-10 {apStatusDescription[apStatus.status]
|
||||
.bg_color}"
|
||||
>
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
{/await}
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
{#if !$features.security || $user.admin}
|
||||
<div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden">
|
||||
<div
|
||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium"
|
||||
>
|
||||
Change AP Settings
|
||||
</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"
|
||||
on:submit|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
|
||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium"
|
||||
>
|
||||
Change AP Settings
|
||||
</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"
|
||||
on:submit|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>
|
||||
{/await}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="place-self-end">
|
||||
<button class="btn btn-primary" type="submit">Apply Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
+265
-284
@@ -5,8 +5,6 @@
|
||||
import { openModal, closeModal } from 'svelte-modals';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { page } from '$app/stores';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import DragDropList, { VerticalDropZone, reorder, type DropEvent } from 'svelte-dnd-list';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
@@ -441,342 +439,325 @@
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
{#if !$features.security || $user.admin}
|
||||
<div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden">
|
||||
<div
|
||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium"
|
||||
>
|
||||
Saved Networks
|
||||
</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"
|
||||
on:click={() => {
|
||||
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"
|
||||
on:click={() => {
|
||||
if (checkNetworkList()) {
|
||||
scanForNetworks();
|
||||
showNetworkEditor = true;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Scan class="h-6 w-6" /></button
|
||||
>
|
||||
<div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden">
|
||||
<div
|
||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium"
|
||||
>
|
||||
Saved Networks
|
||||
</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"
|
||||
on:click={() => {
|
||||
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"
|
||||
on:click={() => {
|
||||
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}
|
||||
let:index
|
||||
>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<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>
|
||||
{#if !$features.security || $user.admin}
|
||||
<div class="flex-grow" />
|
||||
<div class="space-x-0 px-0 mx-0">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
on:click={() => {
|
||||
handleEdit(index);
|
||||
}}
|
||||
>
|
||||
<Edit class="h-6 w-6" /></button
|
||||
>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
on:click={() => {
|
||||
confirmDelete(index);
|
||||
}}
|
||||
>
|
||||
<Delete class="text-error h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</DragDropList>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider mb-0" />
|
||||
<div
|
||||
class="flex flex-col gap-2 p-0"
|
||||
class="overflow-x-auto space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<form
|
||||
class=""
|
||||
on:submit|preventDefault={validateWiFiForm}
|
||||
novalidate
|
||||
bind:this={formField}
|
||||
<DragDropList
|
||||
id="networks"
|
||||
type={VerticalDropZone}
|
||||
itemSize={60}
|
||||
itemCount={dndNetworkList.length}
|
||||
on:drop={onDrop}
|
||||
let:index
|
||||
>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<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="flex-grow" />
|
||||
<div class="space-x-0 px-0 mx-0">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
on:click={() => {
|
||||
handleEdit(index);
|
||||
}}
|
||||
>
|
||||
<Edit class="h-6 w-6" /></button
|
||||
>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
on:click={() => {
|
||||
confirmDelete(index);
|
||||
}}
|
||||
>
|
||||
<Delete class="text-error h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DragDropList>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider mb-0" />
|
||||
<div
|
||||
class="flex flex-col gap-2 p-0"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<form
|
||||
class=""
|
||||
on:submit|preventDefault={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>
|
||||
|
||||
{#if showNetworkEditor}
|
||||
<div class="divider my-0" />
|
||||
<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="channel">
|
||||
<span class="label-text text-md">Host Name</span>
|
||||
<label class="label" for="ssid">
|
||||
<span class="label-text text-md">SSID</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
min="1"
|
||||
max="32"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrorhostname
|
||||
formErrors.ssid
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={wifiSettings.hostname}
|
||||
id="channel"
|
||||
bind:value={networkEditable.ssid}
|
||||
id="ssid"
|
||||
min="2"
|
||||
max="32"
|
||||
required
|
||||
/>
|
||||
<label class="label" for="channel">
|
||||
<label class="label" for="ssid">
|
||||
<span
|
||||
class="label-text-alt text-error {formErrorhostname ? '' : (
|
||||
class="label-text-alt text-error {formErrors.ssid ? '' : (
|
||||
'hidden'
|
||||
)}">Host name must be between 2 and 32 characters long</span
|
||||
)}">SSID must be between 3 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"
|
||||
class="label inline-flex cursor-pointer content-end justify-start gap-4 mt-2 sm:mb-4"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={wifiSettings.priority_RSSI}
|
||||
bind:checked={static_ip_config}
|
||||
class="checkbox checkbox-primary sm:-mb-5"
|
||||
/>
|
||||
<span class="sm:-mb-5">Connect to strongest WiFi</span>
|
||||
<span class="sm:-mb-5">Static IP Config?</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{#if showNetworkEditor}
|
||||
<div class="divider my-0" />
|
||||
{#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="ssid">
|
||||
<span class="label-text text-md">SSID</span>
|
||||
<label class="label" for="localIP">
|
||||
<span class="label-text text-md">Local IP</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.ssid
|
||||
) ?
|
||||
class="input input-bordered w-full {formErrors.local_ip ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={networkEditable.ssid}
|
||||
id="ssid"
|
||||
min="2"
|
||||
max="32"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.local_ip}
|
||||
id="localIP"
|
||||
required
|
||||
/>
|
||||
<label class="label" for="ssid">
|
||||
<label class="label" for="localIP">
|
||||
<span
|
||||
class="label-text-alt text-error {formErrors.ssid ? ''
|
||||
: 'hidden'}"
|
||||
>SSID must be between 3 and 32 characters long</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>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="pwd">
|
||||
<span class="label-text text-md">Password</span>
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text text-md">Subnet Mask</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"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={static_ip_config}
|
||||
class="checkbox checkbox-primary sm:-mb-5"
|
||||
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
|
||||
/>
|
||||
<span class="sm:-mb-5">Static IP Config?</span>
|
||||
</label>
|
||||
</div>
|
||||
{#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>
|
||||
<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 {(
|
||||
<label class="label" for="subnet">
|
||||
<span
|
||||
class="label-text-alt text-error {(
|
||||
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>
|
||||
''
|
||||
: 'hidden'}"
|
||||
>
|
||||
Must be a valid IPv4 address
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
<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 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"
|
||||
on:click={validateHostName}>Apply Settings</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="divider mb-2 mt-0" />
|
||||
<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" on:click={validateHostName}>
|
||||
Apply Settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
Reference in New Issue
Block a user