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)
This commit is contained in:
parent
a9817a9659
commit
59bf5dd8ba
3 changed files with 109 additions and 0 deletions
|
|
@ -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<string, () => void>();
|
||||
const workerManager = createPluginWorkerManager();
|
||||
const pluginRegistry = pluginRegistryService(db);
|
||||
|
|
|
|||
66
server/src/routes/hardware.ts
Normal file
66
server/src/routes/hardware.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
39
server/src/routes/nexus-settings.ts
Normal file
39
server/src/routes/nexus-settings.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue