:)
This commit is contained in:
@@ -63,6 +63,7 @@ from the **Accounts** panel (each gets a one-time access key to hand over).
|
|||||||
| GET | `/api/conversations/search?q=` | Bearer | search titles, topics + old prompts (with snippet) |
|
| GET | `/api/conversations/search?q=` | Bearer | search titles, topics + old prompts (with snippet) |
|
||||||
| POST | `/api/admin/ollama/pull` | admin | pull a model from the Ollama library (NDJSON progress) |
|
| POST | `/api/admin/ollama/pull` | admin | pull a model from the Ollama library (NDJSON progress) |
|
||||||
| DELETE | `/api/admin/ollama/models/:name` | admin | remove a local Ollama model |
|
| DELETE | `/api/admin/ollama/models/:name` | admin | remove a local Ollama model |
|
||||||
|
| GET | `/api/admin/ollama/status` | admin | Ollama connectivity probe: reachability, version, on-disk models |
|
||||||
| POST | `/api/access/request` | public | reserve username+email with a motivation → ticket (`/review/:id`) |
|
| POST | `/api/access/request` | public | reserve username+email with a motivation → ticket (`/review/:id`) |
|
||||||
| GET/POST | `/api/access/ticket/:id` (+`/message`) | ticket bearer | follow + reply on an access request |
|
| GET/POST | `/api/access/ticket/:id` (+`/message`) | ticket bearer | follow + reply on an access request |
|
||||||
| WS | `/api/access/ws/:id` | ticket bearer | live ticket messages + status (ping/pong heartbeat) |
|
| WS | `/api/access/ws/:id` | ticket bearer | live ticket messages + status (ping/pong heartbeat) |
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ an **Enabled** switch and **hourly / daily** per-user request caps
|
|||||||
toward limits even when generation later fails; over-limit answers 429.
|
toward limits even when generation later fails; over-limit answers 429.
|
||||||
- Routes: `GET /api/admin/model-policy[?refresh=1]`,
|
- Routes: `GET /api/admin/model-policy[?refresh=1]`,
|
||||||
`PATCH /api/admin/model-policy` (`{model, enabled?, hourly?, daily?}`).
|
`PATCH /api/admin/model-policy` (`{model, enabled?, hourly?, daily?}`).
|
||||||
|
Ollama library: `POST /api/admin/ollama/pull` (NDJSON progress),
|
||||||
|
`DELETE /api/admin/ollama/models/:name`, and the connectivity probe
|
||||||
|
`GET /api/admin/ollama/status` (reachability, version, on-disk models —
|
||||||
|
surfaced in Admin Center → Models).
|
||||||
|
|
||||||
## Personal providers (BYOK)
|
## Personal providers (BYOK)
|
||||||
|
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export function AdminCenter() {
|
|||||||
<span
|
<span
|
||||||
role="status"
|
role="status"
|
||||||
aria-label={`${t.label}: ${openAccessCount}`}
|
aria-label={`${t.label}: ${openAccessCount}`}
|
||||||
className="absolute -right-0.5 -top-1.5 grid min-h-5 min-w-5 place-items-center rounded-full bg-red-500 px-1 text-[11px] font-bold leading-5 text-white shadow"
|
className="absolute -right-0.5 -top-0.5 z-10 grid min-h-4 min-w-4 place-items-center rounded-full bg-red-500 px-1 text-[10px] font-bold leading-4 text-white shadow"
|
||||||
>
|
>
|
||||||
{openAccessCount > 99 ? "99+" : openAccessCount}
|
{openAccessCount > 99 ? "99+" : openAccessCount}
|
||||||
</span>
|
</span>
|
||||||
@@ -171,7 +171,7 @@ export function AdminCenter() {
|
|||||||
<span
|
<span
|
||||||
role="status"
|
role="status"
|
||||||
aria-label={`${t.label}: ${openReportCount}`}
|
aria-label={`${t.label}: ${openReportCount}`}
|
||||||
className="absolute -right-0.5 -top-1.5 grid min-h-5 min-w-5 place-items-center rounded-full bg-red-500 px-1 text-[11px] font-bold leading-5 text-white shadow"
|
className="absolute -right-0.5 -top-0.5 z-10 grid min-h-4 min-w-4 place-items-center rounded-full bg-red-500 px-1 text-[10px] font-bold leading-4 text-white shadow"
|
||||||
>
|
>
|
||||||
{openReportCount > 99 ? "99+" : openReportCount}
|
{openReportCount > 99 ? "99+" : openReportCount}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { Bot, Undo2 } from "lucide-react";
|
import { Activity, Bot, Undo2 } from "lucide-react";
|
||||||
import { api, type PolicyModel } from "../lib/api.ts";
|
import { api, type OllamaStatus, type PolicyModel } from "../lib/api.ts";
|
||||||
import { OllamaModelsModal } from "./AdminPanel.tsx";
|
import { OllamaModelsModal } from "./AdminPanel.tsx";
|
||||||
import { Button, Input, Spinner, Switch } from "./ui.tsx";
|
import { Button, Input, Spinner, Switch } from "./ui.tsx";
|
||||||
import { useT } from "../lib/i18n.ts";
|
import { useT } from "../lib/i18n.ts";
|
||||||
@@ -48,6 +48,8 @@ export function ModelsPanel() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
|
<OllamaConnectivity />
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div className="mb-2 flex items-center gap-2">
|
<div className="mb-2 flex items-center gap-2">
|
||||||
<p className="text-sm font-medium">{t("models.title")}</p>
|
<p className="text-sm font-medium">{t("models.title")}</p>
|
||||||
@@ -105,6 +107,140 @@ export function ModelsPanel() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Ollama connectivity probe (admin) ---------- */
|
||||||
|
|
||||||
|
function fmtSize(bytes: number): string {
|
||||||
|
if (!bytes || bytes < 0) return "—";
|
||||||
|
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||||
|
let v = bytes;
|
||||||
|
let i = 0;
|
||||||
|
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
|
||||||
|
return `${v >= 100 ? Math.round(v) : v.toFixed(1)} ${units[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Live Ollama host status: reachability, version, latency, on-disk models. */
|
||||||
|
function OllamaConnectivity() {
|
||||||
|
const [status, setStatus] = useState<OllamaStatus | null>(null);
|
||||||
|
const [checking, setChecking] = useState(true);
|
||||||
|
const { t } = useT();
|
||||||
|
|
||||||
|
const check = useCallback(async () => {
|
||||||
|
setChecking(true);
|
||||||
|
try {
|
||||||
|
setStatus(await api.ollamaStatus());
|
||||||
|
} catch {
|
||||||
|
setStatus(null);
|
||||||
|
} finally {
|
||||||
|
setChecking(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void check();
|
||||||
|
}, [check]);
|
||||||
|
|
||||||
|
const state = !status ? "unknown" : !status.enabled ? "disabled" : status.reachable ? "online" : "offline";
|
||||||
|
const dot =
|
||||||
|
state === "online"
|
||||||
|
? "bg-emerald-500"
|
||||||
|
: state === "offline"
|
||||||
|
? "bg-red-500"
|
||||||
|
: "bg-stone-400 dark:bg-zinc-500";
|
||||||
|
const stateLabel =
|
||||||
|
state === "online"
|
||||||
|
? t("models.connOnline")
|
||||||
|
: state === "offline"
|
||||||
|
? t("models.connOffline")
|
||||||
|
: state === "disabled"
|
||||||
|
? t("models.connDisabledState")
|
||||||
|
: t("common.loading");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="rounded-3xl border border-stone-200/70 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||||
|
<div className="mb-1 flex items-center gap-2">
|
||||||
|
<p className="flex items-center gap-1.5 text-sm font-medium">
|
||||||
|
<Activity size={14} className="opacity-60" /> {t("models.connTitle")}
|
||||||
|
</p>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium",
|
||||||
|
state === "online" && "bg-emerald-600/10 text-emerald-700 dark:text-emerald-400",
|
||||||
|
state === "offline" && "bg-red-500/10 text-red-600 dark:text-red-400",
|
||||||
|
(state === "disabled" || state === "unknown") && "bg-stone-200/70 dark:bg-zinc-800",
|
||||||
|
)}
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
<span className={cn("h-2 w-2 rounded-full", dot, checking && "animate-pulse")} />
|
||||||
|
{checking && !status ? t("common.loading") : stateLabel}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => void check()}
|
||||||
|
disabled={checking}
|
||||||
|
className="ml-auto rounded-full p-2 opacity-60 transition hover:bg-stone-200/60 hover:opacity-100 disabled:opacity-30 dark:hover:bg-zinc-800"
|
||||||
|
title={t("models.connCheck")}
|
||||||
|
aria-label={t("models.connCheckAria")}
|
||||||
|
>
|
||||||
|
<Undo2 size={15} className={checking ? "animate-spin" : undefined} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="mb-3 text-sm opacity-60">{t("models.connDesc")}</p>
|
||||||
|
|
||||||
|
{status && (
|
||||||
|
<dl className="mb-3 grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||||
|
<div className="rounded-2xl bg-stone-100/70 px-3 py-2 dark:bg-zinc-800/60">
|
||||||
|
<dt className="text-[11px] uppercase tracking-wider opacity-50">{t("models.connHost")}</dt>
|
||||||
|
<dd className="truncate font-mono text-sm" title={status.host}>{status.host}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl bg-stone-100/70 px-3 py-2 dark:bg-zinc-800/60">
|
||||||
|
<dt className="text-[11px] uppercase tracking-wider opacity-50">{t("models.connVersion")}</dt>
|
||||||
|
<dd className="truncate font-mono text-sm">{status.version ?? "—"}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl bg-stone-100/70 px-3 py-2 dark:bg-zinc-800/60">
|
||||||
|
<dt className="text-[11px] uppercase tracking-wider opacity-50">{t("models.connLatency")}</dt>
|
||||||
|
<dd className="truncate font-mono text-sm">{status.latencyMs === null ? "—" : `${status.latencyMs} ms`}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl bg-stone-100/70 px-3 py-2 dark:bg-zinc-800/60">
|
||||||
|
<dt className="text-[11px] uppercase tracking-wider opacity-50">{t("models.connModels")}</dt>
|
||||||
|
<dd className="truncate font-mono text-sm">{status.models.length}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!checking && !status && (
|
||||||
|
<p className="mb-3 rounded-2xl bg-red-500/10 px-4 py-2.5 text-sm text-red-600 dark:text-red-400" role="alert">
|
||||||
|
{t("admin.loadFailed")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{status && status.error && (
|
||||||
|
<p className="mb-3 rounded-2xl bg-red-500/10 px-4 py-2.5 text-sm text-red-600 dark:text-red-400" role="alert">
|
||||||
|
<span className="block font-mono text-[13px]">{status.error}</span>
|
||||||
|
<span className="mt-0.5 block opacity-80">{t("models.connHint")}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status && status.reachable && (
|
||||||
|
status.models.length === 0 ? (
|
||||||
|
<p className="rounded-2xl border border-dashed border-stone-300 px-4 py-4 text-center text-sm opacity-50 dark:border-zinc-700">
|
||||||
|
{t("models.connEmpty")}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<ul className="max-h-64 space-y-1.5 overflow-y-auto">
|
||||||
|
{status.models.map((m) => (
|
||||||
|
<li
|
||||||
|
key={m.name}
|
||||||
|
className="flex items-center gap-3 rounded-2xl border border-stone-200/70 px-3.5 py-2 dark:border-zinc-800"
|
||||||
|
>
|
||||||
|
<span className="min-w-0 flex-1 truncate font-mono text-sm">{m.name}</span>
|
||||||
|
<span className="shrink-0 text-xs opacity-60">{fmtSize(m.size)}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ModelRow({ model, onChanged, onError }: {
|
function ModelRow({ model, onChanged, onError }: {
|
||||||
model: PolicyModel;
|
model: PolicyModel;
|
||||||
onChanged: (m: PolicyModel) => void;
|
onChanged: (m: PolicyModel) => void;
|
||||||
|
|||||||
@@ -62,6 +62,22 @@ export interface PolicyModel extends DriverModel {
|
|||||||
daily: number;
|
daily: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OllamaHostModel {
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OllamaStatus {
|
||||||
|
enabled: boolean;
|
||||||
|
host: string;
|
||||||
|
reachable: boolean;
|
||||||
|
version: string | null;
|
||||||
|
latencyMs: number | null;
|
||||||
|
models: OllamaHostModel[];
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
const TOKEN_KEY = "kisassistant_token";
|
const TOKEN_KEY = "kisassistant_token";
|
||||||
|
|
||||||
export function getToken(): string | null {
|
export function getToken(): string | null {
|
||||||
@@ -299,6 +315,9 @@ export const api = {
|
|||||||
/** Admin: delete a local Ollama model (frees disk on the Ollama host). */
|
/** 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) => req<{ ok: boolean }>(`/api/admin/ollama/models/${encodeURIComponent(name)}`, { method: "DELETE" }),
|
||||||
|
|
||||||
|
/** Admin: Ollama connectivity probe (reachability, version, on-disk models). */
|
||||||
|
ollamaStatus: () => req<OllamaStatus>("/api/admin/ollama/status"),
|
||||||
|
|
||||||
/** Personal LLM providers (BYOK): presence + last4 only, keys never leave the server. */
|
/** Personal LLM providers (BYOK): presence + last4 only, keys never leave the server. */
|
||||||
providers: () => req<{ providers: UserProvider[] }>("/api/me/providers"),
|
providers: () => req<{ providers: UserProvider[] }>("/api/me/providers"),
|
||||||
setProvider: (provider: string, apiKey: string) =>
|
setProvider: (provider: string, apiKey: string) =>
|
||||||
|
|||||||
@@ -313,6 +313,19 @@ const en = {
|
|||||||
"models.ollamaDesc": "Pull or remove local models.",
|
"models.ollamaDesc": "Pull or remove local models.",
|
||||||
"models.empty": "No models available.",
|
"models.empty": "No models available.",
|
||||||
"models.saveFailed": "Could not save",
|
"models.saveFailed": "Could not save",
|
||||||
|
"models.connTitle": "Ollama connectivity",
|
||||||
|
"models.connDesc": "Live status of the Ollama host: reachability, version, latency and installed models.",
|
||||||
|
"models.connOnline": "Online",
|
||||||
|
"models.connOffline": "Offline",
|
||||||
|
"models.connDisabledState": "Disabled",
|
||||||
|
"models.connCheck": "Check now",
|
||||||
|
"models.connCheckAria": "Check Ollama connectivity now",
|
||||||
|
"models.connHost": "Host",
|
||||||
|
"models.connVersion": "Version",
|
||||||
|
"models.connLatency": "Latency",
|
||||||
|
"models.connModels": "Models",
|
||||||
|
"models.connEmpty": "No models on the Ollama host yet — pull one from the library below.",
|
||||||
|
"models.connHint": "Make sure Ollama is running and OLLAMA_HOST points at it.",
|
||||||
"center.locked": "Admins only",
|
"center.locked": "Admins only",
|
||||||
"center.lockedSub": "Log in with the admin account to open the Admin Center.",
|
"center.lockedSub": "Log in with the admin account to open the Admin Center.",
|
||||||
"center.regTitle": "Public registration",
|
"center.regTitle": "Public registration",
|
||||||
@@ -862,6 +875,19 @@ const es: Dict = {
|
|||||||
"models.ollamaDesc": "Descargar o eliminar modelos locales.",
|
"models.ollamaDesc": "Descargar o eliminar modelos locales.",
|
||||||
"models.empty": "No hay modelos disponibles.",
|
"models.empty": "No hay modelos disponibles.",
|
||||||
"models.saveFailed": "No se pudo guardar",
|
"models.saveFailed": "No se pudo guardar",
|
||||||
|
"models.connTitle": "Conectividad Ollama",
|
||||||
|
"models.connDesc": "Estado en vivo del servidor Ollama: alcance, versión, latencia y modelos instalados.",
|
||||||
|
"models.connOnline": "En línea",
|
||||||
|
"models.connOffline": "Sin conexión",
|
||||||
|
"models.connDisabledState": "Desactivado",
|
||||||
|
"models.connCheck": "Comprobar ahora",
|
||||||
|
"models.connCheckAria": "Comprobar la conectividad Ollama ahora",
|
||||||
|
"models.connHost": "Host",
|
||||||
|
"models.connVersion": "Versión",
|
||||||
|
"models.connLatency": "Latencia",
|
||||||
|
"models.connModels": "Modelos",
|
||||||
|
"models.connEmpty": "Aún no hay modelos en el servidor Ollama — descarga uno desde la biblioteca.",
|
||||||
|
"models.connHint": "Asegúrate de que Ollama esté en marcha y de que OLLAMA_HOST apunte a él.",
|
||||||
"center.locked": "Solo admins",
|
"center.locked": "Solo admins",
|
||||||
"center.lockedSub": "Inicia sesión como admin para abrir el Centro de admin.",
|
"center.lockedSub": "Inicia sesión como admin para abrir el Centro de admin.",
|
||||||
"center.regTitle": "Registro público",
|
"center.regTitle": "Registro público",
|
||||||
@@ -1405,6 +1431,19 @@ const fr: Dict = {
|
|||||||
"models.ollamaDesc": "Télécharger ou supprimer des modèles locaux.",
|
"models.ollamaDesc": "Télécharger ou supprimer des modèles locaux.",
|
||||||
"models.empty": "Aucun modèle disponible.",
|
"models.empty": "Aucun modèle disponible.",
|
||||||
"models.saveFailed": "Enregistrement impossible",
|
"models.saveFailed": "Enregistrement impossible",
|
||||||
|
"models.connTitle": "Connectivité Ollama",
|
||||||
|
"models.connDesc": "État en direct de l'hôte Ollama : joignabilité, version, latence et modèles installés.",
|
||||||
|
"models.connOnline": "En ligne",
|
||||||
|
"models.connOffline": "Hors ligne",
|
||||||
|
"models.connDisabledState": "Désactivé",
|
||||||
|
"models.connCheck": "Vérifier",
|
||||||
|
"models.connCheckAria": "Vérifier la connectivité Ollama",
|
||||||
|
"models.connHost": "Hôte",
|
||||||
|
"models.connVersion": "Version",
|
||||||
|
"models.connLatency": "Latence",
|
||||||
|
"models.connModels": "Modèles",
|
||||||
|
"models.connEmpty": "Aucun modèle sur l'hôte Ollama — téléchargez-en un depuis la bibliothèque ci-dessous.",
|
||||||
|
"models.connHint": "Vérifiez qu'Ollama tourne et que OLLAMA_HOST pointe vers lui.",
|
||||||
"center.locked": "Admins uniquement",
|
"center.locked": "Admins uniquement",
|
||||||
"center.lockedSub": "Connectez-vous en admin pour ouvrir le Centre admin.",
|
"center.lockedSub": "Connectez-vous en admin pour ouvrir le Centre admin.",
|
||||||
"center.regTitle": "Inscription publique",
|
"center.regTitle": "Inscription publique",
|
||||||
@@ -1939,6 +1978,19 @@ const ru: Dict = {
|
|||||||
"models.ollamaDesc": "Скачивание и удаление локальных моделей.",
|
"models.ollamaDesc": "Скачивание и удаление локальных моделей.",
|
||||||
"models.empty": "Нет доступных моделей.",
|
"models.empty": "Нет доступных моделей.",
|
||||||
"models.saveFailed": "Не удалось сохранить",
|
"models.saveFailed": "Не удалось сохранить",
|
||||||
|
"models.connTitle": "Подключение Ollama",
|
||||||
|
"models.connDesc": "Состояние хоста Ollama: доступность, версия, задержка и модели.",
|
||||||
|
"models.connOnline": "В сети",
|
||||||
|
"models.connOffline": "Нет связи",
|
||||||
|
"models.connDisabledState": "Отключено",
|
||||||
|
"models.connCheck": "Проверить",
|
||||||
|
"models.connCheckAria": "Проверить подключение Ollama",
|
||||||
|
"models.connHost": "Хост",
|
||||||
|
"models.connVersion": "Версия",
|
||||||
|
"models.connLatency": "Задержка",
|
||||||
|
"models.connModels": "Модели",
|
||||||
|
"models.connEmpty": "На хосте Ollama пока нет моделей — скачайте из библиотеки ниже.",
|
||||||
|
"models.connHint": "Убедитесь, что Ollama запущена и OLLAMA_HOST указывает на неё.",
|
||||||
"center.locked": "Только админы",
|
"center.locked": "Только админы",
|
||||||
"center.lockedSub": "Войдите как админ, чтобы открыть центр.",
|
"center.lockedSub": "Войдите как админ, чтобы открыть центр.",
|
||||||
"center.regTitle": "Открытая регистрация",
|
"center.regTitle": "Открытая регистрация",
|
||||||
@@ -2471,6 +2523,19 @@ const it: Dict = {
|
|||||||
"models.ollamaDesc": "Scarica o rimuovi modelli locali.",
|
"models.ollamaDesc": "Scarica o rimuovi modelli locali.",
|
||||||
"models.empty": "Nessun modello disponibile.",
|
"models.empty": "Nessun modello disponibile.",
|
||||||
"models.saveFailed": "Salvataggio fallito",
|
"models.saveFailed": "Salvataggio fallito",
|
||||||
|
"models.connTitle": "Connettività Ollama",
|
||||||
|
"models.connDesc": "Stato live dell'host Ollama: raggiungibilità, versione, latenza e modelli installati.",
|
||||||
|
"models.connOnline": "Online",
|
||||||
|
"models.connOffline": "Offline",
|
||||||
|
"models.connDisabledState": "Disabilitato",
|
||||||
|
"models.connCheck": "Verifica ora",
|
||||||
|
"models.connCheckAria": "Verifica la connettività Ollama",
|
||||||
|
"models.connHost": "Host",
|
||||||
|
"models.connVersion": "Versione",
|
||||||
|
"models.connLatency": "Latenza",
|
||||||
|
"models.connModels": "Modelli",
|
||||||
|
"models.connEmpty": "Nessun modello sull'host Ollama — scaricane uno dalla libreria qui sotto.",
|
||||||
|
"models.connHint": "Assicurati che Ollama sia in esecuzione e che OLLAMA_HOST punti a esso.",
|
||||||
"center.locked": "Solo admin",
|
"center.locked": "Solo admin",
|
||||||
"center.lockedSub": "Accedi come admin per aprire il Centro.",
|
"center.lockedSub": "Accedi come admin per aprire il Centro.",
|
||||||
"center.regTitle": "Registrazione pubblica",
|
"center.regTitle": "Registrazione pubblica",
|
||||||
@@ -3001,6 +3066,19 @@ const pt: Dict = {
|
|||||||
"models.ollamaDesc": "Baixar ou remover modelos locais.",
|
"models.ollamaDesc": "Baixar ou remover modelos locais.",
|
||||||
"models.empty": "Nenhum modelo disponível.",
|
"models.empty": "Nenhum modelo disponível.",
|
||||||
"models.saveFailed": "Falha ao guardar",
|
"models.saveFailed": "Falha ao guardar",
|
||||||
|
"models.connTitle": "Conectividade Ollama",
|
||||||
|
"models.connDesc": "Estado ao vivo do servidor Ollama: alcance, versão, latência e modelos instalados.",
|
||||||
|
"models.connOnline": "Online",
|
||||||
|
"models.connOffline": "Offline",
|
||||||
|
"models.connDisabledState": "Desativado",
|
||||||
|
"models.connCheck": "Verificar agora",
|
||||||
|
"models.connCheckAria": "Verificar a conectividade Ollama agora",
|
||||||
|
"models.connHost": "Servidor",
|
||||||
|
"models.connVersion": "Versão",
|
||||||
|
"models.connLatency": "Latência",
|
||||||
|
"models.connModels": "Modelos",
|
||||||
|
"models.connEmpty": "Nenhum modelo no servidor Ollama — baixe um da biblioteca abaixo.",
|
||||||
|
"models.connHint": "Certifique-se de que o Ollama está a correr e de que OLLAMA_HOST aponta para ele.",
|
||||||
"center.locked": "Só admins",
|
"center.locked": "Só admins",
|
||||||
"center.lockedSub": "Entra como admin para abrir o Centro.",
|
"center.lockedSub": "Entra como admin para abrir o Centro.",
|
||||||
"center.regTitle": "Registo público",
|
"center.regTitle": "Registo público",
|
||||||
|
|||||||
+45
-1
@@ -867,7 +867,51 @@ const server = Bun.serve<{ ticketId: string | null; isAdmin: boolean }>({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- admin: Ollama model management (pull from the library, delete) ---
|
// --- admin: Ollama model management (connectivity probe, pull from the library, delete) ---
|
||||||
|
// Connectivity probe: is the Ollama host reachable, which version,
|
||||||
|
// and what sits on its disk (name + bytes + modification time).
|
||||||
|
// Always 200 for admins (reachability is data, not an error code).
|
||||||
|
if (path === "/api/admin/ollama/status" && req.method === "GET") {
|
||||||
|
if (!admin) return json({ error: "forbidden" }, 403);
|
||||||
|
const host = ollamaDriver.host;
|
||||||
|
if (!ollamaDriver.enabled) {
|
||||||
|
return json({ enabled: false, host, reachable: false, version: null, latencyMs: null, models: [], error: "ollama driver is disabled" });
|
||||||
|
}
|
||||||
|
const t0 = Date.now();
|
||||||
|
try {
|
||||||
|
const verRes = await fetch(`${host}/api/version`, { signal: AbortSignal.timeout(8000) });
|
||||||
|
if (!verRes.ok) {
|
||||||
|
return json({ enabled: true, host, reachable: false, version: null, latencyMs: Date.now() - t0, models: [], error: `ollama /api/version failed: ${verRes.status}` });
|
||||||
|
}
|
||||||
|
const ver = (await verRes.json().catch(() => ({}))) as { version?: unknown };
|
||||||
|
const tagsRes = await fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(8000) });
|
||||||
|
if (!tagsRes.ok) {
|
||||||
|
return json({ enabled: true, host, reachable: false, version: typeof ver.version === "string" ? ver.version : null, latencyMs: Date.now() - t0, models: [], error: `ollama /api/tags failed: ${tagsRes.status}` });
|
||||||
|
}
|
||||||
|
const tags = (await tagsRes.json().catch(() => ({}))) as { models?: { name?: unknown; size?: unknown; modified_at?: unknown }[] };
|
||||||
|
const models = Array.isArray(tags.models)
|
||||||
|
? tags.models
|
||||||
|
.filter((m) => typeof m.name === "string")
|
||||||
|
.map((m) => ({
|
||||||
|
name: m.name as string,
|
||||||
|
size: typeof m.size === "number" ? m.size : 0,
|
||||||
|
modifiedAt: typeof m.modified_at === "string" ? (m.modified_at as string) : null,
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
return json({
|
||||||
|
enabled: true,
|
||||||
|
host,
|
||||||
|
reachable: true,
|
||||||
|
version: typeof ver.version === "string" ? ver.version : null,
|
||||||
|
latencyMs: Date.now() - t0,
|
||||||
|
models,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return json({ enabled: true, host, reachable: false, version: null, latencyMs: Date.now() - t0, models: [], error: (e as Error).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Pull streams Ollama's NDJSON progress straight to the admin client.
|
// Pull streams Ollama's NDJSON progress straight to the admin client.
|
||||||
if (path === "/api/admin/ollama/pull" && req.method === "POST") {
|
if (path === "/api/admin/ollama/pull" && req.method === "POST") {
|
||||||
if (!admin) return json({ error: "forbidden" }, 403);
|
if (!admin) return json({ error: "forbidden" }, 403);
|
||||||
|
|||||||
Reference in New Issue
Block a user