feat: enhance project structure with server and workflow management

- Introduced a new server package with Fastify for handling API requests and workflows.
- Implemented user authentication and authorization with JWT and cookie management.
- Added endpoints for managing workflows, runs, and user accounts.
- Established a dashboard for monitoring workflow status and statistics.
- Included a script sandbox for executing user-defined scripts securely.
- Updated README with setup instructions and API documentation.
- Configured database migrations for user management.
- Enhanced logging capabilities for better traceability.
This commit is contained in:
2026-08-14 14:25:37 +07:00
parent 5574d6f723
commit 023cf83cfa
48 changed files with 6127 additions and 359 deletions
@@ -0,0 +1,30 @@
import Editor from "@monaco-editor/react";
import { useTheme } from "../theme.jsx";
export function CodeEditor({ language, value, onChange, height = "50vh" }) {
const { theme } = useTheme();
const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
return (
<div className="overflow-hidden rounded-box border border-base-300">
<Editor
height={height}
language={language}
value={value}
onChange={(v) => onChange(v ?? "")}
theme={theme === "dark" ? "vs-dark" : "light"}
options={{
minimap: { enabled: !isMobile },
lineNumbers: "on",
tabSize: 2,
insertSpaces: true,
autoIndent: "full",
automaticLayout: true,
wordWrap: "on",
scrollBeyondLastLine: false,
fontSize: 13,
}}
/>
</div>
);
}
+93
View File
@@ -0,0 +1,93 @@
import { NavLink, useNavigate } from "react-router-dom";
import {
LuActivity,
LuCode,
LuGitBranch,
LuHouse,
LuLogOut,
LuMenu,
LuMoon,
LuSun,
LuUsers,
} from "react-icons/lu";
import { useLogout } from "../api/hooks.js";
import { useTheme } from "../theme.jsx";
const links = [
{ to: "/", label: "Home", icon: LuHouse, end: true },
{ to: "/scripts", label: "Scripts", icon: LuCode },
{ to: "/workflows", label: "Workflows", icon: LuGitBranch },
{ to: "/events", label: "Events", icon: LuActivity },
];
export function Layout({ user, children }) {
const { theme, toggle } = useTheme();
const logout = useLogout();
const navigate = useNavigate();
const navItems = [
...links,
...(user.role === "admin"
? [{ to: "/users", label: "Users", icon: LuUsers }]
: []),
];
function closeDrawer() {
const el = document.getElementById("nav-drawer");
if (el) el.checked = false;
}
return (
<div className="drawer lg:drawer-open">
<input id="nav-drawer" type="checkbox" className="drawer-toggle" />
<div className="drawer-content flex min-h-screen flex-col">
<div className="navbar bg-base-100 border-base-300 sticky top-0 z-10 border-b">
<div className="flex-none lg:hidden">
<label htmlFor="nav-drawer" className="btn btn-square btn-ghost" aria-label="Open menu">
<LuMenu className="size-5" />
</label>
</div>
<div className="flex-1 px-2 text-lg font-semibold">scrunner</div>
<div className="flex items-center gap-1">
<button
type="button"
className="btn btn-ghost btn-square"
onClick={toggle}
title={theme === "dark" ? "Light" : "Dark"}
aria-label="Toggle theme"
>
{theme === "dark" ? <LuSun className="size-5" /> : <LuMoon className="size-5" />}
</button>
<span className="hidden sm:inline text-sm opacity-70 px-2">{user.username}</span>
<button
type="button"
className="btn btn-ghost btn-square"
title="Log out"
aria-label="Log out"
onClick={() =>
logout.mutate(undefined, { onSuccess: () => navigate("/") })
}
>
<LuLogOut className="size-5" />
</button>
</div>
</div>
<main className="flex-1 p-4">{children}</main>
</div>
<div className="drawer-side z-20">
<label htmlFor="nav-drawer" aria-label="Close menu" className="drawer-overlay" />
<ul className="menu bg-base-200 min-h-full w-64 p-4">
<li className="menu-title">scrunner</li>
{navItems.map(({ to, label, icon: Icon, end }) => (
<li key={to}>
<NavLink to={to} end={end} onClick={closeDrawer}>
<Icon className="size-4" />
{label}
</NavLink>
</li>
))}
</ul>
</div>
</div>
);
}
@@ -0,0 +1,75 @@
import { useEffect, useId, useRef } from "react";
import { useNavigate } from "react-router-dom";
import mermaid from "mermaid";
import { useTheme } from "../theme.jsx";
function findNode(root, id) {
return (
root.querySelector(`#${CSS.escape(id)}`) ||
root.querySelector(`[id="${CSS.escape(id)}"]`) ||
root.querySelector(`[id*="${CSS.escape(id)}"]`)
);
}
export function MermaidDiagram({ chart, scriptIds = {} }) {
const { theme } = useTheme();
const navigate = useNavigate();
const ref = useRef(null);
const reactId = useId().replace(/:/g, "");
const scriptKey = JSON.stringify(scriptIds);
useEffect(() => {
if (!chart || !ref.current) {
if (ref.current) ref.current.innerHTML = "";
return;
}
let cancelled = false;
mermaid.initialize({
startOnLoad: false,
securityLevel: "loose",
suppressErrorRendering: true,
theme: theme === "dark" ? "dark" : "default",
});
const ids = JSON.parse(scriptKey);
const renderId = `mmd-${reactId}-${Date.now()}`;
mermaid
.render(renderId, chart)
.then(({ svg }) => {
if (cancelled || !ref.current) return;
ref.current.innerHTML = svg;
for (const [nodeId, script] of Object.entries(ids)) {
const el = findNode(ref.current, nodeId);
if (!el) continue;
el.style.cursor = "pointer";
el.setAttribute("title", script);
el.addEventListener("click", () => {
navigate(`/scripts?edit=${encodeURIComponent(script)}`);
});
}
})
.catch((err) => {
if (cancelled || !ref.current) return;
ref.current.innerHTML = "";
const p = document.createElement("p");
p.className = "text-error text-sm";
p.textContent = "Could not render diagram";
ref.current.appendChild(p);
});
return () => {
cancelled = true;
};
}, [chart, scriptKey, theme, navigate, reactId]);
if (!chart) return null;
return (
<div
ref={ref}
className="mermaid-clickable overflow-x-auto rounded-box border border-base-300 bg-base-200 p-3"
/>
);
}