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:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 19:12:30 +01:00
1 parent fab9add5d5
commit 5b569b6835
108 files changed
+6785 -279

No files matched your search

+199
View File
@@ -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>
);
}