This commit is contained in:
k
2026-09-24 11:02:24 +02:00
parent 8e64c0c963
commit 6e65d84a1c
17 changed files with 2141 additions and 191 deletions
+90 -5
View File
@@ -68,6 +68,74 @@ export interface OllamaHostModel {
modifiedAt: string | null;
}
export interface AdminSettings {
registrationEnabled: boolean;
accessRequestEnabled: boolean;
ocrEnabled: boolean;
reportsEnabled: boolean;
usernameChangeEnabled: boolean;
ollamaTimeoutS: number;
ollamaDefaultWeight: number;
recoveryLimitMax: number;
recoveryLimitWindowMin: number;
}
export interface AdminSettingsPatch {
registrationEnabled?: boolean;
accessRequestEnabled?: boolean;
ocrEnabled?: boolean;
reportsEnabled?: boolean;
usernameChangeEnabled?: boolean;
ollamaTimeoutS?: number;
ollamaDefaultWeight?: number;
recoveryLimitMax?: number;
recoveryLimitWindowMin?: number;
}
export interface OllamaNode {
id: string;
name: string;
host: string;
weight: number;
enabled: boolean;
created_at: number;
updated_at: number;
}
export interface EffectiveOllamaNode {
id: string;
name: string;
host: string;
weight: number;
}
export interface OllamaNodeStatus {
reachable: boolean;
host: string;
version: string | null;
latencyMs: number | null;
models: OllamaHostModel[];
running: { name: string; sizeVram: number; size: number; expiresAt: string | null }[];
error: string | null;
}
export interface OllamaStatsBucket {
t: number;
req: number;
err: number;
avgMs: number | null;
promptTok: number;
evalTok: number;
}
export interface OllamaNodeStats {
window: string;
from: number;
to: number;
totals: { req: number; err: number; avgMs: number | null; promptTok: number; evalTok: number; tokPerSec: number | null };
buckets: OllamaStatsBucket[];
}
export interface OllamaStatus {
enabled: boolean;
host: string;
@@ -188,9 +256,9 @@ export const api = {
adminPatch: (id: string, patch: { username?: string; displayName?: string; avatarUrl?: string; theme?: string; email?: string }) =>
req<{ user: User }>(`/api/admin/users/${id}`, { method: "PATCH", body: JSON.stringify(patch) }),
adminGetSettings: () => req<{ settings: { registrationEnabled: boolean; accessRequestEnabled: boolean; ocrEnabled: boolean; reportsEnabled: boolean; usernameChangeEnabled: boolean } }>("/api/admin/settings"),
adminPatchSettings: (patch: { registrationEnabled?: boolean; accessRequestEnabled?: boolean; ocrEnabled?: boolean; reportsEnabled?: boolean; usernameChangeEnabled?: boolean }) =>
req<{ settings: { registrationEnabled: boolean; accessRequestEnabled: boolean; ocrEnabled: boolean; reportsEnabled: boolean; usernameChangeEnabled: boolean } }>("/api/admin/settings", { method: "PATCH", body: JSON.stringify(patch) }),
adminGetSettings: () => req<{ settings: AdminSettings }>("/api/admin/settings"),
adminPatchSettings: (patch: AdminSettingsPatch) =>
req<{ settings: AdminSettings }>("/api/admin/settings", { method: "PATCH", body: JSON.stringify(patch) }),
/** Admin: SMTP credentials viewer (password included, blur it by default) + connectivity. */
adminMailStatus: () =>
@@ -281,11 +349,12 @@ export const api = {
ollamaPull: async (
name: string,
onProgress: (p: { status?: string; digest?: string; total?: number; completed?: number; error?: string }) => void,
opts: { nodeId?: string } = {},
): Promise<void> => {
const res = await fetch("/api/admin/ollama/pull", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${getToken()}` },
body: JSON.stringify({ name }),
body: JSON.stringify({ name, ...(opts.nodeId ? { nodeId: opts.nodeId } : {}) }),
});
if (res.status === 401) {
clearToken();
@@ -313,7 +382,23 @@ export const api = {
},
/** Admin: delete a local Ollama model (frees disk on the Ollama host). */
ollamaDelete: (name: string) => req<{ ok: boolean }>(`/api/admin/ollama/models/${encodeURIComponent(name)}`, { method: "DELETE" }),
ollamaDelete: (name: string, nodeId?: string) =>
req<{ ok: boolean }>(`/api/admin/ollama/models/${encodeURIComponent(name)}${nodeId ? `?node=${encodeURIComponent(nodeId)}` : ""}`, { method: "DELETE" }),
/** Admin: Ollama node pool (extra hosts with firewall-like weights). */
ollamaNodes: () => req<{ nodes: OllamaNode[]; effective: EffectiveOllamaNode[] }>("/api/admin/ollama/nodes"),
ollamaNodeCreate: (n: { name: string; host: string; weight: number; enabled: boolean }) =>
req<{ node: OllamaNode }>("/api/admin/ollama/nodes", { method: "POST", body: JSON.stringify(n) }),
ollamaNodePatch: (id: string, patch: { name?: string; host?: string; weight?: number; enabled?: boolean }) =>
req<{ node: OllamaNode }>(`/api/admin/ollama/nodes/${encodeURIComponent(id)}`, { method: "PATCH", body: JSON.stringify(patch) }),
ollamaNodeDelete: (id: string) =>
req<{ ok: boolean }>(`/api/admin/ollama/nodes/${encodeURIComponent(id)}`, { method: "DELETE" }),
ollamaNodeStatus: (id: string) =>
req<OllamaNodeStatus>(`/api/admin/ollama/nodes/${encodeURIComponent(id)}/status`),
ollamaNodeProbe: (host: string) =>
req<OllamaNodeStatus>(`/api/admin/ollama/probe?host=${encodeURIComponent(host)}`),
ollamaNodeStats: (id: string, window: string) =>
req<{ stats: OllamaNodeStats }>(`/api/admin/ollama/nodes/${encodeURIComponent(id)}/stats?window=${encodeURIComponent(window)}`),
/** Admin: Ollama connectivity probe (reachability, version, on-disk models). */
ollamaStatus: () => req<OllamaStatus>("/api/admin/ollama/status"),
+360
View File
@@ -491,6 +491,66 @@ const en = {
"live.newReport": "New content report ({reason})",
"live.adminReply": "Admin replied to your ticket",
"live.ticketStatus": "Ticket {status}",
"nodes.title": "Ollama nodes",
"nodes.desc": "Extra Ollama hosts. Each generation picks a node by weight (firewall-like priority, heaviest first) and fails over to the next one on error. The default node comes from OLLAMA_HOST.",
"nodes.add": "Add node",
"nodes.edit": "Edit node",
"nodes.name": "Name",
"nodes.namePh": "gpu-box-2",
"nodes.host": "Host",
"nodes.hostPh": "http://10.0.0.5:11434",
"nodes.hostHint": "http(s) base URL, no path. Must be reachable from this server.",
"nodes.weight": "Weight",
"nodes.weightHint": "0 disables traffic, higher gets more generations.",
"nodes.enabled": "Enabled",
"nodes.share": "{pct}%",
"nodes.off": "off",
"nodes.test": "Test",
"nodes.probing": "Probing…",
"nodes.testOk": "Reachable",
"nodes.testFail": "Unreachable",
"nodes.deleteTitle": "Delete this node?",
"nodes.deleteMsg": "Generations will no longer be routed to this host.",
"nodes.invalid": "Check the name, host and weight.",
"nodes.dupHost": "This host is already registered.",
"nodes.detail": "Node details",
"nodes.running": "Running now",
"nodes.vram": "VRAM",
"nodes.diskModels": "On disk ({n})",
"nodes.version": "Version",
"nodes.latency": "Latency",
"nodes.timeout": "Generation timeout",
"nodes.timeoutDesc": "Max time for one Ollama generation before failing over. Applies to every node.",
"nodes.timeoutUnit": "seconds (5–3600)",
"nodes.defWeight": "Default node weight",
"nodes.defWeightDesc": "Traffic share of the OLLAMA_HOST node. 0 excludes it when custom nodes exist.",
"nodes.stats": "Load",
"nodes.req": "requests",
"nodes.err": "errors",
"nodes.avg": "avg latency",
"nodes.tps": "tokens/s",
"nodes.tok": "tokens",
"nodes.emptyStats": "No generations in this window yet.",
"nodes.cpuNote": "Ollama exposes no CPU/RAM API — charts come from app-side generation telemetry; per-model VRAM via /api/ps.",
"catalog.title": "Model catalog (Hugging Face)",
"catalog.desc": "GGUF models installable with one click via ollama pull hf.co/…. Needs Ollama with Hugging Face support and internet on the Ollama host.",
"catalog.searchPh": "Search GGUF models…",
"catalog.pull": "Pull",
"catalog.pulling": "Pulling {name}…",
"catalog.node": "Target node",
"catalog.downloads": "downloads",
"catalog.files": "Quantizations",
"catalog.noRes": "No results.",
"catalog.hint": "Pick a quantization, it pulls straight from Hugging Face.",
"rec.title": "Password recovery limits",
"rec.desc": "Dedicated envelope for key-recovery requests (separate from login).",
"rec.max": "Max attempts",
"rec.maxHint": "per window, per IP",
"rec.window": "Window",
"rec.windowHint": "minutes",
"rec.save": "Save limits",
"rec.saved": "Limits saved",
"rec.invalid": "Check the values (attempts 1–100, window 1–1440 min).",
"msg.copy": "Copy message",
"msg.search": "Search the web",
@@ -1068,6 +1128,66 @@ const es: Dict = {
"live.newReport": "New content report ({reason})",
"live.adminReply": "Admin replied to your ticket",
"live.ticketStatus": "Ticket {status}",
"nodes.title": "Ollama nodes",
"nodes.desc": "Extra Ollama hosts. Each generation picks a node by weight (firewall-like priority, heaviest first) and fails over to the next one on error. The default node comes from OLLAMA_HOST.",
"nodes.add": "Add node",
"nodes.edit": "Edit node",
"nodes.name": "Name",
"nodes.namePh": "gpu-box-2",
"nodes.host": "Host",
"nodes.hostPh": "http://10.0.0.5:11434",
"nodes.hostHint": "http(s) base URL, no path. Must be reachable from this server.",
"nodes.weight": "Weight",
"nodes.weightHint": "0 disables traffic, higher gets more generations.",
"nodes.enabled": "Enabled",
"nodes.share": "{pct}%",
"nodes.off": "off",
"nodes.test": "Test",
"nodes.probing": "Probing…",
"nodes.testOk": "Reachable",
"nodes.testFail": "Unreachable",
"nodes.deleteTitle": "Delete this node?",
"nodes.deleteMsg": "Generations will no longer be routed to this host.",
"nodes.invalid": "Check the name, host and weight.",
"nodes.dupHost": "This host is already registered.",
"nodes.detail": "Node details",
"nodes.running": "Running now",
"nodes.vram": "VRAM",
"nodes.diskModels": "On disk ({n})",
"nodes.version": "Version",
"nodes.latency": "Latency",
"nodes.timeout": "Generation timeout",
"nodes.timeoutDesc": "Max time for one Ollama generation before failing over. Applies to every node.",
"nodes.timeoutUnit": "seconds (5–3600)",
"nodes.defWeight": "Default node weight",
"nodes.defWeightDesc": "Traffic share of the OLLAMA_HOST node. 0 excludes it when custom nodes exist.",
"nodes.stats": "Load",
"nodes.req": "requests",
"nodes.err": "errors",
"nodes.avg": "avg latency",
"nodes.tps": "tokens/s",
"nodes.tok": "tokens",
"nodes.emptyStats": "No generations in this window yet.",
"nodes.cpuNote": "Ollama exposes no CPU/RAM API — charts come from app-side generation telemetry; per-model VRAM via /api/ps.",
"catalog.title": "Model catalog (Hugging Face)",
"catalog.desc": "GGUF models installable with one click via ollama pull hf.co/…. Needs Ollama with Hugging Face support and internet on the Ollama host.",
"catalog.searchPh": "Search GGUF models…",
"catalog.pull": "Pull",
"catalog.pulling": "Pulling {name}…",
"catalog.node": "Target node",
"catalog.downloads": "downloads",
"catalog.files": "Quantizations",
"catalog.noRes": "No results.",
"catalog.hint": "Pick a quantization, it pulls straight from Hugging Face.",
"rec.title": "Password recovery limits",
"rec.desc": "Dedicated envelope for key-recovery requests (separate from login).",
"rec.max": "Max attempts",
"rec.maxHint": "per window, per IP",
"rec.window": "Window",
"rec.windowHint": "minutes",
"rec.save": "Save limits",
"rec.saved": "Limits saved",
"rec.invalid": "Check the values (attempts 1–100, window 1–1440 min).",
"msg.copy": "Copiar mensaje",
"msg.search": "Buscar en la web",
@@ -1640,6 +1760,66 @@ const fr: Dict = {
"live.newReport": "New content report ({reason})",
"live.adminReply": "Admin replied to your ticket",
"live.ticketStatus": "Ticket {status}",
"nodes.title": "Ollama nodes",
"nodes.desc": "Extra Ollama hosts. Each generation picks a node by weight (firewall-like priority, heaviest first) and fails over to the next one on error. The default node comes from OLLAMA_HOST.",
"nodes.add": "Add node",
"nodes.edit": "Edit node",
"nodes.name": "Name",
"nodes.namePh": "gpu-box-2",
"nodes.host": "Host",
"nodes.hostPh": "http://10.0.0.5:11434",
"nodes.hostHint": "http(s) base URL, no path. Must be reachable from this server.",
"nodes.weight": "Weight",
"nodes.weightHint": "0 disables traffic, higher gets more generations.",
"nodes.enabled": "Enabled",
"nodes.share": "{pct}%",
"nodes.off": "off",
"nodes.test": "Test",
"nodes.probing": "Probing…",
"nodes.testOk": "Reachable",
"nodes.testFail": "Unreachable",
"nodes.deleteTitle": "Delete this node?",
"nodes.deleteMsg": "Generations will no longer be routed to this host.",
"nodes.invalid": "Check the name, host and weight.",
"nodes.dupHost": "This host is already registered.",
"nodes.detail": "Node details",
"nodes.running": "Running now",
"nodes.vram": "VRAM",
"nodes.diskModels": "On disk ({n})",
"nodes.version": "Version",
"nodes.latency": "Latency",
"nodes.timeout": "Generation timeout",
"nodes.timeoutDesc": "Max time for one Ollama generation before failing over. Applies to every node.",
"nodes.timeoutUnit": "seconds (5–3600)",
"nodes.defWeight": "Default node weight",
"nodes.defWeightDesc": "Traffic share of the OLLAMA_HOST node. 0 excludes it when custom nodes exist.",
"nodes.stats": "Load",
"nodes.req": "requests",
"nodes.err": "errors",
"nodes.avg": "avg latency",
"nodes.tps": "tokens/s",
"nodes.tok": "tokens",
"nodes.emptyStats": "No generations in this window yet.",
"nodes.cpuNote": "Ollama exposes no CPU/RAM API — charts come from app-side generation telemetry; per-model VRAM via /api/ps.",
"catalog.title": "Model catalog (Hugging Face)",
"catalog.desc": "GGUF models installable with one click via ollama pull hf.co/…. Needs Ollama with Hugging Face support and internet on the Ollama host.",
"catalog.searchPh": "Search GGUF models…",
"catalog.pull": "Pull",
"catalog.pulling": "Pulling {name}…",
"catalog.node": "Target node",
"catalog.downloads": "downloads",
"catalog.files": "Quantizations",
"catalog.noRes": "No results.",
"catalog.hint": "Pick a quantization, it pulls straight from Hugging Face.",
"rec.title": "Password recovery limits",
"rec.desc": "Dedicated envelope for key-recovery requests (separate from login).",
"rec.max": "Max attempts",
"rec.maxHint": "per window, per IP",
"rec.window": "Window",
"rec.windowHint": "minutes",
"rec.save": "Save limits",
"rec.saved": "Limits saved",
"rec.invalid": "Check the values (attempts 1–100, window 1–1440 min).",
"msg.copy": "Copier le message",
"msg.search": "Rechercher sur le web",
@@ -2200,6 +2380,66 @@ const ru: Dict = {
"live.newReport": "New content report ({reason})",
"live.adminReply": "Admin replied to your ticket",
"live.ticketStatus": "Ticket {status}",
"nodes.title": "Ollama nodes",
"nodes.desc": "Extra Ollama hosts. Each generation picks a node by weight (firewall-like priority, heaviest first) and fails over to the next one on error. The default node comes from OLLAMA_HOST.",
"nodes.add": "Add node",
"nodes.edit": "Edit node",
"nodes.name": "Name",
"nodes.namePh": "gpu-box-2",
"nodes.host": "Host",
"nodes.hostPh": "http://10.0.0.5:11434",
"nodes.hostHint": "http(s) base URL, no path. Must be reachable from this server.",
"nodes.weight": "Weight",
"nodes.weightHint": "0 disables traffic, higher gets more generations.",
"nodes.enabled": "Enabled",
"nodes.share": "{pct}%",
"nodes.off": "off",
"nodes.test": "Test",
"nodes.probing": "Probing…",
"nodes.testOk": "Reachable",
"nodes.testFail": "Unreachable",
"nodes.deleteTitle": "Delete this node?",
"nodes.deleteMsg": "Generations will no longer be routed to this host.",
"nodes.invalid": "Check the name, host and weight.",
"nodes.dupHost": "This host is already registered.",
"nodes.detail": "Node details",
"nodes.running": "Running now",
"nodes.vram": "VRAM",
"nodes.diskModels": "On disk ({n})",
"nodes.version": "Version",
"nodes.latency": "Latency",
"nodes.timeout": "Generation timeout",
"nodes.timeoutDesc": "Max time for one Ollama generation before failing over. Applies to every node.",
"nodes.timeoutUnit": "seconds (5–3600)",
"nodes.defWeight": "Default node weight",
"nodes.defWeightDesc": "Traffic share of the OLLAMA_HOST node. 0 excludes it when custom nodes exist.",
"nodes.stats": "Load",
"nodes.req": "requests",
"nodes.err": "errors",
"nodes.avg": "avg latency",
"nodes.tps": "tokens/s",
"nodes.tok": "tokens",
"nodes.emptyStats": "No generations in this window yet.",
"nodes.cpuNote": "Ollama exposes no CPU/RAM API — charts come from app-side generation telemetry; per-model VRAM via /api/ps.",
"catalog.title": "Model catalog (Hugging Face)",
"catalog.desc": "GGUF models installable with one click via ollama pull hf.co/…. Needs Ollama with Hugging Face support and internet on the Ollama host.",
"catalog.searchPh": "Search GGUF models…",
"catalog.pull": "Pull",
"catalog.pulling": "Pulling {name}…",
"catalog.node": "Target node",
"catalog.downloads": "downloads",
"catalog.files": "Quantizations",
"catalog.noRes": "No results.",
"catalog.hint": "Pick a quantization, it pulls straight from Hugging Face.",
"rec.title": "Password recovery limits",
"rec.desc": "Dedicated envelope for key-recovery requests (separate from login).",
"rec.max": "Max attempts",
"rec.maxHint": "per window, per IP",
"rec.window": "Window",
"rec.windowHint": "minutes",
"rec.save": "Save limits",
"rec.saved": "Limits saved",
"rec.invalid": "Check the values (attempts 1–100, window 1–1440 min).",
"msg.copy": "Скопировать сообщение",
"msg.search": "Искать в интернете",
@@ -2765,6 +3005,66 @@ const it: Dict = {
"live.newReport": "New content report ({reason})",
"live.adminReply": "Admin replied to your ticket",
"live.ticketStatus": "Ticket {status}",
"nodes.title": "Ollama nodes",
"nodes.desc": "Extra Ollama hosts. Each generation picks a node by weight (firewall-like priority, heaviest first) and fails over to the next one on error. The default node comes from OLLAMA_HOST.",
"nodes.add": "Add node",
"nodes.edit": "Edit node",
"nodes.name": "Name",
"nodes.namePh": "gpu-box-2",
"nodes.host": "Host",
"nodes.hostPh": "http://10.0.0.5:11434",
"nodes.hostHint": "http(s) base URL, no path. Must be reachable from this server.",
"nodes.weight": "Weight",
"nodes.weightHint": "0 disables traffic, higher gets more generations.",
"nodes.enabled": "Enabled",
"nodes.share": "{pct}%",
"nodes.off": "off",
"nodes.test": "Test",
"nodes.probing": "Probing…",
"nodes.testOk": "Reachable",
"nodes.testFail": "Unreachable",
"nodes.deleteTitle": "Delete this node?",
"nodes.deleteMsg": "Generations will no longer be routed to this host.",
"nodes.invalid": "Check the name, host and weight.",
"nodes.dupHost": "This host is already registered.",
"nodes.detail": "Node details",
"nodes.running": "Running now",
"nodes.vram": "VRAM",
"nodes.diskModels": "On disk ({n})",
"nodes.version": "Version",
"nodes.latency": "Latency",
"nodes.timeout": "Generation timeout",
"nodes.timeoutDesc": "Max time for one Ollama generation before failing over. Applies to every node.",
"nodes.timeoutUnit": "seconds (5–3600)",
"nodes.defWeight": "Default node weight",
"nodes.defWeightDesc": "Traffic share of the OLLAMA_HOST node. 0 excludes it when custom nodes exist.",
"nodes.stats": "Load",
"nodes.req": "requests",
"nodes.err": "errors",
"nodes.avg": "avg latency",
"nodes.tps": "tokens/s",
"nodes.tok": "tokens",
"nodes.emptyStats": "No generations in this window yet.",
"nodes.cpuNote": "Ollama exposes no CPU/RAM API — charts come from app-side generation telemetry; per-model VRAM via /api/ps.",
"catalog.title": "Model catalog (Hugging Face)",
"catalog.desc": "GGUF models installable with one click via ollama pull hf.co/…. Needs Ollama with Hugging Face support and internet on the Ollama host.",
"catalog.searchPh": "Search GGUF models…",
"catalog.pull": "Pull",
"catalog.pulling": "Pulling {name}…",
"catalog.node": "Target node",
"catalog.downloads": "downloads",
"catalog.files": "Quantizations",
"catalog.noRes": "No results.",
"catalog.hint": "Pick a quantization, it pulls straight from Hugging Face.",
"rec.title": "Password recovery limits",
"rec.desc": "Dedicated envelope for key-recovery requests (separate from login).",
"rec.max": "Max attempts",
"rec.maxHint": "per window, per IP",
"rec.window": "Window",
"rec.windowHint": "minutes",
"rec.save": "Save limits",
"rec.saved": "Limits saved",
"rec.invalid": "Check the values (attempts 1–100, window 1–1440 min).",
"msg.copy": "Copia messaggio",
"msg.search": "Cerca nel web",
@@ -3321,6 +3621,66 @@ const pt: Dict = {
"live.newReport": "New content report ({reason})",
"live.adminReply": "Admin replied to your ticket",
"live.ticketStatus": "Ticket {status}",
"nodes.title": "Ollama nodes",
"nodes.desc": "Extra Ollama hosts. Each generation picks a node by weight (firewall-like priority, heaviest first) and fails over to the next one on error. The default node comes from OLLAMA_HOST.",
"nodes.add": "Add node",
"nodes.edit": "Edit node",
"nodes.name": "Name",
"nodes.namePh": "gpu-box-2",
"nodes.host": "Host",
"nodes.hostPh": "http://10.0.0.5:11434",
"nodes.hostHint": "http(s) base URL, no path. Must be reachable from this server.",
"nodes.weight": "Weight",
"nodes.weightHint": "0 disables traffic, higher gets more generations.",
"nodes.enabled": "Enabled",
"nodes.share": "{pct}%",
"nodes.off": "off",
"nodes.test": "Test",
"nodes.probing": "Probing…",
"nodes.testOk": "Reachable",
"nodes.testFail": "Unreachable",
"nodes.deleteTitle": "Delete this node?",
"nodes.deleteMsg": "Generations will no longer be routed to this host.",
"nodes.invalid": "Check the name, host and weight.",
"nodes.dupHost": "This host is already registered.",
"nodes.detail": "Node details",
"nodes.running": "Running now",
"nodes.vram": "VRAM",
"nodes.diskModels": "On disk ({n})",
"nodes.version": "Version",
"nodes.latency": "Latency",
"nodes.timeout": "Generation timeout",
"nodes.timeoutDesc": "Max time for one Ollama generation before failing over. Applies to every node.",
"nodes.timeoutUnit": "seconds (5–3600)",
"nodes.defWeight": "Default node weight",
"nodes.defWeightDesc": "Traffic share of the OLLAMA_HOST node. 0 excludes it when custom nodes exist.",
"nodes.stats": "Load",
"nodes.req": "requests",
"nodes.err": "errors",
"nodes.avg": "avg latency",
"nodes.tps": "tokens/s",
"nodes.tok": "tokens",
"nodes.emptyStats": "No generations in this window yet.",
"nodes.cpuNote": "Ollama exposes no CPU/RAM API — charts come from app-side generation telemetry; per-model VRAM via /api/ps.",
"catalog.title": "Model catalog (Hugging Face)",
"catalog.desc": "GGUF models installable with one click via ollama pull hf.co/…. Needs Ollama with Hugging Face support and internet on the Ollama host.",
"catalog.searchPh": "Search GGUF models…",
"catalog.pull": "Pull",
"catalog.pulling": "Pulling {name}…",
"catalog.node": "Target node",
"catalog.downloads": "downloads",
"catalog.files": "Quantizations",
"catalog.noRes": "No results.",
"catalog.hint": "Pick a quantization, it pulls straight from Hugging Face.",
"rec.title": "Password recovery limits",
"rec.desc": "Dedicated envelope for key-recovery requests (separate from login).",
"rec.max": "Max attempts",
"rec.maxHint": "per window, per IP",
"rec.window": "Window",
"rec.windowHint": "minutes",
"rec.save": "Save limits",
"rec.saved": "Limits saved",
"rec.invalid": "Check the values (attempts 1–100, window 1–1440 min).",
"msg.copy": "Copiar mensagem",
"msg.search": "Pesquisar na web",
+8
View File
@@ -23,6 +23,14 @@ export function chatIdFromPath(pathname: string): string | null {
return m ? decodeURIComponent(m[1]!) : null;
}
/** Admin section from /admin itself (null) or /admin/<tab>. */
export function adminTabFromPath(pathname: string): string | null {
const p = normalizePath(pathname);
if (p === "/admin") return null;
const m = p.match(/^\/admin\/([^/]+)$/);
return m ? decodeURIComponent(m[1]!) : null;
}
/** Client-side navigation without reload (same-origin path only). */
export function navigate(to: string, replace = false): void {
const dest = normalizePath(to);