## Summary Wave 8 of package code organization (plan: `docs/plans/2026-07-14-001-refactor-package-code-organization-plan.md`), after #2166. ### Peels | New module | Parent | |---|---| | `app/api/board-workflows.ts` | `legacy.ts` | | `app/api/remote.ts` | `legacy.ts` | | `app/api/memory.ts` | `legacy.ts` | | `types/docker-nodes.ts` | `types.ts` | | `task-store/task-store-helpers.ts` | `remaining-ops-10.ts` (rename) | ### CI fix (main unblocker) FN-8142 requires pi SDK **0.80.8+** (`ModelRuntime`, `CredentialStore.list`). Floored `@earendil-works/pi-*` to `^0.80.8` so lockfile resolves past 0.80.6. Public import paths stay stable via re-exports. ### LOC - `legacy.ts` ~10975 → ~10561 - `types.ts` ~7349 → ~6928 ## Test plan - [x] core / dashboard typecheck + eslint on peels - [x] api-tasks (66) - [x] CI merge gate (Lint / Typecheck / Build / Gate) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added dashboard support for multi-lane board workflows, including workflow metadata retrieval and task custom-field updates. * Added dashboard memory client capabilities for reading/writing memory, managing memory files, compaction/dreaming, insights, auditing, backend status, and retrieval testing. * Added remote connectivity controls, including tunnel start/stop, provider activation, token generation, URL sharing, and QR generation. * Added Docker node configuration validation plus secure redaction for sensitive fields in responses. * **Improvements** * Consolidated dashboard API exports into dedicated modules and preserved the legacy API surface via re-exports. * **Chores** * Updated related agent dependency versions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
152 lines
5.5 KiB
TypeScript
152 lines
5.5 KiB
TypeScript
/**
|
|
* FNXC:CodeOrganization 2026-07-16-20:00:
|
|
* Remote access / tunnel client API peeled from legacy.ts.
|
|
*/
|
|
import { api } from "./client.js";
|
|
import { withProjectId } from "./health.js";
|
|
|
|
export interface RemoteSettings {
|
|
remoteActiveProvider: "tailscale" | "cloudflare" | null;
|
|
remoteTailscaleEnabled: boolean;
|
|
remoteTailscaleHostname: string;
|
|
remoteTailscaleTargetPort: number;
|
|
remoteTailscaleAcceptRoutes: boolean;
|
|
remoteCloudflareEnabled: boolean;
|
|
remoteCloudflareQuickTunnel: boolean;
|
|
remoteCloudflareTunnelName: string;
|
|
remoteCloudflareTunnelToken: string | null;
|
|
remoteCloudflareIngressUrl: string;
|
|
remotePersistentToken: string | null;
|
|
remoteShortLivedEnabled: boolean;
|
|
remoteShortLivedTtlMs: number;
|
|
remoteShortLivedMaxTtlMs: number;
|
|
remoteRememberLastRunning: boolean;
|
|
remoteWasRunningOnShutdown: boolean;
|
|
remoteLastStartedProvider: "tailscale" | "cloudflare" | null;
|
|
}
|
|
|
|
export interface RemoteStatus {
|
|
provider: "tailscale" | "cloudflare" | null;
|
|
state: "stopped" | "starting" | "running" | "stopping" | "failed";
|
|
url: string | null;
|
|
lastError: string | null;
|
|
lastErrorCode?: string | null;
|
|
cloudflaredAvailable?: boolean | null;
|
|
externalTunnel?: {
|
|
provider: "tailscale" | "cloudflare";
|
|
url: string | null;
|
|
} | null;
|
|
restore?: {
|
|
outcome: "applied" | "skipped" | "failed";
|
|
reason: string;
|
|
at: string;
|
|
provider: "tailscale" | "cloudflare" | null;
|
|
message?: string;
|
|
};
|
|
}
|
|
|
|
export function fetchRemoteSettings(projectId?: string): Promise<{ settings: RemoteSettings }> {
|
|
return api<{ settings: RemoteSettings }>(withProjectId("/remote/settings", projectId));
|
|
}
|
|
|
|
export function updateRemoteSettings(
|
|
settings: Partial<RemoteSettings>,
|
|
projectId?: string,
|
|
): Promise<{ settings: RemoteSettings }> {
|
|
return api<{ settings: RemoteSettings }>(withProjectId("/remote/settings", projectId), {
|
|
method: "PUT",
|
|
body: JSON.stringify(settings),
|
|
});
|
|
}
|
|
|
|
export function fetchRemoteStatus(projectId?: string): Promise<RemoteStatus> {
|
|
return api<RemoteStatus>(withProjectId("/remote/status", projectId));
|
|
}
|
|
|
|
export function installCloudflared(projectId?: string): Promise<{ success: boolean; command: string; error?: string }> {
|
|
return api(withProjectId("/remote/install-cloudflared", projectId), {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
export function activateRemoteProvider(provider: "tailscale" | "cloudflare", projectId?: string): Promise<{ activeProvider: "tailscale" | "cloudflare" }> {
|
|
return api<{ activeProvider: "tailscale" | "cloudflare" }>(withProjectId("/remote/provider/activate", projectId), {
|
|
method: "POST",
|
|
body: JSON.stringify({ provider }),
|
|
});
|
|
}
|
|
|
|
export function startRemoteTunnel(projectId?: string): Promise<{ state: "starting" | "running"; provider: string }> {
|
|
return api<{ state: "starting" | "running"; provider: string }>(withProjectId("/remote/tunnel/start", projectId), {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
export function stopRemoteTunnel(projectId?: string): Promise<{ state: "stopped"; provider: string | null }> {
|
|
return api<{ state: "stopped"; provider: string | null }>(withProjectId("/remote/tunnel/stop", projectId), {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
export function killExternalTunnel(projectId?: string): Promise<{ ok: boolean }> {
|
|
return api<{ ok: boolean }>(withProjectId("/remote/tunnel/kill-external", projectId), {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
export function regenerateRemotePersistentToken(projectId?: string): Promise<{ token: string; maskedToken: string }> {
|
|
return api<{ token: string; maskedToken: string }>(withProjectId("/remote/token/persistent/regenerate", projectId), {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
export function generateShortLivedRemoteToken(ttlMs: number, projectId?: string): Promise<{ token: string; expiresAt: string; ttlMs: number }> {
|
|
return api<{ token: string; expiresAt: string; ttlMs: number }>(withProjectId("/remote/token/short-lived/generate", projectId), {
|
|
method: "POST",
|
|
body: JSON.stringify({ ttlMs }),
|
|
});
|
|
}
|
|
|
|
type RemoteAuthTokenType = "persistent" | "short-lived";
|
|
|
|
type RemoteLinkRequestOptions = {
|
|
projectId?: string;
|
|
tokenType?: RemoteAuthTokenType;
|
|
ttlMs?: number;
|
|
};
|
|
|
|
function buildRemoteAuthQuery(
|
|
format: "text" | "image/svg" | null,
|
|
tokenType: RemoteAuthTokenType,
|
|
ttlMs?: number,
|
|
): string {
|
|
const params = new URLSearchParams();
|
|
if (format) {
|
|
params.set("format", format);
|
|
}
|
|
params.set("tokenType", tokenType);
|
|
if (tokenType === "short-lived" && typeof ttlMs === "number" && Number.isFinite(ttlMs)) {
|
|
params.set("ttlMs", String(ttlMs));
|
|
}
|
|
const query = params.toString();
|
|
return query ? `?${query}` : "";
|
|
}
|
|
|
|
export function fetchRemoteUrl(
|
|
options: RemoteLinkRequestOptions = {},
|
|
): Promise<{ url: string; tokenType: RemoteAuthTokenType; expiresAt: string | null }> {
|
|
const { projectId, tokenType = "persistent", ttlMs } = options;
|
|
const query = buildRemoteAuthQuery(null, tokenType, ttlMs);
|
|
return api<{ url: string; tokenType: RemoteAuthTokenType; expiresAt: string | null }>(withProjectId(`/remote/url${query}`, projectId));
|
|
}
|
|
|
|
export function fetchRemoteQr(
|
|
format: "text" | "image/svg" = "text",
|
|
options: RemoteLinkRequestOptions = {},
|
|
): Promise<{ url: string; tokenType: RemoteAuthTokenType; expiresAt: string | null; format: "text" | "image/svg"; data?: string }> {
|
|
const { projectId, tokenType = "persistent", ttlMs } = options;
|
|
const query = buildRemoteAuthQuery(format, tokenType, ttlMs);
|
|
return api<{ url: string; tokenType: RemoteAuthTokenType; expiresAt: string | null; format: "text" | "image/svg"; data?: string }>(withProjectId(`/remote/qr${query}`, projectId));
|
|
}
|
|
|