mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-18 06:23:18 +00:00
f253276303
* fix: make published port links open reliably Container published-port links now render as real anchors that open on desktop and mobile, replacing ad hoc window.open calls. A shared service URL builder centralizes host resolution (configured host, remote node API host, or the browser host, with no browser fallback for unreachable remote nodes), protocol selection (HTTPS for port 443), and known app sub-paths (Plex opens its web path). The container port mapping itself is the link, with a Copy URL action beside it. The stack Open App menu and the anatomy panel footer use the same builder, and the menu only offers Open App when a reachable URL can be built. * fix: skip UDP ports and scope known-app paths to the container port Two follow-ups to the published-port links: - The known-app path (Plex web sub-path) was borrowed from the published host port even when the container port was known and unregistered, so a non-Plex service published on host port 32400 wrongly inherited it. The container-port lookup now wins when known; the published-port lookup stays a fallback for the menu and anatomy footer, which only have the host port. - UDP ports could surface as HTTP links. The backend now carries the port protocol through both container-mapping paths and skips UDP when choosing the main web port (extracted as selectMainWebPort), and the container card filters UDP before selecting a port to link.
115 lines
4.0 KiB
TypeScript
115 lines
4.0 KiB
TypeScript
/**
|
|
* Build and open the browser URL for a container's published service port.
|
|
*
|
|
* Centralizes the host, protocol, and path logic that the container card, the
|
|
* stack "Open App" menu, and the stack anatomy footer all need, so a published
|
|
* port renders as a real, reliable link instead of an ad hoc
|
|
* `window.open('http://host:port')`.
|
|
*/
|
|
|
|
// Some multi-port apps serve their UI at a sub-path, so a bare host:port does
|
|
// not land on the working page. Keyed by the app's container (private) port.
|
|
const KNOWN_SERVICE_PATHS: Record<number, string> = {
|
|
32400: '/web', // Plex
|
|
};
|
|
|
|
function isValidPort(port: number): boolean {
|
|
return Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
}
|
|
|
|
// Accepts a bare host or a full URL (a future configured public host may be
|
|
// pasted either way) and returns just the hostname, or '' when unusable.
|
|
function normalizeHost(value: string): string {
|
|
const trimmed = value.trim();
|
|
if (!trimmed) return '';
|
|
if (trimmed.includes('://')) {
|
|
try {
|
|
return new URL(trimmed).hostname;
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
return trimmed;
|
|
}
|
|
|
|
export interface ServiceUrlOptions {
|
|
/** Active node; a remote node resolves to its own host, never the browser host. */
|
|
node?: { type?: 'local' | 'remote'; api_url?: string } | null;
|
|
/** Published host port (the browser-reachable one). */
|
|
publicPort: number;
|
|
/** Container port, used for protocol/path inference only. */
|
|
privatePort?: number;
|
|
/** Configured public/service host that overrides the node host. Reserved for future use. */
|
|
publicHost?: string | null;
|
|
/** Browser hostname; defaults to window.location.hostname. Injectable for tests. */
|
|
browserHost?: string;
|
|
/** Explicit protocol override. Reserved for future use. */
|
|
protocol?: 'http' | 'https';
|
|
}
|
|
|
|
/**
|
|
* Returns the browser URL for the published port, or null when no
|
|
* browser-reachable host can be resolved (e.g. a remote node with no API URL,
|
|
* such as a pilot-agent node) or the port is out of range. Callers render a
|
|
* link only when this is non-null.
|
|
*/
|
|
export function buildServiceUrl(opts: ServiceUrlOptions): string | null {
|
|
const { node, publicPort, privatePort, publicHost, browserHost, protocol } = opts;
|
|
if (!isValidPort(publicPort)) return null;
|
|
|
|
const host = resolveHost(node, publicHost, browserHost);
|
|
if (!host) return null;
|
|
|
|
const scheme = protocol ?? (publicPort === 443 || privatePort === 443 ? 'https' : 'http');
|
|
// When the container port is known, only it decides the app path. The
|
|
// published-port lookup is a fallback for callers that do not know it (the
|
|
// "Open App" menu and anatomy footer), where the host port is the best signal.
|
|
const path =
|
|
privatePort !== undefined
|
|
? (KNOWN_SERVICE_PATHS[privatePort] ?? '')
|
|
: (KNOWN_SERVICE_PATHS[publicPort] ?? '');
|
|
return `${scheme}://${host}:${publicPort}${path}`;
|
|
}
|
|
|
|
function resolveHost(
|
|
node: ServiceUrlOptions['node'],
|
|
publicHost: string | null | undefined,
|
|
browserHost: string | undefined,
|
|
): string | null {
|
|
if (publicHost) {
|
|
const normalized = normalizeHost(publicHost);
|
|
if (normalized) return normalized;
|
|
}
|
|
if (node?.type === 'remote') {
|
|
// A remote node must resolve to its own reachable host. Never fall back to
|
|
// the browser host: that points at the control instance, not the remote.
|
|
if (node.api_url) {
|
|
try {
|
|
return new URL(node.api_url).hostname || null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
// Local or no node: the browser is talking to the instance that runs the
|
|
// stacks, so its hostname is the right target.
|
|
if (browserHost) return browserHost;
|
|
return typeof window !== 'undefined' ? window.location.hostname : null;
|
|
}
|
|
|
|
/**
|
|
* Open a URL in a new tab via a transient anchor click. More reliable than
|
|
* window.open on mobile browsers, which may block programmatic popups.
|
|
*/
|
|
export function openServiceUrl(url: string): void {
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.target = '_blank';
|
|
a.rel = 'noopener noreferrer';
|
|
a.style.display = 'none';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
}
|