From 59bf5dd8ba345313cd080479057f721b9188aeb3 Mon Sep 17 00:00:00 2001 From: Nexus Dev Date: Thu, 2 Apr 2026 23:22:20 +0000 Subject: [PATCH] feat(30-01): hardware and nexus-settings routes, app.ts mounting - Add hardwareRoutes with unauthenticated GET /system/providers - Add hardwareRoutes with GET /system/providers/recommendation - Add nexusSettingsRoutes with board-auth GET/PATCH /nexus/settings - Mount hardwareRoutes on app before boardMutationGuard (unauthenticated) - Mount nexusSettingsRoutes on api router (board-auth gated) --- server/src/app.ts | 4 ++ server/src/routes/hardware.ts | 66 +++++++++++++++++++++++++++++ server/src/routes/nexus-settings.ts | 39 +++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 server/src/routes/hardware.ts create mode 100644 server/src/routes/nexus-settings.ts diff --git a/server/src/app.ts b/server/src/app.ts index 1e54f213..aeaf189e 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -27,6 +27,8 @@ import { sidebarBadgeRoutes } from "./routes/sidebar-badges.js"; import { instanceSettingsRoutes } from "./routes/instance-settings.js"; import { ollamaRoutes } from "./routes/ollama.js"; import { llmRoutes } from "./routes/llms.js"; +import { hardwareRoutes } from "./routes/hardware.js"; +import { nexusSettingsRoutes } from "./routes/nexus-settings.js"; import { assetRoutes } from "./routes/assets.js"; import { accessRoutes } from "./routes/access.js"; import { pluginRoutes } from "./routes/plugins.js"; @@ -136,6 +138,7 @@ export async function createApp( app.all("/api/auth/*authPath", opts.betterAuthHandler); } app.use(llmRoutes(db)); + app.use("/api", hardwareRoutes()); // Mount API routes const api = Router(); @@ -168,6 +171,7 @@ export async function createApp( api.use(dashboardRoutes(db)); api.use(sidebarBadgeRoutes(db)); api.use(instanceSettingsRoutes(db)); + api.use(nexusSettingsRoutes()); const hostServicesDisposers = new Map void>(); const workerManager = createPluginWorkerManager(); const pluginRegistry = pluginRegistryService(db); diff --git a/server/src/routes/hardware.ts b/server/src/routes/hardware.ts new file mode 100644 index 00000000..0290879c --- /dev/null +++ b/server/src/routes/hardware.ts @@ -0,0 +1,66 @@ +import os from "node:os"; +import { Router } from "express"; +import { hardwareService } from "../services/hardware.js"; + +// Unauthenticated — hardware is a property of the machine, not the user. Safe: read-only, no mutation, no secrets. + +export function hardwareRoutes(): Router { + const router = Router(); + + router.get("/system/providers", async (_req, res) => { + try { + const info = await hardwareService().detect(); + res.json(info); + } catch { + // Graceful degradation — return basic info without GPU data + const totalBytes = os.totalmem(); + const freeBytes = os.freemem(); + const totalGb = Math.round((totalBytes / (1024 * 1024 * 1024)) * 10) / 10; + const freeGb = Math.round((freeBytes / (1024 * 1024 * 1024)) * 10) / 10; + const usableGb = Math.round((freeBytes * 0.75) / (1024 * 1024 * 1024) * 10) / 10; + res.json({ + totalGb, + freeGb, + usableGb, + platform: process.platform, + gpuName: null, + gpuVramGb: null, + unifiedMemory: false, + hardwareTier: "cpu_only", + cpuModel: os.cpus()[0]?.model ?? null, + }); + } + }); + + router.get("/system/providers/recommendation", async (_req, res) => { + try { + const { fileURLToPath } = await import("node:url"); + const path = await import("node:path"); + const fs = await import("node:fs"); + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const catalogPath = path.resolve(__dirname, "../data/ollama-model-catalog.json"); + const catalog = JSON.parse(fs.readFileSync(catalogPath, "utf-8")) as { + models: Array<{ + family: string; + variants: Array<{ name: string; ramGb: number; vramGb: number; quality: string; tier?: string[] }>; + }>; + }; + + const hardwareInfo = await hardwareService().detect(); + + // Filter catalog entries matching the detected hardware tier + const recommendedModels = catalog.models.flatMap((family) => + family.variants + .filter((v) => !v.tier || v.tier.includes(hardwareInfo.hardwareTier)) + .map((v) => ({ ...v, family: family.family })), + ); + + res.json({ hardwareInfo, recommendedModels }); + } catch { + res.status(500).json({ error: "Failed to load hardware recommendations" }); + } + }); + + return router; +} diff --git a/server/src/routes/nexus-settings.ts b/server/src/routes/nexus-settings.ts new file mode 100644 index 00000000..9cb3ff36 --- /dev/null +++ b/server/src/routes/nexus-settings.ts @@ -0,0 +1,39 @@ +import { Router } from "express"; +import { assertBoard } from "./authz.js"; +import { nexusSettingsService } from "../services/nexus-settings.js"; + +export function nexusSettingsRoutes(): Router { + const router = Router(); + + router.get("/nexus/settings", async (req, res) => { + try { + assertBoard(req); + const settings = await nexusSettingsService().get(); + res.json(settings); + } catch (err: unknown) { + if (err && typeof err === "object" && "status" in err) { + const e = err as { status: number; message: string }; + res.status(e.status).json({ error: e.message }); + return; + } + res.status(500).json({ error: "Unexpected error" }); + } + }); + + router.patch("/nexus/settings", async (req, res) => { + try { + assertBoard(req); + const updated = await nexusSettingsService().set(req.body); + res.json(updated); + } catch (err: unknown) { + if (err && typeof err === "object" && "status" in err) { + const e = err as { status: number; message: string }; + res.status(e.status).json({ error: e.message }); + return; + } + res.status(500).json({ error: "Unexpected error" }); + } + }); + + return router; +}