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
1350 lines
36 KiB
TypeScript
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,
|
|
});
|
|
}
|