Sanitized mirror from private repository - 2026-04-05 09:28:04 UTC
This commit is contained in:
17
dashboard/ui/lib/api.ts
Normal file
17
dashboard/ui/lib/api.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// In the browser, API calls go to the same origin (Next.js rewrites to backend).
|
||||
// On the server, they go directly to the backend.
|
||||
const API = typeof window === "undefined"
|
||||
? (process.env.BACKEND_URL || "http://localhost:18888")
|
||||
: "";
|
||||
|
||||
export async function fetchAPI<T>(path: string): Promise<T> {
|
||||
const res = await fetch(`${API}${path}`);
|
||||
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function postAPI<T>(path: string): Promise<T> {
|
||||
const res = await fetch(`${API}${path}`, { method: "POST" });
|
||||
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
80
dashboard/ui/lib/types.ts
Normal file
80
dashboard/ui/lib/types.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
// Types aligned with the actual API response from /api/stats/overview
|
||||
export interface OverviewStats {
|
||||
containers: {
|
||||
total: number;
|
||||
running?: number;
|
||||
endpoints?: Record<string, { total: number; running: number; error?: boolean }>;
|
||||
by_endpoint?: Record<string, { total: number; running: number; error?: boolean }>;
|
||||
};
|
||||
gpu: {
|
||||
available: boolean;
|
||||
name?: string;
|
||||
temp_c?: number;
|
||||
// API may use either naming convention
|
||||
power_w?: number;
|
||||
power_draw_w?: number;
|
||||
power_limit_w?: number;
|
||||
vram_used_mb?: number;
|
||||
vram_total_mb?: number;
|
||||
memory_used_mb?: number;
|
||||
memory_total_mb?: number;
|
||||
utilization_pct?: number;
|
||||
};
|
||||
// API returns either a number or {gmail, dvish, proton, total}
|
||||
emails_today?: number | Record<string, number>;
|
||||
email_today?: number | Record<string, number>;
|
||||
alerts?: number;
|
||||
unhealthy_count?: number;
|
||||
// API returns either an object or a boolean
|
||||
ollama?: { available: boolean; url: string };
|
||||
ollama_available?: boolean;
|
||||
hosts_online?: number;
|
||||
}
|
||||
|
||||
export interface ActivityEvent {
|
||||
type: string;
|
||||
timestamp: string;
|
||||
source: string;
|
||||
raw: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface Container {
|
||||
id: string;
|
||||
name: string;
|
||||
image: string;
|
||||
state: string;
|
||||
status: string;
|
||||
endpoint: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface JellyfinStatus {
|
||||
version: string;
|
||||
server_name: string;
|
||||
libraries: { name: string; type: string; paths: string[] }[];
|
||||
active_sessions: {
|
||||
user: string;
|
||||
device: string;
|
||||
client: string;
|
||||
title: string;
|
||||
type: string;
|
||||
}[];
|
||||
idle_sessions: number;
|
||||
}
|
||||
|
||||
export interface EmailStats {
|
||||
accounts: {
|
||||
account: string;
|
||||
today: number;
|
||||
categories: Record<string, number>;
|
||||
}[];
|
||||
sender_cache: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface ExpenseSummary {
|
||||
month: string;
|
||||
total: number;
|
||||
count: number;
|
||||
top_vendors: { vendor: string; amount: number }[];
|
||||
}
|
||||
9
dashboard/ui/lib/use-poll.ts
Normal file
9
dashboard/ui/lib/use-poll.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import useSWR from "swr";
|
||||
import { fetchAPI } from "./api";
|
||||
|
||||
export function usePoll<T>(path: string, interval: number = 60000) {
|
||||
return useSWR<T>(path, () => fetchAPI<T>(path), {
|
||||
refreshInterval: interval,
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
}
|
||||
52
dashboard/ui/lib/use-sse.ts
Normal file
52
dashboard/ui/lib/use-sse.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import type { ActivityEvent } from "./types";
|
||||
|
||||
// Use same origin — Next.js rewrites /api/* to backend
|
||||
|
||||
export function useSSE(path: string, maxEvents: number = 30) {
|
||||
const [events, setEvents] = useState<ActivityEvent[]>([]);
|
||||
const retryTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let es: EventSource | null = null;
|
||||
|
||||
function connect() {
|
||||
es = new EventSource(path);
|
||||
|
||||
// Backend sends "init" with the full batch and "update" with new events
|
||||
es.addEventListener("init", (e: MessageEvent) => {
|
||||
try {
|
||||
const batch: ActivityEvent[] = JSON.parse(e.data);
|
||||
setEvents(batch.slice(0, maxEvents));
|
||||
} catch {
|
||||
// ignore malformed events
|
||||
}
|
||||
});
|
||||
|
||||
es.addEventListener("update", (e: MessageEvent) => {
|
||||
try {
|
||||
const batch: ActivityEvent[] = JSON.parse(e.data);
|
||||
setEvents((prev) => [...batch, ...prev].slice(0, maxEvents));
|
||||
} catch {
|
||||
// ignore malformed events
|
||||
}
|
||||
});
|
||||
|
||||
es.onerror = () => {
|
||||
es?.close();
|
||||
retryTimeout.current = setTimeout(connect, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
es?.close();
|
||||
if (retryTimeout.current) clearTimeout(retryTimeout.current);
|
||||
};
|
||||
}, [path, maxEvents]);
|
||||
|
||||
return events;
|
||||
}
|
||||
6
dashboard/ui/lib/utils.ts
Normal file
6
dashboard/ui/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user