Users, file manager, image generation, budgets and agent workflow

- User management: local accounts (Argon2) and OpenID Connect, admin/user
  roles, per-user folders plus admin-shared folders, read-only session
  sharing, event filtering per user
- File manager with CodeMirror editor and git client (status, diff,
  stage, commit, branches, fetch/pull/push, log, clone/init)
- Image generation via fal.ai: generate_image tool and image page
- Budgets: monthly limits per provider, provider balances/costs
  (OpenRouter, Anthropic admin cost report, fal.ai), price discovery
- Subagents on other models, background tasks, concurrency limits per
  host/model/subagent
- MCP tools loaded on demand (mcp_search/mcp_call), read-only hints
- Permission mode "auto", safe read-only commands always allowed
- Persistent message queue with attachments, deletable
- Live reasoning incl. <think> tags, activity animations, CSS themes,
  mobile layout, statistics per user, upload size handling
- Docker: named volume for state, localhost-bound port

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 16:54:08 +01:00
1 parent f3a2fe3816
commit 49262c612c
70 files changed
+8495 -714

No files matched your search

+98
View File
@@ -0,0 +1,98 @@
import { useFetch } from "../hooks";
import { fmtCost, fmtPercent, useT } from "../i18n";
interface BudgetRow {
provider: string;
budget: number | null;
spent: number;
remaining: number | null;
share: number | null;
warning: boolean;
exceeded: boolean;
images?: boolean;
remote?: { currency?: string; remaining?: number | null; limit?: number | null; used?: number | null; spent_month?: number; scope?: string; error?: string };
}
interface BudgetData {
currency: string;
total: BudgetRow;
providers: BudgetRow[];
}
function Bar({ share, warning, exceeded }: { share: number; warning: boolean; exceeded: boolean }) {
const color = exceeded ? "var(--red)" : warning ? "var(--accent2)" : "var(--green)";
return (
<div className="budget-bar" role="meter" aria-valuenow={Math.round(share * 100)} aria-valuemin={0} aria-valuemax={100}>
<div style={{ width: `${Math.min(share, 1) * 100}%`, background: color }} />
</div>
);
}
function Remote({ r, t }: { r: BudgetRow["remote"]; t: ReturnType<typeof useT> }) {
if (!r) return <span className="muted">–</span>;
if (r.error) return <span className="muted" title={r.error}>⚠ {t("budget.remoteError")}</span>;
const cur = r.currency ?? "USD";
if (r.spent_month !== undefined) return <span title={t("budget.orgWide")}>{t("budget.providerSpent", { v: fmtCost(r.spent_month, cur) })}</span>;
if (r.remaining !== null && r.remaining !== undefined) return <span>{t("budget.balance", { v: fmtCost(r.remaining, cur) })}</span>;
if (r.used !== null && r.used !== undefined) return <span>{t("budget.providerSpent", { v: fmtCost(r.used, cur) })}</span>;
return <span className="muted">–</span>;
}
/** Monthly budgets per provider (admins). */
export function BudgetPanel() {
const t = useT();
const { data, error } = useFetch<BudgetData>("/budget");
if (error || !data) return null;
const rows = [data.total, ...data.providers];
return (
<div className="panel">
<h3>{t("budget.title")}</h3>
<div style={{ overflowX: "auto" }}>
<table className="grid">
<thead>
<tr>
<th>{t("stats.provider")}</th>
<th className="num">{t("budget.spent")}</th>
<th className="num">{t("budget.budget")}</th>
<th style={{ minWidth: 140 }} />
<th className="num">{t("budget.remaining")}</th>
<th>{t("budget.provider")}</th>
</tr>
</thead>
<tbody>
{rows.map((r) => (
<tr key={r.provider}>
<td>
{r.provider === "total" ? <b>{t("stats.total")}</b> : r.provider}
{r.images && <span className="chip" style={{ marginLeft: 6 }}>{t("nav.images")}</span>}
</td>
<td className="num">{fmtCost(r.spent, data.currency)}</td>
<td className="num">{r.budget ? fmtCost(r.budget, data.currency) : "–"}</td>
<td>{r.share !== null && <Bar share={r.share} warning={r.warning} exceeded={r.exceeded} />}</td>
<td className="num" style={{ color: r.exceeded ? "var(--red)" : r.warning ? "var(--accent2)" : undefined }}>
{r.remaining !== null ? fmtCost(r.remaining, data.currency) : "–"}
{r.share !== null && <span className="muted"> ({fmtPercent(r.share)})</span>}
</td>
<td>{r.provider === "total" ? null : <Remote r={r.remote} t={t} />}</td>
</tr>
))}
</tbody>
</table>
</div>
<div style={{ fontSize: 12, color: "var(--muted)", marginTop: 6 }}>{t("budget.hint")}</div>
</div>
);
}
/** Small header chip when the overall budget is nearly used up. */
export function BudgetChip() {
const t = useT();
const { data } = useFetch<BudgetData>("/budget");
const worst = data ? [data.total, ...data.providers].filter((r) => r.warning).sort((a, b) => (b.share ?? 0) - (a.share ?? 0))[0] : undefined;
if (!worst) return null;
return (
<span className="chip warn hide-mobile" title={t("budget.chipHint")}>
{t("budget.chip", { name: worst.provider === "total" ? t("stats.total") : worst.provider, p: fmtPercent(worst.share ?? 0) })}
</span>
);
}