feat(FN-2976): detect and display external Tailscale funnel tunnels in remo

Merges external Tailscale funnel detection (FN-2976) — adds types, detection logic, status inclusion, kill flow, and a dedicated UI panel for funnel processes started outside Fusion — alongside custom AI providers API routes and a new settings UI section (FN-2965).

Fusion-Task-Id: FN-2976
This commit is contained in:
Fusion
2026-04-30 05:41:25 -07:00
committed by gsxdsm
parent 1b3e68740e
commit 48f2dd0de2
16 changed files with 506 additions and 8 deletions

View File

@@ -494,18 +494,29 @@ export function registerSettingsMemoryRoutes(ctx: ApiRoutesContext, deps: Settin
const restore = engine?.getRemoteTunnelRestoreDiagnostics();
const activeProvider = tunnelStatus?.provider ?? settings.remoteAccess?.activeProvider ?? null;
const tunnelState = tunnelStatus?.state ?? "stopped";
let cloudflaredAvailable: boolean | null = null;
if (activeProvider === "cloudflare") {
cloudflaredAvailable = await isCloudflaredAvailable();
}
const externalTunnel = tunnelState === "stopped"
? await engine?.detectExternalTunnel()
: null;
res.json({
provider: activeProvider,
state: tunnelStatus?.state ?? "stopped",
state: tunnelState,
url: tunnelStatus?.url ?? null,
lastError: tunnelStatus?.lastError?.message ?? null,
lastErrorCode: tunnelStatus?.lastError?.code ?? null,
cloudflaredAvailable,
externalTunnel: externalTunnel
? {
provider: externalTunnel.provider,
url: externalTunnel.url,
}
: null,
restore: restore ?? {
outcome: "skipped",
reason: "not_attempted",
@@ -634,6 +645,19 @@ export function registerSettingsMemoryRoutes(ctx: ApiRoutesContext, deps: Settin
}
});
router.post("/remote/tunnel/kill-external", async (req, res) => {
try {
const { engine } = await getProjectContext(req);
if (engine) {
await engine.killExternalTunnel();
}
res.json({ ok: true });
} catch (err: unknown) {
if (err instanceof ApiError) throw err;
rethrowAsApiError(err, "Failed to kill external remote tunnel");
}
});
router.post("/remote/token/persistent/regenerate", async (req, res) => {
try {
const { store: scopedStore } = await getProjectContext(req);