feat(System): check internet status on backend and allow custom test url

This commit is contained in:
Jake Turner 2025-12-24 11:59:48 -08:00 committed by Jake Turner
parent 7029e1ea81
commit a2206b8c13
7 changed files with 59 additions and 19 deletions

View File

@ -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();
}

View File

@ -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<boolean> {
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<ServiceSlim[]> {
const query = Service.query()
.orderBy('friendly_name', 'asc')

View File

@ -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<boolean>(false);
const { data } = useQuery<boolean>({
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

View File

@ -92,6 +92,13 @@ class API {
})()
}
async getInternetStatus() {
return catchInternal(async () => {
const response = await this.client.get<boolean>('/system/internet-status')
return response.data
})()
}
async getSystemInfo() {
return catchInternal(async () => {
const response = await this.client.get<SystemInformationResponse>('/system/info')

View File

@ -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<boolean> {
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 = ''

View File

@ -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

View File

@ -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'])