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>
81 lines
2.5 KiB
TypeScript
81 lines
2.5 KiB
TypeScript
export const API_BASE =
|
|
process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000";
|
|
|
|
const TOKEN_KEY = "agentic_token";
|
|
|
|
export function getToken(): string | null {
|
|
if (typeof window === "undefined") return null;
|
|
return window.localStorage.getItem(TOKEN_KEY);
|
|
}
|
|
|
|
export function setToken(token: string | null) {
|
|
if (typeof window === "undefined") return;
|
|
if (token) window.localStorage.setItem(TOKEN_KEY, token);
|
|
else window.localStorage.removeItem(TOKEN_KEY);
|
|
}
|
|
|
|
export class ApiError extends Error {
|
|
status: number;
|
|
constructor(status: number, message: string) {
|
|
super(message);
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
export async function api<T = any>(
|
|
path: string,
|
|
options: RequestInit = {}
|
|
): Promise<T> {
|
|
const headers = new Headers(options.headers);
|
|
const token = getToken();
|
|
if (token) headers.set("Authorization", `Bearer ${token}`);
|
|
if (!headers.has("Content-Type") && options.body && !(options.body instanceof FormData)) {
|
|
headers.set("Content-Type", "application/json");
|
|
}
|
|
const res = await fetch(`${API_BASE}${path}`, { ...options, headers });
|
|
if (res.status === 401 && typeof window !== "undefined") {
|
|
setToken(null);
|
|
if (!window.location.pathname.startsWith("/login")) {
|
|
window.location.href = "/login";
|
|
}
|
|
}
|
|
if (!res.ok) {
|
|
let detail = res.statusText;
|
|
try {
|
|
const body = await res.json();
|
|
detail = body.detail || detail;
|
|
} catch {}
|
|
throw new ApiError(res.status, typeof detail === "string" ? detail : JSON.stringify(detail));
|
|
}
|
|
if (res.status === 204) return undefined as T;
|
|
return res.json();
|
|
}
|
|
|
|
export async function login(email: string, password: string) {
|
|
const form = new URLSearchParams();
|
|
form.set("username", email);
|
|
form.set("password", password);
|
|
const res = await fetch(`${API_BASE}/api/auth/login`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
body: form.toString(),
|
|
});
|
|
if (!res.ok) {
|
|
let detail = "Login failed";
|
|
try {
|
|
detail = (await res.json()).detail || detail;
|
|
} catch {}
|
|
throw new ApiError(res.status, detail);
|
|
}
|
|
return res.json() as Promise<{ access_token: string; role: string; email: string }>;
|
|
}
|
|
|
|
export function wsUrl(path: string): string {
|
|
const base = API_BASE.replace(/^http/, "ws");
|
|
const token = getToken();
|
|
const sep = path.includes("?") ? "&" : "?";
|
|
return `${base}${path}${sep}token=${encodeURIComponent(token || "")}`;
|
|
}
|
|
|
|
export const fetcher = (path: string) => api(path);
|