Sanitized mirror from private repository - 2026-04-05 06:44:51 UTC
Some checks failed
Documentation / Build Docusaurus (push) Failing after 5m7s
Documentation / Deploy to GitHub Pages (push) Has been skipped

This commit is contained in:
Gitea Mirror Bot
2026-04-05 06:44:51 +00:00
commit e4159258e8
1390 changed files with 354335 additions and 0 deletions

17
dashboard/ui/lib/api.ts Normal file
View 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
View 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 }[];
}

View 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,
});
}

View File

@@ -0,0 +1,42 @@
"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);
es.addEventListener("activity", (e: MessageEvent) => {
try {
const event: ActivityEvent = JSON.parse(e.data);
setEvents((prev) => [event, ...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;
}

View 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))
}