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:
1 parent
81ee084ff2
commit
d152ad836d
26 files changed
+836
-52
No files matched your search
@@ -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")}>
|
||||
|
||||
Reference in new issue
Block a user