Deletes old project

This commit is contained in:
Rune Harlyk
2024-04-25 21:57:34 +02:00
committed by Rune Harlyk
parent 0b4fe8a0ef
commit 027d5eebc7
189 changed files with 1341 additions and 7239 deletions
+7
View File
@@ -0,0 +1,7 @@
import type { PageLoad } from './$types';
import { goto } from '$app/navigation';
export const load = (async () => {
goto('/');
return;
}) satisfies PageLoad;
@@ -0,0 +1,20 @@
<script lang="ts">
import type { PageData } from './$types';
import SystemMetrics from './SystemMetrics.svelte';
import { user } from '$lib/stores/user';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
export let data: PageData;
if (!$page.data.features.analytics) {
goto('/');
}
</script>
<div
class="mx-0 my-1 flex flex-col space-y-4
sm:mx-8 sm:my-8"
>
<SystemMetrics />
</div>
+5
View File
@@ -0,0 +1,5 @@
import type { PageLoad } from './$types';
export const load = (async () => {
return { title: 'System Metrics' };
}) satisfies PageLoad;
@@ -0,0 +1,303 @@
<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 Metrics from '~icons/tabler/report-analytics';
import { daisyColor } from '$lib/DaisyUiHelper';
import { analytics } from '$lib/stores/analytics';
Chart.register(...registerables);
let heapChartElement: HTMLCanvasElement;
let heapChart: Chart;
let filesystemChartElement: HTMLCanvasElement;
let filesystemChart: Chart;
let temperatureChartElement: HTMLCanvasElement;
let temperatureChart: Chart;
onMount(() => {
heapChart = new Chart(heapChartElement, {
type: 'line',
data: {
labels: $analytics.uptime,
datasets: [
{
label: 'Free Heap',
borderColor: daisyColor('--p'),
backgroundColor: daisyColor('--p', 50),
borderWidth: 2,
data: $analytics.free_heap,
yAxisID: 'y'
},
{
label: 'Max Alloc Heap',
borderColor: daisyColor('--s'),
backgroundColor: daisyColor('--s', 50),
borderWidth: 2,
data: $analytics.max_alloc_heap,
yAxisID: 'y'
}
]
},
options: {
maintainAspectRatio: false,
responsive: true,
plugins: {
legend: {
display: true
},
tooltip: {
mode: 'index',
intersect: false
}
},
elements: {
point: {
radius: 1
}
},
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,
yAxisID: 'y'
}
]
},
options: {
maintainAspectRatio: false,
responsive: true,
plugins: {
legend: {
display: true
},
tooltip: {
mode: 'index',
intersect: false
}
},
elements: {
point: {
radius: 1
}
},
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: 1
}
},
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(), 2000;
});
});
function updateData() {
heapChart.data.labels = $analytics.uptime;
heapChart.data.datasets[0].data = $analytics.free_heap;
heapChart.data.datasets[1].data = $analytics.max_alloc_heap;
heapChart.update('none');
filesystemChart.data.labels = $analytics.uptime;
filesystemChart.data.datasets[0].data = $analytics.fs_used;
filesystemChart.update('none');
temperatureChart.data.labels = $analytics.uptime;
temperatureChart.data.datasets[0].data = $analytics.core_temp;
temperatureChart.update('none');
}
function convertSeconds(seconds: number) {
// Calculate the number of seconds, minutes, hours, and days
let minutes = Math.floor(seconds / 60);
let hours = Math.floor(minutes / 60);
let days = Math.floor(hours / 24);
// Calculate the remaining hours, minutes, and seconds
hours = hours % 24;
minutes = minutes % 60;
seconds = seconds % 60;
// Create the formatted string
let result = '';
if (days > 0) {
result += days + ' day' + (days > 1 ? 's' : '') + ' ';
}
if (hours > 0) {
result += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';
}
if (minutes > 0) {
result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';
}
result += seconds + ' second' + (seconds > 1 ? 's' : '');
return result;
}
</script>
<SettingsCard collapsible={false}>
<Metrics slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
<span slot="title">System Metrics</span>
<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} />
</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} />
</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} />
</div>
</div>
</SettingsCard>
+13
View File
@@ -0,0 +1,13 @@
<script lang="ts">
import type { PageData } from './$types';
import SystemStatus from './SystemStatus.svelte';
export let data: PageData;
</script>
<div
class="mx-0 my-1 flex flex-col space-y-4
sm:mx-8 sm:my-8"
>
<SystemStatus />
</div>
+5
View File
@@ -0,0 +1,5 @@
import type { PageLoad } from './$types';
export const load = (async () => {
return { title: 'System Status' };
}) satisfies PageLoad;
@@ -0,0 +1,364 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { openModal, closeModal } from 'svelte-modals';
import { user } from '$lib/stores/user';
import { page } from '$app/stores';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import SettingsCard from '$lib/components/SettingsCard.svelte';
import Spinner from '$lib/components/Spinner.svelte';
import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import CPU from '~icons/tabler/cpu';
import CPP from '~icons/tabler/binary';
import Power from '~icons/tabler/reload';
import Sleep from '~icons/tabler/zzz';
import FactoryReset from '~icons/tabler/refresh-dot';
import Speed from '~icons/tabler/activity';
import Flash from '~icons/tabler/device-sd-card';
import Pyramid from '~icons/tabler/pyramid';
import Sketch from '~icons/tabler/chart-pie';
import Folder from '~icons/tabler/folder';
import Heap from '~icons/tabler/box-model';
import Cancel from '~icons/tabler/x';
import Temperature from '~icons/tabler/temperature';
import Health from '~icons/tabler/stethoscope';
import Stopwatch from '~icons/tabler/24-hours';
import SDK from '~icons/tabler/sdk';
import type { SystemInformation, Analytics } from '$lib/types/models';
import { socket } from '$lib/stores/socket';
let systemInformation: SystemInformation;
async function getSystemStatus() {
try {
const response = await fetch('/api/systemStatus', {
method: 'GET',
headers: {
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',
'Content-Type': 'application/json'
}
});
systemInformation = await response.json();
} catch (error) {
console.log('Error:', error);
}
return systemInformation;
}
onMount(() => socket.on('analytics', handleSystemData));
onDestroy(() => socket.off('analytics', handleSystemData));
const handleSystemData = (data: Analytics) =>
(systemInformation = { ...systemInformation, ...data });
async function postRestart() {
const response = await fetch('/api/restart', {
method: 'POST',
headers: {
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic'
}
});
}
function confirmRestart() {
openModal(ConfirmDialog, {
title: 'Confirm Restart',
message: 'Are you sure you want to restart the device?',
labels: {
cancel: { label: 'Abort', icon: Cancel },
confirm: { label: 'Restart', icon: Power }
},
onConfirm: () => {
closeModal();
postRestart();
}
});
}
async function postFactoryReset() {
const response = await fetch('/api/factoryReset', {
method: 'POST',
headers: {
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic'
}
});
}
function confirmReset() {
openModal(ConfirmDialog, {
title: 'Confirm Factory Reset',
message: 'Are you sure you want to reset the device to its factory defaults?',
labels: {
cancel: { label: 'Abort', icon: Cancel },
confirm: { label: 'Factory Reset', icon: FactoryReset }
},
onConfirm: () => {
closeModal();
postFactoryReset();
}
});
}
async function postSleep() {
const response = await fetch('/api/sleep', {
method: 'POST',
headers: {
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic'
}
});
}
function confirmSleep() {
openModal(ConfirmDialog, {
title: 'Confirm Going to Sleep',
message: 'Are you sure you want to put the device into sleep?',
labels: {
cancel: { label: 'Abort', icon: Cancel },
confirm: { label: 'Sleep', icon: Sleep }
},
onConfirm: () => {
closeModal();
postSleep();
}
});
}
function convertSeconds(seconds: number) {
// Calculate the number of seconds, minutes, hours, and days
let minutes = Math.floor(seconds / 60);
let hours = Math.floor(minutes / 60);
let days = Math.floor(hours / 24);
// Calculate the remaining hours, minutes, and seconds
hours = hours % 24;
minutes = minutes % 60;
seconds = seconds % 60;
// Create the formatted string
let result = '';
if (days > 0) {
result += days + ' day' + (days > 1 ? 's' : '') + ' ';
}
if (hours > 0) {
result += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';
}
if (minutes > 0) {
result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';
}
result += seconds + ' second' + (seconds > 1 ? 's' : '');
return result;
}
</script>
<SettingsCard collapsible={false}>
<Health slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
<span slot="title">System Status</span>
<div class="w-full overflow-x-auto">
{#await getSystemStatus()}
<Spinner />
{:then nothing}
<div
class="flex w-full flex-col space-y-1"
transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
<CPU class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">Chip</div>
<div class="text-sm opacity-75">
{systemInformation.cpu_type} Rev {systemInformation.cpu_rev}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
<SDK class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">SDK Version</div>
<div class="text-sm opacity-75">
ESP-IDF {systemInformation.sdk_version} / Arduino {systemInformation.arduino_version}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
<CPP class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">Firmware Version</div>
<div class="text-sm opacity-75">
{systemInformation.firmware_version}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
<Speed class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">CPU Frequency</div>
<div class="text-sm opacity-75">
{systemInformation.cpu_freq_mhz} MHz {systemInformation.cpu_cores == 2
? 'Dual Core'
: 'Single Core'}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
<Heap class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">Heap (Free / Max Alloc)</div>
<div class="text-sm opacity-75">
{systemInformation.free_heap.toLocaleString('en-US')} / {systemInformation.max_alloc_heap.toLocaleString(
'en-US'
)} bytes
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
<Pyramid class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">PSRAM (Size / Free)</div>
<div class="text-sm opacity-75">
{systemInformation.psram_size.toLocaleString('en-US')} / {systemInformation.psram_size.toLocaleString(
'en-US'
)} bytes
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
<Sketch class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">Sketch (Used / Free)</div>
<div class="flex flex-wrap justify-start gap-1 text-sm opacity-75">
<span>
{(
(systemInformation.sketch_size / systemInformation.free_sketch_space) *
100
).toFixed(1)} % of
{(systemInformation.free_sketch_space / 1000000).toLocaleString('en-US')} MB used
</span>
<span>
({(
(systemInformation.free_sketch_space - systemInformation.sketch_size) /
1000000
).toLocaleString('en-US')} MB free)
</span>
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
<Flash class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">Flash Chip (Size / Speed)</div>
<div class="text-sm opacity-75">
{(systemInformation.flash_chip_size / 1000000).toLocaleString('en-US')} MB / {(
systemInformation.flash_chip_speed / 1000000
).toLocaleString('en-US')} MHz
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
<Folder class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">File System (Used / Total)</div>
<div class="flex flex-wrap justify-start gap-1 text-sm opacity-75">
<span
>{((systemInformation.fs_used / systemInformation.fs_total) * 100).toFixed(1)} % of {(
systemInformation.fs_total / 1000000
).toLocaleString('en-US')} MB used</span
>
<span
>({(
(systemInformation.fs_total - systemInformation.fs_used) /
1000000
).toLocaleString('en-US')}
MB free)</span
>
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
<Temperature class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">Core Temperature</div>
<div class="text-sm opacity-75">
{systemInformation.core_temp == 53.33
? 'NaN'
: systemInformation.core_temp.toFixed(2) + ' °C'}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10">
<Stopwatch class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">Uptime</div>
<div class="text-sm opacity-75">
{convertSeconds(systemInformation.uptime)}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
<Power class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">Reset Reason</div>
<div class="text-sm opacity-75">
{systemInformation.cpu_reset_reason}
</div>
</div>
</div>
</div>
{/await}
</div>
<div class="mt-4 flex flex-wrap justify-end gap-2">
{#if $page.data.features.sleep}
<button class="btn btn-primary inline-flex items-center" on:click={confirmSleep}
><Sleep class="mr-2 h-5 w-5" /><span>Sleep</span></button
>
{/if}
{#if !$page.data.features.security || $user.admin}
<button class="btn btn-primary inline-flex items-center" on:click={confirmRestart}
><Power class="mr-2 h-5 w-5" /><span>Restart</span></button
>
<button class="btn btn-secondary inline-flex items-center" on:click={confirmReset}
><FactoryReset class="mr-2 h-5 w-5" /><span>Factory Reset</span></button
>
{/if}
</div>
</SettingsCard>
+22
View File
@@ -0,0 +1,22 @@
<script lang="ts">
import type { PageData } from './$types';
import UploadFirmware from './UploadFirmware.svelte';
import GithubFirmwareManager from './GithubFirmwareManager.svelte';
import { user } from '$lib/stores/user';
import { page } from '$app/stores';
export let data: PageData;
</script>
<div
class="mx-0 my-1 flex flex-col space-y-4
sm:mx-8 sm:my-8"
>
{#if $page.data.features.download_firmware && (!$page.data.features.security || $user.admin)}
<GithubFirmwareManager />
{/if}
{#if $page.data.features.upload_firmware && (!$page.data.features.security || $user.admin)}
<UploadFirmware />
{/if}
</div>
+5
View File
@@ -0,0 +1,5 @@
import type { PageLoad } from './$types';
export const load = (async () => {
return { title: 'Firmware Update' };
}) satisfies PageLoad;
@@ -0,0 +1,165 @@
<script lang="ts">
import { user } from '$lib/stores/user';
import { page } from '$app/stores';
import { openModal, closeModal, closeAllModals } 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 Github from '~icons/tabler/brand-github';
import CloudDown from '~icons/tabler/cloud-download';
import Cancel from '~icons/tabler/x';
import Prerelease from '~icons/tabler/test-pipe';
import Error from '~icons/tabler/circle-x';
import { compareVersions } from 'compare-versions';
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
import { assets } from '$app/paths';
import InfoDialog from '$lib/components/InfoDialog.svelte';
import Check from '~icons/tabler/check';
async function getGithubAPI() {
try {
const githubResponse = await fetch(
'https://api.github.com/repos/' + $page.data.github + '/releases',
{
method: 'GET',
headers: {
accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
}
}
);
const results = await githubResponse.json();
return results;
} catch (error) {
console.error('Error:', error);
}
return;
}
async function postGithubDownload(url: string) {
try {
const apiResponse = await fetch('/api/downloadUpdate', {
method: 'POST',
headers: {
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',
'Content-Type': 'application/json'
},
body: JSON.stringify({ download_url: url })
});
} catch (error) {
console.error('Error:', error);
}
}
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($page.data.features.firmware_built_target)
) {
url = assets[i].browser_download_url;
}
}
if (url === '') {
// if no asset was found, use the first one
openModal(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: () => closeModal()
});
return;
}
openModal(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);
openModal(GithubUpdateDialog, {
onConfirm: () => closeAllModals()
});
}
});
}
</script>
<SettingsCard collapsible={false}>
<Github slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" />
<span slot="title">Github Firmware Manager</span>
{#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($page.data.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($page.data.features.firmware_version, release.tag_name) != 0}
<button
class="btn btn-ghost btn-circle btn-sm"
on:click={() => {
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 flex-shrink-0" />
<span>Please connect to a network with internet access to perform a firmware update.</span>
</div>
{/await}
</SettingsCard>
@@ -0,0 +1,65 @@
<script lang="ts">
import { openModal, closeModal } from 'svelte-modals';
import { user } from '$lib/stores/user';
import { page } from '$app/stores';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import SettingsCard from '$lib/components/SettingsCard.svelte';
import OTA from '~icons/tabler/file-upload';
import Warning from '~icons/tabler/alert-triangle';
import Cancel from '~icons/tabler/x';
let files: FileList;
async function uploadBIN() {
try {
const formData = new FormData();
formData.append('file', files[0]);
const response = await fetch('/api/uploadFirmware', {
method: 'POST',
headers: {
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic'
},
body: formData
});
const result = await response.json();
} catch (error) {
console.error('Error:', error);
}
}
function confirmBinUpload() {
openModal(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: () => {
closeModal();
uploadBIN();
}
});
}
</script>
<SettingsCard collapsible={false}>
<OTA slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" />
<span slot="title">Upload Firmware</span>
<div class="alert alert-warning shadow-lg">
<Warning class="h-6 w-6 flex-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"
on:change={confirmBinUpload}
/>
</SettingsCard>