multiple-connection-handling (#3)
Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
import Sidebar from "./lib/Sidebar.svelte";
|
||||
import TaskManager from "./lib/TaskManager.svelte";
|
||||
import UserManager from "./lib/UserManager.svelte";
|
||||
import Spinner from "./lib/Spinner.svelte";
|
||||
|
||||
/** @type {'loading' | 'ok' | 'error' | 'rebooting'} */
|
||||
let status = $state("loading");
|
||||
@@ -389,6 +390,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<Spinner />
|
||||
|
||||
<style>
|
||||
.app-layout {
|
||||
display: flex;
|
||||
|
||||
47
Provider/frontend/src/lib/Spinner.svelte
Normal file
47
Provider/frontend/src/lib/Spinner.svelte
Normal file
@@ -0,0 +1,47 @@
|
||||
<script>
|
||||
import { pendingRequests } from './stores.js';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
|
||||
let showSpinner = false;
|
||||
let timer;
|
||||
|
||||
// Subscribe to the store
|
||||
const unsubscribe = pendingRequests.subscribe(count => {
|
||||
if (count > 0) {
|
||||
// Only show the spinner if the request takes longer than 300ms
|
||||
if (!timer) {
|
||||
timer = setTimeout(() => {
|
||||
showSpinner = true;
|
||||
}, 300);
|
||||
}
|
||||
} else {
|
||||
// Instantly hide the spinner when all requests finish
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
showSpinner = false;
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribe();
|
||||
if (timer) clearTimeout(timer);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if showSpinner}
|
||||
<div class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/40 backdrop-blur-sm transition-opacity duration-300">
|
||||
<div class="flex flex-col items-center p-8 bg-surface-base rounded-2xl shadow-xl border border-divider">
|
||||
<!-- Loading circle animation -->
|
||||
<div class="w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
|
||||
<p class="mt-4 text-text-primary font-medium tracking-wide animate-pulse">Communicating with Device...</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
/*
|
||||
* Note: 'bg-surface-base', 'border-divider', 'text-text-primary'
|
||||
* are assumed to be part of the app's global tailwind theme.
|
||||
* Adjust classes if necessary.
|
||||
*/
|
||||
</style>
|
||||
@@ -7,13 +7,26 @@
|
||||
*/
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || '';
|
||||
import { pendingRequests } from './stores.js';
|
||||
|
||||
/**
|
||||
* Wrapper around fetch that tracks the number of pending requests globally
|
||||
*/
|
||||
async function trackedFetch(url, options = {}) {
|
||||
pendingRequests.update(n => n + 1);
|
||||
try {
|
||||
return await fetch(url, options);
|
||||
} finally {
|
||||
pendingRequests.update(n => Math.max(0, n - 1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch system information from the ESP32.
|
||||
* @returns {Promise<{chip: string, freeHeap: number, uptime: number, firmware: string, connection: string}>}
|
||||
*/
|
||||
export async function getSystemInfo() {
|
||||
const res = await fetch(`${API_BASE}/api/system/info`);
|
||||
const res = await trackedFetch(`${API_BASE}/api/system/info`);
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||
}
|
||||
@@ -29,7 +42,7 @@ export async function getSystemInfo() {
|
||||
* @returns {Promise<{message: string}>}
|
||||
*/
|
||||
export async function reboot() {
|
||||
const res = await fetch(`${API_BASE}/api/system/reboot`, {
|
||||
const res = await trackedFetch(`${API_BASE}/api/system/reboot`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) {
|
||||
@@ -43,7 +56,7 @@ export async function reboot() {
|
||||
* @returns {Promise<{active_slot: number, active_partition: string, target_partition: string, partitions: any[], running_firmware_label: string, running_firmware_slot: number}>}
|
||||
*/
|
||||
export async function getOTAStatus() {
|
||||
const res = await fetch(`${API_BASE}/api/ota/status`);
|
||||
const res = await trackedFetch(`${API_BASE}/api/ota/status`);
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||
}
|
||||
@@ -56,7 +69,7 @@ export async function getOTAStatus() {
|
||||
* @returns {Promise<{status: string, message: string}>}
|
||||
*/
|
||||
export async function uploadOTAFrontend(file) {
|
||||
const res = await fetch(`${API_BASE}/api/ota/frontend`, {
|
||||
const res = await trackedFetch(`${API_BASE}/api/ota/frontend`, {
|
||||
method: 'POST',
|
||||
body: file, // Send the raw file Blob/Buffer
|
||||
headers: {
|
||||
@@ -79,7 +92,7 @@ export async function uploadOTAFrontend(file) {
|
||||
* @returns {Promise<{status: string, message: string}>}
|
||||
*/
|
||||
export async function uploadOTAFirmware(file) {
|
||||
const res = await fetch(`${API_BASE}/api/ota/firmware`, {
|
||||
const res = await trackedFetch(`${API_BASE}/api/ota/firmware`, {
|
||||
method: 'POST',
|
||||
body: file,
|
||||
headers: {
|
||||
@@ -101,7 +114,7 @@ export async function uploadOTAFirmware(file) {
|
||||
* @returns {Promise<{status: string, message: string}>}
|
||||
*/
|
||||
export async function uploadOTABundle(file) {
|
||||
const res = await fetch(`${API_BASE}/api/ota/bundle`, {
|
||||
const res = await trackedFetch(`${API_BASE}/api/ota/bundle`, {
|
||||
method: 'POST',
|
||||
body: file,
|
||||
headers: {
|
||||
@@ -124,7 +137,7 @@ export async function uploadOTABundle(file) {
|
||||
* @returns {Promise<Array<{id: number, name: string}>>}
|
||||
*/
|
||||
export async function getUsers() {
|
||||
const res = await fetch(`${API_BASE}/api/users`);
|
||||
const res = await trackedFetch(`${API_BASE}/api/users`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||
return res.json();
|
||||
}
|
||||
@@ -135,7 +148,7 @@ export async function getUsers() {
|
||||
* @returns {Promise<{id: number, name: string}>}
|
||||
*/
|
||||
export async function addUser(name) {
|
||||
const res = await fetch(`${API_BASE}/api/users`, {
|
||||
const res = await trackedFetch(`${API_BASE}/api/users`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name })
|
||||
@@ -153,7 +166,7 @@ export async function addUser(name) {
|
||||
* @returns {Promise<{status: string}>}
|
||||
*/
|
||||
export async function removeUser(id) {
|
||||
const res = await fetch(`${API_BASE}/api/users?id=${id}`, {
|
||||
const res = await trackedFetch(`${API_BASE}/api/users?id=${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (!res.ok) {
|
||||
@@ -170,7 +183,7 @@ export async function removeUser(id) {
|
||||
* @returns {Promise<{status: string}>}
|
||||
*/
|
||||
export async function updateUser(id, name) {
|
||||
const res = await fetch(`${API_BASE}/api/users/update`, {
|
||||
const res = await trackedFetch(`${API_BASE}/api/users/update`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, name })
|
||||
@@ -190,7 +203,7 @@ export async function updateUser(id, name) {
|
||||
* @returns {Promise<Array<{id: number, user_id: number, title: string, due_date: number, completed: boolean}>>}
|
||||
*/
|
||||
export async function getTasks(userId) {
|
||||
const res = await fetch(`${API_BASE}/api/tasks?user_id=${userId}`);
|
||||
const res = await trackedFetch(`${API_BASE}/api/tasks?user_id=${userId}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||
return res.json();
|
||||
}
|
||||
@@ -200,7 +213,7 @@ export async function getTasks(userId) {
|
||||
* @returns {Promise<{users: Array<{id: number, name: string, tasks: Array}>}>}
|
||||
*/
|
||||
export async function getUpcomingTasks() {
|
||||
const res = await fetch(`${API_BASE}/api/tasks/upcoming`);
|
||||
const res = await trackedFetch(`${API_BASE}/api/tasks/upcoming`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||
return res.json();
|
||||
}
|
||||
@@ -213,7 +226,7 @@ export async function getUpcomingTasks() {
|
||||
* @returns {Promise<{id: number, user_id: number, title: string, due_date: number, completed: boolean}>}
|
||||
*/
|
||||
export async function addTask(userId, title, dueDate) {
|
||||
const res = await fetch(`${API_BASE}/api/tasks`, {
|
||||
const res = await trackedFetch(`${API_BASE}/api/tasks`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ user_id: userId, title, due_date: dueDate })
|
||||
@@ -232,7 +245,7 @@ export async function addTask(userId, title, dueDate) {
|
||||
* @returns {Promise<{status: string}>}
|
||||
*/
|
||||
export async function updateTask(id, fields) {
|
||||
const res = await fetch(`${API_BASE}/api/tasks/update`, {
|
||||
const res = await trackedFetch(`${API_BASE}/api/tasks/update`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, ...fields })
|
||||
@@ -250,7 +263,7 @@ export async function updateTask(id, fields) {
|
||||
* @returns {Promise<{status: string}>}
|
||||
*/
|
||||
export async function deleteTask(id) {
|
||||
const res = await fetch(`${API_BASE}/api/tasks?id=${id}`, {
|
||||
const res = await trackedFetch(`${API_BASE}/api/tasks?id=${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (!res.ok) {
|
||||
|
||||
3
Provider/frontend/src/lib/stores.js
Normal file
3
Provider/frontend/src/lib/stores.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const pendingRequests = writable(0);
|
||||
Reference in New Issue
Block a user