Add security hardening, Ghidra, recreate skill, checkpoints, shells, web search and themes
- Sandbox (bubblewrap) for non-admin shell/git, MCP per-user rights, hard budgets and quotas, login rate limiting, audit log and backups - Ghidra headless decompilation (ARM64 natives built from source), binary_info tool, clean room / deep workflow per session - Recreate skill and commands for analysing and re-building old software - File checkpoints with rewind/restore, background shells (bash_output, kill_shell) - web_search tool (DuckDuckGo, SearXNG, Brave, Tavily) - In-app dialogs and toasts instead of browser dialogs - 15 base themes (trilium-themes palettes plus Nord, VS Code, GitHub, Catppuccin, One, Dracula, Rose Pine, Everforest, Kanagawa) with light/dark/auto switcher and license notes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fab9add5d5
commit
5b569b6835
108 files changed
+6785
-279
No files matched your search
@@ -0,0 +1,199 @@
|
||||
import { useEffect } from "react";
|
||||
import { useFetch, useModels } from "../hooks";
|
||||
import { fmtCompact, fmtCost, fmtDate, fmtNumber, fmtPercent, useT } from "../i18n";
|
||||
import { useStore } from "../store";
|
||||
import type { Aggregate, Session } from "../types";
|
||||
|
||||
interface SessionStats {
|
||||
total: Aggregate;
|
||||
by_model: (Aggregate & { model: string })[];
|
||||
by_agent: (Aggregate & { key: string })[];
|
||||
first: number | null;
|
||||
last: number | null;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
interface BudgetRow {
|
||||
provider: string;
|
||||
budget: number | null;
|
||||
spent: number;
|
||||
share: number | null;
|
||||
warning: boolean;
|
||||
}
|
||||
|
||||
function Row({ label, value, hint }: { label: string; value: React.ReactNode; hint?: string }) {
|
||||
return (
|
||||
<div className="kv" title={hint}>
|
||||
<span>{label}</span>
|
||||
<b>{value}</b>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const perMillion = (a: Aggregate) => {
|
||||
const tokens = a.input_tokens + a.cache_read_tokens + a.cache_write_tokens + a.output_tokens;
|
||||
return tokens > 0 ? (a.cost / tokens) * 1_000_000 : 0;
|
||||
};
|
||||
|
||||
/** Everything about the models used in a session: configuration, usage, speed, cost. */
|
||||
export function UsageOverlay({ session, onClose }: { session: Session; onClose: () => void }) {
|
||||
const t = useT();
|
||||
const { data } = useFetch<SessionStats>(`/sessions/${session.id}/stats`);
|
||||
const models = useModels();
|
||||
const isAdmin = useStore((s) => s.me?.role === "admin");
|
||||
const budget = useFetch<{ providers: BudgetRow[]; currency: string }>(isAdmin ? "/budget" : null).data;
|
||||
|
||||
useEffect(() => {
|
||||
const h = (e: KeyboardEvent) => e.key === "Escape" && onClose();
|
||||
window.addEventListener("keydown", h);
|
||||
return () => window.removeEventListener("keydown", h);
|
||||
}, [onClose]);
|
||||
|
||||
const cur = data?.currency ?? "USD";
|
||||
const info = (spec: string) => {
|
||||
const [p, ...rest] = spec.split("/");
|
||||
return models.find((x) => x.provider === p)?.models.find((m) => m.id === rest.join("/"));
|
||||
};
|
||||
const u = session.last_usage;
|
||||
const ctxTokens = u ? u.input_tokens + u.cache_read_tokens + u.cache_write_tokens + u.output_tokens : 0;
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" onClick={onClose}>
|
||||
<div className="modal wide" role="dialog" aria-label={t("usage.title")} onClick={(e) => e.stopPropagation()}>
|
||||
<div className="row" style={{ marginBottom: 8 }}>
|
||||
<h3 style={{ margin: 0 }}>{t("usage.title")}</h3>
|
||||
<span className="spacer" />
|
||||
<button className="small ghost" onClick={onClose} aria-label={t("common.close")}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
{!data ? (
|
||||
<div className="empty">{t("common.loading")}</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="kpis">
|
||||
<div className="kpi">
|
||||
<div className="k-label">{t("stats.cost")}</div>
|
||||
<div className="k-value">{fmtCost(data.total.cost, cur)}</div>
|
||||
<div className="k-sub">{t("stats.perRequest", { v: fmtCost(data.total.requests ? data.total.cost / data.total.requests : 0, cur) })}</div>
|
||||
</div>
|
||||
<div className="kpi">
|
||||
<div className="k-label">{t("stats.requests")}</div>
|
||||
<div className="k-value">{fmtNumber(data.total.requests)}</div>
|
||||
{data.total.errors > 0 && <div className="k-sub">{t("stats.errors", { n: data.total.errors })}</div>}
|
||||
</div>
|
||||
<div className="kpi">
|
||||
<div className="k-label">{t("stats.tokens")}</div>
|
||||
<div className="k-value">
|
||||
{fmtCompact(data.total.input_tokens + data.total.cache_read_tokens + data.total.cache_write_tokens)}↑ {fmtCompact(data.total.output_tokens)}↓
|
||||
</div>
|
||||
<div className="k-sub">
|
||||
{t("stats.cacheHit")}: {fmtPercent(data.total.cache_hit_rate)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="kpi">
|
||||
<div className="k-label">{t("usage.period")}</div>
|
||||
<div className="k-value" style={{ fontSize: 15 }}>
|
||||
{data.first ? fmtDate(data.first) : "–"}
|
||||
</div>
|
||||
<div className="k-sub">{data.last ? `→ ${fmtDate(data.last)}` : ""}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data.by_model.map((m) => {
|
||||
const mi = info(m.model);
|
||||
const provider = m.model.split("/")[0];
|
||||
const b = budget?.providers.find((x) => x.provider === provider);
|
||||
const isCurrent = m.model === session.model;
|
||||
return (
|
||||
<div className="panel" key={m.model}>
|
||||
<h3>
|
||||
{mi?.name ?? m.model}
|
||||
{isCurrent && <span className="chip">{t("usage.current")}</span>}
|
||||
<span className="chip">{provider}</span>
|
||||
</h3>
|
||||
<div className="kv-grid">
|
||||
<div>
|
||||
<div className="kv-title">{t("usage.model")}</div>
|
||||
<Row label="ID" value={<code>{m.model}</code>} />
|
||||
<Row label={t("settings.context")} value={mi?.context ? `${fmtCompact(mi.context)} tok` : "–"} />
|
||||
<Row label={t("settings.maxOutput")} value={mi?.max_output ? `${fmtCompact(mi.max_output)} tok` : "–"} />
|
||||
<Row label={t("settings.reasoning")} value={mi?.reasoning ? `✓${mi.effort ? ` (${mi.effort})` : ""}` : "–"} />
|
||||
<Row label={t("settings.vision")} value={mi?.vision ? "✓" : "–"} />
|
||||
{isCurrent && mi?.context ? (
|
||||
<Row label={t("usage.contextNow")} value={`${fmtCompact(ctxTokens)} / ${fmtCompact(mi.context)} (${fmtPercent(ctxTokens / mi.context)})`} />
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<div className="kv-title">{t("usage.prices")}</div>
|
||||
<Row label={t("settings.priceIn")} value={mi?.input_price != null ? fmtCost(mi.input_price, cur) : "–"} />
|
||||
<Row label={t("settings.priceOut")} value={mi?.output_price != null ? fmtCost(mi.output_price, cur) : "–"} />
|
||||
<Row label={t("settings.priceCacheRead")} value={mi?.cache_read_price != null ? fmtCost(mi.cache_read_price, cur) : "–"} />
|
||||
<Row label={t("settings.priceCacheWrite")} value={mi?.cache_write_price != null ? fmtCost(mi.cache_write_price, cur) : "–"} />
|
||||
<Row label={t("stats.perMillion")} value={fmtCost(perMillion(m), cur)} hint={t("stats.perMillionHint")} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="kv-title">{t("usage.usage")}</div>
|
||||
<Row label={t("stats.requests")} value={fmtNumber(m.requests)} />
|
||||
<Row label={t("stats.input")} value={fmtNumber(m.input_tokens)} />
|
||||
<Row label={t("stats.cacheRead")} value={fmtNumber(m.cache_read_tokens)} />
|
||||
<Row label={t("usage.cacheWrite")} value={fmtNumber(m.cache_write_tokens)} />
|
||||
<Row label={t("stats.output")} value={fmtNumber(m.output_tokens)} />
|
||||
<Row label={t("stats.cacheHit")} value={fmtPercent(m.cache_hit_rate)} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="kv-title">{t("usage.speed")}</div>
|
||||
<Row label={t("stats.ttft")} value={`${fmtNumber(m.avg_ttft_ms)} ms`} />
|
||||
<Row label={t("stats.prefillSpeed")} value={`${fmtNumber(m.prefill_tps)} tok/s`} />
|
||||
<Row label={t("stats.outputSpeed")} value={`${fmtNumber(m.output_tps, 1)} tok/s`} />
|
||||
<Row label={t("usage.totalTime")} value={`${fmtNumber(m.duration_ms / 1000, 1)} s`} />
|
||||
<div className="kv-title" style={{ marginTop: 8 }}>{t("stats.cost")}</div>
|
||||
<Row label={t("usage.inSession")} value={fmtCost(m.cost, cur)} />
|
||||
{b && (
|
||||
<Row
|
||||
label={t("usage.providerMonth")}
|
||||
value={
|
||||
<span style={{ color: b.warning ? "var(--accent2)" : undefined }}>
|
||||
{fmtCost(b.spent, budget!.currency)}
|
||||
{b.budget ? ` / ${fmtCost(b.budget, budget!.currency)}` : ""}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{data.by_agent.length > 1 && (
|
||||
<div className="panel">
|
||||
<h3>{t("usage.byAgent")}</h3>
|
||||
<table className="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("stats.agent")}</th>
|
||||
<th className="num">{t("stats.requests")}</th>
|
||||
<th className="num">{t("stats.tokens")}</th>
|
||||
<th className="num">{t("stats.cost")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.by_agent.map((a) => (
|
||||
<tr key={a.key}>
|
||||
<td>{a.key}</td>
|
||||
<td className="num">{fmtNumber(a.requests)}</td>
|
||||
<td className="num">{fmtCompact(a.input_tokens + a.cache_read_tokens + a.cache_write_tokens + a.output_tokens)}</td>
|
||||
<td className="num">{fmtCost(a.cost, cur)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user