synops/frontend/src/lib/api.ts
vegard 8e80102f6b Fullfør oppgave 22.2: Frontend-migrering fra SpacetimeDB til portvokteren
Frontend bruker nå kun portvokterens WebSocket for sanntidsdata.
SpacetimeDB-klienten er erstattet med en enkel WebSocket-klient
som kobler til /ws-endepunktet og oppdaterer reactive stores direkte.

Frontend-endringer:
- Nye lokale typer (types.ts) erstatter STDB module_bindings
- connection.svelte.ts: WebSocket til portvokteren med auto-reconnect
- stores.svelte.ts: Prosesserer WS-meldinger (initial_sync + events)
- MixerTrait: STDB-reducers erstattet med HTTP API-kall
- api.ts: Nye mixer-endepunkter (create, gain, mute, effect, role)
- +layout.svelte: Fjernet dual-tilkobling, kun portvokterens WS
- pg-ws.svelte.ts: Slettet (erstattet av connection.svelte.ts)

Dokumentasjon:
- datalaget.md: Fase M1+M2 markert som fullført
- api_grensesnitt.md: Oppdatert arkitekturdiagram, nye mixer-endepunkter
2026-03-18 12:26:33 +00:00

1350 lines
36 KiB
TypeScript

/**
* Client for maskinrommet intentions API.
* Uses the Vite dev proxy (/api → api.sidelinja.org) in development.
* In production, set VITE_API_URL to the maskinrommet URL.
*/
const BASE_URL = import.meta.env.VITE_API_URL ?? '/api';
export interface CreateNodeRequest {
node_kind?: string;
title?: string;
content?: string;
visibility?: string;
metadata?: Record<string, unknown>;
/** Kontekst-node (kommunikasjonsnode). Gir automatisk belongs_to-edge. */
context_id?: string;
}
export interface CreateNodeResponse {
node_id: string;
/** Edge-ID for automatisk belongs_to-edge (kun ved context_id). */
belongs_to_edge_id?: string;
}
export interface CreateEdgeRequest {
source_id: string;
target_id: string;
edge_type: string;
metadata?: Record<string, unknown>;
system?: boolean;
}
export interface CreateEdgeResponse {
edge_id: string;
}
async function post<T>(accessToken: string, path: string, data: unknown): Promise<T> {
const res = await fetch(`${BASE_URL}${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`
},
body: JSON.stringify(data)
});
if (!res.ok) {
const body = await res.text();
throw new Error(`${path} failed (${res.status}): ${body}`);
}
return res.json();
}
export function createNode(
accessToken: string,
data: CreateNodeRequest
): Promise<CreateNodeResponse> {
return post(accessToken, '/intentions/create_node', data);
}
export function createEdge(
accessToken: string,
data: CreateEdgeRequest
): Promise<CreateEdgeResponse> {
return post(accessToken, '/intentions/create_edge', data);
}
// =============================================================================
// Node-oppdatering
// =============================================================================
export interface UpdateNodeRequest {
node_id: string;
node_kind?: string;
title?: string;
content?: string;
visibility?: string;
metadata?: Record<string, unknown>;
}
export interface UpdateNodeResponse {
node_id: string;
}
export function updateNode(
accessToken: string,
data: UpdateNodeRequest
): Promise<UpdateNodeResponse> {
return post(accessToken, '/intentions/update_node', data);
}
// =============================================================================
// Edge-oppdatering
// =============================================================================
export interface UpdateEdgeRequest {
edge_id: string;
edge_type?: string;
metadata?: Record<string, unknown>;
}
export interface UpdateEdgeResponse {
edge_id: string;
}
export function updateEdge(
accessToken: string,
data: UpdateEdgeRequest
): Promise<UpdateEdgeResponse> {
return post(accessToken, '/intentions/update_edge', data);
}
// =============================================================================
// Edge-sletting (avpublisering m.m.)
// =============================================================================
export interface DeleteEdgeRequest {
edge_id: string;
}
export interface DeleteEdgeResponse {
deleted: boolean;
}
export function deleteEdge(
accessToken: string,
data: DeleteEdgeRequest
): Promise<DeleteEdgeResponse> {
return post(accessToken, '/intentions/delete_edge', data);
}
// =============================================================================
// Board / Kanban
// =============================================================================
export interface BoardCard {
node_id: string;
title: string | null;
content: string | null;
node_kind: string;
metadata: Record<string, unknown>;
created_at: string;
created_by: string | null;
status: string | null;
position: number;
belongs_to_edge_id: string;
status_edge_id: string | null;
}
export interface BoardResponse {
board_id: string;
board_title: string | null;
columns: string[];
cards: BoardCard[];
}
export async function fetchBoard(accessToken: string, boardId: string): Promise<BoardResponse> {
const res = await fetch(`${BASE_URL}/query/board?board_id=${encodeURIComponent(boardId)}`, {
headers: { Authorization: `Bearer ${accessToken}` }
});
if (!res.ok) {
const body = await res.text();
throw new Error(`board failed (${res.status}): ${body}`);
}
return res.json();
}
// =============================================================================
// Redaksjonell arbeidsflate (Editorial Board)
// =============================================================================
export interface EditorialCard {
node_id: string;
title: string | null;
content: string | null;
node_kind: string;
metadata: Record<string, unknown>;
created_at: string;
created_by: string | null;
author_name: string | null;
status: string;
submitted_to_edge_id: string;
edge_metadata: Record<string, unknown>;
/** Kommunikasjonsnoder knyttet til artikkelen (redaksjonelle samtaler) */
discussion_ids: string[];
}
export interface EditorialBoardResponse {
collection_id: string;
collection_title: string | null;
columns: string[];
column_labels: Record<string, string>;
cards: EditorialCard[];
}
export async function fetchEditorialBoard(
accessToken: string,
collectionId: string
): Promise<EditorialBoardResponse> {
const res = await fetch(
`${BASE_URL}/query/editorial_board?collection_id=${encodeURIComponent(collectionId)}`,
{ headers: { Authorization: `Bearer ${accessToken}` } }
);
if (!res.ok) {
const body = await res.text();
throw new Error(`editorial_board failed (${res.status}): ${body}`);
}
return res.json();
}
// =============================================================================
// Kommunikasjon
// =============================================================================
export interface CreateCommunicationRequest {
title?: string;
participants?: string[];
visibility?: string;
metadata?: Record<string, unknown>;
/** Kontekst-node (f.eks. artikkel). Gir automatisk belongs_to-edge. */
context_id?: string;
}
export interface CreateCommunicationResponse {
node_id: string;
edge_ids: string[];
}
export function createCommunication(
accessToken: string,
data: CreateCommunicationRequest
): Promise<CreateCommunicationResponse> {
return post(accessToken, '/intentions/create_communication', data);
}
export interface UploadMediaRequest {
file: File;
source_id?: string;
visibility?: string;
title?: string;
}
export interface UploadMediaResponse {
media_node_id: string;
cas_hash: string;
size_bytes: number;
already_existed: boolean;
has_media_edge_id?: string;
}
export async function uploadMedia(
accessToken: string,
data: UploadMediaRequest
): Promise<UploadMediaResponse> {
const form = new FormData();
form.append('file', data.file);
if (data.source_id) form.append('source_id', data.source_id);
if (data.visibility) form.append('visibility', data.visibility);
if (data.title) form.append('title', data.title);
const res = await fetch(`${BASE_URL}/intentions/upload_media`, {
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}` },
body: form
});
if (!res.ok) {
const body = await res.text();
throw new Error(`upload_media failed (${res.status}): ${body}`);
}
return res.json();
}
/** Build the CAS URL for a given hash. */
export function casUrl(hash: string): string {
return `${BASE_URL}/cas/${hash}`;
}
// =============================================================================
// Transkripsjons-segmenter
// =============================================================================
export interface Segment {
id: number;
seq: number;
start_ms: number;
end_ms: number;
content: string;
edited: boolean;
}
export interface SegmentsResponse {
segments: Segment[];
transcribed_at: string | null;
}
/** Hent transkripsjons-segmenter for en media-node. */
export async function fetchSegments(
accessToken: string,
nodeId: string
): Promise<SegmentsResponse> {
const res = await fetch(`${BASE_URL}/query/segments?node_id=${encodeURIComponent(nodeId)}`, {
headers: { Authorization: `Bearer ${accessToken}` }
});
if (!res.ok) {
const body = await res.text();
throw new Error(`segments failed (${res.status}): ${body}`);
}
return res.json();
}
/** Last ned SRT-fil for en media-node. Trigger filnedlasting i nettleseren. */
export async function downloadSrt(accessToken: string, nodeId: string): Promise<void> {
const res = await fetch(
`${BASE_URL}/query/segments/srt?node_id=${encodeURIComponent(nodeId)}`,
{ headers: { Authorization: `Bearer ${accessToken}` } }
);
if (!res.ok) {
const body = await res.text();
throw new Error(`SRT-eksport feilet (${res.status}): ${body}`);
}
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'transcription.srt';
a.click();
URL.revokeObjectURL(url);
}
/** Oppdater teksten i et transkripsjons-segment. */
export function updateSegment(
accessToken: string,
segmentId: number,
content: string
): Promise<{ segment_id: number; edited: boolean }> {
return post(accessToken, '/intentions/update_segment', {
segment_id: segmentId,
content
});
}
// =============================================================================
// Re-transkripsjon
// =============================================================================
export interface TranscriptionVersion {
transcribed_at: string;
segment_count: number;
edited_count: number;
}
export interface TranscriptionVersionsResponse {
versions: TranscriptionVersion[];
}
/** Hent alle transkripsjonsversjoner for en node. */
export async function fetchTranscriptionVersions(
accessToken: string,
nodeId: string
): Promise<TranscriptionVersionsResponse> {
const res = await fetch(
`${BASE_URL}/query/transcription_versions?node_id=${encodeURIComponent(nodeId)}`,
{ headers: { Authorization: `Bearer ${accessToken}` } }
);
if (!res.ok) {
const body = await res.text();
throw new Error(`transcription_versions failed (${res.status}): ${body}`);
}
return res.json();
}
/** Hent segmenter for en spesifikk transkripsjonsversjon. */
export async function fetchSegmentsVersion(
accessToken: string,
nodeId: string,
transcribedAt: string
): Promise<SegmentsResponse> {
const params = new URLSearchParams({
node_id: nodeId,
transcribed_at: transcribedAt
});
const res = await fetch(`${BASE_URL}/query/segments_version?${params}`, {
headers: { Authorization: `Bearer ${accessToken}` }
});
if (!res.ok) {
const body = await res.text();
throw new Error(`segments_version failed (${res.status}): ${body}`);
}
return res.json();
}
// =============================================================================
// Graf / Kunnskapsgraf
// =============================================================================
export interface GraphNode {
id: string;
node_kind: string;
title: string | null;
visibility: string;
metadata: Record<string, unknown>;
created_at: string;
}
export interface GraphEdge {
id: string;
source_id: string;
target_id: string;
edge_type: string;
metadata: Record<string, unknown>;
}
export interface GraphResponse {
nodes: GraphNode[];
edges: GraphEdge[];
}
export interface FetchGraphParams {
focusId?: string;
depth?: number;
edgeTypes?: string[];
nodeKinds?: string[];
}
/** Hent graf-data for visualisering. */
export async function fetchGraph(
accessToken: string,
params: FetchGraphParams = {}
): Promise<GraphResponse> {
const searchParams = new URLSearchParams();
if (params.focusId) searchParams.set('focus_id', params.focusId);
if (params.depth) searchParams.set('depth', String(params.depth));
if (params.edgeTypes?.length) searchParams.set('edge_types', params.edgeTypes.join(','));
if (params.nodeKinds?.length) searchParams.set('node_kinds', params.nodeKinds.join(','));
const res = await fetch(`${BASE_URL}/query/graph?${searchParams}`, {
headers: { Authorization: `Bearer ${accessToken}` }
});
if (!res.ok) {
const body = await res.text();
throw new Error(`graph failed (${res.status}): ${body}`);
}
return res.json();
}
/** Trigger re-transkripsjon for en media-node. */
export function retranscribe(
accessToken: string,
nodeId: string
): Promise<{ job_id: string }> {
return post(accessToken, '/intentions/retranscribe', { node_id: nodeId });
}
export interface SegmentChoice {
seq: number;
choice: 'new' | 'old';
}
// =============================================================================
// Lydstudio
// =============================================================================
export interface AudioInfo {
duration_ms: number;
sample_rate: number;
channels: number;
codec: string;
format: string;
bit_rate: number | null;
}
export interface LoudnessInfo {
input_i: number;
input_tp: number;
input_lra: number;
input_thresh: number;
}
export interface SilenceRegion {
start_ms: number;
end_ms: number;
duration_ms: number;
}
export interface AnalyzeResult {
loudness: LoudnessInfo;
silence_regions: SilenceRegion[];
info: AudioInfo;
}
export interface EdlOperation {
type: string;
[key: string]: unknown;
}
export interface EdlDocument {
source_hash: string;
operations: EdlOperation[];
}
/** Analyser lydfil: loudness, silence-regioner, metadata. */
export function audioAnalyze(
accessToken: string,
casHash: string,
silenceThresholdDb?: number,
silenceMinDurationMs?: number
): Promise<AnalyzeResult> {
return post(accessToken, '/intentions/audio_analyze', {
cas_hash: casHash,
silence_threshold_db: silenceThresholdDb,
silence_min_duration_ms: silenceMinDurationMs
});
}
/** Køer audio-prosessering med EDL. Returnerer job_id. */
export function audioProcess(
accessToken: string,
mediaNodeId: string,
edl: EdlDocument,
outputFormat?: string
): Promise<{ job_id: string }> {
return post(accessToken, '/intentions/audio_process', {
media_node_id: mediaNodeId,
edl,
output_format: outputFormat
});
}
// =============================================================================
// Publisering / Forside-slots
// =============================================================================
export interface SetSlotRequest {
edge_id: string;
slot: string | null;
slot_order?: number;
pinned?: boolean;
}
export interface SetSlotResponse {
edge_id: string;
displaced: string[];
}
/** Sett slot-metadata (hero/featured/strøm) på en belongs_to-edge. */
export function setSlot(
accessToken: string,
data: SetSlotRequest
): Promise<SetSlotResponse> {
return post(accessToken, '/intentions/set_slot', data);
}
/** Hent metadata om lydfil (ffprobe). */
export async function audioInfo(accessToken: string, hash: string): Promise<AudioInfo> {
const res = await fetch(`${BASE_URL}/query/audio_info?hash=${encodeURIComponent(hash)}`, {
headers: { Authorization: `Bearer ${accessToken}` }
});
if (!res.ok) {
const body = await res.text();
throw new Error(`audio_info failed (${res.status}): ${body}`);
}
return res.json();
}
// =============================================================================
// Presentasjonselementer
// =============================================================================
export interface PresentationElement {
node_id: string;
edge_id: string;
element_type: string;
title: string | null;
content: string | null;
node_kind: string;
metadata: Record<string, unknown>;
edge_metadata: Record<string, unknown>;
created_at: string;
}
export interface PresentationElementsResponse {
article_id: string;
elements: PresentationElement[];
}
/** Hent presentasjonselementer (tittel, undertittel, ingress, OG-bilde) for en artikkel. */
export async function fetchPresentationElements(
accessToken: string,
articleId: string
): Promise<PresentationElementsResponse> {
const res = await fetch(
`${BASE_URL}/query/presentation_elements?article_id=${encodeURIComponent(articleId)}`,
{ headers: { Authorization: `Bearer ${accessToken}` } }
);
if (!res.ok) {
const body = await res.text();
throw new Error(`presentation_elements failed (${res.status}): ${body}`);
}
return res.json();
}
/** Slett en node (brukes for å fjerne presentasjonselementer). */
export function deleteNode(
accessToken: string,
nodeId: string
): Promise<{ deleted: boolean }> {
return post(accessToken, '/intentions/delete_node', { node_id: nodeId });
}
// =============================================================================
// Systemvarsler (oppgave 15.1)
// =============================================================================
export interface CreateAnnouncementRequest {
title: string;
content: string;
announcement_type: 'info' | 'warning' | 'critical';
scheduled_at?: string;
expires_at?: string;
blocks_new_sessions?: boolean;
}
export interface CreateAnnouncementResponse {
node_id: string;
}
/** Opprett et systemvarsel (vises for alle klienter umiddelbart via STDB). */
export function createAnnouncement(
accessToken: string,
data: CreateAnnouncementRequest
): Promise<CreateAnnouncementResponse> {
return post(accessToken, '/intentions/create_announcement', data);
}
export interface ExpireAnnouncementRequest {
node_id: string;
}
export interface ExpireAnnouncementResponse {
expired: boolean;
}
/** Fjern/utløp et systemvarsel. */
export function expireAnnouncement(
accessToken: string,
data: ExpireAnnouncementRequest
): Promise<ExpireAnnouncementResponse> {
return post(accessToken, '/intentions/expire_announcement', data);
}
/** Anvend brukerens per-segment-valg etter re-transkripsjon. */
export function resolveRetranscription(
accessToken: string,
nodeId: string,
newVersion: string,
oldVersion: string,
choices: SegmentChoice[]
): Promise<{ resolved: boolean; kept_old: number; kept_new: number }> {
return post(accessToken, '/intentions/resolve_retranscription', {
node_id: nodeId,
new_version: newVersion,
old_version: oldVersion,
choices
});
}
// =============================================================================
// Vedlikeholdsmodus (oppgave 15.2)
// =============================================================================
export interface RunningJob {
id: string;
job_type: string;
started_at: string | null;
collection_node_id: string | null;
}
export interface MaintenanceStatus {
initiated: boolean;
active: boolean;
scheduled_at: string | null;
announcement_node_id: string | null;
initiated_by: string | null;
running_jobs: RunningJob[];
}
/** Hent vedlikeholdsstatus (aktive sesjoner, kjørende jobber). */
export async function fetchMaintenanceStatus(accessToken: string): Promise<MaintenanceStatus> {
const res = await fetch(`${BASE_URL}/admin/maintenance_status`, {
headers: { Authorization: `Bearer ${accessToken}` }
});
if (!res.ok) {
const body = await res.text();
throw new Error(`maintenance_status failed (${res.status}): ${body}`);
}
return res.json();
}
export interface InitiateMaintenanceRequest {
scheduled_at: string;
}
export interface InitiateMaintenanceResponse {
announcement_node_id: string;
scheduled_at: string;
}
/** Initier planlagt vedlikehold med nedtelling. */
export function initiateMaintenance(
accessToken: string,
data: InitiateMaintenanceRequest
): Promise<InitiateMaintenanceResponse> {
return post(accessToken, '/intentions/initiate_maintenance', data);
}
/** Avbryt planlagt vedlikehold. */
export function cancelMaintenance(
accessToken: string
): Promise<{ cancelled: boolean }> {
return post(accessToken, '/intentions/cancel_maintenance', {});
}
// =============================================================================
// Jobbkø-oversikt (oppgave 15.3)
// =============================================================================
export interface JobDetail {
id: string;
collection_node_id: string | null;
job_type: string;
payload: Record<string, unknown>;
status: string;
priority: number;
result: Record<string, unknown> | null;
error_msg: string | null;
attempts: number;
max_attempts: number;
created_at: string;
started_at: string | null;
completed_at: string | null;
scheduled_for: string;
}
export interface JobCountByStatus {
status: string;
count: number;
}
export interface ListJobsResponse {
jobs: JobDetail[];
counts: JobCountByStatus[];
job_types: string[];
}
export interface ListJobsParams {
status?: string;
type?: string;
collection_id?: string;
limit?: number;
offset?: number;
}
/** Hent jobbliste med valgfrie filtre. */
export async function fetchJobs(
accessToken: string,
params: ListJobsParams = {}
): Promise<ListJobsResponse> {
const searchParams = new URLSearchParams();
if (params.status) searchParams.set('status', params.status);
if (params.type) searchParams.set('type', params.type);
if (params.collection_id) searchParams.set('collection_id', params.collection_id);
if (params.limit) searchParams.set('limit', String(params.limit));
if (params.offset) searchParams.set('offset', String(params.offset));
const qs = searchParams.toString();
const url = `${BASE_URL}/admin/jobs${qs ? `?${qs}` : ''}`;
const res = await fetch(url, {
headers: { Authorization: `Bearer ${accessToken}` }
});
if (!res.ok) {
const body = await res.text();
throw new Error(`jobs failed (${res.status}): ${body}`);
}
return res.json();
}
/** Sett en feilet jobb tilbake til pending for nytt forsøk. */
export function retryJob(
accessToken: string,
jobId: string
): Promise<{ success: boolean }> {
return post(accessToken, '/intentions/retry_job', { job_id: jobId });
}
/** Avbryt en ventende jobb. */
export function cancelJob(
accessToken: string,
jobId: string
): Promise<{ success: boolean }> {
return post(accessToken, '/intentions/cancel_job', { job_id: jobId });
}
// =============================================================================
// AI Gateway-konfigurasjon (oppgave 15.4)
// =============================================================================
export interface AiModelAlias {
id: string;
alias: string;
description: string | null;
is_active: boolean;
created_at: string;
}
export interface AiModelProvider {
id: string;
alias_id: string;
provider: string;
model: string;
api_key_env: string;
priority: number;
is_active: boolean;
}
export interface AiJobRouting {
job_type: string;
alias: string;
description: string | null;
}
export interface AiUsageSummary {
collection_node_id: string | null;
collection_title: string | null;
model_alias: string;
job_type: string | null;
total_prompt_tokens: number;
total_completion_tokens: number;
total_tokens: number;
estimated_cost: number;
call_count: number;
}
export interface ApiKeyStatus {
env_var: string;
is_set: boolean;
}
export interface AiOverviewResponse {
aliases: AiModelAlias[];
providers: AiModelProvider[];
routing: AiJobRouting[];
usage: AiUsageSummary[];
api_key_status: ApiKeyStatus[];
}
/** Hent AI Gateway-oversikt (aliaser, providers, ruting, forbruk). */
export async function fetchAiOverview(accessToken: string): Promise<AiOverviewResponse> {
const res = await fetch(`${BASE_URL}/admin/ai`, {
headers: { Authorization: `Bearer ${accessToken}` }
});
if (!res.ok) {
const body = await res.text();
throw new Error(`ai overview failed (${res.status}): ${body}`);
}
return res.json();
}
/** Hent AI-forbruksoversikt med filtre. */
export async function fetchAiUsage(
accessToken: string,
params: { days?: number; collection_id?: string } = {}
): Promise<AiUsageSummary[]> {
const sp = new URLSearchParams();
if (params.days) sp.set('days', String(params.days));
if (params.collection_id) sp.set('collection_id', params.collection_id);
const qs = sp.toString();
const res = await fetch(`${BASE_URL}/admin/ai/usage${qs ? `?${qs}` : ''}`, {
headers: { Authorization: `Bearer ${accessToken}` }
});
if (!res.ok) {
const body = await res.text();
throw new Error(`ai usage failed (${res.status}): ${body}`);
}
return res.json();
}
/** Oppdater modellalias (beskrivelse, aktiv-status). */
export function updateAiAlias(
accessToken: string,
data: { id: string; description: string | null; is_active: boolean }
): Promise<{ success: boolean }> {
return post(accessToken, '/admin/ai/update_alias', data);
}
/** Opprett nytt modellalias. */
export function createAiAlias(
accessToken: string,
data: { alias: string; description: string | null }
): Promise<{ id: string; success: boolean }> {
return post(accessToken, '/admin/ai/create_alias', data);
}
/** Oppdater provider (prioritet, aktiv-status). */
export function updateAiProvider(
accessToken: string,
data: { id: string; priority?: number; is_active?: boolean }
): Promise<{ success: boolean }> {
return post(accessToken, '/admin/ai/update_provider', data);
}
/** Legg til ny provider på et alias. */
export function createAiProvider(
accessToken: string,
data: { alias_id: string; provider: string; model: string; api_key_env: string; priority: number }
): Promise<{ id: string; success: boolean }> {
return post(accessToken, '/admin/ai/create_provider', data);
}
/** Slett en provider. */
export function deleteAiProvider(
accessToken: string,
id: string
): Promise<{ success: boolean }> {
return post(accessToken, '/admin/ai/delete_provider', { id });
}
/** Oppdater eller opprett rutingregel (jobbtype → alias). */
export function updateAiRouting(
accessToken: string,
data: { job_type: string; alias: string; description: string | null }
): Promise<{ success: boolean }> {
return post(accessToken, '/admin/ai/update_routing', data);
}
/** Slett en rutingregel. */
export function deleteAiRouting(
accessToken: string,
jobType: string
): Promise<{ success: boolean }> {
return post(accessToken, '/admin/ai/delete_routing', { job_type: jobType });
}
// =============================================================================
// Serverhelse-dashboard (oppgave 15.6)
// =============================================================================
export interface ServiceStatus {
name: string;
status: 'up' | 'down' | 'degraded';
latency_ms: number | null;
details: string | null;
}
export interface SystemMetrics {
cpu_usage_percent: number;
cpu_cores: number;
load_avg: [number, number, number];
memory_total_bytes: number;
memory_used_bytes: number;
memory_available_bytes: number;
memory_usage_percent: number;
disk: {
mount_point: string;
total_bytes: number;
used_bytes: number;
available_bytes: number;
usage_percent: number;
alert_level: string | null;
};
uptime_seconds: number;
}
export interface BackupInfo {
backup_type: string;
last_success: string | null;
path: string | null;
status: 'ok' | 'missing' | 'stale';
}
export interface PgStats {
active_connections: number;
max_connections: number;
database_size_bytes: number;
active_queries: number;
}
export interface HealthDashboard {
services: ServiceStatus[];
metrics: SystemMetrics;
backups: BackupInfo[];
pg_stats: PgStats;
}
export interface LogEntry {
timestamp: string;
service: string;
level: string;
message: string;
}
export interface LogsResponse {
entries: LogEntry[];
}
/** Hent komplett serverhelse-dashboard. */
export async function fetchHealthDashboard(accessToken: string): Promise<HealthDashboard> {
const res = await fetch(`${BASE_URL}/admin/health`, {
headers: { Authorization: `Bearer ${accessToken}` }
});
if (!res.ok) {
const body = await res.text();
throw new Error(`health dashboard failed (${res.status}): ${body}`);
}
return res.json();
}
/** Hent logger for en tjeneste (eller alle). */
export async function fetchHealthLogs(
accessToken: string,
params: { service?: string; lines?: number } = {}
): Promise<LogsResponse> {
const qs = new URLSearchParams();
if (params.service) qs.set('service', params.service);
if (params.lines) qs.set('lines', String(params.lines));
const query = qs.toString();
const res = await fetch(`${BASE_URL}/admin/health/logs${query ? `?${query}` : ''}`, {
headers: { Authorization: `Bearer ${accessToken}` }
});
if (!res.ok) {
const body = await res.text();
throw new Error(`health logs failed (${res.status}): ${body}`);
}
return res.json();
}
// =============================================================================
// Forbruksoversikt (oppgave 15.8)
// =============================================================================
export interface CollectionUsageSummary {
collection_id: string | null;
collection_title: string | null;
resource_type: string;
event_count: number;
total_value: number;
secondary_value: number;
}
export interface AiDrillDown {
collection_id: string | null;
collection_title: string | null;
job_type: string | null;
model_level: string | null;
tokens_in: number;
tokens_out: number;
event_count: number;
}
export interface DailyUsage {
day: string;
resource_type: string;
event_count: number;
total_value: number;
}
export interface UsageOverviewResponse {
by_collection: CollectionUsageSummary[];
ai_drilldown: AiDrillDown[];
daily: DailyUsage[];
}
/** Hent aggregert forbruksoversikt for admin. */
export async function fetchUsageOverview(
accessToken: string,
params: { days?: number; collection_id?: string } = {}
): Promise<UsageOverviewResponse> {
const sp = new URLSearchParams();
if (params.days) sp.set('days', String(params.days));
if (params.collection_id) sp.set('collection_id', params.collection_id);
const qs = sp.toString();
const res = await fetch(`${BASE_URL}/admin/usage${qs ? `?${qs}` : ''}`, {
headers: { Authorization: `Bearer ${accessToken}` }
});
if (!res.ok) {
const body = await res.text();
throw new Error(`usage overview failed (${res.status}): ${body}`);
}
return res.json();
}
// ============================================================================
// Brukersynlig forbruk (oppgave 15.9)
// ============================================================================
export interface ResourceTypeSummary {
resource_type: string;
event_count: number;
total_value: number;
secondary_value: number;
}
export interface UserDailyUsage {
day: string;
resource_type: string;
event_count: number;
total_value: number;
}
export interface GraphStats {
nodes_created: number;
edges_created: number;
}
export interface UserUsageResponse {
by_type: ResourceTypeSummary[];
daily: UserDailyUsage[];
graph: GraphStats;
}
export interface NodeUsageResponse {
node_id: string;
node_title: string | null;
by_type: ResourceTypeSummary[];
daily: UserDailyUsage[];
}
/** Hent innlogget brukers eget ressursforbruk. */
export async function fetchMyUsage(
accessToken: string,
params: { days?: number } = {}
): Promise<UserUsageResponse> {
const sp = new URLSearchParams();
if (params.days) sp.set('days', String(params.days));
const qs = sp.toString();
const res = await fetch(`${BASE_URL}/my/usage${qs ? `?${qs}` : ''}`, {
headers: { Authorization: `Bearer ${accessToken}` }
});
if (!res.ok) {
const body = await res.text();
throw new Error(`my usage failed (${res.status}): ${body}`);
}
return res.json();
}
// =============================================================================
// LiveKit / Kommunikasjonsrom (oppgave 16.1)
// =============================================================================
export interface JoinCommunicationRequest {
communication_id: string;
role?: 'publisher' | 'subscriber';
}
export interface RoomParticipantInfo {
user_id: string;
display_name: string;
role: string;
}
export interface JoinCommunicationResponse {
livekit_room_name: string;
livekit_token: string;
livekit_url: string;
identity: string;
participants: RoomParticipantInfo[];
}
export interface LeaveCommunicationResponse {
status: string;
}
/** Bli med i et LiveKit-rom for en kommunikasjonsnode. */
export function joinCommunication(
accessToken: string,
data: JoinCommunicationRequest
): Promise<JoinCommunicationResponse> {
return post(accessToken, '/intentions/join_communication', data);
}
/** Forlat et LiveKit-rom. */
export function leaveCommunication(
accessToken: string,
communicationId: string
): Promise<LeaveCommunicationResponse> {
return post(accessToken, '/intentions/leave_communication', {
communication_id: communicationId
});
}
// =============================================================================
// AI-prosessering
// =============================================================================
export interface AiProcessRequest {
source_node_id: string;
ai_preset_id: string;
direction: 'node_to_tool' | 'tool_to_node';
}
export interface AiProcessResponse {
job_id: string;
}
export function aiProcess(
accessToken: string,
data: AiProcessRequest
): Promise<AiProcessResponse> {
return post(accessToken, '/intentions/ai_process', data);
}
// =============================================================================
// Egendefinerte AI-presets (oppgave 18.6)
// =============================================================================
export interface CreateAiPresetRequest {
title: string;
prompt: string;
default_direction: 'node_to_tool' | 'tool_to_node' | 'both';
icon: string;
color: string;
share_with_collection_id?: string;
}
export interface CreateAiPresetResponse {
node_id: string;
shared_edge_id?: string;
}
/** Opprett en egendefinert AI-preset (custom, model_profile=flash). */
export function createAiPreset(
accessToken: string,
data: CreateAiPresetRequest
): Promise<CreateAiPresetResponse> {
return post(accessToken, '/intentions/create_ai_preset', data);
}
// =============================================================================
// Personlig arbeidsflate (oppgave 19.6)
// =============================================================================
export interface WorkspaceResponse {
node_id: string;
title: string;
metadata: Record<string, unknown>;
created: boolean;
}
/** Hent (eller opprett) brukerens personlige workspace. */
export async function fetchMyWorkspace(accessToken: string): Promise<WorkspaceResponse> {
const res = await fetch(`${BASE_URL}/my/workspace`, {
headers: { Authorization: `Bearer ${accessToken}` }
});
if (!res.ok) {
const body = await res.text();
throw new Error(`workspace failed (${res.status}): ${body}`);
}
return res.json();
}
/** Hent ressursforbruk for en spesifikk node (kun eier). */
export async function fetchNodeUsage(
accessToken: string,
nodeId: string,
params: { days?: number } = {}
): Promise<NodeUsageResponse> {
const sp = new URLSearchParams();
sp.set('node_id', nodeId);
if (params.days) sp.set('days', String(params.days));
const res = await fetch(`${BASE_URL}/query/node_usage?${sp.toString()}`, {
headers: { Authorization: `Bearer ${accessToken}` }
});
if (!res.ok) {
const body = await res.text();
throw new Error(`node usage failed (${res.status}): ${body}`);
}
return res.json();
}
// =============================================================================
// Mixer-kanaler (oppgave 22.2 — erstatter STDB-reducers)
// =============================================================================
export async function createMixerChannel(
accessToken: string,
roomId: string,
targetUserId: string,
): Promise<void> {
await post(accessToken, '/intentions/create_mixer_channel', {
room_id: roomId,
target_user_id: targetUserId,
});
}
export async function setMixerGain(
accessToken: string,
roomId: string,
targetUserId: string,
gain: number,
): Promise<void> {
await post(accessToken, '/intentions/set_gain', {
room_id: roomId,
target_user_id: targetUserId,
gain,
});
}
export async function setMixerMute(
accessToken: string,
roomId: string,
targetUserId: string,
isMuted: boolean,
): Promise<void> {
await post(accessToken, '/intentions/set_mute', {
room_id: roomId,
target_user_id: targetUserId,
is_muted: isMuted,
});
}
export async function toggleMixerEffect(
accessToken: string,
roomId: string,
targetUserId: string,
effectName: string,
): Promise<void> {
await post(accessToken, '/intentions/toggle_effect', {
room_id: roomId,
target_user_id: targetUserId,
effect_name: effectName,
});
}
export async function setMixerRole(
accessToken: string,
roomId: string,
targetUserId: string,
role: string,
): Promise<void> {
await post(accessToken, '/intentions/set_mixer_role', {
room_id: roomId,
target_user_id: targetUserId,
role,
});
}