feat: lower memory dedupe threshold for more aggressive cleaning

This commit is contained in:
root
2026-07-07 16:45:30 +02:00
parent c92f238d2d
commit 97be0f1d00
68 changed files with 15783 additions and 15783 deletions
+480 -480
View File
@@ -1,480 +1,480 @@
// Schmaler Fetch-Helfer gegen das MC-2-Backend (/api/*).
export async function api<T = unknown>(path: string, init?: RequestInit): Promise<T> {
const headers = {
"Content-Type": "application/json",
...init?.headers,
} as Record<string, string>
const sudoPassword = localStorage.getItem("mc_sudo_password")
const hfToken = localStorage.getItem("mc_hf_token")
if (sudoPassword) {
headers["X-Sudo-Password"] = sudoPassword
}
let body = init?.body
const method = init?.method?.toUpperCase() || "GET"
if (method === "POST") {
if (typeof body === "string") {
try {
const data = JSON.parse(body)
let changed = false
if (sudoPassword && !("sudo_password" in data)) {
data["sudo_password"] = sudoPassword
changed = true
}
if (hfToken && !("hf_token" in data)) {
data["hf_token"] = hfToken
changed = true
}
if (changed) {
body = JSON.stringify(data)
}
} catch (e) {
// ignore JSON parse errors
}
} else if (!body) {
const data: Record<string, any> = {}
if (sudoPassword) data["sudo_password"] = sudoPassword
if (hfToken) data["hf_token"] = hfToken
if (Object.keys(data).length > 0) {
body = JSON.stringify(data)
}
}
}
const res = await fetch(path, {
...init,
headers,
body,
})
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
return res.json() as Promise<T>
}
// llama-swap-`groups`: Mitglieder mit swap=false dürfen GLEICHZEITIG resident sein (Ko-Residenz).
// Die `brains`-Gruppe hält Hirn (fast) + Augen (vision) gemeinsam warm, statt sich zu verdrängen.
export interface GroupSpec {
swap: boolean
persist: boolean
members: string[]
}
export interface GroupsResp {
groups: Record<string, GroupSpec>
}
export const getGroups = () => api<GroupsResp>("/api/groups")
export const setGroup = (group: string, members: string[], swap = false, persist = true) =>
api("/api/groups", { method: "PUT", body: JSON.stringify({ group, members, swap, persist }) })
export interface Capabilities {
moe: boolean
active_b: number | null
tools: "yes" | "likely" | "no"
vision: boolean
coder: boolean
reasoning: boolean
embedding: boolean
ctx: number | null
params_b: number | null
arch: string | null
}
export interface Fit {
level: "perfect" | "marginal" | "too_tight"
text: string
req_gb: number
tps: number
}
export interface RoleRecModel {
name: string
current_role: string | null
params_b: number
quant: string
fit: Fit
suitable: boolean
incomplete: boolean
score: number
reason: string
recommended: boolean
}
export interface RoleRecResp {
role: string
recommended: string | null
models: RoleRecModel[]
}
export interface FitResp {
params_b: number
fit: Fit
optimal_ctx: number
assigned_ctx: number
budget: { gtt_gb: number; reserved_gb: number; budget_gb: number; mode: string }
sys_ram_gb: number
}
export interface ModelInfo {
name: string
role: string | null
aliases: string[]
api_ids: string[]
ctx: number | null
ttl: number | null
cmd: string
gguf_path: string
filename: string
quant: string
size_bytes: number | null
incomplete: boolean
prompt_cache: boolean
spec_draft_model: string | null
spec_type: string | null
spec_active: boolean
parallel_slots: number
capabilities: Capabilities
}
export interface VocabFingerprint {
model: string | null
pre: string | null
n_vocab: number | null
arch: string | null
}
export interface DraftInfo {
path: string
filename: string
size_bytes: number | null
vocab: VocabFingerprint | null
compatible: boolean | null // null = nicht prüfbar (Ziel-GGUF fehlt)
mtp?: boolean // MTP-Kopf (Multi-Token-Prediction) statt klassischem Draft
}
export interface DraftsResp {
target_path: string
target_exists: boolean
target_vocab: VocabFingerprint | null
drafts: DraftInfo[]
}
export interface DiscoverModel {
name: string
author: string
repo: string
role: string
params_b: number
quant: string
tags: string[]
downloads: number
fit: Fit
optimal_ctx: number
caps: Capabilities
}
export interface DiscoverCategory {
role: string
title: string
icon: string
models: DiscoverModel[]
recommended: string | null
}
export interface DiscoverResp {
updated: number
categories: DiscoverCategory[]
sys_ram_gb: number
}
export interface RoutingResp {
mode?: string
endpoint?: string
heavy_threshold_chars?: number
routes: { name: string; target: string }[]
lanes?: { name: string; target: string; threshold_chars?: number; escalate_chars?: number; aka?: string }[]
fallbacks: Record<string, string[]>[]
context_window_fallbacks: Record<string, string[]>[]
gateway_reachable: boolean
}
// UI-editierbare Routing-Policy (GET/PUT /api/routing/policy). Aliase + Zeichen-Schwellen
// hinter den Lanes; hot-reload im Backend (kein Restart).
export interface RoutingPolicy {
fast: string
heavy: string
coder: string
coder_lite: string
heavy_chars: number
coding_escalate_chars: number
fast_no_think: boolean
}
export interface RoutingPolicyField {
key: keyof RoutingPolicy
label: string
type: "str" | "int" | "bool"
min?: number
max?: number
}
export interface RoutingPolicyMeta {
policy: RoutingPolicy
defaults: RoutingPolicy
fields: RoutingPolicyField[]
}
export const getRoutingPolicy = () => api<RoutingPolicyMeta>("/api/routing/policy")
export const updateRoutingPolicy = (patch: Partial<RoutingPolicy>) =>
api<{ policy: RoutingPolicy }>("/api/routing/policy", { method: "PUT", body: JSON.stringify(patch) })
export interface GitInfo {
hash: string
date: string
subject: string
branch: string
dirty: boolean
path: string
}
// Engine kann git-, binary- oder unbekannte Version sein — Felder je nach `type`.
export interface ComponentVersion {
type: "git" | "binary" | "unknown"
hash?: string
date?: string
subject?: string
branch?: string
dirty?: boolean
path?: string
version_text?: string
}
export interface Versions {
mc2: GitInfo | null
engine: ComponentVersion
hermes_ui: GitInfo | null
hermes_agent: GitInfo | null
}
export interface SystemStatus {
cpu: { percent: number; cores: number | null }
ram: { total: number; used: number; percent: number }
gpu: {
busy_percent: number | null
vram_used: number | null
vram_total: number | null
gtt_used?: number | null
gtt_total?: number | null
} | null
temp: { cpu?: number; gpu?: number } | null
disk: { total: number; used: number; percent: number } | null
versions?: Versions
}
export interface ServicesResp {
services: { name: string; unit: string; url: string; ok: boolean }[]
links: { engine_ui: string; gateway: string; hermes_terminal: string }
}
export interface ComponentUpdate {
key: string // z.B. "hermes_agent"
name: string
current: string | null
latest: string | null
update: boolean | null // null = unbestimmbar (installierte Version remote nicht abfragbar)
reachable: boolean | null
}
export interface UpdatesResp {
os: number
engine: number
swap: number
models: number
model_list: { role: string; title: string; repo: string }[]
last_check?: number | null
components?: ComponentUpdate[]
}
export interface UpdateDetails {
kind: "os" | "engine" | "swap" | "hermes"
error?: string
// LLM-Zusammenfassung in Lucys Stimme (hermes/engine/swap; Breaking Changes zuerst)
summary?: string
// Aktions-Verdikt: muss der Besitzer selbst etwas tun? (null = kein Verdikt/keine Summary)
action_needed?: boolean | null
action_text?: string
// os
count?: number
packages?: { name: string; current: string; candidate: string }[]
// os: vom System zurückgestellte Pakete (Phasen-Rollout / kept back) — ehrlich statt "hängt"
held_back?: { name: string; reason: "phasing" | "kept_back" }[]
// engine
installed_build?: number | null
latest_build?: number | null
latest_tag?: string | null
name?: string | null
url?: string | null
body?: string | null
// hermes
branch?: string | null
behind?: number
commits?: { hash: string; subject: string; when: string }[]
}
export interface ConnectTool {
label: string
lang: string
snippet: string
note: string
}
export interface ConnectResp {
host: string
gateway_url: string
mc_url: string
tools: Record<string, ConnectTool> // Leitung 1 — Modell (IDEs/Agenten → Gateway)
memory: ConnectTool // Leitung 2 — Gedächtnis (separater MCP-Server)
}
export interface ConnectLine {
ok: boolean
detail: string
}
export interface ConnectHealth {
gateway: ConnectLine
memory: ConnectLine
}
export interface Memory {
id: string
content: string
category: string
source: string
created_at: string
updated_at: string
score?: number // Relevanz bei semantischer Suche (q gesetzt); sonst undefined
}
export interface MemoryGraphNode {
id: string
content: string
category: string
source: string
}
export interface MemoryGraph {
nodes: MemoryGraphNode[]
edges: { source: string; target: string; weight: number }[]
}
export interface DedupeResult {
groups: { keep: { id: string; content: string; category: string }; remove: { id: string; content: string }[] }[]
duplicate_count: number
removed: number
applied: boolean
}
export interface AgentStatus {
gateway_url: string
terminal_url: string
box_console_url?: string
hermes_ui_url?: string
gateway_reachable: boolean
terminal_reachable: boolean
box_console_reachable?: boolean
hermes_ui_reachable?: boolean
home_exists: boolean
brain_model?: string
has_config: boolean
has_skills: boolean
has_memories: boolean
telegram_enabled?: boolean
mcp_server_count?: number
pc_executor_reachable?: boolean
}
export interface Job {
id: string
label: string
group?: string | null // "maintenance" = system-veränderndes Update (Wartungs-Riegel)
state: "queued" | "running" | "done" | "failed" | "canceled"
progress?: number
total_bytes?: number
done_bytes?: number
rate_bps?: number
eta_s?: number
}
export interface Health {
status: string
version: string
engine_reachable: boolean
gateway_reachable: boolean
brain?: { role: string; model: string | null; ready: boolean }
}
export interface TokenStats {
prompt_tokens: number
completion_tokens: number
total_tokens: number
saved_usd: number
saved_eur: number
pricing?: Record<string, { in: number; out: number }>
}
// Per-Turn-Latenz-Trace (GET /api/voice/trace). Balken-Stufen (zeitlich disjunkt):
// stt · vision · hirn · gen. mem0 = Unter-Detail INNERHALB von hirn (nicht zum Balken addieren).
export interface VoiceTurn {
id: string
ts: number // Epoch-Sekunden
session: string
kind: string
had_images: boolean
stt_ms: number | null
vision_ms: number | null
hirn_ms: number | null // Zeit bis erstes Inhalts-Token (Agent + Mem0 + LLM-TTFT)
gen_ms: number | null // Generierung nach dem ersten Token bis Stream-Ende
mem0_ms: number | null // Teil VON hirn (Mem0-Retrieve), best-effort
total_ms: number
error: string | null
}
export interface VoiceTraceResp {
turns: VoiceTurn[]
}
export interface ModelsResp {
models: ModelInfo[]
running?: string[]
}
export interface HermesBrainModel {
name: string
filename?: string
params_b: number | null
quant?: string
size_bytes?: number | null
version?: number | null
gguf_path?: string
incomplete?: boolean
}
export interface HermesBrainCandidate {
repo: string
name: string
version: number
params_b: number
downloads: number
fit: Fit
}
export interface HermesBrainBudget {
gtt_gb: number
brain_gb: number // Footprint des (empfohlenen) Brains, das immer resident bleibt
warm_projected_gb: number // alle persist-Modelle warm (Info)
free_after_gb: number // frei nach Brain + größtem on-demand
largest_ondemand_gb: number // größtes on-demand-Modell (z.B. heavy/coder)
fits: boolean // passt Brain + größtes on-demand zusammen ins Budget?
}
export interface HermesBrainResp {
current: HermesBrainModel | null
recommended: HermesBrainCandidate | null
update_available: boolean
budget?: HermesBrainBudget | null
}
// Schmaler Fetch-Helfer gegen das MC-2-Backend (/api/*).
export async function api<T = unknown>(path: string, init?: RequestInit): Promise<T> {
const headers = {
"Content-Type": "application/json",
...init?.headers,
} as Record<string, string>
const sudoPassword = localStorage.getItem("mc_sudo_password")
const hfToken = localStorage.getItem("mc_hf_token")
if (sudoPassword) {
headers["X-Sudo-Password"] = sudoPassword
}
let body = init?.body
const method = init?.method?.toUpperCase() || "GET"
if (method === "POST") {
if (typeof body === "string") {
try {
const data = JSON.parse(body)
let changed = false
if (sudoPassword && !("sudo_password" in data)) {
data["sudo_password"] = sudoPassword
changed = true
}
if (hfToken && !("hf_token" in data)) {
data["hf_token"] = hfToken
changed = true
}
if (changed) {
body = JSON.stringify(data)
}
} catch (e) {
// ignore JSON parse errors
}
} else if (!body) {
const data: Record<string, any> = {}
if (sudoPassword) data["sudo_password"] = sudoPassword
if (hfToken) data["hf_token"] = hfToken
if (Object.keys(data).length > 0) {
body = JSON.stringify(data)
}
}
}
const res = await fetch(path, {
...init,
headers,
body,
})
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
return res.json() as Promise<T>
}
// llama-swap-`groups`: Mitglieder mit swap=false dürfen GLEICHZEITIG resident sein (Ko-Residenz).
// Die `brains`-Gruppe hält Hirn (fast) + Augen (vision) gemeinsam warm, statt sich zu verdrängen.
export interface GroupSpec {
swap: boolean
persist: boolean
members: string[]
}
export interface GroupsResp {
groups: Record<string, GroupSpec>
}
export const getGroups = () => api<GroupsResp>("/api/groups")
export const setGroup = (group: string, members: string[], swap = false, persist = true) =>
api("/api/groups", { method: "PUT", body: JSON.stringify({ group, members, swap, persist }) })
export interface Capabilities {
moe: boolean
active_b: number | null
tools: "yes" | "likely" | "no"
vision: boolean
coder: boolean
reasoning: boolean
embedding: boolean
ctx: number | null
params_b: number | null
arch: string | null
}
export interface Fit {
level: "perfect" | "marginal" | "too_tight"
text: string
req_gb: number
tps: number
}
export interface RoleRecModel {
name: string
current_role: string | null
params_b: number
quant: string
fit: Fit
suitable: boolean
incomplete: boolean
score: number
reason: string
recommended: boolean
}
export interface RoleRecResp {
role: string
recommended: string | null
models: RoleRecModel[]
}
export interface FitResp {
params_b: number
fit: Fit
optimal_ctx: number
assigned_ctx: number
budget: { gtt_gb: number; reserved_gb: number; budget_gb: number; mode: string }
sys_ram_gb: number
}
export interface ModelInfo {
name: string
role: string | null
aliases: string[]
api_ids: string[]
ctx: number | null
ttl: number | null
cmd: string
gguf_path: string
filename: string
quant: string
size_bytes: number | null
incomplete: boolean
prompt_cache: boolean
spec_draft_model: string | null
spec_type: string | null
spec_active: boolean
parallel_slots: number
capabilities: Capabilities
}
export interface VocabFingerprint {
model: string | null
pre: string | null
n_vocab: number | null
arch: string | null
}
export interface DraftInfo {
path: string
filename: string
size_bytes: number | null
vocab: VocabFingerprint | null
compatible: boolean | null // null = nicht prüfbar (Ziel-GGUF fehlt)
mtp?: boolean // MTP-Kopf (Multi-Token-Prediction) statt klassischem Draft
}
export interface DraftsResp {
target_path: string
target_exists: boolean
target_vocab: VocabFingerprint | null
drafts: DraftInfo[]
}
export interface DiscoverModel {
name: string
author: string
repo: string
role: string
params_b: number
quant: string
tags: string[]
downloads: number
fit: Fit
optimal_ctx: number
caps: Capabilities
}
export interface DiscoverCategory {
role: string
title: string
icon: string
models: DiscoverModel[]
recommended: string | null
}
export interface DiscoverResp {
updated: number
categories: DiscoverCategory[]
sys_ram_gb: number
}
export interface RoutingResp {
mode?: string
endpoint?: string
heavy_threshold_chars?: number
routes: { name: string; target: string }[]
lanes?: { name: string; target: string; threshold_chars?: number; escalate_chars?: number; aka?: string }[]
fallbacks: Record<string, string[]>[]
context_window_fallbacks: Record<string, string[]>[]
gateway_reachable: boolean
}
// UI-editierbare Routing-Policy (GET/PUT /api/routing/policy). Aliase + Zeichen-Schwellen
// hinter den Lanes; hot-reload im Backend (kein Restart).
export interface RoutingPolicy {
fast: string
heavy: string
coder: string
coder_lite: string
heavy_chars: number
coding_escalate_chars: number
fast_no_think: boolean
}
export interface RoutingPolicyField {
key: keyof RoutingPolicy
label: string
type: "str" | "int" | "bool"
min?: number
max?: number
}
export interface RoutingPolicyMeta {
policy: RoutingPolicy
defaults: RoutingPolicy
fields: RoutingPolicyField[]
}
export const getRoutingPolicy = () => api<RoutingPolicyMeta>("/api/routing/policy")
export const updateRoutingPolicy = (patch: Partial<RoutingPolicy>) =>
api<{ policy: RoutingPolicy }>("/api/routing/policy", { method: "PUT", body: JSON.stringify(patch) })
export interface GitInfo {
hash: string
date: string
subject: string
branch: string
dirty: boolean
path: string
}
// Engine kann git-, binary- oder unbekannte Version sein — Felder je nach `type`.
export interface ComponentVersion {
type: "git" | "binary" | "unknown"
hash?: string
date?: string
subject?: string
branch?: string
dirty?: boolean
path?: string
version_text?: string
}
export interface Versions {
mc2: GitInfo | null
engine: ComponentVersion
hermes_ui: GitInfo | null
hermes_agent: GitInfo | null
}
export interface SystemStatus {
cpu: { percent: number; cores: number | null }
ram: { total: number; used: number; percent: number }
gpu: {
busy_percent: number | null
vram_used: number | null
vram_total: number | null
gtt_used?: number | null
gtt_total?: number | null
} | null
temp: { cpu?: number; gpu?: number } | null
disk: { total: number; used: number; percent: number } | null
versions?: Versions
}
export interface ServicesResp {
services: { name: string; unit: string; url: string; ok: boolean }[]
links: { engine_ui: string; gateway: string; hermes_terminal: string }
}
export interface ComponentUpdate {
key: string // z.B. "hermes_agent"
name: string
current: string | null
latest: string | null
update: boolean | null // null = unbestimmbar (installierte Version remote nicht abfragbar)
reachable: boolean | null
}
export interface UpdatesResp {
os: number
engine: number
swap: number
models: number
model_list: { role: string; title: string; repo: string }[]
last_check?: number | null
components?: ComponentUpdate[]
}
export interface UpdateDetails {
kind: "os" | "engine" | "swap" | "hermes"
error?: string
// LLM-Zusammenfassung in Lucys Stimme (hermes/engine/swap; Breaking Changes zuerst)
summary?: string
// Aktions-Verdikt: muss der Besitzer selbst etwas tun? (null = kein Verdikt/keine Summary)
action_needed?: boolean | null
action_text?: string
// os
count?: number
packages?: { name: string; current: string; candidate: string }[]
// os: vom System zurückgestellte Pakete (Phasen-Rollout / kept back) — ehrlich statt "hängt"
held_back?: { name: string; reason: "phasing" | "kept_back" }[]
// engine
installed_build?: number | null
latest_build?: number | null
latest_tag?: string | null
name?: string | null
url?: string | null
body?: string | null
// hermes
branch?: string | null
behind?: number
commits?: { hash: string; subject: string; when: string }[]
}
export interface ConnectTool {
label: string
lang: string
snippet: string
note: string
}
export interface ConnectResp {
host: string
gateway_url: string
mc_url: string
tools: Record<string, ConnectTool> // Leitung 1 — Modell (IDEs/Agenten → Gateway)
memory: ConnectTool // Leitung 2 — Gedächtnis (separater MCP-Server)
}
export interface ConnectLine {
ok: boolean
detail: string
}
export interface ConnectHealth {
gateway: ConnectLine
memory: ConnectLine
}
export interface Memory {
id: string
content: string
category: string
source: string
created_at: string
updated_at: string
score?: number // Relevanz bei semantischer Suche (q gesetzt); sonst undefined
}
export interface MemoryGraphNode {
id: string
content: string
category: string
source: string
}
export interface MemoryGraph {
nodes: MemoryGraphNode[]
edges: { source: string; target: string; weight: number }[]
}
export interface DedupeResult {
groups: { keep: { id: string; content: string; category: string }; remove: { id: string; content: string }[] }[]
duplicate_count: number
removed: number
applied: boolean
}
export interface AgentStatus {
gateway_url: string
terminal_url: string
box_console_url?: string
hermes_ui_url?: string
gateway_reachable: boolean
terminal_reachable: boolean
box_console_reachable?: boolean
hermes_ui_reachable?: boolean
home_exists: boolean
brain_model?: string
has_config: boolean
has_skills: boolean
has_memories: boolean
telegram_enabled?: boolean
mcp_server_count?: number
pc_executor_reachable?: boolean
}
export interface Job {
id: string
label: string
group?: string | null // "maintenance" = system-veränderndes Update (Wartungs-Riegel)
state: "queued" | "running" | "done" | "failed" | "canceled"
progress?: number
total_bytes?: number
done_bytes?: number
rate_bps?: number
eta_s?: number
}
export interface Health {
status: string
version: string
engine_reachable: boolean
gateway_reachable: boolean
brain?: { role: string; model: string | null; ready: boolean }
}
export interface TokenStats {
prompt_tokens: number
completion_tokens: number
total_tokens: number
saved_usd: number
saved_eur: number
pricing?: Record<string, { in: number; out: number }>
}
// Per-Turn-Latenz-Trace (GET /api/voice/trace). Balken-Stufen (zeitlich disjunkt):
// stt · vision · hirn · gen. mem0 = Unter-Detail INNERHALB von hirn (nicht zum Balken addieren).
export interface VoiceTurn {
id: string
ts: number // Epoch-Sekunden
session: string
kind: string
had_images: boolean
stt_ms: number | null
vision_ms: number | null
hirn_ms: number | null // Zeit bis erstes Inhalts-Token (Agent + Mem0 + LLM-TTFT)
gen_ms: number | null // Generierung nach dem ersten Token bis Stream-Ende
mem0_ms: number | null // Teil VON hirn (Mem0-Retrieve), best-effort
total_ms: number
error: string | null
}
export interface VoiceTraceResp {
turns: VoiceTurn[]
}
export interface ModelsResp {
models: ModelInfo[]
running?: string[]
}
export interface HermesBrainModel {
name: string
filename?: string
params_b: number | null
quant?: string
size_bytes?: number | null
version?: number | null
gguf_path?: string
incomplete?: boolean
}
export interface HermesBrainCandidate {
repo: string
name: string
version: number
params_b: number
downloads: number
fit: Fit
}
export interface HermesBrainBudget {
gtt_gb: number
brain_gb: number // Footprint des (empfohlenen) Brains, das immer resident bleibt
warm_projected_gb: number // alle persist-Modelle warm (Info)
free_after_gb: number // frei nach Brain + größtem on-demand
largest_ondemand_gb: number // größtes on-demand-Modell (z.B. heavy/coder)
fits: boolean // passt Brain + größtes on-demand zusammen ins Budget?
}
export interface HermesBrainResp {
current: HermesBrainModel | null
recommended: HermesBrainCandidate | null
update_available: boolean
budget?: HermesBrainBudget | null
}
+32 -32
View File
@@ -1,32 +1,32 @@
// Zentrale Formatierungs-Helfer (vorher in einzelnen Views dupliziert).
/** Bytes → GB als String mit einer Nachkommastelle (z.B. "14.1"). */
export function gb(b: number): string {
return (b / 1024 ** 3).toFixed(1)
}
/** Bytes → "1.2 GB" / "512 MB"; leer bei 0/undefined. */
export function fmtBytes(b?: number): string {
if (!b) return ""
if (b > 1024 ** 3) return `${(b / 1024 ** 3).toFixed(1)} GB`
return `${(b / 1024 ** 2).toFixed(0)} MB`
}
/** Bytes → "1.2 GB" / "512 MB"; "—" bei 0/undefined/null. */
export function fmtSize(b?: number | null): string {
if (!b) return "—"
const g = b / 1024 ** 3
return g >= 1 ? `${g.toFixed(1)} GB` : `${(b / 1024 ** 2).toFixed(0)} MB`
}
/** Sekunden → "3 min" / "45 s"; leer bei 0/undefined. */
export function fmtEta(s?: number): string {
if (!s) return ""
const m = Math.floor(s / 60)
return m > 0 ? `${m} min` : `${s} s`
}
/** Kontextlänge → "32k"; "—" bei null. */
export function fmtCtx(c: number | null): string {
return c ? `${Math.round(c / 1024)}k` : "—"
}
// Zentrale Formatierungs-Helfer (vorher in einzelnen Views dupliziert).
/** Bytes → GB als String mit einer Nachkommastelle (z.B. "14.1"). */
export function gb(b: number): string {
return (b / 1024 ** 3).toFixed(1)
}
/** Bytes → "1.2 GB" / "512 MB"; leer bei 0/undefined. */
export function fmtBytes(b?: number): string {
if (!b) return ""
if (b > 1024 ** 3) return `${(b / 1024 ** 3).toFixed(1)} GB`
return `${(b / 1024 ** 2).toFixed(0)} MB`
}
/** Bytes → "1.2 GB" / "512 MB"; "—" bei 0/undefined/null. */
export function fmtSize(b?: number | null): string {
if (!b) return "—"
const g = b / 1024 ** 3
return g >= 1 ? `${g.toFixed(1)} GB` : `${(b / 1024 ** 2).toFixed(0)} MB`
}
/** Sekunden → "3 min" / "45 s"; leer bei 0/undefined. */
export function fmtEta(s?: number): string {
if (!s) return ""
const m = Math.floor(s / 60)
return m > 0 ? `${m} min` : `${s} s`
}
/** Kontextlänge → "32k"; "—" bei null. */
export function fmtCtx(c: number | null): string {
return c ? `${Math.round(c / 1024)}k` : "—"
}
+152 -152
View File
@@ -1,152 +1,152 @@
// Zentrale Daten-Hooks (TanStack Query). Kapseln lib/api.ts und liefern Caching,
// Dedup (gleicher queryKey = eine Anfrage über alle Views), Retry und Polling.
// Mutations invalidieren gezielt die betroffenen Keys, statt manuell neu zu laden.
import { useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query"
import {
api,
type AgentStatus,
type ConnectResp,
type ConnectHealth,
type DiscoverResp,
type DraftsResp,
type GroupsResp,
type HermesBrainResp,
type Health,
type Job,
type Memory,
type MemoryGraph,
type ModelsResp,
type RoutingResp,
type RoutingPolicyMeta,
type ServicesResp,
type SystemStatus,
type TokenStats,
type UpdatesResp,
type VoiceTraceResp,
} from "./api"
// Zentrale Query-Keys (eine Quelle der Wahrheit für invalidate).
export const qk = {
health: ["health"] as const,
systemStatus: ["system-status"] as const,
services: ["services"] as const,
models: ["models"] as const,
groups: ["groups"] as const,
routing: ["routing"] as const,
routingPolicy: ["routing-policy"] as const,
jobs: ["jobs"] as const,
tokenStats: ["token-stats"] as const,
agentStatus: ["agent-status"] as const,
hermesBrain: ["hermes-brain"] as const,
updates: ["updates"] as const,
discover: ["discover"] as const,
drafts: (target?: string) => ["drafts", target ?? ""] as const,
connect: (params?: string) => ["connect", params ?? ""] as const,
connectHealth: ["connect-health"] as const,
memory: (q?: string, category?: string) => ["memory", q ?? "", category ?? ""] as const,
memoryGraph: ["memory-graph"] as const,
voiceTrace: ["voice-trace"] as const,
}
// Per-Turn-Latenz-Trace (letzte N Voice/Lucy-Turns mit Stufen-Breakdown).
export const useVoiceTrace = (limit = 12, refetchInterval = 4_000) =>
useQuery({
queryKey: qk.voiceTrace,
queryFn: () => api<VoiceTraceResp>(`/api/voice/trace?limit=${limit}`),
refetchInterval,
select: (d) => d.turns ?? [],
})
export const useMemoryGraph = (enabled = true) =>
useQuery({
queryKey: qk.memoryGraph,
queryFn: () => api<MemoryGraph>("/api/memory/graph"),
enabled,
})
export const useHealth = () =>
useQuery({ queryKey: qk.health, queryFn: () => api<Health>("/api/health"), refetchInterval: 10_000 })
export const useSystemStatus = (refetchInterval = 5_000) =>
useQuery({ queryKey: qk.systemStatus, queryFn: () => api<SystemStatus>("/api/system/status"), refetchInterval })
export const useServices = (refetchInterval = 3_000) =>
useQuery({ queryKey: qk.services, queryFn: () => api<ServicesResp>("/api/system/services"), refetchInterval })
export const useModels = (refetchInterval = 4_000) =>
useQuery({ queryKey: qk.models, queryFn: () => api<ModelsResp>("/api/models"), refetchInterval })
export const useGroups = (refetchInterval = 8_000) =>
useQuery({ queryKey: qk.groups, queryFn: () => api<GroupsResp>("/api/groups"), refetchInterval })
export const useRouting = (refetchInterval = 4_000) =>
useQuery({ queryKey: qk.routing, queryFn: () => api<RoutingResp>("/api/routing"), refetchInterval })
export const useRoutingPolicy = () =>
useQuery({ queryKey: qk.routingPolicy, queryFn: () => api<RoutingPolicyMeta>("/api/routing/policy") })
export const useJobs = (refetchInterval = 2_000) =>
useQuery({
queryKey: qk.jobs,
queryFn: () => api<{ jobs: Job[] }>("/api/jobs"),
refetchInterval,
select: (d) => d.jobs ?? [],
})
export const useTokenStats = (refetchInterval = 3_000) =>
useQuery({ queryKey: qk.tokenStats, queryFn: () => api<TokenStats>("/api/system/token-stats"), refetchInterval })
export const useAgentStatus = (refetchInterval = 5_000) =>
useQuery({ queryKey: qk.agentStatus, queryFn: () => api<AgentStatus>("/api/agent/status"), refetchInterval })
// Agent-Hirn (Hermes) + bestes NousResearch-Update. Selten pollen (HF-Abfrage).
export const useHermesBrain = (refetchInterval = 60_000) =>
useQuery({ queryKey: qk.hermesBrain, queryFn: () => api<HermesBrainResp>("/api/agent/brain"), refetchInterval })
export const useUpdates = (refetchInterval?: number) =>
useQuery({ queryKey: qk.updates, queryFn: () => api<UpdatesResp>("/api/maintenance/updates"), refetchInterval })
export const useDiscover = () =>
useQuery({ queryKey: qk.discover, queryFn: () => api<DiscoverResp>("/api/discover") })
// Verfügbare Spec-Draft-Modelle + Vocab-Kompatibilität zum Ziel-Modell (GGUF-Pfad).
export const useDrafts = (target?: string) =>
useQuery({
queryKey: qk.drafts(target),
queryFn: () => api<DraftsResp>(`/api/models/drafts?target=${encodeURIComponent(target ?? "")}`),
enabled: !!target,
})
export const useConnect = (params?: string) =>
useQuery({
queryKey: qk.connect(params),
queryFn: () => api<ConnectResp>(params ? `/api/connect?${params}` : "/api/connect"),
})
// Live-Status der zwei Leitungen (Gateway + Gedächtnis) — alle 15s aktualisiert.
export const useConnectHealth = () =>
useQuery({
queryKey: qk.connectHealth,
queryFn: () => api<ConnectHealth>("/api/connect/health"),
refetchInterval: 15000,
})
export const useMemory = (opts?: { q?: string; category?: string; limit?: number }) =>
useQuery({
queryKey: qk.memory(opts?.q, opts?.category),
queryFn: () => {
const p = new URLSearchParams()
if (opts?.q) p.set("q", opts.q)
if (opts?.category) p.set("category", opts.category)
return api<Memory[]>(`/api/memory?${p}`)
},
select: (d) => (opts?.limit ? d.slice(0, opts.limit) : d),
})
/** Nach Mutationen die betroffenen Listen-Queries neu ziehen. */
export function invalidate(qc: QueryClient, ...keys: readonly unknown[][]) {
for (const key of keys) qc.invalidateQueries({ queryKey: key })
}
export { useQueryClient }
// Zentrale Daten-Hooks (TanStack Query). Kapseln lib/api.ts und liefern Caching,
// Dedup (gleicher queryKey = eine Anfrage über alle Views), Retry und Polling.
// Mutations invalidieren gezielt die betroffenen Keys, statt manuell neu zu laden.
import { useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query"
import {
api,
type AgentStatus,
type ConnectResp,
type ConnectHealth,
type DiscoverResp,
type DraftsResp,
type GroupsResp,
type HermesBrainResp,
type Health,
type Job,
type Memory,
type MemoryGraph,
type ModelsResp,
type RoutingResp,
type RoutingPolicyMeta,
type ServicesResp,
type SystemStatus,
type TokenStats,
type UpdatesResp,
type VoiceTraceResp,
} from "./api"
// Zentrale Query-Keys (eine Quelle der Wahrheit für invalidate).
export const qk = {
health: ["health"] as const,
systemStatus: ["system-status"] as const,
services: ["services"] as const,
models: ["models"] as const,
groups: ["groups"] as const,
routing: ["routing"] as const,
routingPolicy: ["routing-policy"] as const,
jobs: ["jobs"] as const,
tokenStats: ["token-stats"] as const,
agentStatus: ["agent-status"] as const,
hermesBrain: ["hermes-brain"] as const,
updates: ["updates"] as const,
discover: ["discover"] as const,
drafts: (target?: string) => ["drafts", target ?? ""] as const,
connect: (params?: string) => ["connect", params ?? ""] as const,
connectHealth: ["connect-health"] as const,
memory: (q?: string, category?: string) => ["memory", q ?? "", category ?? ""] as const,
memoryGraph: ["memory-graph"] as const,
voiceTrace: ["voice-trace"] as const,
}
// Per-Turn-Latenz-Trace (letzte N Voice/Lucy-Turns mit Stufen-Breakdown).
export const useVoiceTrace = (limit = 12, refetchInterval = 4_000) =>
useQuery({
queryKey: qk.voiceTrace,
queryFn: () => api<VoiceTraceResp>(`/api/voice/trace?limit=${limit}`),
refetchInterval,
select: (d) => d.turns ?? [],
})
export const useMemoryGraph = (enabled = true) =>
useQuery({
queryKey: qk.memoryGraph,
queryFn: () => api<MemoryGraph>("/api/memory/graph"),
enabled,
})
export const useHealth = () =>
useQuery({ queryKey: qk.health, queryFn: () => api<Health>("/api/health"), refetchInterval: 10_000 })
export const useSystemStatus = (refetchInterval = 5_000) =>
useQuery({ queryKey: qk.systemStatus, queryFn: () => api<SystemStatus>("/api/system/status"), refetchInterval })
export const useServices = (refetchInterval = 3_000) =>
useQuery({ queryKey: qk.services, queryFn: () => api<ServicesResp>("/api/system/services"), refetchInterval })
export const useModels = (refetchInterval = 4_000) =>
useQuery({ queryKey: qk.models, queryFn: () => api<ModelsResp>("/api/models"), refetchInterval })
export const useGroups = (refetchInterval = 8_000) =>
useQuery({ queryKey: qk.groups, queryFn: () => api<GroupsResp>("/api/groups"), refetchInterval })
export const useRouting = (refetchInterval = 4_000) =>
useQuery({ queryKey: qk.routing, queryFn: () => api<RoutingResp>("/api/routing"), refetchInterval })
export const useRoutingPolicy = () =>
useQuery({ queryKey: qk.routingPolicy, queryFn: () => api<RoutingPolicyMeta>("/api/routing/policy") })
export const useJobs = (refetchInterval = 2_000) =>
useQuery({
queryKey: qk.jobs,
queryFn: () => api<{ jobs: Job[] }>("/api/jobs"),
refetchInterval,
select: (d) => d.jobs ?? [],
})
export const useTokenStats = (refetchInterval = 3_000) =>
useQuery({ queryKey: qk.tokenStats, queryFn: () => api<TokenStats>("/api/system/token-stats"), refetchInterval })
export const useAgentStatus = (refetchInterval = 5_000) =>
useQuery({ queryKey: qk.agentStatus, queryFn: () => api<AgentStatus>("/api/agent/status"), refetchInterval })
// Agent-Hirn (Hermes) + bestes NousResearch-Update. Selten pollen (HF-Abfrage).
export const useHermesBrain = (refetchInterval = 60_000) =>
useQuery({ queryKey: qk.hermesBrain, queryFn: () => api<HermesBrainResp>("/api/agent/brain"), refetchInterval })
export const useUpdates = (refetchInterval?: number) =>
useQuery({ queryKey: qk.updates, queryFn: () => api<UpdatesResp>("/api/maintenance/updates"), refetchInterval })
export const useDiscover = () =>
useQuery({ queryKey: qk.discover, queryFn: () => api<DiscoverResp>("/api/discover") })
// Verfügbare Spec-Draft-Modelle + Vocab-Kompatibilität zum Ziel-Modell (GGUF-Pfad).
export const useDrafts = (target?: string) =>
useQuery({
queryKey: qk.drafts(target),
queryFn: () => api<DraftsResp>(`/api/models/drafts?target=${encodeURIComponent(target ?? "")}`),
enabled: !!target,
})
export const useConnect = (params?: string) =>
useQuery({
queryKey: qk.connect(params),
queryFn: () => api<ConnectResp>(params ? `/api/connect?${params}` : "/api/connect"),
})
// Live-Status der zwei Leitungen (Gateway + Gedächtnis) — alle 15s aktualisiert.
export const useConnectHealth = () =>
useQuery({
queryKey: qk.connectHealth,
queryFn: () => api<ConnectHealth>("/api/connect/health"),
refetchInterval: 15000,
})
export const useMemory = (opts?: { q?: string; category?: string; limit?: number }) =>
useQuery({
queryKey: qk.memory(opts?.q, opts?.category),
queryFn: () => {
const p = new URLSearchParams()
if (opts?.q) p.set("q", opts.q)
if (opts?.category) p.set("category", opts.category)
return api<Memory[]>(`/api/memory?${p}`)
},
select: (d) => (opts?.limit ? d.slice(0, opts.limit) : d),
})
/** Nach Mutationen die betroffenen Listen-Queries neu ziehen. */
export function invalidate(qc: QueryClient, ...keys: readonly unknown[][]) {
for (const key of keys) qc.invalidateQueries({ queryKey: key })
}
export { useQueryClient }
+59 -59
View File
@@ -1,59 +1,59 @@
// Ein Hook für Alert/Confirm/Prompt-Dialoge — ersetzt die zuvor in jeder View
// duplizierte showAlert/showConfirm-Logik + den lokalen Dialog-State.
//
// Nutzung:
// const { showAlert, showConfirm, showPrompt, dialogElement } = useDialog()
// ...
// showConfirm("Titel", "Wirklich?", () => doIt())
// return (<>{dialogElement}...</>)
import { useCallback, useState } from "react"
import { CustomDialog } from "@/components/CustomDialog"
interface DialogState {
type: "alert" | "confirm" | "prompt"
title: string
message: string
defaultValue?: string
autoValue?: string
autoLabel?: string
onConfirm: (val?: string) => void
onCancel?: () => void
}
export function useDialog() {
const [dialog, setDialog] = useState<DialogState | null>(null)
const close = useCallback(() => setDialog(null), [])
const showAlert = useCallback((title: string, message: string, onConfirm?: () => void) => {
setDialog({
type: "alert", title, message,
onConfirm: () => { setDialog(null); onConfirm?.() },
})
}, [])
const showConfirm = useCallback(
(title: string, message: string, onConfirm: () => void, onCancel?: () => void) => {
setDialog({
type: "confirm", title, message,
onConfirm: () => { setDialog(null); onConfirm() },
onCancel: () => { setDialog(null); onCancel?.() },
})
}, [])
const showPrompt = useCallback(
(title: string, message: string, defaultValue: string,
onConfirm: (val?: string) => void, onCancel?: () => void,
opts?: { autoValue?: string; autoLabel?: string }) => {
setDialog({
type: "prompt", title, message, defaultValue,
autoValue: opts?.autoValue, autoLabel: opts?.autoLabel,
onConfirm: (val) => { setDialog(null); onConfirm(val) },
onCancel: () => { setDialog(null); onCancel?.() },
})
}, [])
const dialogElement = dialog ? <CustomDialog {...dialog} /> : null
return { showAlert, showConfirm, showPrompt, close, dialogElement }
}
// Ein Hook für Alert/Confirm/Prompt-Dialoge — ersetzt die zuvor in jeder View
// duplizierte showAlert/showConfirm-Logik + den lokalen Dialog-State.
//
// Nutzung:
// const { showAlert, showConfirm, showPrompt, dialogElement } = useDialog()
// ...
// showConfirm("Titel", "Wirklich?", () => doIt())
// return (<>{dialogElement}...</>)
import { useCallback, useState } from "react"
import { CustomDialog } from "@/components/CustomDialog"
interface DialogState {
type: "alert" | "confirm" | "prompt"
title: string
message: string
defaultValue?: string
autoValue?: string
autoLabel?: string
onConfirm: (val?: string) => void
onCancel?: () => void
}
export function useDialog() {
const [dialog, setDialog] = useState<DialogState | null>(null)
const close = useCallback(() => setDialog(null), [])
const showAlert = useCallback((title: string, message: string, onConfirm?: () => void) => {
setDialog({
type: "alert", title, message,
onConfirm: () => { setDialog(null); onConfirm?.() },
})
}, [])
const showConfirm = useCallback(
(title: string, message: string, onConfirm: () => void, onCancel?: () => void) => {
setDialog({
type: "confirm", title, message,
onConfirm: () => { setDialog(null); onConfirm() },
onCancel: () => { setDialog(null); onCancel?.() },
})
}, [])
const showPrompt = useCallback(
(title: string, message: string, defaultValue: string,
onConfirm: (val?: string) => void, onCancel?: () => void,
opts?: { autoValue?: string; autoLabel?: string }) => {
setDialog({
type: "prompt", title, message, defaultValue,
autoValue: opts?.autoValue, autoLabel: opts?.autoLabel,
onConfirm: (val) => { setDialog(null); onConfirm(val) },
onCancel: () => { setDialog(null); onCancel?.() },
})
}, [])
const dialogElement = dialog ? <CustomDialog {...dialog} /> : null
return { showAlert, showConfirm, showPrompt, close, dialogElement }
}