Files
podman/frontend/src/lib/api.ts
T
Yahya Alhinai 454fdab13b chore(box): snapshot in-progress auth + user-learning WIP before deploy
Captures uncommitted work present on the live droplet so origin/main can be integrated and the new control toolbar deployed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 17:51:21 +00:00

269 lines
7.2 KiB
TypeScript

import type {
InterventionOutcome,
HermesJob,
HermesJobEvent,
MemberWorkHistory,
Pod,
PodActivityEvent,
PodInput,
} from '@podman/shared';
const BACKEND_URL =
import.meta.env.VITE_BACKEND_URL ||
(import.meta.env.DEV || ['localhost', '127.0.0.1'].includes(window.location.hostname)
? 'http://localhost:8787'
: '');
type AuthTokenGetter = () => Promise<string | null>;
let authTokenGetter: AuthTokenGetter | null = null;
export function setAuthTokenGetter(getter: AuthTokenGetter | null): void {
authTokenGetter = getter;
}
async function requestHeaders(init?: HeadersInit): Promise<Headers> {
const next = new Headers(init);
const token = await authTokenGetter?.();
if (token) next.set('authorization', `Bearer ${token}`);
return next;
}
async function apiFetch(input: string, init: RequestInit = {}): Promise<Response> {
return fetch(input, {
...init,
headers: await requestHeaders(init.headers),
});
}
export interface MemoryStats {
observations: number;
collisions: number;
interventions: number;
outcomes: number;
userPodContext?: number;
}
export interface LiveConversationSession {
sessionId: string;
podId: string;
identity: string;
displayName: string;
room: string;
url: string;
token: string;
startedAt: string;
lastEventAt?: string;
endedAt?: string;
}
export interface UserProfilePayload {
displayName?: string;
email?: string;
imageUrl?: string;
}
async function json<T>(res: Response): Promise<T> {
if (!res.ok) {
const body = (await res.json().catch(() => ({}))) as { error?: string };
throw new Error(body.error || `request failed: ${res.status}`);
}
return res.json() as Promise<T>;
}
const JSON_HEADERS = { 'content-type': 'application/json' } as const;
/** Mint a LiveKit token from the backend. */
export async function fetchToken(params: {
room: string;
identity: string;
name: string;
githubLogin?: string;
profile?: UserProfilePayload;
}): Promise<{ token: string; url: string }> {
const res = await apiFetch(`${BACKEND_URL}/api/token`, {
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify(params),
});
return json(res);
}
/** Record an intervention outcome for the policy learning loop. */
export async function postOutcome(outcome: InterventionOutcome): Promise<void> {
const res = await apiFetch(`${BACKEND_URL}/api/outcome`, {
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify(outcome),
});
if (!res.ok) throw new Error(`outcome post failed: ${res.status}`);
}
export async function createSyncPr(input: {
headBranch?: string;
file?: string;
summary?: string;
}): Promise<{ url: string; number: number }> {
return json(
await apiFetch(`${BACKEND_URL}/api/sync-pr`, {
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify(input),
}),
);
}
// --- Pods CRUD ---
export async function listPods(): Promise<Pod[]> {
return json(await apiFetch(`${BACKEND_URL}/api/pods`));
}
/** Display names currently connected per pod id (= LiveKit room name). */
export async function getPresence(): Promise<Record<string, string[]>> {
return json(await apiFetch(`${BACKEND_URL}/api/presence`));
}
export async function getMemoryStats(): Promise<MemoryStats> {
return json(await apiFetch(`${BACKEND_URL}/api/memory/stats`));
}
export async function getPodActivity(id: string, limit = 80): Promise<PodActivityEvent[]> {
return json(
await apiFetch(`${BACKEND_URL}/api/pods/${encodeURIComponent(id)}/activity?limit=${limit}`),
);
}
export function podActivityStreamUrl(id: string): string {
return `${BACKEND_URL}/api/pods/${encodeURIComponent(id)}/activity/stream`;
}
/** URL of the pod's generated background-music MP3 (looped client-side). */
export function podMusicUrl(id: string): string {
return `${BACKEND_URL}/api/pods/${encodeURIComponent(id)}/music`;
}
export async function getMemberWorkHistory(
podId: string,
member: string,
): Promise<MemberWorkHistory> {
return json(
await apiFetch(
`${BACKEND_URL}/api/pods/${encodeURIComponent(podId)}/members/${encodeURIComponent(
member,
)}/history?hours=24&limit=80`,
),
);
}
export async function createPod(input: PodInput, profile?: UserProfilePayload): Promise<Pod> {
return json(
await apiFetch(`${BACKEND_URL}/api/pods`, {
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify({ ...input, profile }),
}),
);
}
export async function updatePod(id: string, patch: PodInput): Promise<Pod> {
return json(
await apiFetch(`${BACKEND_URL}/api/pods/${encodeURIComponent(id)}`, {
method: 'PATCH',
headers: JSON_HEADERS,
body: JSON.stringify(patch),
}),
);
}
export async function deletePod(id: string): Promise<void> {
const res = await apiFetch(`${BACKEND_URL}/api/pods/${encodeURIComponent(id)}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error(`delete pod failed: ${res.status}`);
}
export async function addMember(
id: string,
name: string,
profile?: UserProfilePayload,
): Promise<Pod> {
return json(
await apiFetch(`${BACKEND_URL}/api/pods/${encodeURIComponent(id)}/members`, {
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify({ name, profile }),
}),
);
}
export async function testPodVoice(id: string): Promise<void> {
const res = await apiFetch(`${BACKEND_URL}/api/pods/${encodeURIComponent(id)}/voice-test`, {
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify({
message: 'PodMan voice test. Gemini TTS is playing through LiveKit.',
}),
});
if (!res.ok) throw new Error(`voice test failed: ${res.status}`);
}
export async function startLiveConversation(
podId: string,
input: { identity: string; displayName?: string },
): Promise<LiveConversationSession> {
return json(
await apiFetch(`${BACKEND_URL}/api/pods/${encodeURIComponent(podId)}/live-conversation/start`, {
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify(input),
}),
);
}
export async function stopLiveConversation(podId: string, sessionId: string): Promise<void> {
const res = await apiFetch(
`${BACKEND_URL}/api/pods/${encodeURIComponent(
podId,
)}/live-conversation/${encodeURIComponent(sessionId)}/stop`,
{ method: 'POST' },
);
if (!res.ok) throw new Error(`live conversation stop failed: ${res.status}`);
}
export async function getLiveConversationHermesJob(
podId: string,
sessionId: string,
): Promise<{ job: HermesJob | null; events: HermesJobEvent[] }> {
return json(
await apiFetch(
`${BACKEND_URL}/api/pods/${encodeURIComponent(
podId,
)}/live-conversation/${encodeURIComponent(sessionId)}/hermes-job`,
),
);
}
export async function abortLiveConversationHermesJob(
podId: string,
sessionId: string,
): Promise<{ job: HermesJob | null }> {
return json(
await apiFetch(
`${BACKEND_URL}/api/pods/${encodeURIComponent(
podId,
)}/live-conversation/${encodeURIComponent(sessionId)}/hermes-job/abort`,
{ method: 'POST' },
),
);
}
export async function removeMember(id: string, name: string): Promise<Pod> {
return json(
await apiFetch(
`${BACKEND_URL}/api/pods/${encodeURIComponent(id)}/members/${encodeURIComponent(name)}`,
{ method: 'DELETE' },
),
);
}