Add batched status endpoint and optimize frontend performance

Server-side batched /api/v1/services/status endpoint replaces N
individual browser probes with a single API call (HEAD-first with
GET fallback, concurrency-limited, CA-aware HTTPS agent).

Frontend: clock reuses DOM instead of rebuilding innerHTML every
second with drift-correcting timer that pauses on hidden tabs.
Card animations use CSS transitionDelay + requestAnimationFrame.
Internet dot blink moved from JS intervals to CSS keyframes with
prefers-reduced-motion support. Service worker rewritten with
network-first navigation, stale-while-revalidate assets, and
navigation preload. Font faces drop TTF fallbacks, use font-display
swap.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 22:39:29 -07:00
parent 063bf948b1
commit 0f4bd419e1
6 changed files with 476 additions and 150 deletions

View File

@@ -1,13 +1,135 @@
const express = require('express');
const fs = require('fs');
const http = require('http');
const https = require('https');
const tls = require('tls');
const validatorLib = require('validator');
const { REGEX } = require('../constants');
const { APP, REGEX, TIMEOUTS } = require('../constants');
const { validateServiceConfig, isValidPort } = require('../input-validator');
const { exists } = require('../fs-helpers');
const { paginate, parsePaginationParams } = require('../pagination');
module.exports = function(ctx) {
const router = express.Router();
const CA_CERT_PATH = process.env.CA_CERT_PATH || '/app/pki/root.crt';
const PROBE_CONCURRENCY = 6;
let probeHttpsAgent;
try {
const caCert = fs.readFileSync(CA_CERT_PATH);
probeHttpsAgent = new https.Agent({ ca: [...tls.rootCertificates, caCert] });
} catch (_) {
probeHttpsAgent = new https.Agent();
}
function isServiceUp(statusCode) {
return (statusCode >= 200 && statusCode < 400) || statusCode === 401 || statusCode === 403;
}
async function loadServicesList() {
if (!await exists(ctx.SERVICES_FILE)) return [];
const data = await ctx.servicesStateManager.read();
return Array.isArray(data) ? data : data.services || [];
}
function resolveProbeUrl(id, service) {
if (id === 'internet') return 'https://www.google.com';
if (service?.isExternal && service.externalUrl) return service.externalUrl;
if (service?.url) return service.url.startsWith('http') ? service.url : `https://${service.url}`;
return ctx.buildServiceUrl(id);
}
function requestStatusCode(url, method) {
const parsed = new URL(url);
const isHttps = parsed.protocol === 'https:';
const lib = isHttps ? https : http;
return new Promise((resolve, reject) => {
const req = lib.request({
hostname: parsed.hostname,
port: parsed.port || (isHttps ? 443 : 80),
path: parsed.pathname + parsed.search,
method,
timeout: TIMEOUTS.HTTP_DEFAULT,
agent: isHttps ? probeHttpsAgent : undefined,
headers: { 'User-Agent': APP.USER_AGENTS.PROBE },
}, (response) => {
response.resume();
resolve(response.statusCode || 0);
});
req.on('error', reject);
req.on('timeout', () => {
req.destroy();
reject(new Error('Timeout'));
});
req.end();
});
}
async function probeServiceStatus(id, service) {
const startedAt = process.hrtime.bigint();
let url = resolveProbeUrl(id, service);
let statusCode = 502;
let error = null;
try {
statusCode = await requestStatusCode(url, 'HEAD');
if (statusCode === 405 || statusCode === 501) {
statusCode = await requestStatusCode(url, 'GET');
}
} catch (primaryError) {
error = primaryError;
if (id !== 'internet') {
const fallbackUrl = ctx.buildServiceUrl(id);
if (fallbackUrl !== url) {
try {
statusCode = await requestStatusCode(fallbackUrl, 'GET');
url = fallbackUrl;
error = null;
} catch (fallbackError) {
error = fallbackError;
}
}
}
}
const responseTime = Number((process.hrtime.bigint() - startedAt) / 1000000n);
if (error) {
return {
id,
isUp: false,
statusCode: 502,
responseTime,
error: error.message
};
}
return {
id,
isUp: isServiceUp(statusCode),
statusCode,
responseTime,
url
};
}
async function mapWithConcurrency(items, limit, worker) {
const results = new Array(items.length);
let cursor = 0;
async function next() {
while (true) {
const index = cursor++;
if (index >= items.length) return;
results[index] = await worker(items[index], index);
}
}
const workers = Array.from({ length: Math.min(limit, items.length || 1) }, () => next());
await Promise.all(workers);
return results;
}
// ===== SERVICE CREDENTIAL ENDPOINTS =====
@@ -111,6 +233,40 @@ module.exports = function(ctx) {
// ===== SERVICE CRUD ENDPOINTS =====
// Batched live status for dashboard cards
router.get('/services/status', ctx.asyncHandler(async (req, res) => {
const services = await loadServicesList();
const serviceMap = new Map(services.filter(s => s && s.id).map(s => [s.id, s]));
const ids = [];
const seen = new Set();
function addId(id) {
if (!id || seen.has(id)) return;
seen.add(id);
ids.push(id);
}
addId('internet');
Object.keys(ctx.siteConfig?.dnsServers || {}).forEach(addId);
services.forEach(service => addId(service.id));
const statusResults = await mapWithConcurrency(ids, PROBE_CONCURRENCY, (id) =>
probeServiceStatus(id, serviceMap.get(id))
);
const statuses = {};
statusResults.forEach((result) => {
statuses[result.id] = result;
});
res.set('Cache-Control', 'no-store');
res.json({
success: true,
checkedAt: new Date().toISOString(),
statuses
});
}, 'services-status'));
// List all services
router.get('/services', ctx.asyncHandler(async (req, res) => {
if (!await exists(ctx.SERVICES_FILE)) {