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,73 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user