refactor(ui): move the log-tail pollers onto usePolling

Plan 76 milestone E2, continuing f9657d38. Ratchet 27 sites / 18 files -> 22 / 15.

Terminal's three tab components (file logs, journal, processes), ServicesTab's
unit log tail, and the GUI extension's synthetic desktop. All were the
auto-refresh tail shape: poll only while the toggle is on, never fetch on mount
from the interval effect.

These are the sites where a hidden tab mattered most — a log tail at 3s left
open in a background tab is the single most wasteful poller in the panel, and
every one of them also re-ran on `lineCount`/`priority`/`sort` changes via a
dependency array with `// eslint-disable-line` on it. That silent restart is now
explicit: the tail's own load effects still handle those, the poll handles the
repeat.

Four `intervalRef`s and one `logIntervalRef` went with them.

Gate: npm run build clean, npm run lint 0 errors / 953 warnings, npm test 72
passed, boundary check green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Juan DenisandClaude Opus 5 committed 2026-08-18 21:48:31 -04:00
1 parent f9657d38de
commit 89c3896ec4
4 files changed
+44 -35

No files matched your search

@@ -49,7 +49,6 @@ const LEGACY_POLLERS = new Map(Object.entries({
'components/monitoring/DoctorPanel.jsx': 1,
'components/security/ScannerTab.jsx': 2,
'components/server/OnboardingWizard.jsx': 2,
'components/serverdetail/ServicesTab.jsx': 1,
'hooks/useContainerStatus.js': 1,
'hooks/useDeployJobStream.js': 1,
'hooks/useMetrics.js': 1,
@@ -57,10 +56,8 @@ const LEGACY_POLLERS = new Map(Object.entries({
'pages/DeployConsole.jsx': 1,
'pages/ImportWizard.jsx': 1,
'pages/Monitors.jsx': 1,
'pages/Terminal.jsx': 3,
'pages/TestSandbox.jsx': 2,
'plugins/serverkit-gui/components/ServerGui.jsx': 1,
'plugins/serverkit-gui/components/SyntheticDesktop.jsx': 1,
}));
// The polling door itself, plus the query hook that wraps it.
@@ -13,6 +13,11 @@ import { useColumnVisibility } from '@/hooks/useColumnVisibility';
import LogToolbar from '../log-viewer/LogToolbar';
import LogContent from '../log-viewer/LogContent';
import { downloadBlob } from '@/utils/downloadBlob';
import { usePolling } from '@/hooks/usePolling';
// Unit log tail cadence while auto-refresh is on.
const LOG_TAIL_MS = 3000;
// Built-in saved views. These are the four buttons that used to sit in the
// toolbar (All / Active / Failed / Inactive) — they narrowed the AGENT query
@@ -139,7 +144,6 @@ const ServicesTab = ({ serverId = null, serverStatus = 'online' }) => {
const [logWrap, setLogWrap] = useState(true);
const [logAutoRefresh, setLogAutoRefresh] = useState(false);
const logContentRef = useRef(null);
const logIntervalRef = useRef(null);
// One unfiltered fetch. The state buckets are column rules now, so asking
// the agent for a subset would only hide rows from the rules — and from the
@@ -243,12 +247,11 @@ const ServicesTab = ({ serverId = null, serverStatus = 'online' }) => {
}
}, [isLocal, serverId, logLineCount]);
useEffect(() => {
if (logAutoRefresh && logsFor) {
logIntervalRef.current = setInterval(() => loadLogs(logsFor.unit, { spinner: false }), 3000);
}
return () => { if (logIntervalRef.current) clearInterval(logIntervalRef.current); };
}, [logAutoRefresh, logsFor, loadLogs]);
usePolling(
() => loadLogs(logsFor.unit, { spinner: false }),
LOG_TAIL_MS,
{ enabled: logAutoRefresh && Boolean(logsFor), immediate: false },
);
function openLogs(unit) {
setLogsFor({ unit });
+20 -23
View File
@@ -16,6 +16,13 @@ import SystemdServicesTab from '../components/serverdetail/ServicesTab';
import { useTableSort } from '@/hooks/useTableSort';
import PageLayout from '../layouts/PageLayout';
import { downloadBlob } from '@/utils/downloadBlob';
import { usePolling } from '@/hooks/usePolling';
// Log/journal tail cadence while auto-refresh is on.
const LOG_TAIL_MS = 3000;
// Process table cadence while auto-refresh is on.
const PROCESS_REFRESH_MS = 4000;
import {
FileText, Clock, AlertCircle, Search, X, AlertTriangle, Activity,
Terminal as TerminalIcon, Server as ServerIcon,
@@ -173,7 +180,6 @@ const LogFilesTab = () => {
const [isFullscreen, setIsFullscreen] = useState(false);
const contentRef = useRef(null);
const intervalRef = useRef(null);
const selectedLogObj = useMemo(
() => logFiles.find((l) => l.path === selectedLog) || null,
[logFiles, selectedLog]
@@ -191,14 +197,11 @@ const LogFilesTab = () => {
loadLogFiles();
}, [target.kind, target.server_id]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (autoRefresh && selectedLog) {
intervalRef.current = setInterval(() => {
loadLogContent(selectedLog, false);
}, 3000);
}
return () => { if (intervalRef.current) clearInterval(intervalRef.current); };
}, [autoRefresh, selectedLog, lineCount, appliedSearch]); // eslint-disable-line
usePolling(
() => loadLogContent(selectedLog, false),
LOG_TAIL_MS,
{ enabled: autoRefresh && Boolean(selectedLog), immediate: false },
);
function ensureSupported(op) {
if (isRemote && !REMOTE_LOG_SUPPORTED.has(op)) {
@@ -495,7 +498,6 @@ const JournalTab = () => {
const [lastUpdated, setLastUpdated] = useState(null);
const contentRef = useRef(null);
const intervalRef = useRef(null);
const isJournalctl = source === 'journalctl' || source === '';
useEffect(() => { localStorage.setItem(JOURNAL_PREFS.showLineNumbers, showLineNumbers); }, [showLineNumbers]);
@@ -506,12 +508,10 @@ const JournalTab = () => {
loadJournalLogs();
}, [target.kind, target.server_id]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (autoRefresh) {
intervalRef.current = setInterval(() => loadJournalLogs(false), 3000);
}
return () => { if (intervalRef.current) clearInterval(intervalRef.current); };
}, [autoRefresh, unit, lineCount, priority]); // eslint-disable-line
usePolling(() => loadJournalLogs(false), LOG_TAIL_MS, {
enabled: autoRefresh,
immediate: false,
});
async function loadJournalLogs(showSpinner = true) {
if (isRemote && !REMOTE_JOURNAL_SUPPORTED.has('read')) {
@@ -800,18 +800,15 @@ const ProcessesTab = () => {
});
const fetchSort = sorts[0]?.key === 'memory' ? 'memory' : 'cpu';
const intervalRef = useRef(null);
useEffect(() => {
loadProcesses();
}, [fetchSort, limit, target.kind, target.server_id]); // eslint-disable-line
useEffect(() => {
if (autoRefresh) {
intervalRef.current = setInterval(() => loadProcesses(false), 4000);
}
return () => { if (intervalRef.current) clearInterval(intervalRef.current); };
}, [autoRefresh, fetchSort, limit]); // eslint-disable-line
usePolling(() => loadProcesses(false), PROCESS_REFRESH_MS, {
enabled: autoRefresh,
immediate: false,
});
async function loadProcesses(showSpinner = true) {
if (isRemote) {
@@ -1,4 +1,9 @@
import React, { useEffect, useState } from 'react';
import { usePolling } from '@/hooks/usePolling';
// Synthetic desktop refresh cadence.
const SYNTHETIC_REFRESH_MS = 4000;
/**
* Fake desktop UI rendered from agent data, used when the host has no
@@ -16,10 +21,17 @@ export default function SyntheticDesktop({ api, serverId, fetchJson }) {
.catch(err => { if (!cancelled) setError(err.message); });
};
load();
const id = setInterval(load, 4000);
return () => { cancelled = true; clearInterval(id); };
return () => { cancelled = true; };
}, [serverId, fetchJson]);
usePolling(
() => fetchJson(`/api/v1/server-gui/${serverId}/synthetic`)
.then(setData)
.catch((err) => setError(err.message)),
SYNTHETIC_REFRESH_MS,
{ immediate: false },
);
if (error) return <div className="sk-gui__banner sk-gui__banner--error">{error}</div>;
if (!data) return <div className="sk-gui__loading">Loading synthetic desktop…</div>;