Initial commit: Agentic OS troubleshooting platform

Self-hosted, Docker-based agentic troubleshooting platform: FastAPI backend +
LangGraph agent, Next.js UI, tiered LLM routing (local Ollama -> Gemini ->
DeepSeek -> OpenRouter), MCP server manager, encrypted device credentials,
RBAC, audit log, project-memory + Obsidian integrations, and editable
troubleshooting decision rules tuned for the GeneseasX vessel stack.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-14 22:11:07 +03:00
commit 6185b9b85a
126 changed files with 14565 additions and 0 deletions

229
frontend/app/users/page.tsx Normal file
View File

@@ -0,0 +1,229 @@
"use client";
import { useState } from "react";
import useSWR from "swr";
import AppShell from "@/components/AppShell";
import {
ConfirmDialog,
EmptyState,
FormError,
LoadError,
Modal,
PageHeader,
SkeletonRows,
StatusBadge,
} from "@/components/ui";
import { useToast } from "@/components/toast";
import { api, fetcher } from "@/lib/api";
type User = {
id: number;
email: string;
full_name: string | null;
role: string;
is_active: boolean;
};
const ROLES = ["readonly", "support", "admin"];
export default function UsersPage() {
const { data: users, error: usersError, isLoading, mutate } = useSWR<User[]>(
"/api/users",
fetcher
);
const toast = useToast();
const [open, setOpen] = useState(false);
const [form, setForm] = useState({ email: "", password: "", full_name: "", role: "readonly" });
const [error, setError] = useState<string | null>(null);
const [creating, setCreating] = useState(false);
const [deleteId, setDeleteId] = useState<number | null>(null);
const [deleting, setDeleting] = useState(false);
const create = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setCreating(true);
try {
await api("/api/users", { method: "POST", body: JSON.stringify(form) });
setOpen(false);
setForm({ email: "", password: "", full_name: "", role: "readonly" });
toast.success("User created");
mutate();
} catch (err: any) {
setError(err.message);
toast.error(err.message || "Could not create user");
} finally {
setCreating(false);
}
};
const update = async (id: number, patch: Partial<User>) => {
try {
await api(`/api/users/${id}`, { method: "PATCH", body: JSON.stringify(patch) });
toast.success("User updated");
mutate();
} catch (err: any) {
toast.error(err.message || "Update failed");
}
};
const remove = async () => {
if (deleteId == null) return;
setDeleting(true);
try {
await api(`/api/users/${deleteId}`, { method: "DELETE" });
toast.success("User deleted");
mutate();
} catch (err: any) {
toast.error(err.message || "Delete failed");
} finally {
setDeleting(false);
setDeleteId(null);
}
};
return (
<AppShell>
<PageHeader
title="Users"
subtitle="Manage operators and their roles"
action={
<button className="btn-primary" onClick={() => setOpen(true)}>
+ New user
</button>
}
/>
{isLoading && !users ? (
<SkeletonRows rows={4} />
) : usersError ? (
<LoadError message="Couldnt load users." onRetry={() => mutate()} />
) : (users || []).length === 0 ? (
<EmptyState title="No users yet" description="Create the first operator account." />
) : (
<div className="card overflow-x-auto p-0">
<table className="w-full min-w-[640px]">
<thead>
<tr>
<th className="th">Email</th>
<th className="th">Name</th>
<th className="th">Role</th>
<th className="th">Active</th>
<th className="th"></th>
</tr>
</thead>
<tbody>
{(users || []).map((u) => (
<tr key={u.id}>
<td className="td">{u.email}</td>
<td className="td">{u.full_name || "—"}</td>
<td className="td">
<select
aria-label={`Role for ${u.email}`}
className="input max-w-[140px]"
value={u.role}
onChange={(e) => update(u.id, { role: e.target.value as any })}
>
{ROLES.map((r) => (
<option key={r} value={r}>
{r}
</option>
))}
</select>
</td>
<td className="td">
<button
aria-label={`Toggle active for ${u.email}`}
aria-pressed={u.is_active}
onClick={() => update(u.id, { is_active: !u.is_active })}
>
<StatusBadge status={u.is_active ? "loaded" : "stopped"} />
</button>
</td>
<td className="td text-right">
<button className="btn-danger btn-sm" onClick={() => setDeleteId(u.id)}>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<Modal open={open} onClose={() => setOpen(false)} title="Create user">
<form onSubmit={create} className="space-y-4">
<div>
<label htmlFor="user-email" className="label">
Email
</label>
<input
id="user-email"
className="input"
type="email"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
required
/>
</div>
<div>
<label htmlFor="user-name" className="label">
Full name
</label>
<input
id="user-name"
className="input"
value={form.full_name}
onChange={(e) => setForm({ ...form, full_name: e.target.value })}
/>
</div>
<div>
<label htmlFor="user-password" className="label">
Password
</label>
<input
id="user-password"
className="input"
type="password"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
required
/>
</div>
<div>
<label htmlFor="user-role" className="label">
Role
</label>
<select
id="user-role"
className="input"
value={form.role}
onChange={(e) => setForm({ ...form, role: e.target.value })}
>
{ROLES.map((r) => (
<option key={r} value={r}>
{r}
</option>
))}
</select>
</div>
<FormError message={error} />
<button className="btn-primary w-full" disabled={creating}>
{creating ? "Creating…" : "Create"}
</button>
</form>
</Modal>
<ConfirmDialog
open={deleteId != null}
title="Delete user?"
message="This permanently removes the user account."
confirmLabel="Delete"
destructive
busy={deleting}
onConfirm={remove}
onCancel={() => setDeleteId(null)}
/>
</AppShell>
);
}