From 21054aaa8c9f35873da4c1e935dae33e97cf2b84 Mon Sep 17 00:00:00 2001 From: Bobban Rydh Date: Wed, 30 Sep 2026 00:23:52 +0200 Subject: [PATCH] Fill an admin link's URL from a server's own known ports Both places you add an admin link - a server's own Admin Links section and the Operations > Admin Links summary page - now offer a "fill from a known port" dropdown once a server is picked, listing its agent-reported and manually-noted ports (the same data the Ports page/card shows). Picking one fills in the URL as http(s)://:, guessing https for a handful of common admin-panel ports, and fills the label too if it's still empty. The URL field stays fully editable either way. Shared the address/label logic in a small adminLinkUrl.ts helper used by both pages. No server-side changes needed - built entirely on the existing per-server ports and detail endpoints. Co-Authored-By: Claude Sonnet 5 --- web/src/pages/AdminLinks.tsx | 70 +++++++++++++++++++++++++++++++++- web/src/pages/ServerDetail.tsx | 46 ++++++++++++++++++++++ web/src/utils/adminLinkUrl.ts | 20 ++++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 web/src/utils/adminLinkUrl.ts diff --git a/web/src/pages/AdminLinks.tsx b/web/src/pages/AdminLinks.tsx index 9172929..57df684 100644 --- a/web/src/pages/AdminLinks.tsx +++ b/web/src/pages/AdminLinks.tsx @@ -1,9 +1,10 @@ import { useEffect, useMemo, useState, type FormEvent } from "react"; import { Link } from "react-router-dom"; -import { api, type AdminLink, type CurrentUser, type ServerRecord } from "../api/client"; +import { api, type AdminLink, type CurrentUser, type PortEntry, type ServerRecord } from "../api/client"; import { useSortable } from "../hooks/useSortable"; import SortableTh from "../components/SortableTh"; import { downloadCsv } from "../utils/csv"; +import { guessAdminUrl, portOptionLabel } from "../utils/adminLinkUrl"; interface LinkForm { serverId: number | ""; @@ -26,6 +27,11 @@ export default function AdminLinks({ user }: { user: CurrentUser }) { const [form, setForm] = useState(emptyForm); const [saving, setSaving] = useState(false); + const [portOptions, setPortOptions] = useState(null); + const [portAddress, setPortAddress] = useState(null); + const [loadingPorts, setLoadingPorts] = useState(false); + const [portPick, setPortPick] = useState(""); + function load() { api.servers.allLinks() .then((res) => setLinks(res.links)) @@ -40,6 +46,37 @@ export default function AdminLinks({ user }: { user: CurrentUser }) { .catch(() => {}); }, []); + // Once a server is picked in the add/edit form, offer to fill the URL from one of its own known + // ports (agent-reported or already noted on its Ports card) instead of typing it out by hand. + useEffect(() => { + setPortPick(""); + if (form.serverId === "") { + setPortOptions(null); + setPortAddress(null); + return; + } + const serverId = form.serverId; + setLoadingPorts(true); + Promise.all([api.servers.detail(serverId), api.servers.ports.list(serverId)]) + .then(([detail, portList]) => { + setPortAddress(detail.server.hostname || detail.ipAddresses[0] || null); + setPortOptions(portList.ports); + }) + .catch(() => { + setPortOptions(null); + setPortAddress(null); + }) + .finally(() => setLoadingPorts(false)); + }, [form.serverId]); + + function pickPort(key: string) { + setPortPick(key); + const entry = portOptions?.find((p) => `${p.protocol}:${p.port}` === key); + if (!entry) return; + const url = guessAdminUrl(portAddress, entry); + setForm((f) => ({ ...f, url: url ?? f.url, label: f.label || entry.label || entry.agent?.process || "" })); + } + const filtered = useMemo(() => { if (!links) return []; const q = search.trim().toLowerCase(); @@ -166,6 +203,37 @@ export default function AdminLinks({ user }: { user: CurrentUser }) { onChange={(e) => setForm({ ...form, url: e.target.value })} /> + {form.serverId !== "" && ( +
+ + + {portOptions && portOptions.length > 0 && !portAddress && ( +
+ This server has no known hostname or IP yet, so picking a port won't fill in an address — + see its Ports card under Infrastructure → Servers. +
+ )} +
+ )}
+ {!!adminLinkPorts?.length && ( +
+ + +
+ )}