- 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>
74 lines
2.4 KiB
TypeScript
74 lines
2.4 KiB
TypeScript
import { useState } from "react";
|
||
import { useFetch } from "../../hooks";
|
||
import { fmtDate, useT } from "../../i18n";
|
||
|
||
interface Entry {
|
||
ts: number;
|
||
user: string;
|
||
action: string;
|
||
detail: string;
|
||
ip: string;
|
||
}
|
||
|
||
/** Admin: audit log and backup download. */
|
||
export function Audit() {
|
||
const t = useT();
|
||
const [filter, setFilter] = useState("");
|
||
const { data, reload } = useFetch<Entry[]>(`/audit?limit=500${filter ? `&action=${encodeURIComponent(filter)}` : ""}`);
|
||
return (
|
||
<>
|
||
<div className="panel">
|
||
<h3>{t("audit.backup")}</h3>
|
||
<div style={{ fontSize: 13, color: "var(--fg3)", marginBottom: 8 }}>{t("audit.backupHint")}</div>
|
||
<a className="button primary" href="/api/backup">
|
||
{t("audit.download")}
|
||
</a>
|
||
</div>
|
||
<div className="panel">
|
||
<h3>
|
||
{t("audit.title")}
|
||
<span className="spacer" />
|
||
<select value={filter} onChange={(e) => setFilter(e.target.value)}>
|
||
<option value="">{t("audit.all")}</option>
|
||
{["login", "permission", "config", "user", "folder", "session", "git", "backup"].map((a) => (
|
||
<option key={a} value={a}>
|
||
{a}
|
||
</option>
|
||
))}
|
||
</select>
|
||
<button className="small" onClick={reload}>
|
||
↻
|
||
</button>
|
||
</h3>
|
||
<div style={{ overflowX: "auto" }}>
|
||
<table className="grid">
|
||
<thead>
|
||
<tr>
|
||
<th>{t("audit.time")}</th>
|
||
<th>{t("stats.user")}</th>
|
||
<th>{t("audit.action")}</th>
|
||
<th>{t("audit.detail")}</th>
|
||
<th>IP</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{data?.map((e, i) => (
|
||
<tr key={i} className={e.action.includes("failed") || e.action.includes("blocked") || e.action.includes("reject") ? "audit-warn" : ""}>
|
||
<td style={{ whiteSpace: "nowrap" }}>{fmtDate(e.ts)}</td>
|
||
<td>{e.user || "–"}</td>
|
||
<td>
|
||
<code>{e.action}</code>
|
||
</td>
|
||
<td style={{ wordBreak: "break-word" }}>{e.detail}</td>
|
||
<td>{e.ip}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
{data?.length === 0 && <div className="empty">–</div>}
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|