✅ Fixes build warning and errors
This commit is contained in:
+14
-14
@@ -2,21 +2,21 @@ export const prerender = false;
|
||||
export const ssr = false;
|
||||
|
||||
const registerFetchIntercept = async () => {
|
||||
const { fetch: originalFetch } = window;
|
||||
const fileService = (await import('$lib/services/file-service')).default;
|
||||
window.fetch = async (resource, config) => {
|
||||
let url = resource instanceof Request ? resource.url : resource.toString();
|
||||
let file = await fileService.getFile(url);
|
||||
return file.isOk() ? new Response(file.inner) : originalFetch(resource, config);
|
||||
};
|
||||
const { fetch: originalFetch } = window;
|
||||
const fileService = (await import('$lib/services/file-service')).default;
|
||||
window.fetch = async (resource, config) => {
|
||||
let url = resource instanceof Request ? resource.url : resource.toString();
|
||||
let file = await fileService?.getFile(url);
|
||||
return file?.isOk() ? new Response(file.inner) : originalFetch(resource, config);
|
||||
};
|
||||
};
|
||||
|
||||
export const load = async () => {
|
||||
await registerFetchIntercept();
|
||||
return {
|
||||
title: 'Spot micro controller',
|
||||
github: 'runeharlyk/SpotMicroESP32-Leika',
|
||||
app_name: 'Spot Micro Controller',
|
||||
copyright: '2024 Rune Harlyk'
|
||||
};
|
||||
await registerFetchIntercept();
|
||||
return {
|
||||
title: 'Spot micro controller',
|
||||
github: 'runeharlyk/SpotMicroESP32-Leika',
|
||||
app_name: 'Spot Micro Controller',
|
||||
copyright: '2025 Rune Harlyk',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
<script lang="ts">
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte'
|
||||
import { imu } from '$lib/stores/imu'
|
||||
import { Chart, registerables } from 'chart.js'
|
||||
import { cubicOut } from 'svelte/easing'
|
||||
import { slide } from 'svelte/transition'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { socket } from '$lib/stores'
|
||||
import type { IMU } from '$lib/types/models'
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags'
|
||||
import { Rotate3d } from '$lib/components/icons'
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { imu } from '$lib/stores/imu';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { socket } from '$lib/stores';
|
||||
import type { IMU } from '$lib/types/models';
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags';
|
||||
import { Rotate3d } from '$lib/components/icons';
|
||||
|
||||
Chart.register(...registerables)
|
||||
Chart.register(...registerables);
|
||||
|
||||
const features = useFeatureFlags()
|
||||
let intervalId: number
|
||||
const features = useFeatureFlags();
|
||||
let intervalId: ReturnType<typeof setInterval> | number;
|
||||
|
||||
let angleChartElement: HTMLCanvasElement = $state()
|
||||
let tempChartElement: HTMLCanvasElement = $state()
|
||||
let altitudeChartElement: HTMLCanvasElement = $state()
|
||||
let angleChartElement: HTMLCanvasElement;
|
||||
let tempChartElement: HTMLCanvasElement;
|
||||
let altitudeChartElement: HTMLCanvasElement;
|
||||
|
||||
let angleChart: Chart
|
||||
let tempChart: Chart
|
||||
let altitudeChart: Chart
|
||||
let angleChart: Chart;
|
||||
let tempChart: Chart;
|
||||
let altitudeChart: Chart;
|
||||
|
||||
const getChartColors = () => {
|
||||
const style = getComputedStyle(document.body)
|
||||
const style = getComputedStyle(document.body);
|
||||
return {
|
||||
primary: style.getPropertyValue('--color-primary'),
|
||||
secondary: style.getPropertyValue('--color-secondary'),
|
||||
accent: style.getPropertyValue('--color-accent'),
|
||||
background: style.getPropertyValue('--color-background')
|
||||
}
|
||||
}
|
||||
background: style.getPropertyValue('--color-background'),
|
||||
};
|
||||
};
|
||||
|
||||
const createBaseChartConfig = (bgColor: string) => ({
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: true },
|
||||
tooltip: { mode: 'index', intersect: false }
|
||||
tooltip: { mode: 'index', intersect: false },
|
||||
},
|
||||
elements: { point: { radius: 1 } },
|
||||
scales: {
|
||||
x: {
|
||||
grid: { color: bgColor },
|
||||
ticks: { color: bgColor },
|
||||
display: false
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
@@ -54,14 +54,14 @@
|
||||
max: 10,
|
||||
grid: { color: bgColor },
|
||||
ticks: { color: bgColor },
|
||||
border: { color: bgColor }
|
||||
}
|
||||
}
|
||||
})
|
||||
border: { color: bgColor },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const initializeCharts = () => {
|
||||
const colors = getChartColors()
|
||||
const baseConfig = createBaseChartConfig(colors.background)
|
||||
const colors = getChartColors();
|
||||
const baseConfig = createBaseChartConfig(colors.background);
|
||||
|
||||
angleChart = new Chart(angleChartElement, {
|
||||
type: 'line',
|
||||
@@ -73,7 +73,7 @@
|
||||
backgroundColor: colors.primary,
|
||||
borderWidth: 2,
|
||||
data: $imu.x,
|
||||
yAxisID: 'y'
|
||||
yAxisID: 'y',
|
||||
},
|
||||
{
|
||||
label: 'y',
|
||||
@@ -81,7 +81,7 @@
|
||||
backgroundColor: colors.secondary,
|
||||
borderWidth: 2,
|
||||
data: $imu.y,
|
||||
yAxisID: 'y'
|
||||
yAxisID: 'y',
|
||||
},
|
||||
{
|
||||
label: 'z',
|
||||
@@ -89,9 +89,9 @@
|
||||
backgroundColor: colors.accent,
|
||||
borderWidth: 2,
|
||||
data: $imu.z,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
yAxisID: 'y',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
...baseConfig,
|
||||
@@ -103,12 +103,12 @@
|
||||
display: true,
|
||||
text: 'Angle [°]',
|
||||
color: colors.background,
|
||||
font: { size: 16, weight: 'bold' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
font: { size: 16, weight: 'bold' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
tempChart = new Chart(tempChartElement, {
|
||||
type: 'line',
|
||||
@@ -120,9 +120,9 @@
|
||||
backgroundColor: colors.secondary,
|
||||
borderWidth: 2,
|
||||
data: $imu.bmp_temp,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
yAxisID: 'y',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
...baseConfig,
|
||||
@@ -134,12 +134,12 @@
|
||||
display: true,
|
||||
text: 'Temperature [C°]',
|
||||
color: colors.background,
|
||||
font: { size: 16, weight: 'bold' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
font: { size: 16, weight: 'bold' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
altitudeChart = new Chart(altitudeChartElement, {
|
||||
type: 'line',
|
||||
@@ -151,9 +151,9 @@
|
||||
backgroundColor: colors.primary,
|
||||
borderWidth: 2,
|
||||
data: $imu.altitude,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
yAxisID: 'y',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
...baseConfig,
|
||||
@@ -165,60 +165,60 @@
|
||||
display: true,
|
||||
text: 'Altitude [M]',
|
||||
color: colors.background,
|
||||
font: { size: 16, weight: 'bold' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
font: { size: 16, weight: 'bold' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const updateChartData = (chart: Chart, data: number[], label: string) => {
|
||||
chart.data.labels = data
|
||||
chart.data.datasets[0].data = data
|
||||
chart.options.scales!.y!.min = Math.min(...data) - 1
|
||||
chart.options.scales!.y!.max = Math.max(...data) + 1
|
||||
chart.update('none')
|
||||
}
|
||||
chart.data.labels = data;
|
||||
chart.data.datasets[0].data = data;
|
||||
chart.options.scales!.y!.min = Math.min(...data) - 1;
|
||||
chart.options.scales!.y!.max = Math.max(...data) + 1;
|
||||
chart.update('none');
|
||||
};
|
||||
|
||||
const updateData = () => {
|
||||
if ($features.imu) {
|
||||
angleChart.data.labels = $imu.x
|
||||
angleChart.data.datasets[0].data = $imu.x
|
||||
angleChart.data.datasets[1].data = $imu.y
|
||||
angleChart.data.datasets[2].data = $imu.z
|
||||
angleChart.data.labels = $imu.x;
|
||||
angleChart.data.datasets[0].data = $imu.x;
|
||||
angleChart.data.datasets[1].data = $imu.y;
|
||||
angleChart.data.datasets[2].data = $imu.z;
|
||||
|
||||
const allValues = [...$imu.x, ...$imu.y, ...$imu.z]
|
||||
angleChart.options.scales!.y!.min = Math.min(...allValues) - 1
|
||||
angleChart.options.scales!.y!.max = Math.max(...allValues) + 1
|
||||
angleChart.update('none')
|
||||
const allValues = [...$imu.x, ...$imu.y, ...$imu.z];
|
||||
angleChart.options.scales!.y!.min = Math.min(...allValues) - 1;
|
||||
angleChart.options.scales!.y!.max = Math.max(...allValues) + 1;
|
||||
angleChart.update('none');
|
||||
}
|
||||
|
||||
if ($features.bmp) {
|
||||
updateChartData(tempChart, $imu.bmp_temp, 'Temperature')
|
||||
updateChartData(altitudeChart, $imu.altitude, 'Altitude')
|
||||
updateChartData(tempChart, $imu.bmp_temp, 'Temperature');
|
||||
updateChartData(altitudeChart, $imu.altitude, 'Altitude');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
socket.on('imu', (data: IMU) => {
|
||||
console.log(data)
|
||||
imu.addData(data)
|
||||
})
|
||||
console.log(data);
|
||||
imu.addData(data);
|
||||
});
|
||||
|
||||
initializeCharts()
|
||||
intervalId = setInterval(updateData, 200)
|
||||
})
|
||||
initializeCharts();
|
||||
intervalId = setInterval(updateData, 200);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
socket.off('imu')
|
||||
clearInterval(intervalId)
|
||||
})
|
||||
socket.off('imu');
|
||||
clearInterval(intervalId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
{#snippet icon()}
|
||||
<Rotate3d class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
<Rotate3d class="flex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>IMU</span>
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
<script lang="ts">
|
||||
import SystemMetrics from './SystemMetrics.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags';
|
||||
|
||||
const features = useFeatureFlags();
|
||||
|
||||
if (!$features.analytics) {
|
||||
goto('/');
|
||||
}
|
||||
import SystemMetrics from './SystemMetrics.svelte';
|
||||
</script>
|
||||
|
||||
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
|
||||
<SystemMetrics />
|
||||
<SystemMetrics />
|
||||
</div>
|
||||
|
||||
@@ -1,373 +1,369 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
|
||||
import { daisyColor } from '$lib/utilities';
|
||||
import { analytics } from '$lib/stores/analytics';
|
||||
import { Metrics } from '$lib/components/icons';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
|
||||
Chart.register(...registerables);
|
||||
import { daisyColor } from '$lib/utilities';
|
||||
import { analytics } from '$lib/stores/analytics';
|
||||
import { Metrics } from '$lib/components/icons';
|
||||
|
||||
let cpuChartElement: HTMLCanvasElement = $state();
|
||||
let cpuChart: Chart;
|
||||
Chart.register(...registerables);
|
||||
|
||||
let heapChartElement: HTMLCanvasElement = $state();
|
||||
let heapChart: Chart;
|
||||
let cpuChartElement: HTMLCanvasElement;
|
||||
let cpuChart: Chart;
|
||||
|
||||
let filesystemChartElement: HTMLCanvasElement = $state();
|
||||
let filesystemChart: Chart;
|
||||
let heapChartElement: HTMLCanvasElement;
|
||||
let heapChart: Chart;
|
||||
|
||||
let temperatureChartElement: HTMLCanvasElement = $state();
|
||||
let temperatureChart: Chart;
|
||||
let filesystemChartElement: HTMLCanvasElement;
|
||||
let filesystemChart: Chart;
|
||||
|
||||
onMount(() => {
|
||||
cpuChart = new Chart(cpuChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.cpu_usage,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Cpu usage core 0',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.cpu0_usage,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: 'Cpu usage core 1',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.cpu1_usage,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: 'Cpu usage total',
|
||||
borderColor: daisyColor('--s'),
|
||||
backgroundColor: daisyColor('--s', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.cpu_usage,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Cpu usage [%]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
heapChart = new Chart(heapChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Used Heap',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.used_heap,
|
||||
fill:true,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Heap [kb]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: Math.round($analytics.total_heap[0]),
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
filesystemChart = new Chart(filesystemChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'File System Used',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.fs_used,
|
||||
fill:true,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'File System [kb]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: Math.round($analytics.fs_total[0]),
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
temperatureChart = new Chart(temperatureChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Core Temperature',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.core_temp,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Core Temperature [°C]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
position: 'left',
|
||||
suggestedMin: 20,
|
||||
suggestedMax: 100,
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
setInterval(updateData, 500);
|
||||
});
|
||||
let temperatureChartElement: HTMLCanvasElement;
|
||||
let temperatureChart: Chart;
|
||||
|
||||
function updateData() {
|
||||
cpuChart.data.labels = $analytics.cpu_usage;
|
||||
cpuChart.data.datasets[0].data = $analytics.cpu0_usage;
|
||||
cpuChart.data.datasets[1].data = $analytics.cpu1_usage;
|
||||
cpuChart.data.datasets[2].data = $analytics.cpu_usage;
|
||||
cpuChart.update('none');
|
||||
onMount(() => {
|
||||
cpuChart = new Chart(cpuChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.cpu_usage,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Cpu usage core 0',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.cpu0_usage,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
{
|
||||
label: 'Cpu usage core 1',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.cpu1_usage,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
{
|
||||
label: 'Cpu usage total',
|
||||
borderColor: daisyColor('--s'),
|
||||
backgroundColor: daisyColor('--s', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.cpu_usage,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10),
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc'),
|
||||
},
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Cpu usage [%]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold',
|
||||
},
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc'),
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
heapChart = new Chart(heapChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Used Heap',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.used_heap,
|
||||
fill: true,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10),
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc'),
|
||||
},
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Heap [kb]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold',
|
||||
},
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: Math.round($analytics.total_heap[0]),
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc'),
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
filesystemChart = new Chart(filesystemChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'File System Used',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.fs_used,
|
||||
fill: true,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10),
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc'),
|
||||
},
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'File System [kb]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold',
|
||||
},
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: Math.round($analytics.fs_total[0]),
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc'),
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
temperatureChart = new Chart(temperatureChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Core Temperature',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.core_temp,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10),
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc'),
|
||||
},
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Core Temperature [°C]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold',
|
||||
},
|
||||
},
|
||||
position: 'left',
|
||||
suggestedMin: 20,
|
||||
suggestedMax: 100,
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc'),
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
setInterval(updateData, 500);
|
||||
});
|
||||
|
||||
heapChart.data.labels = $analytics.uptime;
|
||||
heapChart.data.datasets[0].data = $analytics.used_heap;
|
||||
heapChart.options.scales!.y!.max = Math.ceil($analytics.total_heap[0]);
|
||||
heapChart.update('none');
|
||||
function updateData() {
|
||||
cpuChart.data.labels = $analytics.cpu_usage;
|
||||
cpuChart.data.datasets[0].data = $analytics.cpu0_usage;
|
||||
cpuChart.data.datasets[1].data = $analytics.cpu1_usage;
|
||||
cpuChart.data.datasets[2].data = $analytics.cpu_usage;
|
||||
cpuChart.update('none');
|
||||
|
||||
filesystemChart.data.labels = $analytics.uptime;
|
||||
filesystemChart.data.datasets[0].data = $analytics.fs_used;
|
||||
heapChart.options.scales!.y!.max = Math.ceil($analytics.fs_total[0]);
|
||||
filesystemChart.update('none');
|
||||
heapChart.data.labels = $analytics.uptime;
|
||||
heapChart.data.datasets[0].data = $analytics.used_heap;
|
||||
heapChart.options.scales!.y!.max = Math.ceil($analytics.total_heap[0]);
|
||||
heapChart.update('none');
|
||||
|
||||
temperatureChart.data.labels = $analytics.uptime;
|
||||
temperatureChart.data.datasets[0].data = $analytics.core_temp;
|
||||
temperatureChart.update('none');
|
||||
}
|
||||
filesystemChart.data.labels = $analytics.uptime;
|
||||
filesystemChart.data.datasets[0].data = $analytics.fs_used;
|
||||
heapChart.options.scales!.y!.max = Math.ceil($analytics.fs_total[0]);
|
||||
filesystemChart.update('none');
|
||||
|
||||
temperatureChart.data.labels = $analytics.uptime;
|
||||
temperatureChart.data.datasets[0].data = $analytics.core_temp;
|
||||
temperatureChart.update('none');
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
{#snippet icon()}
|
||||
<Metrics class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span >System Metrics</span>
|
||||
{/snippet}
|
||||
{#snippet icon()}
|
||||
<Metrics class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>System Metrics</span>
|
||||
{/snippet}
|
||||
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<canvas bind:this={cpuChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<canvas bind:this={cpuChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<canvas bind:this={heapChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-52"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<canvas bind:this={filesystemChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-52"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<canvas bind:this={temperatureChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<canvas bind:this={heapChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-52"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<canvas bind:this={filesystemChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-52"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<canvas bind:this={temperatureChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { modals } from 'svelte-modals'
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte'
|
||||
import Spinner from '$lib/components/Spinner.svelte'
|
||||
import { slide } from 'svelte/transition'
|
||||
import { cubicOut } from 'svelte/easing'
|
||||
import type { SystemInformation, Analytics } from '$lib/types/models'
|
||||
import { socket } from '$lib/stores/socket'
|
||||
import { api } from '$lib/api'
|
||||
import { convertSeconds } from '$lib/utilities'
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags'
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { modals } from 'svelte-modals';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import type { SystemInformation, Analytics } from '$lib/types/models';
|
||||
import { socket } from '$lib/stores/socket';
|
||||
import { api } from '$lib/api';
|
||||
import { convertSeconds } from '$lib/utilities';
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags';
|
||||
import {
|
||||
Cancel,
|
||||
Power,
|
||||
@@ -27,37 +27,42 @@
|
||||
Flash,
|
||||
Folder,
|
||||
Temperature,
|
||||
Stopwatch
|
||||
} from '$lib/components/icons'
|
||||
import StatusItem from '$lib/components/StatusItem.svelte'
|
||||
import ActionButton from './ActionButton.svelte'
|
||||
Stopwatch,
|
||||
} from '$lib/components/icons';
|
||||
import StatusItem from '$lib/components/StatusItem.svelte';
|
||||
import ActionButton from './ActionButton.svelte';
|
||||
|
||||
const features = useFeatureFlags()
|
||||
const features = useFeatureFlags();
|
||||
|
||||
let systemInformation: SystemInformation = $state()
|
||||
let systemInformation: SystemInformation | null = $state(null);
|
||||
|
||||
async function getSystemStatus() {
|
||||
const result = await api.get<SystemInformation>('/api/system/status')
|
||||
const result = await api.get<SystemInformation>('/api/system/status');
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner)
|
||||
return
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
systemInformation = result.inner
|
||||
return systemInformation
|
||||
systemInformation = result.inner;
|
||||
return systemInformation;
|
||||
}
|
||||
|
||||
const postFactoryReset = async () => await api.post('/api/system/reset')
|
||||
const postFactoryReset = async () => await api.post('/api/system/reset');
|
||||
|
||||
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) => {
|
||||
if (systemInformation) {
|
||||
systemInformation = {
|
||||
...systemInformation,
|
||||
...(data as unknown as SystemInformation),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const handleSystemData = (data: Analytics) =>
|
||||
(systemInformation = { ...systemInformation, ...data })
|
||||
|
||||
const postRestart = async () => await api.post('/api/system/restart')
|
||||
const postRestart = async () => await api.post('/api/system/restart');
|
||||
|
||||
function confirmRestart() {
|
||||
modals.open(ConfirmDialog, {
|
||||
@@ -65,13 +70,13 @@
|
||||
message: 'Are you sure you want to restart the device?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Restart', icon: Power }
|
||||
confirm: { label: 'Restart', icon: Power },
|
||||
},
|
||||
onConfirm: () => {
|
||||
modals.close()
|
||||
postRestart()
|
||||
}
|
||||
})
|
||||
modals.close();
|
||||
postRestart();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function confirmReset() {
|
||||
@@ -80,13 +85,13 @@
|
||||
message: 'Are you sure you want to reset the device to its factory defaults?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Factory Reset', icon: FactoryReset }
|
||||
confirm: { label: 'Factory Reset', icon: FactoryReset },
|
||||
},
|
||||
onConfirm: () => {
|
||||
modals.close()
|
||||
postFactoryReset()
|
||||
}
|
||||
})
|
||||
modals.close();
|
||||
postFactoryReset();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function confirmSleep() {
|
||||
@@ -95,21 +100,21 @@
|
||||
message: 'Are you sure you want to put the device into sleep?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Sleep', icon: Sleep }
|
||||
confirm: { label: 'Sleep', icon: Sleep },
|
||||
},
|
||||
onConfirm: () => {
|
||||
modals.close()
|
||||
postSleep()
|
||||
}
|
||||
})
|
||||
modals.close();
|
||||
postSleep();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
interface ActionButtonDef {
|
||||
icon: any
|
||||
label: string
|
||||
onClick: () => void
|
||||
type?: string
|
||||
condition?: () => boolean
|
||||
icon: any;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
type?: string;
|
||||
condition?: () => boolean;
|
||||
}
|
||||
|
||||
const actionButtons: ActionButtonDef[] = [
|
||||
@@ -117,20 +122,20 @@
|
||||
icon: Sleep,
|
||||
label: 'Sleep',
|
||||
onClick: confirmSleep,
|
||||
condition: () => Boolean($features.sleep)
|
||||
condition: () => Boolean($features.sleep),
|
||||
},
|
||||
{
|
||||
icon: Power,
|
||||
label: 'Restart',
|
||||
onClick: confirmRestart
|
||||
onClick: confirmRestart,
|
||||
},
|
||||
{
|
||||
icon: FactoryReset,
|
||||
label: 'Factory Reset',
|
||||
onClick: confirmReset,
|
||||
type: 'secondary'
|
||||
}
|
||||
]
|
||||
type: 'secondary',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
@@ -144,89 +149,92 @@
|
||||
<div class="w-full overflow-x-auto">
|
||||
{#await getSystemStatus()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<StatusItem
|
||||
icon={CPU}
|
||||
title="Chip"
|
||||
description={`${systemInformation.cpu_type} Rev ${systemInformation.cpu_rev}`} />
|
||||
{:then}
|
||||
{#if systemInformation}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<StatusItem
|
||||
icon={CPU}
|
||||
title="Chip"
|
||||
description={`${systemInformation.cpu_type} Rev ${systemInformation.cpu_rev}`} />
|
||||
|
||||
<StatusItem
|
||||
icon={SDK}
|
||||
title="SDK Version"
|
||||
description={`ESP-IDF ${systemInformation.sdk_version} / Arduino ${systemInformation.arduino_version}`} />
|
||||
<StatusItem
|
||||
icon={SDK}
|
||||
title="SDK Version"
|
||||
description={`ESP-IDF ${systemInformation.sdk_version} / Arduino ${systemInformation.arduino_version}`} />
|
||||
|
||||
<StatusItem
|
||||
icon={CPP}
|
||||
title="Firmware Version"
|
||||
description={systemInformation.firmware_version} />
|
||||
<StatusItem
|
||||
icon={CPP}
|
||||
title="Firmware Version"
|
||||
description={systemInformation.firmware_version} />
|
||||
|
||||
<StatusItem
|
||||
icon={Speed}
|
||||
title="CPU Frequency"
|
||||
description={`${systemInformation.cpu_freq_mhz} MHz ${
|
||||
systemInformation.cpu_cores == 2 ? 'Dual Core' : 'Single Core'
|
||||
}`} />
|
||||
<StatusItem
|
||||
icon={Speed}
|
||||
title="CPU Frequency"
|
||||
description={`${systemInformation.cpu_freq_mhz} MHz ${
|
||||
systemInformation.cpu_cores == 2 ? 'Dual Core' : 'Single Core'
|
||||
}`} />
|
||||
|
||||
<StatusItem
|
||||
icon={Heap}
|
||||
title="Heap (Free / Max Alloc)"
|
||||
description={`${systemInformation.free_heap} / ${systemInformation.max_alloc_heap} bytes`} />
|
||||
<StatusItem
|
||||
icon={Heap}
|
||||
title="Heap (Free / Max Alloc)"
|
||||
description={`${systemInformation.free_heap} / ${systemInformation.max_alloc_heap} bytes`} />
|
||||
|
||||
<StatusItem
|
||||
icon={Pyramid}
|
||||
title="PSRAM (Size / Free)"
|
||||
description={`${systemInformation.psram_size} / ${systemInformation.psram_size} bytes`} />
|
||||
<StatusItem
|
||||
icon={Pyramid}
|
||||
title="PSRAM (Size / Free)"
|
||||
description={`${systemInformation.psram_size} / ${systemInformation.psram_size} bytes`} />
|
||||
|
||||
<StatusItem
|
||||
icon={Sketch}
|
||||
title="Sketch (Used / Free)"
|
||||
description={`${(
|
||||
(systemInformation.sketch_size / systemInformation.free_sketch_space) *
|
||||
100
|
||||
).toFixed(1)} % of
|
||||
<StatusItem
|
||||
icon={Sketch}
|
||||
title="Sketch (Used / Free)"
|
||||
description={`${(
|
||||
(systemInformation.sketch_size / systemInformation.free_sketch_space) *
|
||||
100
|
||||
).toFixed(1)} % of
|
||||
${systemInformation.free_sketch_space / 1000000} MB used (${
|
||||
(systemInformation.free_sketch_space - systemInformation.sketch_size) / 1000000
|
||||
} MB free)`} />
|
||||
|
||||
<StatusItem
|
||||
icon={Flash}
|
||||
title="Flash Chip (Size / Speed)"
|
||||
description={`${systemInformation.flash_chip_size / 1000000} MB / ${
|
||||
systemInformation.flash_chip_speed / 1000000
|
||||
} MHz`} />
|
||||
<StatusItem
|
||||
icon={Flash}
|
||||
title="Flash Chip (Size / Speed)"
|
||||
description={`${systemInformation.flash_chip_size / 1000000} MB / ${
|
||||
systemInformation.flash_chip_speed / 1000000
|
||||
} MHz`} />
|
||||
|
||||
<StatusItem
|
||||
icon={Folder}
|
||||
title="File System (Used / Total)"
|
||||
description={`${((systemInformation.fs_used / systemInformation.fs_total) * 100).toFixed(
|
||||
1
|
||||
)} % of ${systemInformation.fs_total / 1000000} MB used (${
|
||||
(systemInformation.fs_total - systemInformation.fs_used) / 1000000
|
||||
}
|
||||
<StatusItem
|
||||
icon={Folder}
|
||||
title="File System (Used / Total)"
|
||||
description={`${(
|
||||
(systemInformation.fs_used / systemInformation.fs_total) *
|
||||
100
|
||||
).toFixed(1)} % of ${systemInformation.fs_total / 1000000} MB used (${
|
||||
(systemInformation.fs_total - systemInformation.fs_used) / 1000000
|
||||
}
|
||||
MB free)`} />
|
||||
|
||||
<StatusItem
|
||||
icon={Temperature}
|
||||
title="Core Temperature"
|
||||
description={`${
|
||||
systemInformation.core_temp == 53.33 ?
|
||||
'NaN'
|
||||
: systemInformation.core_temp.toFixed(2) + ' °C'
|
||||
}`} />
|
||||
<StatusItem
|
||||
icon={Temperature}
|
||||
title="Core Temperature"
|
||||
description={`${
|
||||
systemInformation.core_temp == 53.33 ?
|
||||
'NaN'
|
||||
: systemInformation.core_temp.toFixed(2) + ' °C'
|
||||
}`} />
|
||||
|
||||
<StatusItem
|
||||
icon={Stopwatch}
|
||||
title="Uptime"
|
||||
description={convertSeconds(systemInformation.uptime)} />
|
||||
<StatusItem
|
||||
icon={Stopwatch}
|
||||
title="Uptime"
|
||||
description={convertSeconds(systemInformation.uptime)} />
|
||||
|
||||
<StatusItem
|
||||
icon={Power}
|
||||
title="Reset Reason"
|
||||
description={systemInformation.cpu_reset_reason} />
|
||||
</div>
|
||||
<StatusItem
|
||||
icon={Power}
|
||||
title="Reset Reason"
|
||||
description={systemInformation.cpu_reset_reason} />
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,165 +1,154 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { modals } from 'svelte-modals';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { page } from '$app/state';
|
||||
import { modals } from 'svelte-modals';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
|
||||
import InfoDialog from '$lib/components/InfoDialog.svelte';
|
||||
import { api } from '$lib/api';
|
||||
import { useFeatureFlags } from '$lib/stores';
|
||||
import { Error, Cancel, Check, CloudDown, Github, Prerelease } from '$lib/components/icons';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
|
||||
import InfoDialog from '$lib/components/InfoDialog.svelte';
|
||||
import { api } from '$lib/api';
|
||||
import { useFeatureFlags } from '$lib/stores';
|
||||
import { Error, Cancel, Check, CloudDown, Github, Prerelease } from '$lib/components/icons';
|
||||
|
||||
const features = useFeatureFlags();
|
||||
const features = useFeatureFlags();
|
||||
|
||||
async function getGithubAPI() {
|
||||
const headers = {
|
||||
accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
};
|
||||
const result = await api.get(`https://api.github.com/repos/${page.data.github}/releases`, {
|
||||
headers
|
||||
async function getGithubAPI() {
|
||||
const headers = {
|
||||
accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
};
|
||||
const result = await api.get(`https://api.github.com/repos/${page.data.github}/releases`, {
|
||||
headers,
|
||||
});
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
return result.inner as any;
|
||||
}
|
||||
|
||||
async function postGithubDownload(url: string) {
|
||||
const result = await api.post('/api/firmware/download', { download_url: url });
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmGithubUpdate(assets: any) {
|
||||
let url = '';
|
||||
// iterate over assets and find the correct one
|
||||
for (let i = 0; i < assets.length; i++) {
|
||||
// check if the asset is of type *.bin
|
||||
if (
|
||||
assets[i].name.includes('.bin') &&
|
||||
assets[i].name.includes($features.firmware_built_target)
|
||||
) {
|
||||
url = assets[i].browser_download_url;
|
||||
}
|
||||
}
|
||||
if (url === '') {
|
||||
// if no asset was found, use the first one
|
||||
modals.open(InfoDialog, {
|
||||
title: 'No matching firmware found',
|
||||
message:
|
||||
'No matching firmware was found for the current device. Upload the firmware manually or build from sources.',
|
||||
dismiss: { label: 'OK', icon: Check },
|
||||
onDismiss: () => modals.close(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm flashing new firmware to the device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Update', icon: CloudDown },
|
||||
},
|
||||
onConfirm: () => {
|
||||
postGithubDownload(url);
|
||||
modals.open(GithubUpdateDialog, {
|
||||
onConfirm: () => modals.closeAll(),
|
||||
});
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
return result.inner as any;
|
||||
}
|
||||
|
||||
async function postGithubDownload(url: string) {
|
||||
const result = await api.post('/api/firmware/download', { download_url: url });
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmGithubUpdate(assets: any) {
|
||||
let url = '';
|
||||
// iterate over assets and find the correct one
|
||||
for (let i = 0; i < assets.length; i++) {
|
||||
// check if the asset is of type *.bin
|
||||
if (
|
||||
assets[i].name.includes('.bin') &&
|
||||
assets[i].name.includes($features.firmware_built_target)
|
||||
) {
|
||||
url = assets[i].browser_download_url;
|
||||
}
|
||||
}
|
||||
if (url === '') {
|
||||
// if no asset was found, use the first one
|
||||
modals.open(InfoDialog, {
|
||||
title: 'No matching firmware found',
|
||||
message:
|
||||
'No matching firmware was found for the current device. Upload the firmware manually or build from sources.',
|
||||
dismiss: { label: 'OK', icon: Check },
|
||||
onDismiss: () => modals.close()
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm flashing new firmware to the device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Update', icon: CloudDown }
|
||||
},
|
||||
onConfirm: () => {
|
||||
postGithubDownload(url);
|
||||
modals.open(GithubUpdateDialog, {
|
||||
onConfirm: () => modals.closeAll()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
{#snippet icon()}
|
||||
<Github class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>Github Firmware Manager</span>
|
||||
{/snippet}
|
||||
{#await getGithubAPI()}
|
||||
<Spinner />
|
||||
{:then githubReleases}
|
||||
<div class="relative w-full overflow-visible">
|
||||
<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">Release</th>
|
||||
<th align="center" class="hidden sm:block">Release Date</th>
|
||||
<th align="center">Experimental</th>
|
||||
<th align="center">Install</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each githubReleases as release}
|
||||
<tr
|
||||
class={(
|
||||
compareVersions(
|
||||
$features.firmware_version,
|
||||
release.tag_name
|
||||
) === 0
|
||||
) ?
|
||||
'bg-primary text-primary-content'
|
||||
: 'bg-base-100 h-14'}
|
||||
>
|
||||
<td align="left" class="text-base font-semibold">
|
||||
<a
|
||||
href={release.html_url}
|
||||
class="link link-hover"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">{release.name}</a
|
||||
></td
|
||||
>
|
||||
<td align="center" class="hidden min-h-full align-middle sm:block">
|
||||
<div class="my-2">
|
||||
{new Intl.DateTimeFormat('en-GB', {
|
||||
dateStyle: 'medium'
|
||||
}).format(new Date(release.published_at))}
|
||||
</div>
|
||||
</td>
|
||||
<td align="center">
|
||||
{#if release.prerelease}
|
||||
<Prerelease class="text-accent h-5 w-5" />
|
||||
{/if}
|
||||
</td>
|
||||
<td align="center">
|
||||
{#if compareVersions($features.firmware_version, release.tag_name) != 0}
|
||||
<button
|
||||
class="btn btn-ghost btn-circle btn-sm"
|
||||
onclick={() => {
|
||||
confirmGithubUpdate(release.assets);
|
||||
}}
|
||||
>
|
||||
<CloudDown class="text-secondary h-6 w-6" />
|
||||
</button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{:catch error}
|
||||
<div class="alert alert-error shadow-lg">
|
||||
<Error class="h-6 w-6 shrink-0" />
|
||||
<span
|
||||
>Please connect to a network with internet access to perform a firmware update.</span
|
||||
>
|
||||
</div>
|
||||
{/await}
|
||||
{#snippet icon()}
|
||||
<Github class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>Github Firmware Manager</span>
|
||||
{/snippet}
|
||||
{#await getGithubAPI()}
|
||||
<Spinner />
|
||||
{:then githubReleases}
|
||||
<div class="relative w-full overflow-visible">
|
||||
<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">Release</th>
|
||||
<th align="center" class="hidden sm:block">Release Date</th>
|
||||
<th align="center">Experimental</th>
|
||||
<th align="center">Install</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each githubReleases as release}
|
||||
<tr
|
||||
class={(
|
||||
compareVersions($features.firmware_version as string, release.tag_name) === 0
|
||||
) ?
|
||||
'bg-primary text-primary-content'
|
||||
: 'bg-base-100 h-14'}>
|
||||
<td align="left" class="text-base font-semibold">
|
||||
<a
|
||||
href={release.html_url}
|
||||
class="link link-hover"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">{release.name}</a
|
||||
></td>
|
||||
<td align="center" class="hidden min-h-full align-middle sm:block">
|
||||
<div class="my-2">
|
||||
{new Intl.DateTimeFormat('en-GB', {
|
||||
dateStyle: 'medium',
|
||||
}).format(new Date(release.published_at))}
|
||||
</div>
|
||||
</td>
|
||||
<td align="center">
|
||||
{#if release.prerelease}
|
||||
<Prerelease class="text-accent h-5 w-5" />
|
||||
{/if}
|
||||
</td>
|
||||
<td align="center">
|
||||
{#if compareVersions($features.firmware_version as string, release.tag_name) != 0}
|
||||
<button
|
||||
class="btn btn-ghost btn-circle btn-sm"
|
||||
onclick={() => {
|
||||
confirmGithubUpdate(release.assets);
|
||||
}}>
|
||||
<CloudDown class="text-secondary h-6 w-6" />
|
||||
</button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{:catch error}
|
||||
<div class="alert alert-error shadow-lg">
|
||||
<Error class="h-6 w-6 shrink-0" />
|
||||
<span>Please connect to a network with internet access to perform a firmware update.</span>
|
||||
</div>
|
||||
{/await}
|
||||
</SettingsCard>
|
||||
|
||||
@@ -1,57 +1,56 @@
|
||||
<script lang="ts">
|
||||
import { modals } from 'svelte-modals';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { modals } from 'svelte-modals';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
|
||||
import { api } from '$lib/api';
|
||||
import { Cancel, OTA, Warning } from '$lib/components/icons';
|
||||
import { api } from '$lib/api';
|
||||
import { Cancel, OTA, Warning } from '$lib/components/icons';
|
||||
|
||||
let files: FileList = $state();
|
||||
let files: FileList | undefined = $state();
|
||||
|
||||
async function uploadBIN() {
|
||||
const formData = new FormData();
|
||||
formData.append('file', files[0]);
|
||||
const result = await api.post('/api/firmware', formData);
|
||||
if (result.isErr()) console.error('Error:', result.inner);
|
||||
}
|
||||
async function uploadBIN() {
|
||||
const formData = new FormData();
|
||||
formData.append('file', files![0]);
|
||||
const result = await api.post('/api/firmware', formData);
|
||||
if (result.isErr()) console.error('Error:', result.inner);
|
||||
}
|
||||
|
||||
function confirmBinUpload() {
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm Flashing the Device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Upload', icon: OTA }
|
||||
},
|
||||
onConfirm: () => {
|
||||
modals.close();
|
||||
uploadBIN();
|
||||
}
|
||||
});
|
||||
}
|
||||
function confirmBinUpload() {
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm Flashing the Device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Upload', icon: OTA },
|
||||
},
|
||||
onConfirm: () => {
|
||||
modals.close();
|
||||
uploadBIN();
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
{#snippet icon()}
|
||||
<OTA class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>Upload Firmware</span>
|
||||
{/snippet}
|
||||
<div class="alert alert-warning shadow-lg">
|
||||
<Warning class="h-6 w-6 shrink-0" />
|
||||
<span
|
||||
>Uploading a new firmware (.bin) file will replace the existing firmware. You may upload
|
||||
a (.md5) file first to verify the uploaded firmware.
|
||||
</span>
|
||||
</div>
|
||||
{#snippet icon()}
|
||||
<OTA class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>Upload Firmware</span>
|
||||
{/snippet}
|
||||
<div class="alert alert-warning shadow-lg">
|
||||
<Warning class="h-6 w-6 shrink-0" />
|
||||
<span
|
||||
>Uploading a new firmware (.bin) file will replace the existing firmware. You may upload a
|
||||
(.md5) file first to verify the uploaded firmware.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
id="binFile"
|
||||
class="file-input file-input-bordered file-input-secondary mt-4 w-full"
|
||||
bind:files
|
||||
accept=".bin,.md5"
|
||||
onchange={confirmBinUpload}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
id="binFile"
|
||||
class="file-input file-input-bordered file-input-secondary mt-4 w-full"
|
||||
bind:files
|
||||
accept=".bin,.md5"
|
||||
onchange={confirmBinUpload} />
|
||||
</SettingsCard>
|
||||
|
||||
@@ -1,74 +1,71 @@
|
||||
<script lang="ts">
|
||||
import { preventDefault } from 'svelte/legacy'
|
||||
import { preventDefault } from 'svelte/legacy';
|
||||
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import { slide } from 'svelte/transition'
|
||||
import { cubicOut } from 'svelte/easing'
|
||||
import { PasswordInput } from '$lib/components/input'
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte'
|
||||
import { notifications } from '$lib/components/toasts/notifications'
|
||||
import Spinner from '$lib/components/Spinner.svelte'
|
||||
import type { ApSettings, ApStatus } from '$lib/types/models'
|
||||
import { api } from '$lib/api'
|
||||
import { useFeatureFlags } from '$lib/stores'
|
||||
import { AP, Devices, Home, MAC } from '$lib/components/icons'
|
||||
import StatusItem from '$lib/components/StatusItem.svelte'
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { PasswordInput } from '$lib/components/input';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import type { ApSettings, ApStatus } from '$lib/types/models';
|
||||
import { api } from '$lib/api';
|
||||
import { AP, Devices, Home, MAC } from '$lib/components/icons';
|
||||
import StatusItem from '$lib/components/StatusItem.svelte';
|
||||
|
||||
const features = useFeatureFlags()
|
||||
let apSettings: ApSettings | null = $state(null);
|
||||
let apStatus: ApStatus | null = $state(null);
|
||||
|
||||
let apSettings: ApSettings = $state()
|
||||
let apStatus: ApStatus = $state()
|
||||
|
||||
let formField: any = $state()
|
||||
let formField: any = $state();
|
||||
|
||||
async function getAPStatus() {
|
||||
const result = await api.get<ApStatus>('/api/wifi/ap/status')
|
||||
const result = await api.get<ApStatus>('/api/wifi/ap/status');
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner)
|
||||
return
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
apStatus = result.inner
|
||||
return apStatus
|
||||
apStatus = result.inner;
|
||||
return apStatus;
|
||||
}
|
||||
|
||||
async function getAPSettings() {
|
||||
const result = await api.get<ApSettings>('/api/wifi/ap/settings')
|
||||
const result = await api.get<ApSettings>('/api/wifi/ap/settings');
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner)
|
||||
return
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
apSettings = result.inner
|
||||
return apSettings
|
||||
apSettings = result.inner;
|
||||
return apSettings;
|
||||
}
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
getAPStatus()
|
||||
}, 5000)
|
||||
getAPStatus();
|
||||
}, 5000);
|
||||
|
||||
onDestroy(() => clearInterval(interval))
|
||||
onDestroy(() => clearInterval(interval));
|
||||
|
||||
onMount(getAPSettings)
|
||||
onMount(getAPSettings);
|
||||
|
||||
let provisionMode = [
|
||||
{
|
||||
id: 0,
|
||||
text: `Always`
|
||||
text: `Always`,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
text: `When WiFi Disconnected`
|
||||
text: `When WiFi Disconnected`,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
text: `Never`
|
||||
}
|
||||
]
|
||||
text: `Never`,
|
||||
},
|
||||
];
|
||||
|
||||
type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning'
|
||||
type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning';
|
||||
|
||||
let apStatusVariant: Variant[] = ['success', 'error', 'warning']
|
||||
let apStatusVariant: Variant[] = ['success', 'error', 'warning'];
|
||||
|
||||
let apStatusDescription = ['Active', 'Inactive', 'Lingering']
|
||||
let apStatusDescription = ['Active', 'Inactive', 'Lingering'];
|
||||
|
||||
let formErrors = $state({
|
||||
ssid: false,
|
||||
@@ -76,80 +73,81 @@
|
||||
max_clients: false,
|
||||
local_ip: false,
|
||||
gateway_ip: false,
|
||||
subnet_mask: false
|
||||
})
|
||||
subnet_mask: false,
|
||||
});
|
||||
|
||||
async function postAPSettings(data: ApSettings) {
|
||||
const result = await api.post<ApSettings>('/api/wifi/ap/settings', data)
|
||||
const result = await api.post<ApSettings>('/api/wifi/ap/settings', data);
|
||||
if (result.isErr()) {
|
||||
notifications.error('User not authorized.', 3000)
|
||||
console.error('Error:', result.inner)
|
||||
return
|
||||
notifications.error('User not authorized.', 3000);
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
notifications.success('Access Point settings updated.', 3000)
|
||||
apSettings = result.inner
|
||||
notifications.success('Access Point settings updated.', 3000);
|
||||
apSettings = result.inner;
|
||||
}
|
||||
|
||||
function handleSubmitAP() {
|
||||
let valid = true
|
||||
if (!apSettings) return;
|
||||
let valid = true;
|
||||
|
||||
// Validate SSID
|
||||
if (apSettings.ssid.length < 3 || apSettings.ssid.length > 32) {
|
||||
valid = false
|
||||
formErrors.ssid = true
|
||||
valid = false;
|
||||
formErrors.ssid = true;
|
||||
} else {
|
||||
formErrors.ssid = false
|
||||
formErrors.ssid = false;
|
||||
}
|
||||
|
||||
// Validate Channel
|
||||
let channel = Number(apSettings.channel)
|
||||
let channel = Number(apSettings.channel);
|
||||
if (1 > channel || channel > 13) {
|
||||
valid = false
|
||||
formErrors.channel = true
|
||||
valid = false;
|
||||
formErrors.channel = true;
|
||||
} else {
|
||||
formErrors.channel = false
|
||||
formErrors.channel = false;
|
||||
}
|
||||
|
||||
// Validate max_clients
|
||||
let maxClients = Number(apSettings.max_clients)
|
||||
let maxClients = Number(apSettings.max_clients);
|
||||
if (1 > maxClients || maxClients > 8) {
|
||||
valid = false
|
||||
formErrors.max_clients = true
|
||||
valid = false;
|
||||
formErrors.max_clients = true;
|
||||
} else {
|
||||
formErrors.max_clients = false
|
||||
formErrors.max_clients = false;
|
||||
}
|
||||
|
||||
// RegEx for IPv4
|
||||
const regexExp =
|
||||
/\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/
|
||||
/\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/;
|
||||
|
||||
// Validate gateway IP
|
||||
if (!regexExp.test(apSettings.gateway_ip)) {
|
||||
valid = false
|
||||
formErrors.gateway_ip = true
|
||||
valid = false;
|
||||
formErrors.gateway_ip = true;
|
||||
} else {
|
||||
formErrors.gateway_ip = false
|
||||
formErrors.gateway_ip = false;
|
||||
}
|
||||
|
||||
// Validate Subnet Mask
|
||||
if (!regexExp.test(apSettings.subnet_mask)) {
|
||||
valid = false
|
||||
formErrors.subnet_mask = true
|
||||
valid = false;
|
||||
formErrors.subnet_mask = true;
|
||||
} else {
|
||||
formErrors.subnet_mask = false
|
||||
formErrors.subnet_mask = false;
|
||||
}
|
||||
|
||||
// Validate local IP
|
||||
if (!regexExp.test(apSettings.local_ip)) {
|
||||
valid = false
|
||||
formErrors.local_ip = true
|
||||
valid = false;
|
||||
formErrors.local_ip = true;
|
||||
} else {
|
||||
formErrors.local_ip = false
|
||||
formErrors.local_ip = false;
|
||||
}
|
||||
|
||||
// Submit JSON to REST API
|
||||
if (valid) {
|
||||
postAPSettings(apSettings)
|
||||
postAPSettings(apSettings);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -164,22 +162,24 @@
|
||||
<div class="w-full overflow-x-auto">
|
||||
{#await getAPStatus()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<StatusItem
|
||||
icon={AP}
|
||||
title="Status"
|
||||
variant={apStatusVariant[apStatus.status]}
|
||||
description={apStatusDescription[apStatus.status]} />
|
||||
{:then}
|
||||
{#if apStatus}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<StatusItem
|
||||
icon={AP}
|
||||
title="Status"
|
||||
variant={apStatusVariant[apStatus.status]}
|
||||
description={apStatusDescription[apStatus.status]} />
|
||||
|
||||
<StatusItem icon={Home} title="IP Address" description={apStatus.ip_address} />
|
||||
<StatusItem icon={Home} title="IP Address" description={apStatus.ip_address} />
|
||||
|
||||
<StatusItem icon={MAC} title="MAC Address" description={apStatus.mac_address} />
|
||||
<StatusItem icon={MAC} title="MAC Address" description={apStatus.mac_address} />
|
||||
|
||||
<StatusItem icon={Devices} title="AP Clients" description={apStatus.station_num} />
|
||||
</div>
|
||||
<StatusItem icon={Devices} title="AP Clients" description={apStatus.station_num} />
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
@@ -190,175 +190,176 @@
|
||||
</div>
|
||||
{#await getAPSettings()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div
|
||||
class="flex flex-col gap-2 p-0"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<form
|
||||
class="grid w-full grid-cols-1 content-center gap-x-4 p-0s sm:grid-cols-2"
|
||||
onsubmit={preventDefault(handleSubmitAP)}
|
||||
novalidate
|
||||
bind:this={formField}>
|
||||
<div>
|
||||
<label class="label" for="apmode">
|
||||
<span class="label-text">Provide Access Point ...</span>
|
||||
</label>
|
||||
<select
|
||||
class="select select-bordered w-full"
|
||||
id="apmode"
|
||||
bind:value={apSettings.provision_mode}>
|
||||
{#each provisionMode as mode}
|
||||
<option value={mode.id}>
|
||||
{mode.text}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="ssid">
|
||||
<span class="label-text text-md">SSID</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.ssid
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={apSettings.ssid}
|
||||
id="ssid"
|
||||
min="2"
|
||||
max="32"
|
||||
required />
|
||||
<label class="label" for="ssid">
|
||||
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
|
||||
>SSID must be between 2 and 32 characters long</span>
|
||||
</label>
|
||||
</div>
|
||||
{:then}
|
||||
{#if apSettings}
|
||||
<div
|
||||
class="flex flex-col gap-2 p-0"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<form
|
||||
class="grid w-full grid-cols-1 content-center gap-x-4 p-0s sm:grid-cols-2"
|
||||
onsubmit={preventDefault(handleSubmitAP)}
|
||||
novalidate
|
||||
bind:this={formField}>
|
||||
<div>
|
||||
<label class="label" for="apmode">
|
||||
<span class="label-text">Provide Access Point ...</span>
|
||||
</label>
|
||||
<select
|
||||
class="select select-bordered w-full"
|
||||
id="apmode"
|
||||
bind:value={apSettings.provision_mode}>
|
||||
{#each provisionMode as mode}
|
||||
<option value={mode.id}>
|
||||
{mode.text}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="ssid">
|
||||
<span class="label-text text-md">SSID</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.ssid
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={apSettings.ssid}
|
||||
id="ssid"
|
||||
min="2"
|
||||
max="32"
|
||||
required />
|
||||
<label class="label" for="ssid">
|
||||
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
|
||||
>SSID must be between 2 and 32 characters long</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="pwd">
|
||||
<span class="label-text text-md">Password</span>
|
||||
</label>
|
||||
<PasswordInput bind:value={apSettings.password} id="pwd" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="channel">
|
||||
<span class="label-text text-md">Preferred Channel</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="13"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.channel
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={apSettings.channel}
|
||||
id="channel"
|
||||
required />
|
||||
<label class="label" for="channel">
|
||||
<span class="label-text-alt text-error {formErrors.channel ? '' : 'hidden'}"
|
||||
>Must be channel 1 to 13</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="pwd">
|
||||
<span class="label-text text-md">Password</span>
|
||||
</label>
|
||||
<PasswordInput bind:value={apSettings.password} id="pwd" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="channel">
|
||||
<span class="label-text text-md">Preferred Channel</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="13"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.channel
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={apSettings.channel}
|
||||
id="channel"
|
||||
required />
|
||||
<label class="label" for="channel">
|
||||
<span class="label-text-alt text-error {formErrors.channel ? '' : 'hidden'}"
|
||||
>Must be channel 1 to 13</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="clients">
|
||||
<span class="label-text text-md">Max Clients</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="8"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.max_clients
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={apSettings.max_clients}
|
||||
id="clients"
|
||||
required />
|
||||
<label class="label" for="clients">
|
||||
<span class="label-text-alt text-error {formErrors.max_clients ? '' : 'hidden'}"
|
||||
>Maximum 8 clients allowed</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="clients">
|
||||
<span class="label-text text-md">Max Clients</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="8"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.max_clients
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={apSettings.max_clients}
|
||||
id="clients"
|
||||
required />
|
||||
<label class="label" for="clients">
|
||||
<span class="label-text-alt text-error {formErrors.max_clients ? '' : 'hidden'}"
|
||||
>Maximum 8 clients allowed</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="localIP">
|
||||
<span class="label-text text-md">Local IP</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.local_ip ? 'border-error border-2' : (
|
||||
''
|
||||
)}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={apSettings.local_ip}
|
||||
id="localIP"
|
||||
required />
|
||||
<label class="label" for="localIP">
|
||||
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="localIP">
|
||||
<span class="label-text text-md">Local IP</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.local_ip ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={apSettings.local_ip}
|
||||
id="localIP"
|
||||
required />
|
||||
<label class="label" for="localIP">
|
||||
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text text-md">Gateway IP</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.gateway_ip ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={apSettings.gateway_ip}
|
||||
id="gateway"
|
||||
required />
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text text-md">Subnet Mask</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.subnet_mask ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={apSettings.subnet_mask}
|
||||
id="subnet"
|
||||
required />
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text text-md">Gateway IP</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.gateway_ip ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={apSettings.gateway_ip}
|
||||
id="gateway"
|
||||
required />
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text text-md">Subnet Mask</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.subnet_mask ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={apSettings.subnet_mask}
|
||||
id="subnet"
|
||||
required />
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="label my-auto cursor-pointer justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={apSettings.ssid_hidden}
|
||||
class="checkbox checkbox-primary" />
|
||||
<span class="">Hide SSID</span>
|
||||
</label>
|
||||
<label class="label my-auto cursor-pointer justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={apSettings.ssid_hidden}
|
||||
class="checkbox checkbox-primary" />
|
||||
<span class="">Hide SSID</span>
|
||||
</label>
|
||||
|
||||
<div class="place-self-end">
|
||||
<button class="btn btn-primary" type="submit">Apply Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="place-self-end">
|
||||
<button class="btn btn-primary" type="submit">Apply Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
+115
-131
@@ -1,147 +1,131 @@
|
||||
<script lang="ts">
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import RssiIndicator from '$lib/components/statusbar/RSSIIndicator.svelte';
|
||||
import type { NetworkItem, NetworkList } from '$lib/types/models';
|
||||
import { api } from '$lib/api';
|
||||
import { AP, Network, Reload, Cancel } from '$lib/components/icons';
|
||||
import { modals, exitBeforeEnter } from 'svelte-modals';
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import RssiIndicator from '$lib/components/statusbar/RSSIIndicator.svelte';
|
||||
import type { NetworkItem, NetworkList } from '$lib/types/models';
|
||||
import { api } from '$lib/api';
|
||||
import { AP, Network, Reload, Cancel } from '$lib/components/icons';
|
||||
import { modals, exitBeforeEnter, type ModalProps } from 'svelte-modals';
|
||||
|
||||
// provided by <Modals />
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
storeNetwork: any;
|
||||
let { isOpen, storeNetwork }: ModalProps = $props();
|
||||
|
||||
const encryptionType = [
|
||||
'Open',
|
||||
'WEP',
|
||||
'WPA PSK',
|
||||
'WPA2 PSK',
|
||||
'WPA WPA2 PSK',
|
||||
'WPA2 Enterprise',
|
||||
'WPA3 PSK',
|
||||
'WPA2 WPA3 PSK',
|
||||
'WAPI PSK',
|
||||
];
|
||||
|
||||
let listOfNetworks: NetworkItem[] = $state([]);
|
||||
|
||||
let scanActive = $state(false);
|
||||
|
||||
let pollingId: ReturnType<typeof setTimeout> | number;
|
||||
|
||||
async function scanNetworks() {
|
||||
scanActive = true;
|
||||
await api.get('/api/wifi/scan');
|
||||
if ((await pollingResults()) == false) {
|
||||
pollingId = setInterval(() => pollingResults(), 1000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let { isOpen, storeNetwork }: Props = $props();
|
||||
|
||||
const encryptionType = [
|
||||
'Open',
|
||||
'WEP',
|
||||
'WPA PSK',
|
||||
'WPA2 PSK',
|
||||
'WPA WPA2 PSK',
|
||||
'WPA2 Enterprise',
|
||||
'WPA3 PSK',
|
||||
'WPA2 WPA3 PSK',
|
||||
'WAPI PSK'
|
||||
];
|
||||
|
||||
let listOfNetworks: NetworkItem[] = $state([]);
|
||||
|
||||
let scanActive = $state(false);
|
||||
|
||||
let pollingId: number;
|
||||
|
||||
async function scanNetworks() {
|
||||
scanActive = true;
|
||||
await api.get('/api/wifi/scan');
|
||||
if ((await pollingResults()) == false) {
|
||||
pollingId = setInterval(() => pollingResults(), 1000);
|
||||
}
|
||||
return;
|
||||
async function pollingResults() {
|
||||
const result = await api.get<NetworkList>('/api/wifi/networks');
|
||||
if (result.isErr()) {
|
||||
console.error(`Error occurred while fetching: `, result.inner);
|
||||
return false;
|
||||
}
|
||||
|
||||
async function pollingResults() {
|
||||
const result = await api.get<NetworkList>('/api/wifi/networks');
|
||||
if (result.isErr()) {
|
||||
console.error(`Error occurred while fetching: `, result.inner);
|
||||
return false;
|
||||
}
|
||||
let response = result.inner;
|
||||
listOfNetworks = response.networks;
|
||||
scanActive = false;
|
||||
if (listOfNetworks.length) {
|
||||
clearInterval(pollingId);
|
||||
pollingId = 0;
|
||||
}
|
||||
return listOfNetworks.length;
|
||||
let response = result.inner;
|
||||
listOfNetworks = response.networks;
|
||||
scanActive = false;
|
||||
if (listOfNetworks.length) {
|
||||
clearInterval(pollingId);
|
||||
pollingId = 0;
|
||||
}
|
||||
return listOfNetworks.length;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
scanNetworks();
|
||||
});
|
||||
onMount(() => {
|
||||
scanNetworks();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (pollingId) {
|
||||
clearInterval(pollingId);
|
||||
pollingId = 0;
|
||||
}
|
||||
});
|
||||
onDestroy(() => {
|
||||
if (pollingId) {
|
||||
clearInterval(pollingId);
|
||||
pollingId = 0;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
use:exitBeforeEnter
|
||||
use:focusTrap>
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
use:exitBeforeEnter
|
||||
use:focusTrap
|
||||
>
|
||||
<div
|
||||
class="bg-base-100 rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
|
||||
>
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">Scan Networks</h2>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="overflow-y-auto">
|
||||
{#if scanActive}<div
|
||||
class="bg-base-100 flex flex-col items-center justify-center p-6"
|
||||
>
|
||||
<AP class="text-secondary h-32 w-32 shrink animate-ping stroke-2" />
|
||||
<p class="mt-8 text-2xl">Scanning ...</p>
|
||||
class="bg-base-100 rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg">
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">Scan Networks</h2>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="overflow-y-auto">
|
||||
{#if scanActive}<div class="bg-base-100 flex flex-col items-center justify-center p-6">
|
||||
<AP class="text-secondary h-32 w-32 shrink animate-ping stroke-2" />
|
||||
<p class="mt-8 text-2xl">Scanning ...</p>
|
||||
</div>
|
||||
{:else}
|
||||
<ul class="menu">
|
||||
{#each listOfNetworks as network, i}
|
||||
<li>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="bg-base-200 rounded-btn my-1 flex items-center space-x-3 hover:scale-[1.02] active:scale-[0.98]"
|
||||
onclick={() => {
|
||||
storeNetwork(network.ssid);
|
||||
}}
|
||||
role="button"
|
||||
tabindex="0">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10 shrink-0">
|
||||
<Network class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">{network.ssid}</div>
|
||||
<div class="text-sm opacity-75">
|
||||
Security: {encryptionType[network.encryption_type]}, Channel: {network.channel}
|
||||
</div>
|
||||
{:else}
|
||||
<ul class="menu">
|
||||
{#each listOfNetworks as network, i}
|
||||
<li>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="bg-base-200 rounded-btn my-1 flex items-center space-x-3 hover:scale-[1.02] active:scale-[0.98]"
|
||||
onclick={() => {
|
||||
storeNetwork(network.ssid);
|
||||
}}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10 shrink-0">
|
||||
<Network
|
||||
class="text-primary-content h-auto w-full scale-75"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">{network.ssid}</div>
|
||||
<div class="text-sm opacity-75">
|
||||
Security: {encryptionType[network.encryption_type]},
|
||||
Channel: {network.channel}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<RssiIndicator showDBm={true} rssi={network.rssi} />
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
<button
|
||||
class="btn btn-primary inline-flex flex-none items-center"
|
||||
disabled={scanActive}
|
||||
onclick={scanNetworks}
|
||||
>
|
||||
<Reload class="mr-2 h-5 w-5" /><span>Scan again</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<RssiIndicator showDBm={true} rssi={network.rssi} />
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
<button
|
||||
class="btn btn-primary inline-flex flex-none items-center"
|
||||
disabled={scanActive}
|
||||
onclick={scanNetworks}>
|
||||
<Reload class="mr-2 h-5 w-5" /><span>Scan again</span>
|
||||
</button>
|
||||
|
||||
<div class="grow"></div>
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex flex-none items-center"
|
||||
onclick={() => modals.close()}
|
||||
>
|
||||
<Cancel class="mr-2 h-5 w-5" /><span>Cancel</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex flex-none items-center"
|
||||
onclick={() => modals.close()}>
|
||||
<Cancel class="mr-2 h-5 w-5" /><span>Cancel</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
+375
-369
@@ -1,19 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import { modals } from 'svelte-modals'
|
||||
import { slide } from 'svelte/transition'
|
||||
import { cubicOut } from 'svelte/easing'
|
||||
import { notifications } from '$lib/components/toasts/notifications'
|
||||
import DragDropList, { VerticalDropZone, reorder, type DropEvent } from 'svelte-dnd-list'
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte'
|
||||
import { PasswordInput } from '$lib/components/input'
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'
|
||||
import ScanNetworks from './Scan.svelte'
|
||||
import Spinner from '$lib/components/Spinner.svelte'
|
||||
import InfoDialog from '$lib/components/InfoDialog.svelte'
|
||||
import type { KnownNetworkItem, WifiSettings, WifiStatus } from '$lib/types/models'
|
||||
import { socket } from '$lib/stores'
|
||||
import { api } from '$lib/api'
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { modals } from 'svelte-modals';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import DragDropList, { VerticalDropZone, reorder, type DropEvent } from 'svelte-dnd-list';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { PasswordInput } from '$lib/components/input';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import ScanNetworks from './Scan.svelte';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import InfoDialog from '$lib/components/InfoDialog.svelte';
|
||||
import type { KnownNetworkItem, WifiSettings, WifiStatus } from '$lib/types/models';
|
||||
import { socket } from '$lib/stores';
|
||||
import { api } from '$lib/api';
|
||||
import {
|
||||
Cancel,
|
||||
Delete,
|
||||
@@ -31,9 +31,9 @@
|
||||
DNS,
|
||||
Add,
|
||||
Scan,
|
||||
Edit
|
||||
} from '$lib/components/icons'
|
||||
import StatusItem from '$lib/components/StatusItem.svelte'
|
||||
Edit,
|
||||
} from '$lib/components/icons';
|
||||
import StatusItem from '$lib/components/StatusItem.svelte';
|
||||
|
||||
let networkEditable: KnownNetworkItem = $state({
|
||||
ssid: '',
|
||||
@@ -43,22 +43,22 @@
|
||||
subnet_mask: undefined,
|
||||
gateway_ip: undefined,
|
||||
dns_ip_1: undefined,
|
||||
dns_ip_2: undefined
|
||||
})
|
||||
dns_ip_2: undefined,
|
||||
});
|
||||
|
||||
let static_ip_config = $state(false)
|
||||
let static_ip_config = $state(false);
|
||||
|
||||
let newNetwork: boolean = $state(true)
|
||||
let showNetworkEditor: boolean = $state(false)
|
||||
let newNetwork: boolean = $state(true);
|
||||
let showNetworkEditor: boolean = $state(false);
|
||||
|
||||
let wifiStatus: WifiStatus = $state()
|
||||
let wifiSettings: WifiSettings = $state()
|
||||
let wifiStatus: WifiStatus | null = $state(null);
|
||||
let wifiSettings: WifiSettings | null = $state(null);
|
||||
|
||||
let dndNetworkList: KnownNetworkItem[] = $state([])
|
||||
let dndNetworkList: KnownNetworkItem[] = $state([]);
|
||||
|
||||
let showWifiDetails = $state(false)
|
||||
let showWifiDetails = $state(false);
|
||||
|
||||
let formField: any = $state()
|
||||
let formField: any = $state();
|
||||
|
||||
let formErrors = $state({
|
||||
ssid: false,
|
||||
@@ -66,156 +66,157 @@
|
||||
gateway_ip: false,
|
||||
subnet_mask: false,
|
||||
dns_1: false,
|
||||
dns_2: false
|
||||
})
|
||||
dns_2: false,
|
||||
});
|
||||
|
||||
let formErrorhostname = $state(false)
|
||||
let formErrorhostname = $state(false);
|
||||
|
||||
async function getWifiStatus() {
|
||||
const result = await api.get<WifiStatus>('/api/wifi/sta/status')
|
||||
const result = await api.get<WifiStatus>('/api/wifi/sta/status');
|
||||
if (result.isErr()) {
|
||||
console.error(`Error occurred while fetching: `, result.inner)
|
||||
return
|
||||
console.error(`Error occurred while fetching: `, result.inner);
|
||||
return;
|
||||
}
|
||||
wifiStatus = result.inner
|
||||
return wifiStatus
|
||||
wifiStatus = result.inner;
|
||||
return wifiStatus;
|
||||
}
|
||||
|
||||
async function getWifiSettings() {
|
||||
const result = await api.get<WifiSettings>('/api/wifi/sta/settings')
|
||||
const result = await api.get<WifiSettings>('/api/wifi/sta/settings');
|
||||
if (result.isErr()) {
|
||||
console.error(`Error occurred while fetching: `, result.inner)
|
||||
return
|
||||
console.error(`Error occurred while fetching: `, result.inner);
|
||||
return;
|
||||
}
|
||||
wifiSettings = result.inner
|
||||
dndNetworkList = wifiSettings.wifi_networks
|
||||
return wifiSettings
|
||||
wifiSettings = result.inner;
|
||||
dndNetworkList = wifiSettings.wifi_networks;
|
||||
return wifiSettings;
|
||||
}
|
||||
|
||||
onDestroy(() => socket.off('WiFiSettings'))
|
||||
onDestroy(() => socket.off('WiFiSettings'));
|
||||
|
||||
onMount(() => {
|
||||
socket.on<WifiSettings>('WiFiSettings', data => {
|
||||
wifiSettings = data
|
||||
dndNetworkList = wifiSettings.wifi_networks
|
||||
})
|
||||
})
|
||||
wifiSettings = data;
|
||||
dndNetworkList = wifiSettings.wifi_networks;
|
||||
});
|
||||
});
|
||||
|
||||
async function postWiFiSettings(data: WifiSettings) {
|
||||
const result = await api.post<WifiSettings>('/api/wifi/sta/settings', data)
|
||||
const result = await api.post<WifiSettings>('/api/wifi/sta/settings', data);
|
||||
if (result.isErr()) {
|
||||
console.error(`Error occurred while fetching: `, result.inner)
|
||||
notifications.error('User not authorized.', 3000)
|
||||
return
|
||||
console.error(`Error occurred while fetching: `, result.inner);
|
||||
notifications.error('User not authorized.', 3000);
|
||||
return;
|
||||
}
|
||||
wifiSettings = result.inner
|
||||
notifications.success('Wi-Fi settings updated.', 3000)
|
||||
wifiSettings = result.inner;
|
||||
notifications.success('Wi-Fi settings updated.', 3000);
|
||||
}
|
||||
|
||||
function validateHostName() {
|
||||
if (!wifiSettings) return false;
|
||||
if (wifiSettings.hostname.length < 3 || wifiSettings.hostname.length > 32) {
|
||||
formErrorhostname = true
|
||||
formErrorhostname = true;
|
||||
} else {
|
||||
formErrorhostname = false
|
||||
formErrorhostname = false;
|
||||
// Update global wifiSettings object
|
||||
wifiSettings.wifi_networks = dndNetworkList
|
||||
wifiSettings.wifi_networks = dndNetworkList;
|
||||
// Post to REST API
|
||||
postWiFiSettings(wifiSettings)
|
||||
console.log(wifiSettings)
|
||||
postWiFiSettings(wifiSettings);
|
||||
console.log(wifiSettings);
|
||||
}
|
||||
}
|
||||
|
||||
function validateWiFiForm(event: SubmitEvent) {
|
||||
event.preventDefault()
|
||||
let valid = true
|
||||
event.preventDefault();
|
||||
let valid = true;
|
||||
|
||||
// Validate SSID
|
||||
if (networkEditable.ssid.length < 3 || networkEditable.ssid.length > 32) {
|
||||
valid = false
|
||||
formErrors.ssid = true
|
||||
valid = false;
|
||||
formErrors.ssid = true;
|
||||
} else {
|
||||
formErrors.ssid = false
|
||||
formErrors.ssid = false;
|
||||
}
|
||||
|
||||
networkEditable.static_ip_config = static_ip_config
|
||||
networkEditable.static_ip_config = static_ip_config;
|
||||
|
||||
if (networkEditable.static_ip_config) {
|
||||
// RegEx for IPv4
|
||||
const regexExp =
|
||||
/\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/
|
||||
/\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/;
|
||||
|
||||
// Validate gateway IP
|
||||
if (!regexExp.test(networkEditable.gateway_ip!)) {
|
||||
valid = false
|
||||
formErrors.gateway_ip = true
|
||||
valid = false;
|
||||
formErrors.gateway_ip = true;
|
||||
} else {
|
||||
formErrors.gateway_ip = false
|
||||
formErrors.gateway_ip = false;
|
||||
}
|
||||
|
||||
// Validate Subnet Mask
|
||||
if (!regexExp.test(networkEditable.subnet_mask!)) {
|
||||
valid = false
|
||||
formErrors.subnet_mask = true
|
||||
valid = false;
|
||||
formErrors.subnet_mask = true;
|
||||
} else {
|
||||
formErrors.subnet_mask = false
|
||||
formErrors.subnet_mask = false;
|
||||
}
|
||||
|
||||
// Validate local IP
|
||||
if (!regexExp.test(networkEditable.local_ip!)) {
|
||||
valid = false
|
||||
formErrors.local_ip = true
|
||||
valid = false;
|
||||
formErrors.local_ip = true;
|
||||
} else {
|
||||
formErrors.local_ip = false
|
||||
formErrors.local_ip = false;
|
||||
}
|
||||
|
||||
// Validate DNS 1
|
||||
if (!regexExp.test(networkEditable.dns_ip_1!)) {
|
||||
valid = false
|
||||
formErrors.dns_1 = true
|
||||
valid = false;
|
||||
formErrors.dns_1 = true;
|
||||
} else {
|
||||
formErrors.dns_1 = false
|
||||
formErrors.dns_1 = false;
|
||||
}
|
||||
|
||||
// Validate DNS 2
|
||||
if (!regexExp.test(networkEditable.dns_ip_2!)) {
|
||||
valid = false
|
||||
formErrors.dns_2 = true
|
||||
valid = false;
|
||||
formErrors.dns_2 = true;
|
||||
} else {
|
||||
formErrors.dns_2 = false
|
||||
formErrors.dns_2 = false;
|
||||
}
|
||||
} else {
|
||||
formErrors.local_ip = false
|
||||
formErrors.subnet_mask = false
|
||||
formErrors.gateway_ip = false
|
||||
formErrors.dns_1 = false
|
||||
formErrors.dns_2 = false
|
||||
formErrors.local_ip = false;
|
||||
formErrors.subnet_mask = false;
|
||||
formErrors.gateway_ip = false;
|
||||
formErrors.dns_1 = false;
|
||||
formErrors.dns_2 = false;
|
||||
}
|
||||
// Submit JSON to REST API
|
||||
if (valid) {
|
||||
if (newNetwork) {
|
||||
dndNetworkList.push(networkEditable)
|
||||
dndNetworkList.push(networkEditable);
|
||||
} else {
|
||||
dndNetworkList.splice(dndNetworkList.indexOf(networkEditable), 1, networkEditable)
|
||||
dndNetworkList.splice(dndNetworkList.indexOf(networkEditable), 1, networkEditable);
|
||||
}
|
||||
addNetwork()
|
||||
dndNetworkList = [...dndNetworkList] //Trigger reactivity
|
||||
showNetworkEditor = false
|
||||
addNetwork();
|
||||
dndNetworkList = [...dndNetworkList]; //Trigger reactivity
|
||||
showNetworkEditor = false;
|
||||
}
|
||||
}
|
||||
|
||||
function scanForNetworks() {
|
||||
modals.open(ScanNetworks, {
|
||||
storeNetwork: (network: string) => {
|
||||
addNetwork()
|
||||
networkEditable.ssid = network
|
||||
showNetworkEditor = true
|
||||
modals.close()
|
||||
}
|
||||
})
|
||||
addNetwork();
|
||||
networkEditable.ssid = network;
|
||||
showNetworkEditor = true;
|
||||
modals.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function addNetwork() {
|
||||
newNetwork = true
|
||||
newNetwork = true;
|
||||
networkEditable = {
|
||||
ssid: '',
|
||||
password: '',
|
||||
@@ -224,14 +225,14 @@
|
||||
subnet_mask: undefined,
|
||||
gateway_ip: undefined,
|
||||
dns_ip_1: undefined,
|
||||
dns_ip_2: undefined
|
||||
}
|
||||
dns_ip_2: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function handleEdit(index: number) {
|
||||
newNetwork = false
|
||||
showNetworkEditor = true
|
||||
networkEditable = dndNetworkList[index]
|
||||
newNetwork = false;
|
||||
showNetworkEditor = true;
|
||||
networkEditable = dndNetworkList[index];
|
||||
}
|
||||
|
||||
function confirmDelete(index: number) {
|
||||
@@ -240,20 +241,20 @@
|
||||
message: 'Are you sure you want to delete this network?',
|
||||
labels: {
|
||||
cancel: { label: 'Cancel', icon: Cancel },
|
||||
confirm: { label: 'Delete', icon: Delete }
|
||||
confirm: { label: 'Delete', icon: Delete },
|
||||
},
|
||||
onConfirm: () => {
|
||||
// Check if network is currently been edited and delete as well
|
||||
if (dndNetworkList[index].ssid === networkEditable.ssid) {
|
||||
addNetwork()
|
||||
addNetwork();
|
||||
}
|
||||
// Remove network from array
|
||||
dndNetworkList.splice(index, 1)
|
||||
dndNetworkList = [...dndNetworkList] //Trigger reactivity
|
||||
showNetworkEditor = false
|
||||
modals.close()
|
||||
}
|
||||
})
|
||||
dndNetworkList.splice(index, 1);
|
||||
dndNetworkList = [...dndNetworkList]; //Trigger reactivity
|
||||
showNetworkEditor = false;
|
||||
modals.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function checkNetworkList() {
|
||||
@@ -263,21 +264,21 @@
|
||||
message:
|
||||
'You have reached the maximum number of networks. Please delete one to add another.',
|
||||
dismiss: { label: 'OK', icon: Check },
|
||||
onDismiss: () => modals.close()
|
||||
})
|
||||
return false
|
||||
onDismiss: () => modals.close(),
|
||||
});
|
||||
return false;
|
||||
} else {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function onDrop({ detail: { from, to } }: CustomEvent<DropEvent>) {
|
||||
if (!to || from === to) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
dndNetworkList = reorder(dndNetworkList, from.index, to.index)
|
||||
console.log(dndNetworkList)
|
||||
dndNetworkList = reorder(dndNetworkList, from.index, to.index);
|
||||
console.log(dndNetworkList);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -291,53 +292,55 @@
|
||||
<div class="w-full overflow-x-auto">
|
||||
{#await getWifiStatus()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<StatusItem
|
||||
icon={AP}
|
||||
title="Status"
|
||||
variant={wifiStatus.status === 3 ? 'success' : 'error'}
|
||||
description={wifiStatus.status === 3 ? 'Connected' : 'Inactive'} />
|
||||
|
||||
{#if wifiStatus.status === 3}
|
||||
<StatusItem icon={SSID} title="SSID" description={wifiStatus.ssid} />
|
||||
|
||||
<StatusItem icon={Home} title="IP Address" description={wifiStatus.local_ip} />
|
||||
|
||||
<StatusItem icon={WiFi} title="RSSI" description={`${wifiStatus.rssi} dBm`}>
|
||||
<button
|
||||
class="btn btn-circle btn-ghost btn-sm modal-button"
|
||||
onclick={() => {
|
||||
showWifiDetails = !showWifiDetails
|
||||
}}>
|
||||
<Down
|
||||
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
|
||||
showWifiDetails
|
||||
) ?
|
||||
'rotate-180'
|
||||
: ''}" />
|
||||
</button>
|
||||
</StatusItem>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Folds open -->
|
||||
{#if showWifiDetails}
|
||||
{:then}
|
||||
{#if wifiStatus}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 pt-1"
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<StatusItem icon={MAC} title="MAC Address" description={wifiStatus.mac_address} />
|
||||
<StatusItem
|
||||
icon={AP}
|
||||
title="Status"
|
||||
variant={wifiStatus.status === 3 ? 'success' : 'error'}
|
||||
description={wifiStatus.status === 3 ? 'Connected' : 'Inactive'} />
|
||||
|
||||
<StatusItem icon={Channel} title="Channel" description={wifiStatus.channel} />
|
||||
{#if wifiStatus.status === 3}
|
||||
<StatusItem icon={SSID} title="SSID" description={wifiStatus.ssid} />
|
||||
|
||||
<StatusItem icon={Gateway} title="Gateway IP" description={wifiStatus.gateway_ip} />
|
||||
<StatusItem icon={Home} title="IP Address" description={wifiStatus.local_ip} />
|
||||
|
||||
<StatusItem icon={Subnet} title="Subnet Mask" description={wifiStatus.subnet_mask} />
|
||||
|
||||
<StatusItem icon={DNS} title="DNS" description={wifiStatus.dns_ip_1} />
|
||||
<StatusItem icon={WiFi} title="RSSI" description={`${wifiStatus.rssi} dBm`}>
|
||||
<button
|
||||
class="btn btn-circle btn-ghost btn-sm modal-button"
|
||||
onclick={() => {
|
||||
showWifiDetails = !showWifiDetails;
|
||||
}}>
|
||||
<Down
|
||||
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
|
||||
showWifiDetails
|
||||
) ?
|
||||
'rotate-180'
|
||||
: ''}" />
|
||||
</button>
|
||||
</StatusItem>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Folds open -->
|
||||
{#if showWifiDetails}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 pt-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<StatusItem icon={MAC} title="MAC Address" description={wifiStatus.mac_address} />
|
||||
|
||||
<StatusItem icon={Channel} title="Channel" description={wifiStatus.channel} />
|
||||
|
||||
<StatusItem icon={Gateway} title="Gateway IP" description={wifiStatus.gateway_ip} />
|
||||
|
||||
<StatusItem icon={Subnet} title="Subnet Mask" description={wifiStatus.subnet_mask} />
|
||||
|
||||
<StatusItem icon={DNS} title="DNS" description={wifiStatus.dns_ip_1} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
@@ -349,254 +352,257 @@
|
||||
</div>
|
||||
{#await getWifiSettings()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div class="relative w-full overflow-visible">
|
||||
<button
|
||||
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-16"
|
||||
onclick={() => {
|
||||
if (checkNetworkList()) {
|
||||
addNetwork()
|
||||
showNetworkEditor = true
|
||||
}
|
||||
}}>
|
||||
<Add class="h-6 w-6" /></button>
|
||||
<button
|
||||
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0"
|
||||
onclick={() => {
|
||||
if (checkNetworkList()) {
|
||||
scanForNetworks()
|
||||
showNetworkEditor = true
|
||||
}
|
||||
}}>
|
||||
<Scan class="h-6 w-6" /></button>
|
||||
{:then}
|
||||
{#if wifiSettings}
|
||||
<div class="relative w-full overflow-visible">
|
||||
<button
|
||||
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-16"
|
||||
onclick={() => {
|
||||
if (checkNetworkList()) {
|
||||
addNetwork();
|
||||
showNetworkEditor = true;
|
||||
}
|
||||
}}>
|
||||
<Add class="h-6 w-6" /></button>
|
||||
<button
|
||||
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0"
|
||||
onclick={() => {
|
||||
if (checkNetworkList()) {
|
||||
scanForNetworks();
|
||||
showNetworkEditor = true;
|
||||
}
|
||||
}}>
|
||||
<Scan class="h-6 w-6" /></button>
|
||||
|
||||
<div
|
||||
class="overflow-x-auto space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<DragDropList
|
||||
id="networks"
|
||||
type={VerticalDropZone}
|
||||
itemSize={60}
|
||||
itemCount={dndNetworkList.length}
|
||||
on:drop={onDrop}>
|
||||
{#snippet children({ index })}
|
||||
<StatusItem icon={Router} title={dndNetworkList[index].ssid}>
|
||||
<div class="space-x-0 px-0 mx-0">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
onclick={() => {
|
||||
handleEdit(index)
|
||||
}}>
|
||||
<Edit class="h-6 w-6" /></button>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
onclick={() => {
|
||||
confirmDelete(index)
|
||||
}}>
|
||||
<Delete class="text-error h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
</StatusItem>
|
||||
{/snippet}
|
||||
</DragDropList>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider mb-0"></div>
|
||||
<div
|
||||
class="flex flex-col gap-2 p-0"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<form class="" onsubmit={validateWiFiForm} novalidate bind:this={formField}>
|
||||
<div class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="label" for="channel">
|
||||
<span class="label-text text-md">Host Name</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
min="1"
|
||||
max="32"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrorhostname
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={wifiSettings.hostname}
|
||||
id="channel"
|
||||
required />
|
||||
<label class="label" for="channel">
|
||||
<span class="label-text-alt text-error {formErrorhostname ? '' : 'hidden'}"
|
||||
>Host name must be between 2 and 32 characters long</span>
|
||||
</label>
|
||||
</div>
|
||||
<label class="label inline-flex cursor-pointer content-end justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={wifiSettings.priority_RSSI}
|
||||
class="checkbox checkbox-primary sm:-mb-5" />
|
||||
<span class="sm:-mb-5">Connect to strongest WiFi</span>
|
||||
</label>
|
||||
<div
|
||||
class="overflow-x-auto space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<DragDropList
|
||||
id="networks"
|
||||
type={VerticalDropZone}
|
||||
itemSize={60}
|
||||
itemCount={dndNetworkList.length}
|
||||
on:drop={onDrop}>
|
||||
{#snippet children({ index }: { index: number })}
|
||||
<StatusItem icon={Router} title={dndNetworkList[index].ssid}>
|
||||
<div class="space-x-0 px-0 mx-0">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
onclick={() => {
|
||||
handleEdit(index);
|
||||
}}>
|
||||
<Edit class="h-6 w-6" /></button>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
onclick={() => {
|
||||
confirmDelete(index);
|
||||
}}>
|
||||
<Delete class="text-error h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
</StatusItem>
|
||||
{/snippet}
|
||||
</DragDropList>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showNetworkEditor}
|
||||
<div class="divider my-0"></div>
|
||||
<div
|
||||
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<div class="divider mb-0"></div>
|
||||
<div
|
||||
class="flex flex-col gap-2 p-0"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<form class="" onsubmit={validateWiFiForm} novalidate bind:this={formField}>
|
||||
<div class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="label" for="ssid">
|
||||
<span class="label-text text-md">SSID</span>
|
||||
<label class="label" for="channel">
|
||||
<span class="label-text text-md">Host Name</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
min="1"
|
||||
max="32"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.ssid
|
||||
formErrorhostname
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={networkEditable.ssid}
|
||||
id="ssid"
|
||||
min="2"
|
||||
max="32"
|
||||
bind:value={wifiSettings.hostname}
|
||||
id="channel"
|
||||
required />
|
||||
<label class="label" for="ssid">
|
||||
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
|
||||
>SSID must be between 3 and 32 characters long</span>
|
||||
<label class="label" for="channel">
|
||||
<span class="label-text-alt text-error {formErrorhostname ? '' : 'hidden'}"
|
||||
>Host name must be between 2 and 32 characters long</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="pwd">
|
||||
<span class="label-text text-md">Password</span>
|
||||
</label>
|
||||
<PasswordInput bind:value={networkEditable.password} id="pwd" />
|
||||
</div>
|
||||
<label
|
||||
class="label inline-flex cursor-pointer content-end justify-start gap-4 mt-2 sm:mb-4">
|
||||
<label class="label inline-flex cursor-pointer content-end justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={static_ip_config}
|
||||
bind:checked={wifiSettings.priority_RSSI}
|
||||
class="checkbox checkbox-primary sm:-mb-5" />
|
||||
<span class="sm:-mb-5">Static IP Config?</span>
|
||||
<span class="sm:-mb-5">Connect to strongest WiFi</span>
|
||||
</label>
|
||||
</div>
|
||||
{#if static_ip_config}
|
||||
|
||||
{#if showNetworkEditor}
|
||||
<div class="divider my-0"></div>
|
||||
<div
|
||||
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<div>
|
||||
<label class="label" for="localIP">
|
||||
<span class="label-text text-md">Local IP</span>
|
||||
<label class="label" for="ssid">
|
||||
<span class="label-text text-md">SSID</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.local_ip ?
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.ssid
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.local_ip}
|
||||
id="localIP"
|
||||
bind:value={networkEditable.ssid}
|
||||
id="ssid"
|
||||
min="2"
|
||||
max="32"
|
||||
required />
|
||||
<label class="label" for="localIP">
|
||||
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text text-md">Gateway IP</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.gateway_ip ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.gateway_ip}
|
||||
required />
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
<label class="label" for="ssid">
|
||||
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
|
||||
>SSID must be between 3 and 32 characters long</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text text-md">Subnet Mask</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.subnet_mask ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.subnet_mask}
|
||||
required />
|
||||
<label class="label" for="subnet">
|
||||
<span
|
||||
class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}">
|
||||
Must be a valid IPv4 address
|
||||
</span>
|
||||
<label class="label" for="pwd">
|
||||
<span class="label-text text-md">Password</span>
|
||||
</label>
|
||||
<PasswordInput bind:value={networkEditable.password} id="pwd" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text text-md">DNS 1</span>
|
||||
</label>
|
||||
<label
|
||||
class="label inline-flex cursor-pointer content-end justify-start gap-4 mt-2 sm:mb-4">
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.dns_1 ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.dns_ip_1}
|
||||
required />
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text-alt text-error {formErrors.dns_1 ? '' : 'hidden'}">
|
||||
Must be a valid IPv4 address
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text text-md">DNS 2</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.dns_2 ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.dns_ip_2}
|
||||
required />
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text-alt text-error {formErrors.dns_2 ? '' : 'hidden'}">
|
||||
Must be a valid IPv4 address
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
type="checkbox"
|
||||
bind:checked={static_ip_config}
|
||||
class="checkbox checkbox-primary sm:-mb-5" />
|
||||
<span class="sm:-mb-5">Static IP Config?</span>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if static_ip_config}
|
||||
<div
|
||||
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<div>
|
||||
<label class="label" for="localIP">
|
||||
<span class="label-text text-md">Local IP</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.local_ip ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.local_ip}
|
||||
id="localIP"
|
||||
required />
|
||||
<label class="label" for="localIP">
|
||||
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="divider mb-2 mt-0"></div>
|
||||
<div class="mx-4 flex flex-wrap justify-end gap-2">
|
||||
<button class="btn btn-primary" type="submit" disabled={!showNetworkEditor}>
|
||||
{newNetwork ? 'Add Network' : 'Update Network'}
|
||||
</button>
|
||||
<button class="btn btn-primary" type="button" onclick={validateHostName}>
|
||||
Apply Settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text text-md">Gateway IP</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.gateway_ip ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.gateway_ip}
|
||||
required />
|
||||
<label class="label" for="gateway">
|
||||
<span
|
||||
class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text text-md">Subnet Mask</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.subnet_mask ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.subnet_mask}
|
||||
required />
|
||||
<label class="label" for="subnet">
|
||||
<span
|
||||
class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}">
|
||||
Must be a valid IPv4 address
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text text-md">DNS 1</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.dns_1 ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.dns_ip_1}
|
||||
required />
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text-alt text-error {formErrors.dns_1 ? '' : 'hidden'}">
|
||||
Must be a valid IPv4 address
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text text-md">DNS 2</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.dns_2 ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.dns_ip_2}
|
||||
required />
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text-alt text-error {formErrors.dns_2 ? '' : 'hidden'}">
|
||||
Must be a valid IPv4 address
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="divider mb-2 mt-0"></div>
|
||||
<div class="mx-4 flex flex-wrap justify-end gap-2">
|
||||
<button class="btn btn-primary" type="submit" disabled={!showNetworkEditor}>
|
||||
{newNetwork ? 'Add Network' : 'Update Network'}
|
||||
</button>
|
||||
<button class="btn btn-primary" type="button" onclick={validateHostName}>
|
||||
Apply Settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
Reference in New Issue
Block a user