diff --git a/agent/hpagent/websocket.go b/agent/hpagent/websocket.go index fc544ca..1434441 100644 --- a/agent/hpagent/websocket.go +++ b/agent/hpagent/websocket.go @@ -53,5 +53,11 @@ func httpToWs(controlURL string) (string, error) { return "", fmt.Errorf("unsupported scheme: %s", u.Scheme) } + // We also need to append /_dial to the path + if u.Path[len(u.Path)-1] != '/' { + u.Path += "/" + } + + u.Path += "_dial" return u.String(), nil } diff --git a/app/routes.ts b/app/routes.ts index 7fcf112..51a6363 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -11,9 +11,6 @@ export default [ route('/oidc/callback', 'routes/auth/oidc-callback.ts'), route('/oidc/start', 'routes/auth/oidc-start.ts'), - // API - route('/api/agent', 'routes/api/agent.ts'), - // All the main logged-in dashboard routes // Double nested to separate error propagations layout('layouts/shell.tsx', [ diff --git a/app/routes/api/agent.ts b/app/routes/api/agent.ts deleted file mode 100644 index 7cdc32d..0000000 --- a/app/routes/api/agent.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { LoaderFunctionArgs } from 'react-router'; -import { hp_getSingleton, hp_getSingletonUnsafe } from '~server/context/global'; - -export async function loader({ request }: LoaderFunctionArgs) { - const data = hp_getSingletonUnsafe('ws_agent_data'); - - if (!data) { - return new Response(JSON.stringify({ error: 'Agent data unavailable' }), { - status: 400, - headers: { - 'Content-Type': 'application/json', - }, - }); - } - - const qp = new URLSearchParams(request.url.split('?')[1]); - const nodeIds = qp.get('node_ids')?.split(','); - if (!nodeIds) { - return new Response(JSON.stringify({ error: 'No node IDs provided' }), { - status: 400, - headers: { - 'Content-Type': 'application/json', - }, - }); - } - - const entries = data.toJSON(); - const missing = nodeIds.filter((nodeID) => !entries[nodeID]); - if (missing.length > 0) { - const requestCall = hp_getSingleton('ws_fetch_data'); - requestCall(missing); - } - - return new Response(JSON.stringify(data), { - headers: { - 'Content-Type': 'application/json', - }, - }); -} diff --git a/app/routes/machines/overview.tsx b/app/routes/machines/overview.tsx index e963328..b6bbd24 100644 --- a/app/routes/machines/overview.tsx +++ b/app/routes/machines/overview.tsx @@ -8,7 +8,6 @@ import Tooltip from '~/components/Tooltip'; import type { LoadContext } from '~/server'; import type { Machine, Route, User } from '~/types'; import cn from '~/utils/cn'; -import useAgent from '~/utils/use-agent'; import { menuAction } from './action'; import MachineRow from './components/machine'; import NewMachine from './dialogs/new'; @@ -45,6 +44,7 @@ export async function loader({ server: context.config.headscale.url, publicServer: context.config.headscale.public_url, agents: context.agents?.tailnetIDs(), + stats: context.agents?.lookup(machines.nodes.map((node) => node.nodeKey)), }; } @@ -54,7 +54,6 @@ export async function action(request: ActionFunctionArgs) { export default function Page() { const data = useLoaderData(); - const { data: stats } = useAgent(data.nodes.map((node) => node.nodeKey)); return ( <> @@ -120,10 +119,10 @@ export default function Page() { )} users={data.users} magic={data.magic} - stats={stats?.[machine.nodeKey]} // If we pass undefined, the column will not be rendered // This is useful for when there are no agents configured isAgent={data.agents?.includes(machine.id)} + stats={data.stats?.[machine.nodeKey]} /> ))} diff --git a/app/server/index.ts b/app/server/index.ts index cc8a2c6..89fefcc 100644 --- a/app/server/index.ts +++ b/app/server/index.ts @@ -74,11 +74,17 @@ export default await createHonoServer({ return appLoadContext; }, - configure(server, { upgradeWebSocket }) { - if (appLoadContext.agents !== undefined) { - // We need this since we cannot pass the WSEvents context - (upgradeWebSocket as UpgradeWebSocket)( - appLoadContext.agents.configureSocket, + configure(app, { upgradeWebSocket }) { + const agentManager = appLoadContext.agents; + if (agentManager) { + app.get( + `${__PREFIX__}/_dial`, + // We need this since we cannot pass the WSEvents context + // Also important to not pass the callback directly + // since we need to retain `this` context + (upgradeWebSocket as UpgradeWebSocket)((c) => + agentManager.configureSocket(c), + ), ); } }, diff --git a/app/server/web/agent.ts b/app/server/web/agent.ts index dfeae51..62c1f0c 100644 --- a/app/server/web/agent.ts +++ b/app/server/web/agent.ts @@ -49,10 +49,20 @@ class AgentManager { return Array.from(this.agents.keys()); } + lookup(nodeIds: string[]) { + const entries = this.cache.toJSON(); + const missing = nodeIds.filter((nodeId) => !entries[nodeId]); + if (missing.length > 0) { + this.requestData(missing); + } + + return entries; + } + // Request data from all connected agents // This does not return anything, but caches the data which then needs to be // queried by the caller separately. - requestData(nodeList: string[]) { + private requestData(nodeList: string[]) { const NodeIDs = [...new Set(nodeList)]; NodeIDs.map((node) => { log.debug('agent', 'Requesting agent data for %s', node); diff --git a/app/utils/use-agent.tsx b/app/utils/use-agent.tsx deleted file mode 100644 index a879313..0000000 --- a/app/utils/use-agent.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { useEffect, useMemo, useRef } from 'react'; -import { useFetcher } from 'react-router'; -import { HostInfo } from '~/types'; - -export default function useAgent(nodeIds: string[]) { - const fetcher = useFetcher>(); - const qp = useMemo( - () => new URLSearchParams({ node_ids: nodeIds.join(',') }), - [nodeIds], - ); - - const idRef = useRef([]); - useEffect(() => { - if (idRef.current.join(',') !== nodeIds.join(',')) { - fetcher.load(`/api/agent?${qp.toString()}`); - idRef.current = nodeIds; - } - }, [qp.toString()]); - - return { - data: fetcher.data, - isLoading: fetcher.state === 'loading', - }; -}