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 { instanceSettingsRoutes } from "./routes/instance-settings.js";
|
||||||
import { ollamaRoutes } from "./routes/ollama.js";
|
import { ollamaRoutes } from "./routes/ollama.js";
|
||||||
import { llmRoutes } from "./routes/llms.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 { assetRoutes } from "./routes/assets.js";
|
||||||
import { accessRoutes } from "./routes/access.js";
|
import { accessRoutes } from "./routes/access.js";
|
||||||
import { pluginRoutes } from "./routes/plugins.js";
|
import { pluginRoutes } from "./routes/plugins.js";
|
||||||
|
|
@ -136,6 +138,7 @@ export async function createApp(
|
||||||
app.all("/api/auth/*authPath", opts.betterAuthHandler);
|
app.all("/api/auth/*authPath", opts.betterAuthHandler);
|
||||||
}
|
}
|
||||||
app.use(llmRoutes(db));
|
app.use(llmRoutes(db));
|
||||||
|
app.use("/api", hardwareRoutes());
|
||||||
|
|
||||||
// Mount API routes
|
// Mount API routes
|
||||||
const api = Router();
|
const api = Router();
|
||||||
|
|
@ -168,6 +171,7 @@ export async function createApp(
|
||||||
api.use(dashboardRoutes(db));
|
api.use(dashboardRoutes(db));
|
||||||
api.use(sidebarBadgeRoutes(db));
|
api.use(sidebarBadgeRoutes(db));
|
||||||
api.use(instanceSettingsRoutes(db));
|
api.use(instanceSettingsRoutes(db));
|
||||||
|
api.use(nexusSettingsRoutes());
|
||||||
const hostServicesDisposers = new Map<string, () => void>();
|
const hostServicesDisposers = new Map<string, () => void>();
|
||||||
const workerManager = createPluginWorkerManager();
|
const workerManager = createPluginWorkerManager();
|
||||||
const pluginRegistry = pluginRegistryService(db);
|
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