🔐 Removes auth from frontend
This commit is contained in:
Generated
+2528
-2059
File diff suppressed because it is too large
Load Diff
+56
-57
@@ -1,81 +1,80 @@
|
|||||||
import { user } from '$lib/stores/user';
|
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { Err, Ok, type Result } from './utilities';
|
import { Err, Ok, type Result } from './utilities';
|
||||||
import { location } from './stores';
|
import { location } from './stores';
|
||||||
|
|
||||||
export namespace api {
|
export namespace api {
|
||||||
export function get<TResponse>(endpoint: string, params?: RequestInit) {
|
export function get<TResponse>(endpoint: string, params?: RequestInit) {
|
||||||
return sendRequest<TResponse>(endpoint, 'GET', null, params);
|
return sendRequest<TResponse>(endpoint, 'GET', null, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function post<TResponse>(endpoint: string, data?: unknown) {
|
export function post<TResponse>(endpoint: string, data?: unknown) {
|
||||||
return sendRequest<TResponse>(endpoint, 'POST', data);
|
return sendRequest<TResponse>(endpoint, 'POST', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function put<TResponse>(endpoint: string, data?: unknown) {
|
export function put<TResponse>(endpoint: string, data?: unknown) {
|
||||||
return sendRequest<TResponse>(endpoint, 'PUT', data);
|
return sendRequest<TResponse>(endpoint, 'PUT', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function remove<TResponse>(endpoint: string) {
|
export function remove<TResponse>(endpoint: string) {
|
||||||
return sendRequest<TResponse>(endpoint, 'DELETE');
|
return sendRequest<TResponse>(endpoint, 'DELETE');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendRequest<TResponse>(
|
async function sendRequest<TResponse>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
method: string,
|
method: string,
|
||||||
data?: unknown,
|
data?: unknown,
|
||||||
params?: RequestInit
|
params?: RequestInit
|
||||||
): Promise<Result<TResponse, Error>> {
|
): Promise<Result<TResponse, Error>> {
|
||||||
endpoint = resolveUrl(endpoint);
|
endpoint = resolveUrl(endpoint);
|
||||||
const user_token = get(user).bearer_token;
|
const body = data !== null && typeof data !== 'undefined' ? JSON.stringify(data) : undefined;
|
||||||
const body = data !== null && typeof data !== 'undefined' ? JSON.stringify(data) : undefined;
|
|
||||||
|
|
||||||
const request = {
|
const request = {
|
||||||
...params,
|
...params,
|
||||||
method,
|
method,
|
||||||
body,
|
body,
|
||||||
headers: {
|
headers: {
|
||||||
...params?.headers,
|
...params?.headers,
|
||||||
Authorization: user_token ? 'Bearer ' + user_token : 'Basic',
|
Authorization: 'Basic',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
response = await fetch(endpoint, request);
|
response = await fetch(endpoint, request);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Err.new(new Error(), 'An error has occurred');
|
return Err.new(new Error(), 'An error has occurred');
|
||||||
}
|
}
|
||||||
|
|
||||||
const isResponseOk = response.status >= 200 && response.status < 400;
|
const isResponseOk = response.status >= 200 && response.status < 400;
|
||||||
if (!isResponseOk) {
|
if (!isResponseOk) {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
return Err.new(new ApiError(response), 'User was not authorized');
|
return Err.new(new ApiError(response), 'User was not authorized');
|
||||||
}
|
}
|
||||||
return Err.new(new ApiError(response), 'An error has occurred');
|
return Err.new(new ApiError(response), 'An error has occurred');
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentType = response.headers.get('Content-Type') ?? response.headers.get('Content-Type');
|
const contentType =
|
||||||
if (contentType && contentType.includes('application/json')) {
|
response.headers.get('Content-Type') ?? response.headers.get('Content-Type');
|
||||||
const data = await response.json();
|
if (contentType && contentType.includes('application/json')) {
|
||||||
return Ok.new(data as TResponse);
|
const data = await response.json();
|
||||||
} else {
|
return Ok.new(data as TResponse);
|
||||||
// Handle empty object as response
|
} else {
|
||||||
return Ok.new(null as TResponse);
|
// Handle empty object as response
|
||||||
}
|
return Ok.new(null as TResponse);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveUrl(url: string): string {
|
function resolveUrl(url: string): string {
|
||||||
if (url.startsWith('http') || !get(location)) return url;
|
if (url.startsWith('http') || !get(location)) return url;
|
||||||
const protocol = window.location.protocol;
|
const protocol = window.location.protocol;
|
||||||
return `${protocol}//${get(location)}${url.startsWith('/') ? '' : '/'}${url}`;
|
return `${protocol}//${get(location)}${url.startsWith('/') ? '' : '/'}${url}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ApiError extends Error {
|
export class ApiError extends Error {
|
||||||
constructor(public readonly response: Response) {
|
constructor(public readonly response: Response) {
|
||||||
super(`${response.status}`);
|
super(`${response.status}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import { user, location } from '$lib/stores';
|
import { location } from '$lib/stores';
|
||||||
|
|
||||||
let source = `${$location}/api/camera/stream?access_token=${$user.bearer_token}`;
|
let source = `${$location}/api/camera/stream`;
|
||||||
|
|
||||||
onDestroy(() => (source = '#'));
|
onDestroy(() => (source = '#'));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full h-full">
|
<div class="w-full h-full">
|
||||||
<img
|
<img
|
||||||
src={source}
|
src={source}
|
||||||
class="absolute object-cover blur-3xl w-full h-full -z-10"
|
class="absolute object-cover blur-3xl w-full h-full -z-10"
|
||||||
alt="Live stream is down"
|
alt="Live stream is down"
|
||||||
/>
|
/>
|
||||||
<img src={source} class="object-contain w-full h-full" alt="Live stream is down" />
|
<img src={source} class="object-contain w-full h-full" alt="Live stream is down" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import logo from '$lib/assets/logo512.png';
|
|
||||||
import { PasswordInput } from '$lib/components/input';
|
|
||||||
import { user } from '$lib/stores/user';
|
|
||||||
import { notifications } from '$lib/components/toasts/notifications';
|
|
||||||
import { fade, fly } from 'svelte/transition';
|
|
||||||
import { api } from '$lib/api';
|
|
||||||
import type { JWT } from '$lib/types/models';
|
|
||||||
import { Login } from './icons';
|
|
||||||
|
|
||||||
type SignInData = {
|
|
||||||
password: string;
|
|
||||||
username: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
let username = '';
|
|
||||||
let password = '';
|
|
||||||
|
|
||||||
let loginFailed = false;
|
|
||||||
|
|
||||||
let token = { access_token: '' };
|
|
||||||
|
|
||||||
async function signInUser(data: SignInData) {
|
|
||||||
const result = await api.post<JWT>('/api/signIn', data);
|
|
||||||
if (result.isErr()) {
|
|
||||||
username = '';
|
|
||||||
password = '';
|
|
||||||
notifications.error('Wrong Username or Password!', 5000);
|
|
||||||
loginFailed = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
loginFailed = false;
|
|
||||||
}, 1500);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
token = result.inner;
|
|
||||||
user.init(token.access_token);
|
|
||||||
username = $user.username;
|
|
||||||
notifications.success('User ' + username + ' signed in', 5000);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="hero from-primary/30 to-secondary/30 min-h-screen bg-gradient-to-br">
|
|
||||||
<div
|
|
||||||
class="card lg:card-side bg-base-100 face shadow-2xl {loginFailed
|
|
||||||
? 'failure border-error border-2'
|
|
||||||
: ''}"
|
|
||||||
in:fly={{ delay: 200, y: 100, duration: 500 }}
|
|
||||||
out:fade={{ duration: 200 }}
|
|
||||||
>
|
|
||||||
<figure class="bg-base-200"><img src={logo} alt="Logo" class="h-auto w-48 lg:w-64" /></figure>
|
|
||||||
<div class="card-body w-80">
|
|
||||||
<h2 class="card-title text-2xl">Login</h2>
|
|
||||||
<form class="form-control w-full max-w-xs">
|
|
||||||
<label class="label" for="user">
|
|
||||||
<span class="label-text text-md">Username</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="input input-bordered w-full max-w-xs"
|
|
||||||
id="user"
|
|
||||||
bind:value={username}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label class="label" for="pwd">
|
|
||||||
<span class="label-text text-md">Password</span>
|
|
||||||
</label>
|
|
||||||
<PasswordInput id="pwd" bind:value={password} />
|
|
||||||
|
|
||||||
<div class="card-actions mt-4 justify-end">
|
|
||||||
<button
|
|
||||||
class="btn btn-primary inline-flex items-center"
|
|
||||||
on:click={() => signInUser({ username, password })}
|
|
||||||
>
|
|
||||||
<Login class="mr-2 h-5 w-5" /><span>Login</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.failure {
|
|
||||||
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
backface-visibility: hidden;
|
|
||||||
perspective: 1000px;
|
|
||||||
}
|
|
||||||
@keyframes shake {
|
|
||||||
10%,
|
|
||||||
90% {
|
|
||||||
transform: translatex(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
20%,
|
|
||||||
80% {
|
|
||||||
transform: translatex(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
30%,
|
|
||||||
50%,
|
|
||||||
70% {
|
|
||||||
transform: translatex(-4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
40%,
|
|
||||||
60% {
|
|
||||||
transform: translatex(4px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,201 +1,189 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { user } from '$lib/stores/user';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { useFeatureFlags } from '$lib/stores/featureFlags';
|
||||||
import { useFeatureFlags } from '$lib/stores/featureFlags';
|
import GithubButton from '../menu/GithubButton.svelte';
|
||||||
import UserButton from '../menu/UserButton.svelte';
|
import LogoButton from '../menu/LogoButton.svelte';
|
||||||
import GithubButton from '../menu/GithubButton.svelte';
|
import MenuList from '../menu/MenuList.svelte';
|
||||||
import LogoButton from '../menu/LogoButton.svelte';
|
import {
|
||||||
import MenuList from '../menu/MenuList.svelte';
|
Connection,
|
||||||
import {
|
Settings,
|
||||||
Connection,
|
MdiController,
|
||||||
Users,
|
Devices,
|
||||||
Settings,
|
Camera,
|
||||||
MdiController,
|
Rotate3d,
|
||||||
Devices,
|
MotorOutline,
|
||||||
Camera,
|
Health,
|
||||||
Rotate3d,
|
Folder,
|
||||||
MotorOutline,
|
Update,
|
||||||
Health,
|
WiFi,
|
||||||
Folder,
|
Router,
|
||||||
Update,
|
AP,
|
||||||
WiFi,
|
Remote,
|
||||||
Router,
|
Copyright,
|
||||||
AP,
|
NTP,
|
||||||
Remote,
|
Metrics
|
||||||
Copyright,
|
} from '$lib/components/icons';
|
||||||
NTP,
|
import appEnv from 'app-env';
|
||||||
Metrics
|
|
||||||
} from '$lib/components/icons';
|
|
||||||
import appEnv from 'app-env';
|
|
||||||
|
|
||||||
const features = useFeatureFlags();
|
const features = useFeatureFlags();
|
||||||
|
|
||||||
const appName = $page.data.app_name;
|
const appName = $page.data.app_name;
|
||||||
|
|
||||||
const copyright = $page.data.copyright;
|
const copyright = $page.data.copyright;
|
||||||
|
|
||||||
const github = { href: 'https://github.com/' + $page.data.github, active: true };
|
const github = { href: 'https://github.com/' + $page.data.github, active: true };
|
||||||
|
|
||||||
type menuItem = {
|
type menuItem = {
|
||||||
title: string;
|
title: string;
|
||||||
icon: ConstructorOfATypedSvelteComponent;
|
icon: ConstructorOfATypedSvelteComponent;
|
||||||
href?: string;
|
href?: string;
|
||||||
feature: boolean;
|
feature: boolean;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
submenu?: menuItem[];
|
submenu?: menuItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
$: menuItems = [
|
$: menuItems = [
|
||||||
{
|
{
|
||||||
title: 'Connection',
|
title: 'Connection',
|
||||||
icon: WiFi,
|
icon: WiFi,
|
||||||
href: '/connection',
|
href: '/connection',
|
||||||
feature: !appEnv.VITE_USE_HOST_NAME
|
feature: !appEnv.VITE_USE_HOST_NAME
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Controller',
|
title: 'Controller',
|
||||||
icon: MdiController,
|
icon: MdiController,
|
||||||
href: '/controller',
|
href: '/controller',
|
||||||
feature: true
|
feature: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Peripherals',
|
title: 'Peripherals',
|
||||||
icon: Devices,
|
icon: Devices,
|
||||||
feature: true,
|
feature: true,
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
title: 'I2C',
|
title: 'I2C',
|
||||||
icon: Connection,
|
icon: Connection,
|
||||||
href: '/peripherals/i2c',
|
href: '/peripherals/i2c',
|
||||||
feature: true
|
feature: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Camera',
|
title: 'Camera',
|
||||||
icon: Camera,
|
icon: Camera,
|
||||||
href: '/peripherals/camera',
|
href: '/peripherals/camera',
|
||||||
feature: $features.camera
|
feature: $features.camera
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Servo',
|
title: 'Servo',
|
||||||
icon: MotorOutline,
|
icon: MotorOutline,
|
||||||
href: '/peripherals/servo',
|
href: '/peripherals/servo',
|
||||||
feature: true
|
feature: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'IMU',
|
title: 'IMU',
|
||||||
icon: Rotate3d,
|
icon: Rotate3d,
|
||||||
href: '/peripherals/imu',
|
href: '/peripherals/imu',
|
||||||
feature: $features.imu || $features.mag || $features.bmp
|
feature: $features.imu || $features.mag || $features.bmp
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Connections',
|
title: 'Connections',
|
||||||
icon: Remote,
|
icon: Remote,
|
||||||
feature: $features.ntp,
|
feature: $features.ntp,
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
title: 'NTP',
|
title: 'NTP',
|
||||||
icon: NTP,
|
icon: NTP,
|
||||||
href: '/connections/ntp',
|
href: '/connections/ntp',
|
||||||
feature: $features.ntp
|
feature: $features.ntp
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'WiFi',
|
title: 'WiFi',
|
||||||
icon: WiFi,
|
icon: WiFi,
|
||||||
feature: true,
|
feature: true,
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
title: 'WiFi Station',
|
title: 'WiFi Station',
|
||||||
icon: Router,
|
icon: Router,
|
||||||
href: '/wifi/sta',
|
href: '/wifi/sta',
|
||||||
feature: true
|
feature: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Access Point',
|
title: 'Access Point',
|
||||||
icon: AP,
|
icon: AP,
|
||||||
href: '/wifi/ap',
|
href: '/wifi/ap',
|
||||||
feature: true
|
feature: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Users',
|
title: 'System',
|
||||||
icon: Users,
|
icon: Settings,
|
||||||
href: '/user',
|
feature: true,
|
||||||
feature: $features.security && $user.admin
|
submenu: [
|
||||||
},
|
{
|
||||||
{
|
title: 'System Status',
|
||||||
title: 'System',
|
icon: Health,
|
||||||
icon: Settings,
|
href: '/system/status',
|
||||||
feature: true,
|
feature: true
|
||||||
submenu: [
|
},
|
||||||
{
|
{
|
||||||
title: 'System Status',
|
title: 'File System',
|
||||||
icon: Health,
|
icon: Folder,
|
||||||
href: '/system/status',
|
href: '/system/filesystem',
|
||||||
feature: true
|
feature: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'File System',
|
title: 'System Metrics',
|
||||||
icon: Folder,
|
icon: Metrics,
|
||||||
href: '/system/filesystem',
|
href: '/system/metrics',
|
||||||
feature: true
|
feature: $features.analytics
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'System Metrics',
|
title: 'Firmware Update',
|
||||||
icon: Metrics,
|
icon: Update,
|
||||||
href: '/system/metrics',
|
href: '/system/update',
|
||||||
feature: $features.analytics
|
feature:
|
||||||
},
|
$features.ota || $features.upload_firmware || $features.download_firmware
|
||||||
{
|
}
|
||||||
title: 'Firmware Update',
|
]
|
||||||
icon: Update,
|
}
|
||||||
href: '/system/update',
|
] as menuItem[];
|
||||||
feature:
|
|
||||||
($features.ota || $features.upload_firmware || $features.download_firmware) &&
|
|
||||||
(!$features.security || $user.admin)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
] as menuItem[];
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
function setActiveMenuItem(targetTitle: string) {
|
function setActiveMenuItem(targetTitle: string) {
|
||||||
menuItems.forEach(item => {
|
menuItems.forEach(item => {
|
||||||
item.active = item.title === targetTitle;
|
item.active = item.title === targetTitle;
|
||||||
item.submenu?.forEach(subItem => {
|
item.submenu?.forEach(subItem => {
|
||||||
subItem.active = subItem.title === targetTitle;
|
subItem.active = subItem.title === targetTitle;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
menuItems = menuItems;
|
menuItems = menuItems;
|
||||||
dispatch('menuClicked');
|
dispatch('menuClicked');
|
||||||
}
|
}
|
||||||
|
|
||||||
$: setActiveMenuItem($page.data.title);
|
$: setActiveMenuItem($page.data.title);
|
||||||
|
|
||||||
const updateMenu = (event: any) => {
|
const updateMenu = (event: any) => {
|
||||||
setActiveMenuItem(event.details);
|
setActiveMenuItem(event.details);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="bg-base-200 text-base-content flex h-full w-80 flex-col p-4">
|
<div class="bg-base-200 text-base-content flex h-full w-80 flex-col p-4">
|
||||||
<LogoButton {appName} />
|
<LogoButton {appName} />
|
||||||
|
|
||||||
<MenuList {menuItems} on:select{updateMenu} class="flex-grow flex-nowrap overflow-y-auto" />
|
<MenuList {menuItems} on:select{updateMenu} class="flex-grow flex-nowrap overflow-y-auto" />
|
||||||
|
|
||||||
<UserButton />
|
<div class="divider my-0" />
|
||||||
|
|
||||||
<div class="divider my-0" />
|
<div class="flex items-center justify-between">
|
||||||
|
<GithubButton {github} />
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-end text-sm gap-2">
|
||||||
<GithubButton {github} />
|
<Copyright class="h-4 w-4" />{copyright}
|
||||||
<div class="flex items-center justify-end text-sm gap-2">
|
</div>
|
||||||
<Copyright class="h-4 w-4" />{copyright}
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { user } from '$lib/stores';
|
|
||||||
import { useFeatureFlags } from "$lib/stores";
|
|
||||||
import { Avatar, Logout } from '../icons';
|
|
||||||
|
|
||||||
const features = useFeatureFlags();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if $features.security}
|
|
||||||
<div class="flex items-center">
|
|
||||||
<Avatar class="h-8 w-8" />
|
|
||||||
<span class="flex-grow px-4 text-xl font-bold">{$user.username}</span>
|
|
||||||
<button class="btn btn-ghost" on:click={user.invalidate}>
|
|
||||||
<Logout class="h-8 w-8 rotate-180" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
@@ -1,113 +1,112 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { openModal, closeAllModals } from 'svelte-modals';
|
import { openModal, closeAllModals } from 'svelte-modals';
|
||||||
import { user } from '$lib/stores/user';
|
import { notifications } from '$lib/components/toasts/notifications';
|
||||||
import { notifications } from '$lib/components/toasts/notifications';
|
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
|
||||||
|
|
||||||
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
|
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
|
||||||
import { compareVersions } from 'compare-versions';
|
import { compareVersions } from 'compare-versions';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { api } from '$lib/api';
|
import { api } from '$lib/api';
|
||||||
import type { GithubRelease } from '$lib/types/models';
|
import type { GithubRelease } from '$lib/types/models';
|
||||||
import { useFeatureFlags } from '$lib/stores/featureFlags';
|
import { useFeatureFlags } from '$lib/stores/featureFlags';
|
||||||
import { Cancel, CloudDown, Firmware } from '../icons';
|
import { Cancel, CloudDown, Firmware } from '../icons';
|
||||||
|
|
||||||
const features = useFeatureFlags();
|
const features = useFeatureFlags();
|
||||||
|
|
||||||
export let update = false;
|
export let update = false;
|
||||||
|
|
||||||
let firmwareVersion: string;
|
let firmwareVersion: string;
|
||||||
let firmwareDownloadLink: string;
|
let firmwareDownloadLink: string;
|
||||||
|
|
||||||
async function getGithubAPI() {
|
async function getGithubAPI() {
|
||||||
const headers = {
|
const headers = {
|
||||||
accept: 'application/vnd.github+json',
|
accept: 'application/vnd.github+json',
|
||||||
'X-GitHub-Api-Version': '2022-11-28'
|
'X-GitHub-Api-Version': '2022-11-28'
|
||||||
};
|
};
|
||||||
const result = await api.get<GithubRelease>(
|
const result = await api.get<GithubRelease>(
|
||||||
`https://api.github.com/repos/${$page.data.github}/releases/latest`,
|
`https://api.github.com/repos/${$page.data.github}/releases/latest`,
|
||||||
{ headers }
|
{ headers }
|
||||||
);
|
);
|
||||||
if (result.inner.message === '404' || result.inner.message == 'Not Found') {
|
if (result.inner.message === '404' || result.inner.message == 'Not Found') {
|
||||||
console.warn('Error: Could not find releases in the repository');
|
console.warn('Error: Could not find releases in the repository');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result.isErr()) {
|
if (result.isErr()) {
|
||||||
console.error('Error:', result.inner);
|
console.error('Error:', result.inner);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = result.inner;
|
const results = result.inner;
|
||||||
update = false;
|
update = false;
|
||||||
firmwareVersion = '';
|
firmwareVersion = '';
|
||||||
|
|
||||||
if (compareVersions(results.tag_name, $features.firmware_version) === 1) {
|
if (compareVersions(results.tag_name, $features.firmware_version) === 1) {
|
||||||
// iterate over assets and find the correct one
|
// iterate over assets and find the correct one
|
||||||
for (let i = 0; i < results.assets.length; i++) {
|
for (let i = 0; i < results.assets.length; i++) {
|
||||||
// check if the asset is of type *.bin
|
// check if the asset is of type *.bin
|
||||||
if (
|
if (
|
||||||
results.assets[i].name.includes('.bin') &&
|
results.assets[i].name.includes('.bin') &&
|
||||||
results.assets[i].name.includes($features.firmware_built_target)
|
results.assets[i].name.includes($features.firmware_built_target)
|
||||||
) {
|
) {
|
||||||
update = true;
|
update = true;
|
||||||
firmwareVersion = results.tag_name;
|
firmwareVersion = results.tag_name;
|
||||||
firmwareDownloadLink = results.assets[i].browser_download_url;
|
firmwareDownloadLink = results.assets[i].browser_download_url;
|
||||||
notifications.info('Firmware update available.', 5000);
|
notifications.info('Firmware update available.', 5000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function postGithubDownload(url: string) {
|
async function postGithubDownload(url: string) {
|
||||||
const result = await api.post('/api/downloadUpdate', { download_url: url });
|
const result = await api.post('/api/downloadUpdate', { download_url: url });
|
||||||
if (result.isErr()) {
|
if (result.isErr()) {
|
||||||
console.error('Error:', result.inner);
|
console.error('Error:', result.inner);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if ($features.download_firmware && (!$features.security || $user.admin)) {
|
if ($features.download_firmware) {
|
||||||
await getGithubAPI();
|
await getGithubAPI();
|
||||||
const interval = setInterval(
|
const interval = setInterval(
|
||||||
async () => {
|
async () => {
|
||||||
await getGithubAPI();
|
await getGithubAPI();
|
||||||
},
|
},
|
||||||
60 * 60 * 1000
|
60 * 60 * 1000
|
||||||
); // once per hour
|
); // once per hour
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function confirmGithubUpdate(url: string) {
|
function confirmGithubUpdate(url: string) {
|
||||||
openModal(ConfirmDialog, {
|
openModal(ConfirmDialog, {
|
||||||
title: 'Confirm flashing new firmware to the device',
|
title: 'Confirm flashing new firmware to the device',
|
||||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||||
labels: {
|
labels: {
|
||||||
cancel: { label: 'Abort', icon: Cancel },
|
cancel: { label: 'Abort', icon: Cancel },
|
||||||
confirm: { label: 'Update', icon: CloudDown }
|
confirm: { label: 'Update', icon: CloudDown }
|
||||||
},
|
},
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
postGithubDownload(url);
|
postGithubDownload(url);
|
||||||
openModal(GithubUpdateDialog, {
|
openModal(GithubUpdateDialog, {
|
||||||
onConfirm: () => closeAllModals()
|
onConfirm: () => closeAllModals()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if update}
|
{#if update}
|
||||||
<div class="indicator flex-none">
|
<div class="indicator flex-none">
|
||||||
<button
|
<button
|
||||||
class="btn btn-square btn-ghost h-9 w-9"
|
class="btn btn-square btn-ghost h-9 w-9"
|
||||||
on:click={() => confirmGithubUpdate(firmwareDownloadLink)}
|
on:click={() => confirmGithubUpdate(firmwareDownloadLink)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1"
|
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1"
|
||||||
>{firmwareVersion}</span
|
>{firmwareVersion}</span
|
||||||
>
|
>
|
||||||
<Firmware class="h-7 w-7" />
|
<Firmware class="h-7 w-7" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -5,6 +5,5 @@ export * from './socket';
|
|||||||
export * from './fullscreen';
|
export * from './fullscreen';
|
||||||
export * from './telemetry';
|
export * from './telemetry';
|
||||||
export * from './analytics';
|
export * from './analytics';
|
||||||
export * from './user';
|
|
||||||
export * from './featureFlags';
|
export * from './featureFlags';
|
||||||
export * from './location-store';
|
export * from './location-store';
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import { goto } from '$app/navigation';
|
|
||||||
import { jwtDecode } from 'jwt-decode';
|
|
||||||
import { persistentStore } from '$lib/utilities';
|
|
||||||
|
|
||||||
export type UserProfile = {
|
|
||||||
username: string;
|
|
||||||
admin: boolean;
|
|
||||||
bearer_token: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DecodedJWT = Omit<UserProfile, 'bearer_token'>;
|
|
||||||
|
|
||||||
const emptyUser: UserProfile = {
|
|
||||||
username: '',
|
|
||||||
admin: false,
|
|
||||||
bearer_token: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
function createUserStore() {
|
|
||||||
const store = persistentStore<UserProfile>('user', emptyUser);
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe: store.subscribe,
|
|
||||||
init: (access_token: string) => {
|
|
||||||
const decoded: DecodedJWT = jwtDecode(access_token);
|
|
||||||
const userProfile: UserProfile = {
|
|
||||||
bearer_token: access_token,
|
|
||||||
username: decoded.username,
|
|
||||||
admin: decoded.admin
|
|
||||||
};
|
|
||||||
store.set(userProfile);
|
|
||||||
},
|
|
||||||
invalidate: () => {
|
|
||||||
store.set(emptyUser);
|
|
||||||
goto('/');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const user = createUserStore();
|
|
||||||
+123
-126
@@ -1,201 +1,198 @@
|
|||||||
export type vector = { x: number; y: number };
|
export type vector = { x: number; y: number };
|
||||||
|
|
||||||
export interface ControllerInput {
|
export interface ControllerInput {
|
||||||
left: vector;
|
left: vector;
|
||||||
right: vector;
|
right: vector;
|
||||||
height: number;
|
height: number;
|
||||||
speed: number;
|
speed: number;
|
||||||
s1: number;
|
s1: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GithubRelease = {
|
export type GithubRelease = {
|
||||||
message: string;
|
message: string;
|
||||||
tag_name: string;
|
tag_name: string;
|
||||||
assets: Array<{
|
assets: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
browser_download_url: string;
|
browser_download_url: string;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type JWT = { access_token: string };
|
|
||||||
|
|
||||||
export type angles = number[] | Int16Array;
|
export type angles = number[] | Int16Array;
|
||||||
|
|
||||||
export type WifiStatus = {
|
export type WifiStatus = {
|
||||||
status: number;
|
status: number;
|
||||||
local_ip: string;
|
local_ip: string;
|
||||||
mac_address: string;
|
mac_address: string;
|
||||||
rssi: number;
|
rssi: number;
|
||||||
ssid: string;
|
ssid: string;
|
||||||
bssid: string;
|
bssid: string;
|
||||||
channel: number;
|
channel: number;
|
||||||
subnet_mask: string;
|
subnet_mask: string;
|
||||||
gateway_ip: string;
|
gateway_ip: string;
|
||||||
dns_ip_1: string;
|
dns_ip_1: string;
|
||||||
dns_ip_2?: string;
|
dns_ip_2?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WifiSettings = {
|
export type WifiSettings = {
|
||||||
hostname: string;
|
hostname: string;
|
||||||
priority_RSSI: boolean;
|
priority_RSSI: boolean;
|
||||||
wifi_networks: KnownNetworkItem[];
|
wifi_networks: KnownNetworkItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NetworkList = {
|
export type NetworkList = {
|
||||||
networks: NetworkItem[];
|
networks: NetworkItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type KnownNetworkItem = {
|
export type KnownNetworkItem = {
|
||||||
ssid: string;
|
ssid: string;
|
||||||
password: string;
|
password: string;
|
||||||
static_ip_config: boolean;
|
static_ip_config: boolean;
|
||||||
local_ip?: string;
|
local_ip?: string;
|
||||||
subnet_mask?: string;
|
subnet_mask?: string;
|
||||||
gateway_ip?: string;
|
gateway_ip?: string;
|
||||||
dns_ip_1?: string;
|
dns_ip_1?: string;
|
||||||
dns_ip_2?: string;
|
dns_ip_2?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NetworkItem = {
|
export type NetworkItem = {
|
||||||
rssi: number;
|
rssi: number;
|
||||||
ssid: string;
|
ssid: string;
|
||||||
bssid: string;
|
bssid: string;
|
||||||
channel: number;
|
channel: number;
|
||||||
encryption_type: number;
|
encryption_type: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ApStatus = {
|
export type ApStatus = {
|
||||||
status: number;
|
status: number;
|
||||||
ip_address: string;
|
ip_address: string;
|
||||||
mac_address: string;
|
mac_address: string;
|
||||||
station_num: number;
|
station_num: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ApSettings = {
|
export type ApSettings = {
|
||||||
provision_mode: number;
|
provision_mode: number;
|
||||||
ssid: string;
|
ssid: string;
|
||||||
password: string;
|
password: string;
|
||||||
channel: number;
|
channel: number;
|
||||||
ssid_hidden: boolean;
|
ssid_hidden: boolean;
|
||||||
max_clients: number;
|
max_clients: number;
|
||||||
local_ip: string;
|
local_ip: string;
|
||||||
gateway_ip: string;
|
gateway_ip: string;
|
||||||
subnet_mask: string;
|
subnet_mask: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NTPStatus = {
|
export type NTPStatus = {
|
||||||
status: number;
|
status: number;
|
||||||
utc_time: string;
|
utc_time: string;
|
||||||
local_time: string;
|
local_time: string;
|
||||||
server: string;
|
server: string;
|
||||||
uptime: number;
|
uptime: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type Battery = {
|
export type Battery = {
|
||||||
voltage: number;
|
voltage: number;
|
||||||
current: number;
|
current: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DownloadOTA = {
|
export type DownloadOTA = {
|
||||||
status: string;
|
status: string;
|
||||||
progress: number;
|
progress: number;
|
||||||
error: string;
|
error: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NTPSettings = {
|
export type NTPSettings = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
server: string;
|
server: string;
|
||||||
tz_label: string;
|
tz_label: string;
|
||||||
tz_format: string;
|
tz_format: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Analytics = {
|
export type Analytics = {
|
||||||
max_alloc_heap: number;
|
max_alloc_heap: number;
|
||||||
psram_size: number;
|
psram_size: number;
|
||||||
free_psram: number;
|
free_psram: number;
|
||||||
free_heap: number;
|
free_heap: number;
|
||||||
total_heap: number;
|
total_heap: number;
|
||||||
min_free_heap: number;
|
min_free_heap: number;
|
||||||
core_temp: number;
|
core_temp: number;
|
||||||
fs_total: number;
|
fs_total: number;
|
||||||
fs_used: number;
|
fs_used: number;
|
||||||
uptime: number;
|
uptime: number;
|
||||||
cpu0_usage: number;
|
cpu0_usage: number;
|
||||||
cpu1_usage: number;
|
cpu1_usage: number;
|
||||||
cpu_usage: number;
|
cpu_usage: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Rssi = {
|
export type Rssi = {
|
||||||
rssi: number;
|
rssi: number;
|
||||||
ssid: string;
|
ssid: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StaticSystemInformation = {
|
export type StaticSystemInformation = {
|
||||||
esp_platform: string;
|
esp_platform: string;
|
||||||
firmware_version: string;
|
firmware_version: string;
|
||||||
cpu_freq_mhz: number;
|
cpu_freq_mhz: number;
|
||||||
cpu_type: string;
|
cpu_type: string;
|
||||||
cpu_rev: number;
|
cpu_rev: number;
|
||||||
cpu_cores: number;
|
cpu_cores: number;
|
||||||
sketch_size: number;
|
sketch_size: number;
|
||||||
free_sketch_space: number;
|
free_sketch_space: number;
|
||||||
sdk_version: string;
|
sdk_version: string;
|
||||||
arduino_version: string;
|
arduino_version: string;
|
||||||
flash_chip_size: number;
|
flash_chip_size: number;
|
||||||
flash_chip_speed: number;
|
flash_chip_speed: number;
|
||||||
cpu_reset_reason: string;
|
cpu_reset_reason: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SystemInformation = Analytics & StaticSystemInformation;
|
export type SystemInformation = Analytics & StaticSystemInformation;
|
||||||
|
|
||||||
export type IMU = {
|
export type IMU = {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
z: number;
|
z: number;
|
||||||
imu_temp: number;
|
imu_temp: number;
|
||||||
altitude: number;
|
altitude: number;
|
||||||
bmp_temp: number;
|
bmp_temp: number;
|
||||||
pressure: number;
|
pressure: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface I2CDevice {
|
export interface I2CDevice {
|
||||||
address: number;
|
address: number;
|
||||||
part_number: string;
|
part_number: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
export type CameraSettings = {
|
export type CameraSettings = {
|
||||||
framesize: number;
|
framesize: number;
|
||||||
quality: number;
|
quality: number;
|
||||||
brightness: number;
|
brightness: number;
|
||||||
contrast: number;
|
contrast: number;
|
||||||
saturation: number;
|
saturation: number;
|
||||||
sharpness: number;
|
sharpness: number;
|
||||||
denoise: number;
|
denoise: number;
|
||||||
special_effect: number;
|
special_effect: number;
|
||||||
wb_mode: number;
|
wb_mode: number;
|
||||||
vflip: boolean;
|
vflip: boolean;
|
||||||
hmirror: boolean;
|
hmirror: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type File = number;
|
export type File = number;
|
||||||
|
|
||||||
export interface Directory {
|
export interface Directory {
|
||||||
[key: string]: File | Directory;
|
[key: string]: File | Directory;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Servo = {
|
export type Servo = {
|
||||||
name: string;
|
name: string;
|
||||||
channel: number;
|
channel: number;
|
||||||
inverted: boolean;
|
inverted: boolean;
|
||||||
angle: number;
|
angle: number;
|
||||||
center_angle: number;
|
center_angle: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ServoConfiguration = {
|
export type ServoConfiguration = {
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
servo_pwm_frequency: number;
|
servo_pwm_frequency: number;
|
||||||
servo_oscillator_frequency: number;
|
servo_oscillator_frequency: number;
|
||||||
servos: Servo[];
|
servos: Servo[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,12 +8,9 @@
|
|||||||
import '../app.css';
|
import '../app.css';
|
||||||
import Menu from '../lib/components/menu/Menu.svelte';
|
import Menu from '../lib/components/menu/Menu.svelte';
|
||||||
import Statusbar from '../lib/components/statusbar/statusbar.svelte';
|
import Statusbar from '../lib/components/statusbar/statusbar.svelte';
|
||||||
import Login from '../lib/components/login.svelte';
|
|
||||||
import {
|
import {
|
||||||
telemetry,
|
telemetry,
|
||||||
analytics,
|
analytics,
|
||||||
user,
|
|
||||||
type UserProfile,
|
|
||||||
ModesEnum,
|
ModesEnum,
|
||||||
kinematicData,
|
kinematicData,
|
||||||
mode,
|
mode,
|
||||||
@@ -21,21 +18,16 @@
|
|||||||
servoAngles,
|
servoAngles,
|
||||||
servoAnglesOut,
|
servoAnglesOut,
|
||||||
socket,
|
socket,
|
||||||
location
|
location,
|
||||||
|
useFeatureFlags
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import type { Analytics, Battery, DownloadOTA } from '$lib/types/models';
|
import type { Analytics, Battery, DownloadOTA } from '$lib/types/models';
|
||||||
import { api } from '$lib/api';
|
|
||||||
import { useFeatureFlags } from '$lib/stores/featureFlags';
|
|
||||||
|
|
||||||
const features = useFeatureFlags();
|
const features = useFeatureFlags();
|
||||||
|
|
||||||
onMount(async () => {
|
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;
|
const ws = $location ? $location : window.location.host;
|
||||||
socket.init(`ws://${ws}/ws/events${ws_token}`);
|
socket.init(`ws://${ws}/ws/events`);
|
||||||
|
|
||||||
addEventListeners();
|
addEventListeners();
|
||||||
|
|
||||||
@@ -75,14 +67,6 @@
|
|||||||
socket.off('otastatus', handleOAT);
|
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 = () => {
|
const handleOpen = () => {
|
||||||
notifications.success('Connection to device established', 5000);
|
notifications.success('Connection to device established', 5000);
|
||||||
};
|
};
|
||||||
@@ -109,25 +93,21 @@
|
|||||||
<title>{$page.data.title}</title>
|
<title>{$page.data.title}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#if $features?.security && $user.bearer_token === ''}
|
<div class="drawer">
|
||||||
<Login />
|
<input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} />
|
||||||
{:else}
|
<div class="drawer-content flex flex-col">
|
||||||
<div class="drawer">
|
<!-- Status bar content here -->
|
||||||
<input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} />
|
<Statusbar />
|
||||||
<div class="drawer-content flex flex-col">
|
|
||||||
<!-- Status bar content here -->
|
|
||||||
<Statusbar />
|
|
||||||
|
|
||||||
<!-- Main page content here -->
|
<!-- Main page content here -->
|
||||||
<slot />
|
<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>
|
|
||||||
</div>
|
</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>
|
<Modals>
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
|||||||
@@ -1,25 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||||
import { WiFi } from '$lib/components/icons';
|
import { WiFi } from '$lib/components/icons';
|
||||||
import { location, socket, useFeatureFlags, user } from '$lib/stores';
|
import { location, socket, useFeatureFlags } from '$lib/stores';
|
||||||
|
|
||||||
const features = useFeatureFlags();
|
const features = useFeatureFlags();
|
||||||
|
|
||||||
const update = () => {
|
const update = () => {
|
||||||
const ws_token = $features.security ? '?access_token=' + $user.bearer_token : '';
|
const ws = $location ? $location : window.location.host;
|
||||||
const ws = $location ? $location : window.location.host;
|
socket.init(`ws://${ws}/ws/events`);
|
||||||
socket.init(`ws://${ws}/ws/events${ws_token}`);
|
};
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SettingsCard collapsible={false}>
|
<SettingsCard collapsible={false}>
|
||||||
<WiFi slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
<WiFi slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||||
<span slot="title">Connection</span>
|
<span slot="title">Connection</span>
|
||||||
|
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<label class="label w-32" for="server">Address:</label>
|
<label class="label w-32" for="server">Address:</label>
|
||||||
<input class="input" bind:value={$location} />
|
<input class="input" bind:value={$location} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-primary" on:click={update}>Update</button>
|
<button class="btn btn-primary" on:click={update}>Update</button>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
|||||||
@@ -1,260 +1,254 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||||
import Collapsible from '$lib/components/Collapsible.svelte';
|
import Collapsible from '$lib/components/Collapsible.svelte';
|
||||||
import Spinner from '$lib/components/Spinner.svelte';
|
import Spinner from '$lib/components/Spinner.svelte';
|
||||||
import { user, useFeatureFlags } from '$lib/stores';
|
import { useFeatureFlags } from '$lib/stores';
|
||||||
import { notifications } from '$lib/components/toasts/notifications';
|
import { notifications } from '$lib/components/toasts/notifications';
|
||||||
import { TIME_ZONES } from './timezones';
|
import { TIME_ZONES } from './timezones';
|
||||||
import type { NTPSettings, NTPStatus } from '$lib/types/models';
|
import type { NTPSettings, NTPStatus } from '$lib/types/models';
|
||||||
import { api } from '$lib/api';
|
import { api } from '$lib/api';
|
||||||
import { NTP, UTC, Stopwatch, Clock, Server } from '$lib/components/icons';
|
import { NTP, UTC, Stopwatch, Clock, Server } from '$lib/components/icons';
|
||||||
|
|
||||||
const features = useFeatureFlags();
|
const features = useFeatureFlags();
|
||||||
|
|
||||||
let ntpSettings: NTPSettings;
|
let ntpSettings: NTPSettings;
|
||||||
let ntpStatus: NTPStatus;
|
let ntpStatus: NTPStatus;
|
||||||
|
|
||||||
async function getNTPStatus() {
|
async function getNTPStatus() {
|
||||||
const result = await api.get<NTPStatus>('/api/ntpStatus');
|
const result = await api.get<NTPStatus>('/api/ntpStatus');
|
||||||
if (result.isErr()){
|
if (result.isErr()) {
|
||||||
console.error('Error:', result.inner);
|
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');
|
const result = await api.get<NTPSettings>('/api/ntpSettings');
|
||||||
if (result.isErr()){
|
if (result.isErr()) {
|
||||||
console.error('Error:', result.inner);
|
console.error('Error:', result.inner);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
ntpSettings = result.inner
|
ntpSettings = result.inner;
|
||||||
}
|
}
|
||||||
|
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => getNTPStatus(), 5000);
|
||||||
getNTPStatus();
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
onDestroy(() => clearInterval(interval));
|
onDestroy(() => clearInterval(interval));
|
||||||
|
|
||||||
onMount(() => {
|
onMount(getNTPSettings);
|
||||||
if (!$features.security || $user.admin) {
|
|
||||||
getNTPSettings();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let formField: any;
|
let formField: any;
|
||||||
|
|
||||||
let formErrors = {
|
let formErrors = {
|
||||||
server: false
|
server: false
|
||||||
};
|
};
|
||||||
|
|
||||||
async function postNTPSettings(data: NTPSettings) {
|
async function postNTPSettings(data: NTPSettings) {
|
||||||
const result = await api.post<NTPSettings>('/api/ntpSettings', data);
|
const result = await api.post<NTPSettings>('/api/ntpSettings', data);
|
||||||
if (result.isErr()){
|
if (result.isErr()) {
|
||||||
notifications.error('User not authorized.', 3000);
|
notifications.error('User not authorized.', 3000);
|
||||||
console.error('Error:', result.inner);
|
console.error('Error:', result.inner);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
ntpSettings = result.inner
|
ntpSettings = result.inner;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmitNTP() {
|
function handleSubmitNTP() {
|
||||||
let valid = true;
|
let valid = true;
|
||||||
|
|
||||||
// Validate Server
|
// Validate Server
|
||||||
// RegEx for IPv4
|
// RegEx for IPv4
|
||||||
const regexExpIPv4 =
|
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/;
|
/\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 =
|
const regexExpURL =
|
||||||
/[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/i;
|
/[-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)) {
|
if (!regexExpURL.test(ntpSettings.server) && !regexExpIPv4.test(ntpSettings.server)) {
|
||||||
valid = false;
|
valid = false;
|
||||||
formErrors.server = true;
|
formErrors.server = true;
|
||||||
} else {
|
} else {
|
||||||
formErrors.server = false;
|
formErrors.server = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ntpSettings.tz_format = TIME_ZONES[ntpSettings.tz_label];
|
ntpSettings.tz_format = TIME_ZONES[ntpSettings.tz_label];
|
||||||
|
|
||||||
// Submit JSON to REST API
|
// Submit JSON to REST API
|
||||||
if (valid) {
|
if (valid) {
|
||||||
postNTPSettings(ntpSettings);
|
postNTPSettings(ntpSettings);
|
||||||
//alert('Form Valid');
|
//alert('Form Valid');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertSeconds(seconds: number) {
|
function convertSeconds(seconds: number) {
|
||||||
// Calculate the number of seconds, minutes, hours, and days
|
// Calculate the number of seconds, minutes, hours, and days
|
||||||
let minutes = Math.floor(seconds / 60);
|
let minutes = Math.floor(seconds / 60);
|
||||||
let hours = Math.floor(minutes / 60);
|
let hours = Math.floor(minutes / 60);
|
||||||
let days = Math.floor(hours / 24);
|
let days = Math.floor(hours / 24);
|
||||||
|
|
||||||
// Calculate the remaining hours, minutes, and seconds
|
// Calculate the remaining hours, minutes, and seconds
|
||||||
hours = hours % 24;
|
hours = hours % 24;
|
||||||
minutes = minutes % 60;
|
minutes = minutes % 60;
|
||||||
seconds = seconds % 60;
|
seconds = seconds % 60;
|
||||||
|
|
||||||
// Create the formatted string
|
// Create the formatted string
|
||||||
let result = '';
|
let result = '';
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
result += days + ' day' + (days > 1 ? 's' : '') + ' ';
|
result += days + ' day' + (days > 1 ? 's' : '') + ' ';
|
||||||
}
|
}
|
||||||
if (hours > 0) {
|
if (hours > 0) {
|
||||||
result += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';
|
result += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';
|
||||||
}
|
}
|
||||||
if (minutes > 0) {
|
if (minutes > 0) {
|
||||||
result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';
|
result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';
|
||||||
}
|
}
|
||||||
result += seconds + ' second' + (seconds > 1 ? 's' : '');
|
result += seconds + ' second' + (seconds > 1 ? 's' : '');
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SettingsCard collapsible={false}>
|
<SettingsCard collapsible={false}>
|
||||||
<Clock slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
<Clock slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||||
<span slot="title">Network Time</span>
|
<span slot="title">Network Time</span>
|
||||||
<div class="w-full overflow-x-auto">
|
<div class="w-full overflow-x-auto">
|
||||||
{#await getNTPStatus()}
|
{#await getNTPStatus()}
|
||||||
<Spinner />
|
<Spinner />
|
||||||
{:then nothing}
|
{:then nothing}
|
||||||
<div
|
<div
|
||||||
class="flex w-full flex-col space-y-1"
|
class="flex w-full flex-col space-y-1"
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||||
>
|
>
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||||
<div
|
<div
|
||||||
class="mask mask-hexagon h-auto w-10 {ntpStatus.status === 1
|
class="mask mask-hexagon h-auto w-10 {ntpStatus.status === 1 ?
|
||||||
? 'bg-success'
|
'bg-success'
|
||||||
: 'bg-error'}"
|
: 'bg-error'}"
|
||||||
>
|
>
|
||||||
<NTP
|
<NTP
|
||||||
class="h-auto w-full scale-75 {ntpStatus.status === 1
|
class="h-auto w-full scale-75 {ntpStatus.status === 1 ?
|
||||||
? 'text-success-content'
|
'text-success-content'
|
||||||
: 'text-error-content'}"
|
: 'text-error-content'}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">Status</div>
|
<div class="font-bold">Status</div>
|
||||||
<div class="text-sm opacity-75">
|
<div class="text-sm opacity-75">
|
||||||
{ntpStatus.status === 1 ? 'Active' : 'Inactive'}
|
{ntpStatus.status === 1 ? 'Active' : 'Inactive'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<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">
|
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
||||||
<Server class="text-primary-content h-auto w-full scale-75" />
|
<Server class="text-primary-content h-auto w-full scale-75" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">NTP Server</div>
|
<div class="font-bold">NTP Server</div>
|
||||||
<div class="text-sm opacity-75">
|
<div class="text-sm opacity-75">
|
||||||
{ntpStatus.server}
|
{ntpStatus.server}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<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">
|
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
||||||
<Clock class="text-primary-content h-auto w-full scale-75" />
|
<Clock class="text-primary-content h-auto w-full scale-75" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">Local Time</div>
|
<div class="font-bold">Local Time</div>
|
||||||
<div class="text-sm opacity-75">
|
<div class="text-sm opacity-75">
|
||||||
{new Intl.DateTimeFormat('en-GB', {
|
{new Intl.DateTimeFormat('en-GB', {
|
||||||
dateStyle: 'long',
|
dateStyle: 'long',
|
||||||
timeStyle: 'long'
|
timeStyle: 'long'
|
||||||
}).format(new Date(ntpStatus.local_time))}
|
}).format(new Date(ntpStatus.local_time))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<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">
|
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
||||||
<UTC class="text-primary-content h-auto w-full scale-75" />
|
<UTC class="text-primary-content h-auto w-full scale-75" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">UTC Time</div>
|
<div class="font-bold">UTC Time</div>
|
||||||
<div class="text-sm opacity-75">
|
<div class="text-sm opacity-75">
|
||||||
{new Intl.DateTimeFormat('en-GB', {
|
{new Intl.DateTimeFormat('en-GB', {
|
||||||
dateStyle: 'long',
|
dateStyle: 'long',
|
||||||
timeStyle: 'long',
|
timeStyle: 'long',
|
||||||
timeZone: 'UTC'
|
timeZone: 'UTC'
|
||||||
}).format(new Date(ntpStatus.utc_time))}
|
}).format(new Date(ntpStatus.utc_time))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<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">
|
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
||||||
<Stopwatch class="text-primary-content h-auto w-full scale-75" />
|
<Stopwatch class="text-primary-content h-auto w-full scale-75" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">Uptime</div>
|
<div class="font-bold">Uptime</div>
|
||||||
<div class="text-sm opacity-75">
|
<div class="text-sm opacity-75">
|
||||||
{convertSeconds(ntpStatus.uptime)}
|
{convertSeconds(ntpStatus.uptime)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !$features.security || $user.admin}
|
<Collapsible open={false} on:closed={getNTPSettings}>
|
||||||
<Collapsible open={false} on:closed={getNTPSettings}>
|
<span slot="title">Change NTP Settings</span>
|
||||||
<span slot="title">Change NTP Settings</span>
|
<form
|
||||||
<form
|
class="form-control w-full"
|
||||||
class="form-control w-full"
|
on:submit|preventDefault={handleSubmitNTP}
|
||||||
on:submit|preventDefault={handleSubmitNTP}
|
novalidate
|
||||||
novalidate
|
bind:this={formField}
|
||||||
bind:this={formField}
|
>
|
||||||
>
|
<label class="label cursor-pointer justify-start gap-4">
|
||||||
<label class="label cursor-pointer justify-start gap-4">
|
<input
|
||||||
<input
|
type="checkbox"
|
||||||
type="checkbox"
|
bind:checked={ntpSettings.enabled}
|
||||||
bind:checked={ntpSettings.enabled}
|
class="checkbox checkbox-primary"
|
||||||
class="checkbox checkbox-primary"
|
/>
|
||||||
/>
|
<span class="">Enable NTP</span>
|
||||||
<span class="">Enable NTP</span>
|
</label>
|
||||||
</label>
|
<label class="label" for="server">
|
||||||
<label class="label" for="server">
|
<span class="label-text text-md">Server</span>
|
||||||
<span class="label-text text-md">Server</span>
|
</label>
|
||||||
</label>
|
<input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
min="3"
|
||||||
min="3"
|
max="64"
|
||||||
max="64"
|
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {formErrors.server
|
formErrors.server
|
||||||
? 'border-error border-2'
|
) ?
|
||||||
: ''}"
|
'border-error border-2'
|
||||||
bind:value={ntpSettings.server}
|
: ''}"
|
||||||
id="server"
|
bind:value={ntpSettings.server}
|
||||||
required
|
id="server"
|
||||||
/>
|
required
|
||||||
<label class="label" for="subnet">
|
/>
|
||||||
<span class="label-text-alt text-error {formErrors.server ? '' : 'hidden'}"
|
<label class="label" for="subnet">
|
||||||
>Must be a valid IPv4 address or URL</span
|
<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">
|
</label>
|
||||||
<span class="label-text text-md">Pick Time Zone</span>
|
<label class="label" for="tz">
|
||||||
</label>
|
<span class="label-text text-md">Pick Time Zone</span>
|
||||||
<select class="select select-bordered" bind:value={ntpSettings.tz_label} id="tz">
|
</label>
|
||||||
{#each Object.entries(TIME_ZONES) as [tz_label, tz_format]}
|
<select class="select select-bordered" bind:value={ntpSettings.tz_label} id="tz">
|
||||||
<option value={tz_label}>{tz_label}</option>
|
{#each Object.entries(TIME_ZONES) as [tz_label, tz_format]}
|
||||||
{/each}
|
<option value={tz_label}>{tz_label}</option>
|
||||||
</select>
|
{/each}
|
||||||
|
</select>
|
||||||
|
|
||||||
<div class="mt-6 place-self-end">
|
<div class="mt-6 place-self-end">
|
||||||
<button class="btn btn-primary" type="submit">Apply Settings</button>
|
<button class="btn btn-primary" type="submit">Apply Settings</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
{/if}
|
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
|||||||
@@ -1,319 +1,323 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { openModal, closeModal } from 'svelte-modals';
|
import { openModal, closeModal } from 'svelte-modals';
|
||||||
import { user } from '$lib/stores/user';
|
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||||
import { page } from '$app/stores';
|
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
import Spinner from '$lib/components/Spinner.svelte';
|
||||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
import { slide } from 'svelte/transition';
|
||||||
import Spinner from '$lib/components/Spinner.svelte';
|
import { cubicOut } from 'svelte/easing';
|
||||||
import { slide } from 'svelte/transition';
|
|
||||||
import { cubicOut } from 'svelte/easing';
|
|
||||||
|
|
||||||
import type { SystemInformation, Analytics } from '$lib/types/models';
|
import type { SystemInformation, Analytics } from '$lib/types/models';
|
||||||
import { socket } from '$lib/stores/socket';
|
import { socket } from '$lib/stores/socket';
|
||||||
import { api } from '$lib/api';
|
import { api } from '$lib/api';
|
||||||
import { convertSeconds } from '$lib/utilities';
|
import { convertSeconds } from '$lib/utilities';
|
||||||
|
|
||||||
import { useFeatureFlags } from '$lib/stores/featureFlags';
|
import { useFeatureFlags } from '$lib/stores/featureFlags';
|
||||||
import {
|
import {
|
||||||
Cancel,
|
Cancel,
|
||||||
Power,
|
Power,
|
||||||
FactoryReset,
|
FactoryReset,
|
||||||
Sleep,
|
Sleep,
|
||||||
Health,
|
Health,
|
||||||
CPU,
|
CPU,
|
||||||
SDK,
|
SDK,
|
||||||
CPP,
|
CPP,
|
||||||
Speed,
|
Speed,
|
||||||
Heap,
|
Heap,
|
||||||
Pyramid,
|
Pyramid,
|
||||||
Sketch,
|
Sketch,
|
||||||
Flash,
|
Flash,
|
||||||
Folder,
|
Folder,
|
||||||
Temperature,
|
Temperature,
|
||||||
Stopwatch
|
Stopwatch
|
||||||
} from '$lib/components/icons';
|
} from '$lib/components/icons';
|
||||||
|
|
||||||
const features = useFeatureFlags();
|
const features = useFeatureFlags();
|
||||||
|
|
||||||
let systemInformation: SystemInformation;
|
let systemInformation: SystemInformation;
|
||||||
|
|
||||||
async function getSystemStatus() {
|
async function getSystemStatus() {
|
||||||
const result = await api.get<SystemInformation>('/api/systemStatus');
|
const result = await api.get<SystemInformation>('/api/systemStatus');
|
||||||
if (result.isErr()) {
|
if (result.isErr()) {
|
||||||
console.error('Error:', result.inner);
|
console.error('Error:', result.inner);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
systemInformation = result.inner;
|
systemInformation = result.inner;
|
||||||
return systemInformation;
|
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) =>
|
const handleSystemData = (data: Analytics) =>
|
||||||
(systemInformation = { ...systemInformation, ...data });
|
(systemInformation = { ...systemInformation, ...data });
|
||||||
|
|
||||||
const postRestart = async () => await api.post('/api/restart');
|
const postRestart = async () => await api.post('/api/restart');
|
||||||
|
|
||||||
function confirmRestart() {
|
function confirmRestart() {
|
||||||
openModal(ConfirmDialog, {
|
openModal(ConfirmDialog, {
|
||||||
title: 'Confirm Restart',
|
title: 'Confirm Restart',
|
||||||
message: 'Are you sure you want to restart the device?',
|
message: 'Are you sure you want to restart the device?',
|
||||||
labels: {
|
labels: {
|
||||||
cancel: { label: 'Abort', icon: Cancel },
|
cancel: { label: 'Abort', icon: Cancel },
|
||||||
confirm: { label: 'Restart', icon: Power }
|
confirm: { label: 'Restart', icon: Power }
|
||||||
},
|
},
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
closeModal();
|
closeModal();
|
||||||
postRestart();
|
postRestart();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmReset() {
|
function confirmReset() {
|
||||||
openModal(ConfirmDialog, {
|
openModal(ConfirmDialog, {
|
||||||
title: 'Confirm Factory Reset',
|
title: 'Confirm Factory Reset',
|
||||||
message: 'Are you sure you want to reset the device to its factory defaults?',
|
message: 'Are you sure you want to reset the device to its factory defaults?',
|
||||||
labels: {
|
labels: {
|
||||||
cancel: { label: 'Abort', icon: Cancel },
|
cancel: { label: 'Abort', icon: Cancel },
|
||||||
confirm: { label: 'Factory Reset', icon: FactoryReset }
|
confirm: { label: 'Factory Reset', icon: FactoryReset }
|
||||||
},
|
},
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
closeModal();
|
closeModal();
|
||||||
postFactoryReset();
|
postFactoryReset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmSleep() {
|
function confirmSleep() {
|
||||||
openModal(ConfirmDialog, {
|
openModal(ConfirmDialog, {
|
||||||
title: 'Confirm Going to Sleep',
|
title: 'Confirm Going to Sleep',
|
||||||
message: 'Are you sure you want to put the device into sleep?',
|
message: 'Are you sure you want to put the device into sleep?',
|
||||||
labels: {
|
labels: {
|
||||||
cancel: { label: 'Abort', icon: Cancel },
|
cancel: { label: 'Abort', icon: Cancel },
|
||||||
confirm: { label: 'Sleep', icon: Sleep }
|
confirm: { label: 'Sleep', icon: Sleep }
|
||||||
},
|
},
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
closeModal();
|
closeModal();
|
||||||
postSleep();
|
postSleep();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SettingsCard collapsible={false}>
|
<SettingsCard collapsible={false}>
|
||||||
<Health slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
<Health slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||||
<span slot="title">System Status</span>
|
<span slot="title">System Status</span>
|
||||||
|
|
||||||
<div class="w-full overflow-x-auto">
|
<div class="w-full overflow-x-auto">
|
||||||
{#await getSystemStatus()}
|
{#await getSystemStatus()}
|
||||||
<Spinner />
|
<Spinner />
|
||||||
{:then nothing}
|
{:then nothing}
|
||||||
<div
|
<div
|
||||||
class="flex w-full flex-col space-y-1"
|
class="flex w-full flex-col space-y-1"
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||||
>
|
>
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<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">
|
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||||
<CPU class="text-primary-content h-auto w-full scale-75" />
|
<CPU class="text-primary-content h-auto w-full scale-75" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">Chip</div>
|
<div class="font-bold">Chip</div>
|
||||||
<div class="text-sm opacity-75">
|
<div class="text-sm opacity-75">
|
||||||
{systemInformation.cpu_type} Rev {systemInformation.cpu_rev}
|
{systemInformation.cpu_type} Rev {systemInformation.cpu_rev}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<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">
|
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||||
<SDK class="text-primary-content h-auto w-full scale-75" />
|
<SDK class="text-primary-content h-auto w-full scale-75" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">SDK Version</div>
|
<div class="font-bold">SDK Version</div>
|
||||||
<div class="text-sm opacity-75">
|
<div class="text-sm opacity-75">
|
||||||
ESP-IDF {systemInformation.sdk_version} / Arduino {systemInformation.arduino_version}
|
ESP-IDF {systemInformation.sdk_version} / Arduino {systemInformation.arduino_version}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<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">
|
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||||
<CPP class="text-primary-content h-auto w-full scale-75" />
|
<CPP class="text-primary-content h-auto w-full scale-75" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">Firmware Version</div>
|
<div class="font-bold">Firmware Version</div>
|
||||||
<div class="text-sm opacity-75">
|
<div class="text-sm opacity-75">
|
||||||
{systemInformation.firmware_version}
|
{systemInformation.firmware_version}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<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">
|
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||||
<Speed class="text-primary-content h-auto w-full scale-75" />
|
<Speed class="text-primary-content h-auto w-full scale-75" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">CPU Frequency</div>
|
<div class="font-bold">CPU Frequency</div>
|
||||||
<div class="text-sm opacity-75">
|
<div class="text-sm opacity-75">
|
||||||
{systemInformation.cpu_freq_mhz} MHz {systemInformation.cpu_cores == 2
|
{systemInformation.cpu_freq_mhz} MHz {systemInformation.cpu_cores == 2 ?
|
||||||
? 'Dual Core'
|
'Dual Core'
|
||||||
: 'Single Core'}
|
: 'Single Core'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<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">
|
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||||
<Heap class="text-primary-content h-auto w-full scale-75" />
|
<Heap class="text-primary-content h-auto w-full scale-75" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">Heap (Free / Max Alloc)</div>
|
<div class="font-bold">Heap (Free / Max Alloc)</div>
|
||||||
<div class="text-sm opacity-75">
|
<div class="text-sm opacity-75">
|
||||||
{systemInformation.free_heap.toLocaleString('en-US')} / {systemInformation.max_alloc_heap.toLocaleString(
|
{systemInformation.free_heap.toLocaleString('en-US')} / {systemInformation.max_alloc_heap.toLocaleString(
|
||||||
'en-US'
|
'en-US'
|
||||||
)} bytes
|
)} bytes
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<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">
|
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||||
<Pyramid class="text-primary-content h-auto w-full scale-75" />
|
<Pyramid class="text-primary-content h-auto w-full scale-75" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">PSRAM (Size / Free)</div>
|
<div class="font-bold">PSRAM (Size / Free)</div>
|
||||||
<div class="text-sm opacity-75">
|
<div class="text-sm opacity-75">
|
||||||
{systemInformation.psram_size.toLocaleString('en-US')} / {systemInformation.psram_size.toLocaleString(
|
{systemInformation.psram_size.toLocaleString('en-US')} / {systemInformation.psram_size.toLocaleString(
|
||||||
'en-US'
|
'en-US'
|
||||||
)} bytes
|
)} bytes
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<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">
|
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||||
<Sketch class="text-primary-content h-auto w-full scale-75" />
|
<Sketch class="text-primary-content h-auto w-full scale-75" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">Sketch (Used / Free)</div>
|
<div class="font-bold">Sketch (Used / Free)</div>
|
||||||
<div class="flex flex-wrap justify-start gap-1 text-sm opacity-75">
|
<div class="flex flex-wrap justify-start gap-1 text-sm opacity-75">
|
||||||
<span>
|
<span>
|
||||||
{(
|
{(
|
||||||
(systemInformation.sketch_size / systemInformation.free_sketch_space) *
|
(systemInformation.sketch_size /
|
||||||
100
|
systemInformation.free_sketch_space) *
|
||||||
).toFixed(1)} % of
|
100
|
||||||
{(systemInformation.free_sketch_space / 1000000).toLocaleString('en-US')} MB used
|
).toFixed(1)} % of
|
||||||
</span>
|
{(systemInformation.free_sketch_space / 1000000).toLocaleString(
|
||||||
|
'en-US'
|
||||||
|
)} MB used
|
||||||
|
</span>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
({(
|
({(
|
||||||
(systemInformation.free_sketch_space - systemInformation.sketch_size) /
|
(systemInformation.free_sketch_space -
|
||||||
1000000
|
systemInformation.sketch_size) /
|
||||||
).toLocaleString('en-US')} MB free)
|
1000000
|
||||||
</span>
|
).toLocaleString('en-US')} MB free)
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<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">
|
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||||
<Flash class="text-primary-content h-auto w-full scale-75" />
|
<Flash class="text-primary-content h-auto w-full scale-75" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">Flash Chip (Size / Speed)</div>
|
<div class="font-bold">Flash Chip (Size / Speed)</div>
|
||||||
<div class="text-sm opacity-75">
|
<div class="text-sm opacity-75">
|
||||||
{(systemInformation.flash_chip_size / 1000000).toLocaleString('en-US')} MB / {(
|
{(systemInformation.flash_chip_size / 1000000).toLocaleString('en-US')} MB
|
||||||
systemInformation.flash_chip_speed / 1000000
|
/ {(systemInformation.flash_chip_speed / 1000000).toLocaleString(
|
||||||
).toLocaleString('en-US')} MHz
|
'en-US'
|
||||||
</div>
|
)} MHz
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<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">
|
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||||
<Folder class="text-primary-content h-auto w-full scale-75" />
|
<Folder class="text-primary-content h-auto w-full scale-75" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">File System (Used / Total)</div>
|
<div class="font-bold">File System (Used / Total)</div>
|
||||||
<div class="flex flex-wrap justify-start gap-1 text-sm opacity-75">
|
<div class="flex flex-wrap justify-start gap-1 text-sm opacity-75">
|
||||||
<span
|
<span
|
||||||
>{((systemInformation.fs_used / systemInformation.fs_total) * 100).toFixed(1)} % of {(
|
>{(
|
||||||
systemInformation.fs_total / 1000000
|
(systemInformation.fs_used / systemInformation.fs_total) *
|
||||||
).toLocaleString('en-US')} MB used</span
|
100
|
||||||
>
|
).toFixed(1)} % of {(
|
||||||
|
systemInformation.fs_total / 1000000
|
||||||
|
).toLocaleString('en-US')} MB used</span
|
||||||
|
>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
>({(
|
>({(
|
||||||
(systemInformation.fs_total - systemInformation.fs_used) /
|
(systemInformation.fs_total - systemInformation.fs_used) /
|
||||||
1000000
|
1000000
|
||||||
).toLocaleString('en-US')}
|
).toLocaleString('en-US')}
|
||||||
MB free)</span
|
MB free)</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<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">
|
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||||
<Temperature class="text-primary-content h-auto w-full scale-75" />
|
<Temperature class="text-primary-content h-auto w-full scale-75" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">Core Temperature</div>
|
<div class="font-bold">Core Temperature</div>
|
||||||
<div class="text-sm opacity-75">
|
<div class="text-sm opacity-75">
|
||||||
{systemInformation.core_temp == 53.33
|
{systemInformation.core_temp == 53.33 ?
|
||||||
? 'NaN'
|
'NaN'
|
||||||
: systemInformation.core_temp.toFixed(2) + ' °C'}
|
: systemInformation.core_temp.toFixed(2) + ' °C'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<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">
|
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
||||||
<Stopwatch class="text-primary-content h-auto w-full scale-75" />
|
<Stopwatch class="text-primary-content h-auto w-full scale-75" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">Uptime</div>
|
<div class="font-bold">Uptime</div>
|
||||||
<div class="text-sm opacity-75">
|
<div class="text-sm opacity-75">
|
||||||
{convertSeconds(systemInformation.uptime)}
|
{convertSeconds(systemInformation.uptime)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<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">
|
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
||||||
<Power class="text-primary-content h-auto w-full scale-75" />
|
<Power class="text-primary-content h-auto w-full scale-75" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">Reset Reason</div>
|
<div class="font-bold">Reset Reason</div>
|
||||||
<div class="text-sm opacity-75">
|
<div class="text-sm opacity-75">
|
||||||
{systemInformation.cpu_reset_reason}
|
{systemInformation.cpu_reset_reason}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex flex-wrap justify-end gap-2">
|
<div class="mt-4 flex flex-wrap justify-end gap-2">
|
||||||
{#if $features.sleep}
|
{#if $features.sleep}
|
||||||
<button class="btn btn-primary inline-flex items-center" on:click={confirmSleep}
|
<button class="btn btn-primary inline-flex items-center" on:click={confirmSleep}>
|
||||||
><Sleep class="mr-2 h-5 w-5" /><span>Sleep</span></button
|
<Sleep class="mr-2 h-5 w-5" /><span>Sleep</span>
|
||||||
>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !$features.security || $user.admin}
|
<button class="btn btn-primary inline-flex items-center" on:click={confirmRestart}>
|
||||||
<button class="btn btn-primary inline-flex items-center" on:click={confirmRestart}
|
<Power class="mr-2 h-5 w-5" /><span>Restart</span>
|
||||||
><Power class="mr-2 h-5 w-5" /><span>Restart</span></button
|
</button>
|
||||||
>
|
<button class="btn btn-secondary inline-flex items-center" on:click={confirmReset}>
|
||||||
<button class="btn btn-secondary inline-flex items-center" on:click={confirmReset}
|
<FactoryReset class="mr-2 h-5 w-5" /><span>Factory Reset</span>
|
||||||
><FactoryReset class="mr-2 h-5 w-5" /><span>Factory Reset</span></button
|
</button>
|
||||||
>
|
</div>
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import UploadFirmware from './UploadFirmware.svelte';
|
import UploadFirmware from './UploadFirmware.svelte';
|
||||||
import GithubFirmwareManager from './GithubFirmwareManager.svelte';
|
import GithubFirmwareManager from './GithubFirmwareManager.svelte';
|
||||||
import { user } from '$lib/stores/user';
|
import { useFeatureFlags } from '$lib/stores';
|
||||||
import { useFeatureFlags } from '$lib/stores';
|
|
||||||
|
|
||||||
const features = useFeatureFlags();
|
const features = useFeatureFlags();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
|
<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)}
|
{#if $features.download_firmware}
|
||||||
<GithubFirmwareManager />
|
<GithubFirmwareManager />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $features.upload_firmware && (!$features.security || $user.admin)}
|
{#if $features.upload_firmware}
|
||||||
<UploadFirmware />
|
<UploadFirmware />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
import { PasswordInput } from '$lib/components/input';
|
import { PasswordInput } from '$lib/components/input';
|
||||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||||
import { user } from '$lib/stores/user';
|
import { notifications } from '$lib/components/toasts/notifications';
|
||||||
import { page } from '$app/stores';
|
import Spinner from '$lib/components/Spinner.svelte';
|
||||||
import { notifications } from '$lib/components/toasts/notifications';
|
import type { ApSettings, ApStatus } from '$lib/types/models';
|
||||||
import Spinner from '$lib/components/Spinner.svelte';
|
import { api } from '$lib/api';
|
||||||
import type { ApSettings, ApStatus } from '$lib/types/models';
|
import { useFeatureFlags } from '$lib/stores';
|
||||||
import { api } from '$lib/api';
|
import { AP, Devices, Home, MAC } from '$lib/components/icons';
|
||||||
import { useFeatureFlags } from '$lib/stores';
|
|
||||||
import { AP, Devices, Home, MAC } from '$lib/components/icons';
|
|
||||||
|
|
||||||
const features = useFeatureFlags();
|
const features = useFeatureFlags();
|
||||||
|
|
||||||
let apSettings: ApSettings;
|
let apSettings: ApSettings;
|
||||||
let apStatus: ApStatus;
|
let apStatus: ApStatus;
|
||||||
|
|
||||||
let formField: any;
|
let formField: any;
|
||||||
|
|
||||||
async function getAPStatus() {
|
async function getAPStatus() {
|
||||||
const result = await api.get<ApStatus>('/api/apStatus');
|
const result = await api.get<ApStatus>('/api/apStatus');
|
||||||
if (result.isErr()){
|
if (result.isErr()) {
|
||||||
console.error('Error:', result.inner);
|
console.error('Error:', result.inner);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
apStatus = result.inner
|
apStatus = result.inner;
|
||||||
return apStatus;
|
return apStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAPSettings() {
|
async function getAPSettings() {
|
||||||
const result = await api.get<ApSettings>('/api/apSetting');
|
const result = await api.get<ApSettings>('/api/apSetting');
|
||||||
if (result.isErr()){
|
if (result.isErr()) {
|
||||||
console.error('Error:', result.inner);
|
console.error('Error:', result.inner);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
apSettings = result.inner
|
apSettings = result.inner;
|
||||||
return apSettings;
|
return apSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
getAPStatus();
|
getAPStatus();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
onDestroy(() => clearInterval(interval));
|
onDestroy(() => clearInterval(interval));
|
||||||
|
|
||||||
onMount(() => {
|
onMount(getAPSettings);
|
||||||
if (!$features.security || $user.admin) {
|
|
||||||
getAPSettings();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let provisionMode = [
|
let provisionMode = [
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
text: `Always`
|
text: `Always`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
text: `When WiFi Disconnected`
|
text: `When WiFi Disconnected`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
text: `Never`
|
text: `Never`
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
let apStatusDescription = [
|
let apStatusDescription = [
|
||||||
{ bg_color: 'bg-success', text_color: 'text-success-content', description: 'Active' },
|
{ 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-error', text_color: 'text-error-content', description: 'Inactive' },
|
||||||
{ bg_color: 'bg-warning', text_color: 'text-warning-content', description: 'Lingering' }
|
{ bg_color: 'bg-warning', text_color: 'text-warning-content', description: 'Lingering' }
|
||||||
];
|
];
|
||||||
|
|
||||||
let formErrors = {
|
let formErrors = {
|
||||||
ssid: false,
|
ssid: false,
|
||||||
channel: false,
|
channel: false,
|
||||||
max_clients: false,
|
max_clients: false,
|
||||||
local_ip: false,
|
local_ip: false,
|
||||||
gateway_ip: false,
|
gateway_ip: false,
|
||||||
subnet_mask: false
|
subnet_mask: false
|
||||||
};
|
};
|
||||||
|
|
||||||
async function postAPSettings(data: ApSettings) {
|
async function postAPSettings(data: ApSettings) {
|
||||||
const result = await api.post<ApSettings>('/api/apSettings', data);
|
const result = await api.post<ApSettings>('/api/apSettings', data);
|
||||||
if (result.isErr()){
|
if (result.isErr()) {
|
||||||
notifications.error('User not authorized.', 3000);
|
notifications.error('User not authorized.', 3000);
|
||||||
console.error('Error:', result.inner);
|
console.error('Error:', result.inner);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
notifications.success('Access Point settings updated.', 3000);
|
notifications.success('Access Point settings updated.', 3000);
|
||||||
apSettings = result.inner
|
apSettings = result.inner;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmitAP() {
|
function handleSubmitAP() {
|
||||||
let valid = true;
|
let valid = true;
|
||||||
|
|
||||||
// Validate SSID
|
// Validate SSID
|
||||||
if (apSettings.ssid.length < 3 || apSettings.ssid.length > 32) {
|
if (apSettings.ssid.length < 3 || apSettings.ssid.length > 32) {
|
||||||
valid = false;
|
valid = false;
|
||||||
formErrors.ssid = true;
|
formErrors.ssid = true;
|
||||||
} else {
|
} else {
|
||||||
formErrors.ssid = false;
|
formErrors.ssid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Channel
|
// Validate Channel
|
||||||
let channel = Number(apSettings.channel);
|
let channel = Number(apSettings.channel);
|
||||||
if (1 > channel || channel > 13) {
|
if (1 > channel || channel > 13) {
|
||||||
valid = false;
|
valid = false;
|
||||||
formErrors.channel = true;
|
formErrors.channel = true;
|
||||||
} else {
|
} else {
|
||||||
formErrors.channel = false;
|
formErrors.channel = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate max_clients
|
// Validate max_clients
|
||||||
let maxClients = Number(apSettings.max_clients);
|
let maxClients = Number(apSettings.max_clients);
|
||||||
if (1 > maxClients || maxClients > 8) {
|
if (1 > maxClients || maxClients > 8) {
|
||||||
valid = false;
|
valid = false;
|
||||||
formErrors.max_clients = true;
|
formErrors.max_clients = true;
|
||||||
} else {
|
} else {
|
||||||
formErrors.max_clients = false;
|
formErrors.max_clients = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegEx for IPv4
|
// RegEx for IPv4
|
||||||
const regexExp =
|
const regexExp =
|
||||||
/\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/;
|
/\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/;
|
||||||
|
|
||||||
// Validate gateway IP
|
// Validate gateway IP
|
||||||
if (!regexExp.test(apSettings.gateway_ip)) {
|
if (!regexExp.test(apSettings.gateway_ip)) {
|
||||||
valid = false;
|
valid = false;
|
||||||
formErrors.gateway_ip = true;
|
formErrors.gateway_ip = true;
|
||||||
} else {
|
} else {
|
||||||
formErrors.gateway_ip = false;
|
formErrors.gateway_ip = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Subnet Mask
|
// Validate Subnet Mask
|
||||||
if (!regexExp.test(apSettings.subnet_mask)) {
|
if (!regexExp.test(apSettings.subnet_mask)) {
|
||||||
valid = false;
|
valid = false;
|
||||||
formErrors.subnet_mask = true;
|
formErrors.subnet_mask = true;
|
||||||
} else {
|
} else {
|
||||||
formErrors.subnet_mask = false;
|
formErrors.subnet_mask = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate local IP
|
// Validate local IP
|
||||||
if (!regexExp.test(apSettings.local_ip)) {
|
if (!regexExp.test(apSettings.local_ip)) {
|
||||||
valid = false;
|
valid = false;
|
||||||
formErrors.local_ip = true;
|
formErrors.local_ip = true;
|
||||||
} else {
|
} else {
|
||||||
formErrors.local_ip = false;
|
formErrors.local_ip = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit JSON to REST API
|
// Submit JSON to REST API
|
||||||
if (valid) {
|
if (valid) {
|
||||||
postAPSettings(apSettings);
|
postAPSettings(apSettings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SettingsCard collapsible={false}>
|
<SettingsCard collapsible={false}>
|
||||||
<AP slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
<AP slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||||
<span slot="title">Access Point</span>
|
<span slot="title">Access Point</span>
|
||||||
<div class="w-full overflow-x-auto">
|
<div class="w-full overflow-x-auto">
|
||||||
{#await getAPStatus()}
|
{#await getAPStatus()}
|
||||||
<Spinner />
|
<Spinner />
|
||||||
{:then nothing}
|
{:then nothing}
|
||||||
<div
|
<div
|
||||||
class="flex w-full flex-col space-y-1"
|
class="flex w-full flex-col space-y-1"
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||||
>
|
>
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||||
<div
|
<div
|
||||||
class="mask mask-hexagon h-auto w-10 {apStatusDescription[apStatus.status].bg_color}"
|
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>
|
<AP
|
||||||
<div>
|
class="h-auto w-full scale-75 {apStatusDescription[apStatus.status]
|
||||||
<div class="font-bold">Status</div>
|
.text_color}"
|
||||||
<div class="text-sm opacity-75">
|
/>
|
||||||
{apStatusDescription[apStatus.status].description}
|
</div>
|
||||||
</div>
|
<div>
|
||||||
</div>
|
<div class="font-bold">Status</div>
|
||||||
</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="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">
|
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
||||||
<Home class="text-primary-content h-auto w-full scale-75" />
|
<Home class="text-primary-content h-auto w-full scale-75" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">IP Address</div>
|
<div class="font-bold">IP Address</div>
|
||||||
<div class="text-sm opacity-75">
|
<div class="text-sm opacity-75">
|
||||||
{apStatus.ip_address}
|
{apStatus.ip_address}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<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">
|
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
||||||
<MAC class="text-primary-content h-auto w-full scale-75" />
|
<MAC class="text-primary-content h-auto w-full scale-75" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">MAC Address</div>
|
<div class="font-bold">MAC Address</div>
|
||||||
<div class="text-sm opacity-75">
|
<div class="text-sm opacity-75">
|
||||||
{apStatus.mac_address}
|
{apStatus.mac_address}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<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">
|
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
||||||
<Devices class="text-primary-content h-auto w-full scale-75" />
|
<Devices class="text-primary-content h-auto w-full scale-75" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">AP Clients</div>
|
<div class="font-bold">AP Clients</div>
|
||||||
<div class="text-sm opacity-75">
|
<div class="text-sm opacity-75">
|
||||||
{apStatus.station_num}
|
{apStatus.station_num}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !$features.security || $user.admin}
|
<div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden">
|
||||||
<div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden">
|
<div
|
||||||
<div
|
class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium"
|
||||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium"
|
>
|
||||||
>
|
Change AP Settings
|
||||||
Change AP Settings
|
</div>
|
||||||
</div>
|
{#await getAPSettings()}
|
||||||
{#await getAPSettings()}
|
<Spinner />
|
||||||
<Spinner />
|
{:then nothing}
|
||||||
{:then nothing}
|
<div
|
||||||
<div
|
class="flex flex-col gap-2 p-0"
|
||||||
class="flex flex-col gap-2 p-0"
|
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
>
|
||||||
>
|
<form
|
||||||
<form
|
class="grid w-full grid-cols-1 content-center gap-x-4 p-0s sm:grid-cols-2"
|
||||||
class="grid w-full grid-cols-1 content-center gap-x-4 p-0s sm:grid-cols-2"
|
on:submit|preventDefault={handleSubmitAP}
|
||||||
on:submit|preventDefault={handleSubmitAP}
|
novalidate
|
||||||
novalidate
|
bind:this={formField}
|
||||||
bind:this={formField}
|
>
|
||||||
>
|
<div>
|
||||||
<div>
|
<label class="label" for="apmode">
|
||||||
<label class="label" for="apmode">
|
<span class="label-text">Provide Access Point ...</span>
|
||||||
<span class="label-text">Provide Access Point ...</span>
|
</label>
|
||||||
</label>
|
<select
|
||||||
<select
|
class="select select-bordered w-full"
|
||||||
class="select select-bordered w-full"
|
id="apmode"
|
||||||
id="apmode"
|
bind:value={apSettings.provision_mode}
|
||||||
bind:value={apSettings.provision_mode}
|
>
|
||||||
>
|
{#each provisionMode as mode}
|
||||||
{#each provisionMode as mode}
|
<option value={mode.id}>
|
||||||
<option value={mode.id}>
|
{mode.text}
|
||||||
{mode.text}
|
</option>
|
||||||
</option>
|
{/each}
|
||||||
{/each}
|
</select>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<label class="label" for="ssid">
|
||||||
<label class="label" for="ssid">
|
<span class="label-text text-md">SSID</span>
|
||||||
<span class="label-text text-md">SSID</span>
|
</label>
|
||||||
</label>
|
<input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {formErrors.ssid
|
formErrors.ssid
|
||||||
? 'border-error border-2'
|
) ?
|
||||||
: ''}"
|
'border-error border-2'
|
||||||
bind:value={apSettings.ssid}
|
: ''}"
|
||||||
id="ssid"
|
bind:value={apSettings.ssid}
|
||||||
min="2"
|
id="ssid"
|
||||||
max="32"
|
min="2"
|
||||||
required
|
max="32"
|
||||||
/>
|
required
|
||||||
<label class="label" for="ssid">
|
/>
|
||||||
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
|
<label class="label" for="ssid">
|
||||||
>SSID must be between 2 and 32 characters long</span
|
<span
|
||||||
>
|
class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
|
||||||
</label>
|
>SSID must be between 2 and 32 characters long</span
|
||||||
</div>
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="label" for="pwd">
|
<label class="label" for="pwd">
|
||||||
<span class="label-text text-md">Password</span>
|
<span class="label-text text-md">Password</span>
|
||||||
</label>
|
</label>
|
||||||
<PasswordInput bind:value={apSettings.password} id="pwd" />
|
<PasswordInput bind:value={apSettings.password} id="pwd" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="label" for="channel">
|
<label class="label" for="channel">
|
||||||
<span class="label-text text-md">Preferred Channel</span>
|
<span class="label-text text-md">Preferred Channel</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="13"
|
max="13"
|
||||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {formErrors.channel
|
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||||
? 'border-error border-2'
|
formErrors.channel
|
||||||
: ''}"
|
) ?
|
||||||
bind:value={apSettings.channel}
|
'border-error border-2'
|
||||||
id="channel"
|
: ''}"
|
||||||
required
|
bind:value={apSettings.channel}
|
||||||
/>
|
id="channel"
|
||||||
<label class="label" for="channel">
|
required
|
||||||
<span class="label-text-alt text-error {formErrors.channel ? '' : 'hidden'}"
|
/>
|
||||||
>Must be channel 1 to 13</span
|
<label class="label" for="channel">
|
||||||
>
|
<span
|
||||||
</label>
|
class="label-text-alt text-error {formErrors.channel ? '' : (
|
||||||
</div>
|
'hidden'
|
||||||
|
)}">Must be channel 1 to 13</span
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="label" for="clients">
|
<label class="label" for="clients">
|
||||||
<span class="label-text text-md">Max Clients</span>
|
<span class="label-text text-md">Max Clients</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="8"
|
max="8"
|
||||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {formErrors.max_clients
|
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||||
? 'border-error border-2'
|
formErrors.max_clients
|
||||||
: ''}"
|
) ?
|
||||||
bind:value={apSettings.max_clients}
|
'border-error border-2'
|
||||||
id="clients"
|
: ''}"
|
||||||
required
|
bind:value={apSettings.max_clients}
|
||||||
/>
|
id="clients"
|
||||||
<label class="label" for="clients">
|
required
|
||||||
<span class="label-text-alt text-error {formErrors.max_clients ? '' : 'hidden'}"
|
/>
|
||||||
>Maximum 8 clients allowed</span
|
<label class="label" for="clients">
|
||||||
>
|
<span
|
||||||
</label>
|
class="label-text-alt text-error {formErrors.max_clients ? '' : (
|
||||||
</div>
|
'hidden'
|
||||||
|
)}">Maximum 8 clients allowed</span
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="label" for="localIP">
|
<label class="label" for="localIP">
|
||||||
<span class="label-text text-md">Local IP</span>
|
<span class="label-text text-md">Local IP</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full {formErrors.local_ip
|
class="input input-bordered w-full {formErrors.local_ip ?
|
||||||
? 'border-error border-2'
|
'border-error border-2'
|
||||||
: ''}"
|
: ''}"
|
||||||
minlength="7"
|
minlength="7"
|
||||||
maxlength="15"
|
maxlength="15"
|
||||||
size="15"
|
size="15"
|
||||||
bind:value={apSettings.local_ip}
|
bind:value={apSettings.local_ip}
|
||||||
id="localIP"
|
id="localIP"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<label class="label" for="localIP">
|
<label class="label" for="localIP">
|
||||||
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
|
<span
|
||||||
>Must be a valid IPv4 address</span
|
class="label-text-alt text-error {formErrors.local_ip ? '' : (
|
||||||
>
|
'hidden'
|
||||||
</label>
|
)}">Must be a valid IPv4 address</span
|
||||||
</div>
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="label" for="gateway">
|
<label class="label" for="gateway">
|
||||||
<span class="label-text text-md">Gateway IP</span>
|
<span class="label-text text-md">Gateway IP</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full {formErrors.gateway_ip
|
class="input input-bordered w-full {formErrors.gateway_ip ?
|
||||||
? 'border-error border-2'
|
'border-error border-2'
|
||||||
: ''}"
|
: ''}"
|
||||||
minlength="7"
|
minlength="7"
|
||||||
maxlength="15"
|
maxlength="15"
|
||||||
size="15"
|
size="15"
|
||||||
bind:value={apSettings.gateway_ip}
|
bind:value={apSettings.gateway_ip}
|
||||||
id="gateway"
|
id="gateway"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<label class="label" for="gateway">
|
<label class="label" for="gateway">
|
||||||
<span class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
|
<span
|
||||||
>Must be a valid IPv4 address</span
|
class="label-text-alt text-error {formErrors.gateway_ip ? '' : (
|
||||||
>
|
'hidden'
|
||||||
</label>
|
)}">Must be a valid IPv4 address</span
|
||||||
</div>
|
>
|
||||||
<div>
|
</label>
|
||||||
<label class="label" for="subnet">
|
</div>
|
||||||
<span class="label-text text-md">Subnet Mask</span>
|
<div>
|
||||||
</label>
|
<label class="label" for="subnet">
|
||||||
<input
|
<span class="label-text text-md">Subnet Mask</span>
|
||||||
type="text"
|
</label>
|
||||||
class="input input-bordered w-full {formErrors.subnet_mask
|
<input
|
||||||
? 'border-error border-2'
|
type="text"
|
||||||
: ''}"
|
class="input input-bordered w-full {formErrors.subnet_mask ?
|
||||||
minlength="7"
|
'border-error border-2'
|
||||||
maxlength="15"
|
: ''}"
|
||||||
size="15"
|
minlength="7"
|
||||||
bind:value={apSettings.subnet_mask}
|
maxlength="15"
|
||||||
id="subnet"
|
size="15"
|
||||||
required
|
bind:value={apSettings.subnet_mask}
|
||||||
/>
|
id="subnet"
|
||||||
<label class="label" for="subnet">
|
required
|
||||||
<span class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}"
|
/>
|
||||||
>Must be a valid IPv4 address</span
|
<label class="label" for="subnet">
|
||||||
>
|
<span
|
||||||
</label>
|
class="label-text-alt text-error {formErrors.subnet_mask ? '' : (
|
||||||
</div>
|
'hidden'
|
||||||
|
)}">Must be a valid IPv4 address</span
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label class="label my-auto cursor-pointer justify-start gap-4">
|
<label class="label my-auto cursor-pointer justify-start gap-4">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
bind:checked={apSettings.ssid_hidden}
|
bind:checked={apSettings.ssid_hidden}
|
||||||
class="checkbox checkbox-primary"
|
class="checkbox checkbox-primary"
|
||||||
/>
|
/>
|
||||||
<span class="">Hide SSID</span>
|
<span class="">Hide SSID</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="place-self-end">
|
<div class="place-self-end">
|
||||||
<button class="btn btn-primary" type="submit">Apply Settings</button>
|
<button class="btn btn-primary" type="submit">Apply Settings</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
|||||||
+265
-284
@@ -5,8 +5,6 @@
|
|||||||
import { openModal, closeModal } from 'svelte-modals';
|
import { openModal, closeModal } from 'svelte-modals';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
import { user } from '$lib/stores/user';
|
|
||||||
import { page } from '$app/stores';
|
|
||||||
import { notifications } from '$lib/components/toasts/notifications';
|
import { notifications } from '$lib/components/toasts/notifications';
|
||||||
import DragDropList, { VerticalDropZone, reorder, type DropEvent } from 'svelte-dnd-list';
|
import DragDropList, { VerticalDropZone, reorder, type DropEvent } from 'svelte-dnd-list';
|
||||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||||
@@ -441,342 +439,325 @@
|
|||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !$features.security || $user.admin}
|
<div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden">
|
||||||
<div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden">
|
<div
|
||||||
<div
|
class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium"
|
||||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium"
|
>
|
||||||
>
|
Saved Networks
|
||||||
Saved Networks
|
</div>
|
||||||
</div>
|
{#await getWifiSettings()}
|
||||||
{#await getWifiSettings()}
|
<Spinner />
|
||||||
<Spinner />
|
{:then nothing}
|
||||||
{:then nothing}
|
<div class="relative w-full overflow-visible">
|
||||||
<div class="relative w-full overflow-visible">
|
<button
|
||||||
<button
|
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-16"
|
||||||
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-16"
|
on:click={() => {
|
||||||
on:click={() => {
|
if (checkNetworkList()) {
|
||||||
if (checkNetworkList()) {
|
addNetwork();
|
||||||
addNetwork();
|
showNetworkEditor = true;
|
||||||
showNetworkEditor = true;
|
}
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Add class="h-6 w-6" /></button
|
||||||
<Add class="h-6 w-6" /></button
|
>
|
||||||
>
|
<button
|
||||||
<button
|
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0"
|
||||||
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0"
|
on:click={() => {
|
||||||
on:click={() => {
|
if (checkNetworkList()) {
|
||||||
if (checkNetworkList()) {
|
scanForNetworks();
|
||||||
scanForNetworks();
|
showNetworkEditor = true;
|
||||||
showNetworkEditor = true;
|
}
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Scan class="h-6 w-6" /></button
|
||||||
<Scan class="h-6 w-6" /></button
|
>
|
||||||
>
|
|
||||||
|
|
||||||
<div
|
|
||||||
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
|
<div
|
||||||
class="flex flex-col gap-2 p-0"
|
class="overflow-x-auto space-y-1"
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||||
>
|
>
|
||||||
<form
|
<DragDropList
|
||||||
class=""
|
id="networks"
|
||||||
on:submit|preventDefault={validateWiFiForm}
|
type={VerticalDropZone}
|
||||||
novalidate
|
itemSize={60}
|
||||||
bind:this={formField}
|
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
|
<div
|
||||||
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
|
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
|
||||||
|
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<label class="label" for="channel">
|
<label class="label" for="ssid">
|
||||||
<span class="label-text text-md">Host Name</span>
|
<span class="label-text text-md">SSID</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
min="1"
|
|
||||||
max="32"
|
|
||||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||||
formErrorhostname
|
formErrors.ssid
|
||||||
) ?
|
) ?
|
||||||
'border-error border-2'
|
'border-error border-2'
|
||||||
: ''}"
|
: ''}"
|
||||||
bind:value={wifiSettings.hostname}
|
bind:value={networkEditable.ssid}
|
||||||
id="channel"
|
id="ssid"
|
||||||
|
min="2"
|
||||||
|
max="32"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<label class="label" for="channel">
|
<label class="label" for="ssid">
|
||||||
<span
|
<span
|
||||||
class="label-text-alt text-error {formErrorhostname ? '' : (
|
class="label-text-alt text-error {formErrors.ssid ? '' : (
|
||||||
'hidden'
|
'hidden'
|
||||||
)}">Host name must be between 2 and 32 characters long</span
|
)}">SSID must be between 3 and 32 characters long</span
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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
|
<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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
bind:checked={wifiSettings.priority_RSSI}
|
bind:checked={static_ip_config}
|
||||||
class="checkbox checkbox-primary sm:-mb-5"
|
class="checkbox checkbox-primary sm:-mb-5"
|
||||||
/>
|
/>
|
||||||
<span class="sm:-mb-5">Connect to strongest WiFi</span>
|
<span class="sm:-mb-5">Static IP Config?</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
{#if static_ip_config}
|
||||||
{#if showNetworkEditor}
|
|
||||||
<div class="divider my-0" />
|
|
||||||
<div
|
<div
|
||||||
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
|
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<label class="label" for="ssid">
|
<label class="label" for="localIP">
|
||||||
<span class="label-text text-md">SSID</span>
|
<span class="label-text text-md">Local IP</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
class="input input-bordered w-full {formErrors.local_ip ?
|
||||||
formErrors.ssid
|
|
||||||
) ?
|
|
||||||
'border-error border-2'
|
'border-error border-2'
|
||||||
: ''}"
|
: ''}"
|
||||||
bind:value={networkEditable.ssid}
|
minlength="7"
|
||||||
id="ssid"
|
maxlength="15"
|
||||||
min="2"
|
size="15"
|
||||||
max="32"
|
bind:value={networkEditable.local_ip}
|
||||||
|
id="localIP"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<label class="label" for="ssid">
|
<label class="label" for="localIP">
|
||||||
<span
|
<span
|
||||||
class="label-text-alt text-error {formErrors.ssid ? ''
|
class="label-text-alt text-error {formErrors.local_ip ?
|
||||||
: 'hidden'}"
|
''
|
||||||
>SSID must be between 3 and 32 characters long</span
|
: '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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="label" for="pwd">
|
<label class="label" for="subnet">
|
||||||
<span class="label-text text-md">Password</span>
|
<span class="label-text text-md">Subnet Mask</span>
|
||||||
</label>
|
</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
|
<input
|
||||||
type="checkbox"
|
type="text"
|
||||||
bind:checked={static_ip_config}
|
class="input input-bordered w-full {formErrors.subnet_mask ?
|
||||||
class="checkbox checkbox-primary sm:-mb-5"
|
'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 class="label" for="subnet">
|
||||||
</label>
|
<span
|
||||||
</div>
|
class="label-text-alt text-error {(
|
||||||
{#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 {(
|
|
||||||
formErrors.subnet_mask
|
formErrors.subnet_mask
|
||||||
) ?
|
) ?
|
||||||
'border-error border-2'
|
''
|
||||||
: ''}"
|
: 'hidden'}"
|
||||||
minlength="7"
|
>
|
||||||
maxlength="15"
|
Must be a valid IPv4 address
|
||||||
size="15"
|
</span>
|
||||||
bind:value={networkEditable.subnet_mask}
|
</label>
|
||||||
required
|
|
||||||
/>
|
|
||||||
<label class="label" for="subnet">
|
|
||||||
<span
|
|
||||||
class="label-text-alt text-error {(
|
|
||||||
formErrors.subnet_mask
|
|
||||||
) ?
|
|
||||||
''
|
|
||||||
: 'hidden'}">Must be a valid IPv4 address</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="label" for="gateway">
|
|
||||||
<span class="label-text text-md">DNS 1</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="input input-bordered w-full {formErrors.dns_1 ?
|
|
||||||
'border-error border-2'
|
|
||||||
: ''}"
|
|
||||||
minlength="7"
|
|
||||||
maxlength="15"
|
|
||||||
size="15"
|
|
||||||
bind:value={networkEditable.dns_ip_1}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<label class="label" for="gateway">
|
|
||||||
<span
|
|
||||||
class="label-text-alt text-error {formErrors.dns_1 ?
|
|
||||||
''
|
|
||||||
: 'hidden'}">Must be a valid IPv4 address</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="label" for="subnet">
|
|
||||||
<span class="label-text text-md">DNS 2</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="input input-bordered w-full {formErrors.dns_2 ?
|
|
||||||
'border-error border-2'
|
|
||||||
: ''}"
|
|
||||||
minlength="7"
|
|
||||||
maxlength="15"
|
|
||||||
size="15"
|
|
||||||
bind:value={networkEditable.dns_ip_2}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<label class="label" for="subnet">
|
|
||||||
<span
|
|
||||||
class="label-text-alt text-error {formErrors.dns_2 ?
|
|
||||||
''
|
|
||||||
: 'hidden'}">Must be a valid IPv4 address</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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}
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="divider mb-2 mt-0" />
|
<div class="divider mb-2 mt-0" />
|
||||||
<div class="mx-4 flex flex-wrap justify-end gap-2">
|
<div class="mx-4 flex flex-wrap justify-end gap-2">
|
||||||
<button
|
<button class="btn btn-primary" type="submit" disabled={!showNetworkEditor}>
|
||||||
class="btn btn-primary"
|
{newNetwork ? 'Add Network' : 'Update Network'}
|
||||||
type="submit"
|
</button>
|
||||||
disabled={!showNetworkEditor}
|
<button class="btn btn-primary" type="button" on:click={validateHostName}>
|
||||||
>{newNetwork ? 'Add Network' : 'Update Network'}</button
|
Apply Settings
|
||||||
>
|
</button>
|
||||||
<button
|
</div>
|
||||||
class="btn btn-primary"
|
</form>
|
||||||
type="button"
|
</div>
|
||||||
on:click={validateHostName}>Apply Settings</button
|
{/await}
|
||||||
>
|
</div>
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{/await}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
|||||||
+20
-25
@@ -4,31 +4,26 @@
|
|||||||
|
|
||||||
The back end exposes a number of API endpoints which are referenced in the table below.
|
The back end exposes a number of API endpoints which are referenced in the table below.
|
||||||
|
|
||||||
| Method | Endpoint | Authentication | POST JSON Body | Info |
|
| Method | Endpoint | Authentication | POST JSON Body | Info |
|
||||||
| ------ | --------------------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
| ------ | -------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
||||||
| GET | /rest/features | `NONE_REQUIRED` | none | Tells the client which features of the UI should be use |
|
| GET | /rest/features | `NONE_REQUIRED` | none | Tells the client which features of the UI should be use |
|
||||||
| GET | /rest/ntpStatus | `IS_AUTHENTICATED` | none | Current NTP connection status |
|
| GET | /rest/ntpStatus | `IS_AUTHENTICATED` | none | Current NTP connection status |
|
||||||
| GET | /rest/ntpSettings | `IS_ADMIN` | none | Current NTP settings |
|
| GET | /rest/ntpSettings | `IS_ADMIN` | none | Current NTP settings |
|
||||||
| POST | /rest/ntpSettings | `IS_ADMIN` | `{"enabled": true,"server": "time.google.com","tz_label": "Europe/London","tz_format": "GMT0BST,M3.5.0/1,M10.5.0"}` | Update the NTP settings |
|
| POST | /rest/ntpSettings | `IS_ADMIN` | `{"enabled": true,"server": "time.google.com","tz_label": "Europe/London","tz_format": "GMT0BST,M3.5.0/1,M10.5.0"}` | Update the NTP settings |
|
||||||
| GET | /rest/apStatus | `IS_AUTHENTICATED` | none | Current AP status and client information |
|
| GET | /rest/apStatus | `IS_AUTHENTICATED` | none | Current AP status and client information |
|
||||||
| GET | /rest/apSettings | `IS_ADMIN` | none | Current AP settings |
|
| GET | /rest/apSettings | `IS_ADMIN` | none | Current AP settings |
|
||||||
| POST | /rest/apSettings | `IS_ADMIN` | `{"provision_mode": 1,"ssid": "ESP32-SvelteKit-e89f6d20372c","password": "esp-sveltekit","channel": 1,"ssid_hidden": false,"max_clients": 4,"local_ip": "192.168.4.1","gateway_ip": "192.168.4.1","subnet_mask": "255.255.255.0"}` | Update AP settings |
|
| POST | /rest/apSettings | `IS_ADMIN` | `{"provision_mode": 1,"ssid": "ESP32-SvelteKit-e89f6d20372c","password": "esp-sveltekit","channel": 1,"ssid_hidden": false,"max_clients": 4,"local_ip": "192.168.4.1","gateway_ip": "192.168.4.1","subnet_mask": "255.255.255.0"}` | Update AP settings |
|
||||||
| GET | /rest/wifiStatus | `IS_AUTHENTICATED` | none | Current status of the wifi client connection |
|
| GET | /rest/wifiStatus | `IS_AUTHENTICATED` | none | Current status of the wifi client connection |
|
||||||
| GET | /rest/scanNetworks | `IS_ADMIN` | none | Async Scan for Networks in Range |
|
| GET | /rest/scanNetworks | `IS_ADMIN` | none | Async Scan for Networks in Range |
|
||||||
| GET | /rest/listNetworks | `IS_ADMIN` | none | List networks in range after successful scanning. Otherwise triggers scanning. |
|
| GET | /rest/listNetworks | `IS_ADMIN` | none | List networks in range after successful scanning. Otherwise triggers scanning. |
|
||||||
| GET | /rest/wifiSettings | `IS_ADMIN` | none | Current WiFi settings |
|
| GET | /rest/wifiSettings | `IS_ADMIN` | none | Current WiFi settings |
|
||||||
| POST | /rest/wifiSettings | `IS_ADMIN` | `{"hostname":"esp32-f412fa4495f8","priority_RSSI":true,"wifi_networks":[{"ssid":"YourSSID","password":"YourPassword","static_ip_config":false}]}` | Update WiFi settings and credentials |
|
| POST | /rest/wifiSettings | `IS_ADMIN` | `{"hostname":"esp32-f412fa4495f8","priority_RSSI":true,"wifi_networks":[{"ssid":"YourSSID","password":"YourPassword","static_ip_config":false}]}` | Update WiFi settings and credentials |
|
||||||
| GET | /rest/systemStatus | `IS_AUTHENTICATED` | none | Get system information about the ESP. |
|
| GET | /rest/systemStatus | `IS_AUTHENTICATED` | none | Get system information about the ESP. |
|
||||||
| POST | /rest/restart | `IS_ADMIN` | none | Restart the ESP32 |
|
| POST | /rest/restart | `IS_ADMIN` | none | Restart the ESP32 |
|
||||||
| POST | /rest/factoryReset | `IS_ADMIN` | none | Reset the ESP32 and all settings to their default values |
|
| POST | /rest/factoryReset | `IS_ADMIN` | none | Reset the ESP32 and all settings to their default values |
|
||||||
| POST | /rest/uploadFirmware | `IS_ADMIN` | none | File upload of firmware.bin |
|
| POST | /rest/uploadFirmware | `IS_ADMIN` | none | File upload of firmware.bin |
|
||||||
| POST | /rest/signIn | `NONE_REQUIRED` | `{"password": "admin","username": "admin"}` | Signs a user in and returns access token |
|
| POST | /rest/sleep | `IS_AUTHENTICATED` | none | Puts the device in deep sleep mode |
|
||||||
| GET | /rest/securitySettings | `IS_ADMIN` | none | retrieves all user information and roles |
|
| POST | /rest/downloadUpdate | `IS_ADMIN` | `{"download_url": "https://github.com/theelims/ESP32-sveltekit/releases/download/v0.1.0/firmware_esp32s3.bin"}` | Download link for OTA. This requires a valid SSL certificate and will follow redirects. |
|
||||||
| POST | /rest/securitySettings | `IS_ADMIN` | `{"jwt_secret": "734cb5bb-5597b722", "users": [{"username": "admin", "password": "admin", "admin": true}, {"username": "guest", "password": "guest", "admin": false, }]}` | retrieves all user information and roles |
|
|
||||||
| GET | /rest/verifyAuthorization | `NONE_REQUIRED` | none | Verifies the content of the auth bearer token |
|
|
||||||
| GET | /rest/generateToken?username={username} | `IS_ADMIN` | `{"token": "734cb5bb-5597b722"}` | Generates a new JWT token for the user from username |
|
|
||||||
| POST | /rest/sleep | `IS_AUTHENTICATED` | none | Puts the device in deep sleep mode |
|
|
||||||
| POST | /rest/downloadUpdate | `IS_ADMIN` | `{"download_url": "https://github.com/theelims/ESP32-sveltekit/releases/download/v0.1.0/firmware_esp32s3.bin"}` | Download link for OTA. This requires a valid SSL certificate and will follow redirects. |
|
|
||||||
|
|
||||||
<!-- | HTTP Method | Endpoint | Description | Parameters |
|
<!-- | HTTP Method | Endpoint | Description | Parameters |
|
||||||
|-------------|----------------|----------------------------|---------------------------|
|
|-------------|----------------|----------------------------|---------------------------|
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
# Software description
|
# Software description
|
||||||
|
|
||||||
|
|
||||||
The software make use of a range of different libraries to enhance the functionality.
|
The software make use of a range of different libraries to enhance the functionality.
|
||||||
Up to date list can be seen in platformio.ini file.
|
Up to date list can be seen in platformio.ini file.
|
||||||
The libraries includes:
|
The libraries includes:
|
||||||
|
|
||||||
* Esp32SvelteKit
|
- Esp32SvelteKit
|
||||||
* PsychicHttp
|
- PsychicHttp
|
||||||
* ArduinoJson
|
- ArduinoJson
|
||||||
* Adafruit SSD1306
|
- Adafruit SSD1306
|
||||||
* Adafruit GFX Library
|
- Adafruit GFX Library
|
||||||
* Adafruit BusIO
|
- Adafruit BusIO
|
||||||
* Adafruit PWM Servo Driver Library
|
- Adafruit PWM Servo Driver Library
|
||||||
* Adafruit ADS1X15
|
- Adafruit ADS1X15
|
||||||
* Adafruit HMC5883 Unified
|
- Adafruit HMC5883 Unified
|
||||||
* Adafruit Unified Sensor
|
- Adafruit Unified Sensor
|
||||||
* I2Cdevlib-MPU6050
|
- I2Cdevlib-MPU6050
|
||||||
* NewPing
|
- NewPing
|
||||||
* SPI
|
- SPI
|
||||||
|
|
||||||
#### Structure
|
#### Structure
|
||||||
|
|
||||||
@@ -36,7 +35,6 @@ To dis-/enable the major feature defines are used. Define them in either feature
|
|||||||
| --- | --- | ---
|
| --- | --- | ---
|
||||||
| FT_BATTERY | Whether or not to use battery | 0
|
| FT_BATTERY | Whether or not to use battery | 0
|
||||||
| FT_NTP | Whether or not to use time server | 1
|
| FT_NTP | Whether or not to use time server | 1
|
||||||
| FT_SECURITY | Whether or not to use login system | 0
|
|
||||||
| FT_SLEEP | Whether or not include sleep management | 0
|
| FT_SLEEP | Whether or not include sleep management | 0
|
||||||
| FT_UPLOAD_FIRMWARE | Whether or not to use OAT | 0
|
| FT_UPLOAD_FIRMWARE | Whether or not to use OAT | 0
|
||||||
| FT_DOWNLOAD_FIRMWARE | Whether or not to use github for firmware updates | 0
|
| FT_DOWNLOAD_FIRMWARE | Whether or not to use github for firmware updates | 0
|
||||||
@@ -50,7 +48,9 @@ To dis-/enable the major feature defines are used. Define them in either feature
|
|||||||
### 📲 Controller
|
### 📲 Controller
|
||||||
|
|
||||||
The controller is a SvelteKit app, which main focus is to calibrate and control the robot.
|
The controller is a SvelteKit app, which main focus is to calibrate and control the robot.
|
||||||
|
|
||||||
<!-- Write about the emulation, stream, controls and link to the space issues -->
|
<!-- Write about the emulation, stream, controls and link to the space issues -->
|
||||||
|
|
||||||
It is made to be included and hosted by the robot.
|
It is made to be included and hosted by the robot.
|
||||||
Therefore there is placed a lot of thought behind the functionality and dependencies.
|
Therefore there is placed a lot of thought behind the functionality and dependencies.
|
||||||
|
|
||||||
@@ -58,22 +58,22 @@ Therefore there is placed a lot of thought behind the functionality and dependen
|
|||||||
|
|
||||||
For the development dependencies I choose the following
|
For the development dependencies I choose the following
|
||||||
|
|
||||||
| Dependencies | Description
|
| Dependencies | Description |
|
||||||
| --- | ---
|
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| SvelteKit | SvelteKit is an application framework built on top of Svelte, enhancing it with features like routing, server-side rendering, and static site generation. It streamlines the development process by integrating server-side capabilities with Svelte's client-side benefits. Furthermore it make the development process fast and enjoyable.
|
| SvelteKit | SvelteKit is an application framework built on top of Svelte, enhancing it with features like routing, server-side rendering, and static site generation. It streamlines the development process by integrating server-side capabilities with Svelte's client-side benefits. Furthermore it make the development process fast and enjoyable. |
|
||||||
| Vite | Vite is a frontend tool that is used for building fast and optimized web applications. Is serves code local during development and bundles assets for production
|
| Vite | Vite is a frontend tool that is used for building fast and optimized web applications. Is serves code local during development and bundles assets for production |
|
||||||
| Typescript | TypeScript's integration of static typing enhances code reliability and maintainability.
|
| Typescript | TypeScript's integration of static typing enhances code reliability and maintainability. |
|
||||||
| Tailwind CSS | Tailwind CSS accelerates web development with its utility-first approach, ensuring rapid styling and consistent design.
|
| Tailwind CSS | Tailwind CSS accelerates web development with its utility-first approach, ensuring rapid styling and consistent design. |
|
||||||
|
|
||||||
#### Libraries
|
#### Libraries
|
||||||
|
|
||||||
For the app functionality I choose the following:
|
For the app functionality I choose the following:
|
||||||
|
|
||||||
| Dependencies | Description
|
| Dependencies | Description |
|
||||||
| --- | ---
|
| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||||
| [Three](https://www.npmjs.com/package/three) | Easy to use, lightweight, cross-browser, general purpose 3D library.
|
| [Three](https://www.npmjs.com/package/three) | Easy to use, lightweight, cross-browser, general purpose 3D library. |
|
||||||
| [Urdf-loader](https://www.npmjs.com/package/urdf-loader) | Utilities for loading URDF files into THREE.js and a Web Component that loads and renders the model.
|
| [Urdf-loader](https://www.npmjs.com/package/urdf-loader) | Utilities for loading URDF files into THREE.js and a Web Component that loads and renders the model. |
|
||||||
| [Xacro-parser](https://www.npmjs.com/package/xacro-parser) | Javascript parser and loader for processing the ROS Xacro file format.
|
| [Xacro-parser](https://www.npmjs.com/package/xacro-parser) | Javascript parser and loader for processing the ROS Xacro file format. |
|
||||||
| [NippleJS](https://www.npmjs.com/package/nipplejs) | A vanilla virtual joystick for touch capable interfaces.
|
| [NippleJS](https://www.npmjs.com/package/nipplejs) | A vanilla virtual joystick for touch capable interfaces. |
|
||||||
| [Uzip](https://www.npmjs.com/package/uzip) | Simple, tiny and fast ZIP library.
|
| [Uzip](https://www.npmjs.com/package/uzip) | Simple, tiny and fast ZIP library. |
|
||||||
| [ChartJS](https://www.npmjs.com/package/chart.js) | Simple and flexible charting library.
|
| [ChartJS](https://www.npmjs.com/package/chart.js) | Simple and flexible charting library. |
|
||||||
|
|||||||
Reference in New Issue
Block a user