Backend (maskinrommet): - Nytt modul podcast_import.rs med 4 endepunkter: POST /admin/podcast/import-preview (dry-run via CLI) POST /admin/podcast/import (starter jobb i køen) GET /admin/podcast/import-status (poll jobbstatus) GET /admin/podcast/collections (samlinger med podcast-trait) - Ny jobbtype import_podcast i jobs.rs dispatcher Frontend: - Ny wizard-side /admin/podcast-import med 5 steg: 1. RSS-URL + samling → forhåndsvisning 2. Import (spinner med jobbstatus-polling) 3. Resultat med sammenligning av feeds 4. Re-import for nye episoder 5. 301-redirect-info - API-funksjoner i api.ts - Navigasjonslenke i admin-panelet
1741 lines
46 KiB
TypeScript
1741 lines
46 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;
|
|
/** Ekstra metadata som merges inn i media-nodens metadata (f.eks. { source: "screenshot" }) */
|
|
metadata_extra?: Record<string, unknown>;
|
|
}
|
|
|
|
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);
|
|
if (data.metadata_extra) form.append('metadata_extra', JSON.stringify(data.metadata_extra));
|
|
|
|
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 WebSocket). */
|
|
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
|
|
// =============================================================================
|
|
|
|
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,
|
|
});
|
|
}
|
|
|
|
// =============================================================================
|
|
// Orkestrering (oppgave 24.6)
|
|
// =============================================================================
|
|
|
|
export interface CompileScriptResponse {
|
|
diagnostics: Array<{
|
|
line: number;
|
|
severity: 'Ok' | 'Error';
|
|
message: string;
|
|
suggestion: string | null;
|
|
raw_input: string;
|
|
compiled_output: string | null;
|
|
}>;
|
|
compiled: {
|
|
steps: Array<{
|
|
step_number: number;
|
|
binary: string;
|
|
args: string[];
|
|
}>;
|
|
global_fallback: {
|
|
binary: string;
|
|
args: string[];
|
|
} | null;
|
|
technical: string;
|
|
} | null;
|
|
}
|
|
|
|
/** Kompiler et orkestreringsscript og få diagnostikk + kompilert resultat. */
|
|
export function compileScript(
|
|
accessToken: string,
|
|
script: string
|
|
): Promise<CompileScriptResponse> {
|
|
return post(accessToken, '/intentions/compile_script', { script });
|
|
}
|
|
|
|
export interface TestOrchestrationResponse {
|
|
job_id: string;
|
|
}
|
|
|
|
/** Trigger en manuell testkjøring av en orkestrering. */
|
|
export function testOrchestration(
|
|
accessToken: string,
|
|
orchestrationId: string
|
|
): Promise<TestOrchestrationResponse> {
|
|
return post(accessToken, '/intentions/test_orchestration', {
|
|
orchestration_id: orchestrationId
|
|
});
|
|
}
|
|
|
|
export interface OrchestrationLogEntry {
|
|
id: string;
|
|
job_id: string | null;
|
|
step_number: number;
|
|
tool_binary: string;
|
|
args: unknown[];
|
|
is_fallback: boolean;
|
|
status: string;
|
|
exit_code: number | null;
|
|
error_msg: string | null;
|
|
duration_ms: number | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface OrchestrationLogResponse {
|
|
entries: OrchestrationLogEntry[];
|
|
}
|
|
|
|
/** Hent kjørehistorikk for en orkestrering. */
|
|
export async function fetchOrchestrationLog(
|
|
accessToken: string,
|
|
orchestrationId: string,
|
|
limit?: number
|
|
): Promise<OrchestrationLogResponse> {
|
|
const sp = new URLSearchParams({ orchestration_id: orchestrationId });
|
|
if (limit) sp.set('limit', String(limit));
|
|
const res = await fetch(`${BASE_URL}/query/orchestration_log?${sp}`, {
|
|
headers: { Authorization: `Bearer ${accessToken}` }
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(`orchestration_log failed (${res.status}): ${body}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
// =============================================================================
|
|
// Webhook-admin (oppgave 29.5)
|
|
// =============================================================================
|
|
|
|
export interface WebhookInfo {
|
|
id: string;
|
|
title: string | null;
|
|
token: string;
|
|
template_id: string | null;
|
|
collection_id: string;
|
|
collection_title: string | null;
|
|
created_at: string;
|
|
event_count: number;
|
|
last_event_at: string | null;
|
|
}
|
|
|
|
export interface WebhookTemplateInfo {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
service: string;
|
|
}
|
|
|
|
export interface WebhookEvent {
|
|
node_id: string;
|
|
title: string | null;
|
|
created_at: string;
|
|
payload: Record<string, unknown> | null;
|
|
}
|
|
|
|
export interface WebhooksOverviewResponse {
|
|
webhooks: WebhookInfo[];
|
|
}
|
|
|
|
export interface CreateWebhookResponse {
|
|
webhook_id: string;
|
|
token: string;
|
|
}
|
|
|
|
export interface RegenerateTokenResponse {
|
|
new_token: string;
|
|
}
|
|
|
|
/** Hent alle webhooks med aktivitetsinfo. */
|
|
export async function fetchWebhooks(accessToken: string): Promise<WebhooksOverviewResponse> {
|
|
const res = await fetch(`${BASE_URL}/admin/webhooks`, {
|
|
headers: { Authorization: `Bearer ${accessToken}` }
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(`webhooks failed (${res.status}): ${body}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
/** Hent siste hendelser for en webhook. */
|
|
export async function fetchWebhookEvents(
|
|
accessToken: string,
|
|
webhookId: string,
|
|
limit?: number
|
|
): Promise<WebhookEvent[]> {
|
|
const sp = new URLSearchParams({ webhook_id: webhookId });
|
|
if (limit) sp.set('limit', String(limit));
|
|
const res = await fetch(`${BASE_URL}/admin/webhooks/events?${sp}`, {
|
|
headers: { Authorization: `Bearer ${accessToken}` }
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(`webhook events failed (${res.status}): ${body}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
/** Opprett ny webhook for en samling. */
|
|
export function createWebhook(
|
|
accessToken: string,
|
|
data: { title?: string; collection_id: string; template_id?: string }
|
|
): Promise<CreateWebhookResponse> {
|
|
return post(accessToken, '/admin/webhooks/create', data);
|
|
}
|
|
|
|
/** Regenerer webhook-token. */
|
|
export function regenerateWebhookToken(
|
|
accessToken: string,
|
|
webhookId: string
|
|
): Promise<RegenerateTokenResponse> {
|
|
return post(accessToken, '/admin/webhooks/regenerate_token', { webhook_id: webhookId });
|
|
}
|
|
|
|
/** Slett en webhook. */
|
|
export function deleteWebhook(
|
|
accessToken: string,
|
|
webhookId: string
|
|
): Promise<{ deleted: boolean }> {
|
|
return post(accessToken, '/admin/webhooks/delete', { webhook_id: webhookId });
|
|
}
|
|
|
|
/** Hent tilgjengelige webhook-templates. */
|
|
export async function fetchWebhookTemplates(accessToken: string): Promise<WebhookTemplateInfo[]> {
|
|
const res = await fetch(`${BASE_URL}/admin/webhooks/templates`, {
|
|
headers: { Authorization: `Bearer ${accessToken}` }
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(`webhook templates failed (${res.status}): ${body}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
/** Sett eller fjern template på en webhook. */
|
|
export function setWebhookTemplate(
|
|
accessToken: string,
|
|
webhookId: string,
|
|
templateId: string | null
|
|
): Promise<{ template_id: string | null }> {
|
|
return post(accessToken, '/admin/webhooks/set_template', {
|
|
webhook_id: webhookId,
|
|
template_id: templateId
|
|
});
|
|
}
|
|
|
|
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,
|
|
});
|
|
}
|
|
|
|
// =========================================================================
|
|
// AI-assistert script-generering (oppgave 24.7)
|
|
// =========================================================================
|
|
|
|
export interface AiSuggestScriptRequest {
|
|
description: string;
|
|
trigger_event?: string;
|
|
trigger_conditions?: Record<string, unknown>;
|
|
eventually?: boolean;
|
|
collection_id?: string;
|
|
}
|
|
|
|
export interface AiSuggestScriptResponse {
|
|
status: string;
|
|
script?: string;
|
|
compile_result?: CompileScriptResponse;
|
|
work_item_id?: string;
|
|
message?: string;
|
|
}
|
|
|
|
/** AI-assistert generering av orkestreringsscript fra fritekst-beskrivelse. */
|
|
export function aiSuggestScript(
|
|
accessToken: string,
|
|
req: AiSuggestScriptRequest
|
|
): Promise<AiSuggestScriptResponse> {
|
|
return post(accessToken, '/intentions/ai_suggest_script', req);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Podcast-statistikk (oppgave 30.4)
|
|
// =============================================================================
|
|
|
|
export interface EpisodeTotal {
|
|
episode_id: string | null;
|
|
episode_title: string | null;
|
|
total_downloads: number;
|
|
total_unique_listeners: number;
|
|
first_date: string | null;
|
|
last_date: string | null;
|
|
days_with_data: number;
|
|
}
|
|
|
|
export interface DailyDownloads {
|
|
date: string;
|
|
downloads: number;
|
|
unique_listeners: number;
|
|
}
|
|
|
|
export interface ClientBreakdown {
|
|
client: string;
|
|
count: number;
|
|
}
|
|
|
|
export interface PodcastStatsResponse {
|
|
total_downloads: number;
|
|
total_unique_listeners: number;
|
|
episodes: EpisodeTotal[];
|
|
daily: DailyDownloads[];
|
|
clients: ClientBreakdown[];
|
|
}
|
|
|
|
/** Hent podcast-nedlastingsstatistikk for admin. */
|
|
export async function fetchPodcastStats(
|
|
accessToken: string,
|
|
params: { days?: number; episode_id?: string } = {}
|
|
): Promise<PodcastStatsResponse> {
|
|
const sp = new URLSearchParams();
|
|
if (params.days) sp.set('days', String(params.days));
|
|
if (params.episode_id) sp.set('episode_id', params.episode_id);
|
|
const qs = sp.toString();
|
|
const res = await fetch(`${BASE_URL}/admin/podcast/stats${qs ? `?${qs}` : ''}`, {
|
|
headers: { Authorization: `Bearer ${accessToken}` }
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(`podcast stats failed (${res.status}): ${body}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
// =============================================================================
|
|
// Podcast-import wizard (oppgave 30.7)
|
|
// =============================================================================
|
|
|
|
export interface EpisodePreview {
|
|
guid: string;
|
|
title: string;
|
|
published_at: string | null;
|
|
duration: string | null;
|
|
episode_number: number | null;
|
|
season_number: number | null;
|
|
action: string;
|
|
node_id: string | null;
|
|
error: string | null;
|
|
}
|
|
|
|
export interface ImportPreviewResponse {
|
|
status: string;
|
|
feed_url: string;
|
|
feed_title: string | null;
|
|
episodes_found: number;
|
|
episodes_imported: number;
|
|
episodes_skipped: number;
|
|
dry_run: boolean;
|
|
episodes: EpisodePreview[];
|
|
errors: string[];
|
|
}
|
|
|
|
export interface ImportStartResponse {
|
|
job_id: string;
|
|
}
|
|
|
|
export interface ImportStatusResponse {
|
|
job_id: string;
|
|
status: string;
|
|
result: ImportPreviewResponse | null;
|
|
error_msg: string | null;
|
|
created_at: string;
|
|
started_at: string | null;
|
|
completed_at: string | null;
|
|
}
|
|
|
|
export interface PodcastCollection {
|
|
id: string;
|
|
title: string | null;
|
|
slug: string | null;
|
|
}
|
|
|
|
/** Forhåndsvisning av podcast-import (dry-run). */
|
|
export function podcastImportPreview(
|
|
accessToken: string,
|
|
feedUrl: string,
|
|
collectionId: string
|
|
): Promise<ImportPreviewResponse> {
|
|
return post(accessToken, '/admin/podcast/import-preview', {
|
|
feed_url: feedUrl,
|
|
collection_id: collectionId
|
|
});
|
|
}
|
|
|
|
/** Start podcast-import via jobbkø. Returnerer job_id. */
|
|
export function podcastImportStart(
|
|
accessToken: string,
|
|
feedUrl: string,
|
|
collectionId: string
|
|
): Promise<ImportStartResponse> {
|
|
return post(accessToken, '/admin/podcast/import', {
|
|
feed_url: feedUrl,
|
|
collection_id: collectionId
|
|
});
|
|
}
|
|
|
|
/** Hent status for en podcast-import-jobb. */
|
|
export async function podcastImportStatus(
|
|
accessToken: string,
|
|
jobId: string
|
|
): Promise<ImportStatusResponse> {
|
|
const res = await fetch(
|
|
`${BASE_URL}/admin/podcast/import-status?job_id=${encodeURIComponent(jobId)}`,
|
|
{ headers: { Authorization: `Bearer ${accessToken}` } }
|
|
);
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(`import status failed (${res.status}): ${body}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
/** Hent samlinger med podcast-trait (for import-wizard dropdown). */
|
|
export async function fetchPodcastCollections(
|
|
accessToken: string
|
|
): Promise<PodcastCollection[]> {
|
|
const res = await fetch(`${BASE_URL}/admin/podcast/collections`, {
|
|
headers: { Authorization: `Bearer ${accessToken}` }
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(`podcast collections failed (${res.status}): ${body}`);
|
|
}
|
|
return res.json();
|
|
}
|