From a2206b8c13bfb93fccacb7e4a413df1c0eac9a29 Mon Sep 17 00:00:00 2001 From: Jake Turner Date: Wed, 24 Dec 2025 11:59:48 -0800 Subject: [PATCH] feat(System): check internet status on backend and allow custom test url --- admin/app/controllers/system_controller.ts | 4 +++ admin/app/services/system_service.ts | 41 ++++++++++++++++++++++ admin/inertia/hooks/useInternetStatus.ts | 10 +++--- admin/inertia/lib/api.ts | 7 ++++ admin/inertia/lib/util.ts | 13 ------- admin/start/env.ts | 2 +- admin/start/routes.ts | 1 + 7 files changed, 59 insertions(+), 19 deletions(-) diff --git a/admin/app/controllers/system_controller.ts b/admin/app/controllers/system_controller.ts index 6dd26d2..f70d0f6 100644 --- a/admin/app/controllers/system_controller.ts +++ b/admin/app/controllers/system_controller.ts @@ -11,6 +11,10 @@ export default class SystemController { private dockerService: DockerService ) { } + async getInternetStatus({ }: HttpContext) { + return await this.systemService.getInternetStatus(); + } + async getSystemInfo({ }: HttpContext) { return await this.systemService.getSystemInfo(); } diff --git a/admin/app/services/system_service.ts b/admin/app/services/system_service.ts index eb90fca..c6c3b94 100644 --- a/admin/app/services/system_service.ts +++ b/admin/app/services/system_service.ts @@ -8,6 +8,8 @@ import { NomadDiskInfo, NomadDiskInfoRaw, SystemInformationResponse } from '../. import { readFileSync } from 'fs' import path, { join } from 'path' import { getAllFilesystems, getFile } from '../utils/fs.js' +import axios from 'axios' +import env from '#start/env' @inject() export class SystemService { @@ -16,6 +18,45 @@ export class SystemService { constructor(private dockerService: DockerService) {} + async getInternetStatus(): Promise { + const DEFAULT_TEST_URL = 'https://1.1.1.1/cdn-cgi/trace' + const MAX_ATTEMPTS = 3 + + let testUrl = DEFAULT_TEST_URL + let customTestUrl = env.get('INTERNET_STATUS_TEST_URL')?.trim() + + // check that customTestUrl is a valid URL, if provided + if (customTestUrl && customTestUrl !== '') { + try { + new URL(customTestUrl) + testUrl = customTestUrl + } catch (error) { + logger.warn( + `Invalid INTERNET_STATUS_TEST_URL: ${customTestUrl}. Falling back to default URL.` + ) + } + } + + for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + try { + const res = await axios.get(testUrl, { timeout: 5000 }) + return res.status === 200 + } catch (error) { + logger.warn( + `Internet status check attempt ${attempt}/${MAX_ATTEMPTS} failed: ${error instanceof Error ? error.message : error}` + ) + + if (attempt < MAX_ATTEMPTS) { + // delay before next attempt + await new Promise((resolve) => setTimeout(resolve, 1000)) + } + } + } + + logger.warn('All internet status check attempts failed.') + return false + } + async getServices({ installedOnly = true }: { installedOnly?: boolean }): Promise { const query = Service.query() .orderBy('friendly_name', 'asc') diff --git a/admin/inertia/hooks/useInternetStatus.ts b/admin/inertia/hooks/useInternetStatus.ts index 21aee5a..cd119e3 100644 --- a/admin/inertia/hooks/useInternetStatus.ts +++ b/admin/inertia/hooks/useInternetStatus.ts @@ -1,18 +1,18 @@ // Helper hook to check internet connection status import { useQuery } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; -import { testInternetConnection } from '~/lib/util'; +import api from '~/lib/api'; const useInternetStatus = () => { const [isOnline, setIsOnline] = useState(false); const { data } = useQuery({ queryKey: ['internetStatus'], - queryFn: testInternetConnection, + queryFn: async () => (await api.getInternetStatus()) ?? false, refetchOnWindowFocus: false, // Don't refetch on window focus - refetchOnReconnect: false, // Refetch when the browser reconnects + refetchOnReconnect: true, // Refetch when the browser reconnects refetchOnMount: false, // Don't refetch when the component mounts - retry: 2, // Retry up to 2 times on failure - staleTime: 1000 * 60 * 10, // Data is fresh for 10 minutes + retry: 0, // Retry already handled in backend + staleTime: 1000 * 60 * 5, // Data is fresh for 5 minutes }); // Update the online status when data changes diff --git a/admin/inertia/lib/api.ts b/admin/inertia/lib/api.ts index 76f3fb1..69eb629 100644 --- a/admin/inertia/lib/api.ts +++ b/admin/inertia/lib/api.ts @@ -92,6 +92,13 @@ class API { })() } + async getInternetStatus() { + return catchInternal(async () => { + const response = await this.client.get('/system/internet-status') + return response.data + })() + } + async getSystemInfo() { return catchInternal(async () => { const response = await this.client.get('/system/info') diff --git a/admin/inertia/lib/util.ts b/admin/inertia/lib/util.ts index 9b24bde..baad6a6 100644 --- a/admin/inertia/lib/util.ts +++ b/admin/inertia/lib/util.ts @@ -1,4 +1,3 @@ -import axios from 'axios' export function capitalizeFirstLetter(str?: string | null): string { if (!str) return '' @@ -14,18 +13,6 @@ export function formatBytes(bytes: number, decimals = 2): string { return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] } -export async function testInternetConnection(): Promise { - try { - const response = await axios.get('https://1.1.1.1/cdn-cgi/trace', { - timeout: 5000, - }) - return response.status === 200 - } catch (error) { - console.error('Error testing internet connection:', error) - return false - } -} - export function generateRandomString(length: number): string { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' let result = '' diff --git a/admin/start/env.ts b/admin/start/env.ts index ab1ec20..1536404 100644 --- a/admin/start/env.ts +++ b/admin/start/env.ts @@ -18,6 +18,7 @@ export default await Env.create(new URL('../', import.meta.url), { HOST: Env.schema.string({ format: 'host' }), URL: Env.schema.string(), LOG_LEVEL: Env.schema.string(), + INTERNET_STATUS_TEST_URL: Env.schema.string.optional(), /* |---------------------------------------------------------- @@ -26,7 +27,6 @@ export default await Env.create(new URL('../', import.meta.url), { */ //SESSION_DRIVER: Env.schema.enum(['cookie', 'memory'] as const), - /* |---------------------------------------------------------- | Variables for configuring the database package diff --git a/admin/start/routes.ts b/admin/start/routes.ts index 02adcda..d92af9e 100644 --- a/admin/start/routes.ts +++ b/admin/start/routes.ts @@ -78,6 +78,7 @@ router router .group(() => { router.get('/info', [SystemController, 'getSystemInfo']) + router.get('/internet-status', [SystemController, 'getInternetStatus']) router.get('/services', [SystemController, 'getServices']) router.post('/services/affect', [SystemController, 'affectService']) router.post('/services/install', [SystemController, 'installService'])