Split view, personal modes, budget warnings, server move script

- Up to three chats side by side (#/s/a+b+c), "Open beside" in the sidebar menu
- File manager tabs of a chat stored per user on the server (/sessions/{id}/tabs)
- Personal modes for every user (Settings → My modes, /account/modes)
- Budget meters (total, provider, images, opencode Go windows) with push warnings
  for admins at the warning share and 100 %, shown in the status overlay
- scripts/move-server.sh: move to another Docker host with bind mounts (Unraid);
  scripts/smoke-test.sh

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-09 12:06:20 +01:00
1 parent 81ee084ff2
commit d152ad836d
26 files changed
+836 -52

No files matched your search

+31 -2
View File
@@ -1,10 +1,10 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { get, post } from "../api";
import { fmtPercent, useT, type Key } from "../i18n";
import { fmtCost, fmtPercent, useT, type Key } from "../i18n";
import { navigate } from "../router";
import { useStore } from "../store";
import { Icon } from "./Icon";
import type { Plan } from "./Budget";
import { Bar, type Plan } from "./Budget";
type Level = "ok" | "warn" | "err" | "off";
@@ -21,12 +21,25 @@ interface McpEntry {
tools?: number;
error?: string | null;
}
interface Meter {
key: string;
provider: string;
kind: "total" | "provider" | "images" | "5h" | "week" | "month";
used: number;
limit: number;
ratio: number;
}
interface Overview {
version: string;
providers: Record<string, ProviderState>;
mcp: Record<string, McpEntry>;
/** Budgets and plan windows (admins only). */
budgets?: Meter[];
currency?: string;
}
const meterLevel = (m: Meter): Level => (m.ratio >= 1 ? "err" : m.ratio >= 0.8 ? "warn" : "ok");
const POLL_MS = 30_000;
/** A dropped connection shows yellow (reconnecting) for this long, then red. */
const RECONNECT_GRACE_MS = 10_000;
@@ -197,6 +210,22 @@ export function StatusLeds() {
))}
</div>
{!!data?.budgets?.length && (
<div className="status-section">
<div className="status-head">
<Led level={combine(data.budgets.map(meterLevel))} /> {t("status.budgets")}
</div>
{data.budgets.map((m) => (
<div key={m.key} className="status-row status-meter" title={`${fmtCost(m.used, data.currency ?? "USD")} / ${fmtCost(m.limit, data.currency ?? "USD")}`}>
<Led level={meterLevel(m)} />
<span className="grow">{m.kind === "total" ? t("stats.total") : m.kind === "images" ? t("nav.images") : m.kind === "provider" ? m.provider : `${m.provider} · ${t(`budget.window.${m.kind}` as Key)}`}</span>
<Bar share={m.ratio} warning={m.ratio >= 0.8} exceeded={m.ratio >= 1} />
<span className="hint pct">{fmtPercent(m.ratio)}</span>
</div>
))}
</div>
)}
{admin && (
<div className="status-foot">
<button className="small ghost" onClick={() => go("providers")}>