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

+141
View File
@@ -0,0 +1,141 @@
import { lazy, Suspense, useEffect, useState } from "react";
import { get } from "../api";
import { useT } from "../i18n";
import { Spinner } from "./Activity";
const ModelViewer = lazy(() => import("./ModelViewer").then((m) => ({ default: m.ModelViewer })));
export type PreviewKind = "image" | "pdf" | "doc" | "audio" | "module" | "model";
const KINDS: [RegExp, PreviewKind][] = [
[/\.(png|jpe?g|gif|webp|bmp|svg)$/i, "image"],
[/\.pdf$/i, "pdf"],
[/\.(docx|pptx|xlsx|xlsm|xls|ods)$/i, "doc"],
[/\.(wav|mp3|ogg|oga|flac|m4a)$/i, "audio"],
[/\.(xm|mod|s3m|it)$/i, "module"],
[/\.(glb|gltf|stl|obj|step|stp|igs|iges)$/i, "model"],
];
export function previewKind(path: string): PreviewKind | undefined {
return KINDS.find(([re]) => re.test(path))?.[1];
}
const q = (p: string) => `path=${encodeURIComponent(p)}`;
function PdfPreview({ path }: { path: string }) {
const t = useT();
const [pages, setPages] = useState<number>();
const [page, setPage] = useState(1);
useEffect(() => {
setPage(1);
get<{ pages: number }>(`/files/preview/pdf-info?${q(path)}`).then((r) => setPages(r.pages)).catch(() => setPages(1));
}, [path]);
return (
<div className="pdf-preview">
<div className="row pdf-nav">
<button className="small" disabled={page <= 1} onClick={() => setPage(page - 1)}>
‹
</button>
<span>{t("preview.page", { n: page, total: pages ?? "…" })}</span>
<button className="small" disabled={!pages || page >= pages} onClick={() => setPage(page + 1)}>
›
</button>
<span className="spacer" />
<a className="button small" href={`/api/files/raw?${q(path)}`} target="_blank" rel="noreferrer">
{t("preview.openBrowser")}
</a>
</div>
<div className="pane-scroll image-preview">
<img key={page} src={`/api/files/preview/pdf-page?${q(path)}&page=${page}`} alt={`page ${page}`} />
</div>
</div>
);
}
interface DocData {
kind: "text" | "table";
text?: string;
sheets?: { name: string; rows: string[][]; total_rows: number }[];
}
function DocPreview({ path }: { path: string }) {
const t = useT();
const [data, setData] = useState<DocData>();
const [error, setError] = useState<string>();
const [sheet, setSheet] = useState(0);
useEffect(() => {
setData(undefined);
setSheet(0);
get<DocData>(`/files/preview/doc?${q(path)}`).then(setData).catch((e) => setError(e.message));
}, [path]);
if (error) return <div className="notice err">{error}</div>;
if (!data) return <div className="empty"><Spinner /></div>;
if (data.kind === "text") return <pre className="pane-scroll doc-text">{data.text}</pre>;
const s = data.sheets?.[sheet];
return (
<div className="pdf-preview">
<div className="mode-tabs sheet-tabs">
{data.sheets?.map((x, i) => (
<button key={x.name} className={i === sheet ? "active" : ""} onClick={() => setSheet(i)}>
{x.name}
</button>
))}
</div>
<div className="pane-scroll">
{s && (
<table className="grid sheet">
<tbody>
{s.rows.map((r, i) => (
<tr key={i}>
<th className="num">{i + 1}</th>
{r.map((c, j) => (
<td key={j}>{c}</td>
))}
</tr>
))}
</tbody>
</table>
)}
{s && s.total_rows > s.rows.length && <div className="empty">{t("preview.moreRows", { n: s.total_rows - s.rows.length })}</div>}
</div>
</div>
);
}
export function FilePreview({ path, kind }: { path: string; kind: PreviewKind }) {
const t = useT();
const raw = `/api/files/raw?${q(path)}`;
switch (kind) {
case "image":
return (
<div className="pane-scroll image-preview">
<img src={raw} alt="" />
</div>
);
case "pdf":
return <PdfPreview path={path} />;
case "doc":
return <DocPreview path={path} />;
case "audio":
return (
<div className="audio-preview">
<audio controls src={raw} preload="metadata" />
</div>
);
case "module":
return (
<div className="audio-preview">
<div className="muted">{t("preview.moduleHint")}</div>
<audio controls src={`/api/files/preview/module?${q(path)}`} preload="none" />
</div>
);
case "model": {
const ext = path.split(".").pop()!.toLowerCase();
return (
<Suspense fallback={<div className="empty"><Spinner /></div>}>
<ModelViewer url={raw} ext={ext} />
</Suspense>
);
}
}
}