mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
231 lines
7.3 KiB
TypeScript
231 lines
7.3 KiB
TypeScript
import type { Express, RequestHandler } from "express";
|
|
import type { AuthenticatedRequest } from "../../../src/types/index.js";
|
|
import { createCurrentHostMetricsPreferenceRepository } from "../../../src/backend/database/repositories/factory.js";
|
|
import { statsLogger } from "../../../src/backend/utils/logger.js";
|
|
import {
|
|
deriveEnabledWidgets,
|
|
defaultLayoutFromWidgets,
|
|
type HostMetricsLayout,
|
|
} from "../../../src/types/host-metrics.js";
|
|
import type { HostAction } from "../../../src/backend/utils/permission-manager.js";
|
|
|
|
interface PrefStatsConfig {
|
|
enabledWidgets?: string[];
|
|
}
|
|
|
|
interface PrefHost {
|
|
id: number;
|
|
userId: string;
|
|
statsConfig?: unknown;
|
|
}
|
|
|
|
type HostMetricsPreferencesRoutesDeps = {
|
|
validateHostId: RequestHandler;
|
|
fetchHostById: (hostId: number, userId: string) => Promise<PrefHost | null>;
|
|
parseStatsConfig: (statsConfig: unknown) => PrefStatsConfig;
|
|
canAccessHost: (
|
|
userId: string,
|
|
hostId: number,
|
|
level: HostAction,
|
|
) => Promise<boolean>;
|
|
};
|
|
|
|
function sanitizeLayout(input: unknown): HostMetricsLayout | null {
|
|
if (!input || typeof input !== "object") return null;
|
|
const obj = input as Record<string, unknown>;
|
|
if (!Array.isArray(obj.slots)) return null;
|
|
const columns =
|
|
typeof obj.columns === "number" && obj.columns >= 1 && obj.columns <= 4
|
|
? Math.round(obj.columns)
|
|
: 3;
|
|
const slots = obj.slots
|
|
.filter((s): s is Record<string, unknown> => !!s && typeof s === "object")
|
|
.map((s, i) => ({
|
|
id: String(s.id),
|
|
order: typeof s.order === "number" ? s.order : i,
|
|
colSpan:
|
|
s.colSpan === 1 || s.colSpan === 2 || s.colSpan === 3 ? s.colSpan : 1,
|
|
height:
|
|
typeof s.height === "number" && s.height > 0
|
|
? Math.round(s.height)
|
|
: null,
|
|
}))
|
|
.filter((s) => s.id && s.id !== "undefined");
|
|
return { slots, columns } as HostMetricsLayout;
|
|
}
|
|
|
|
export function registerHostMetricsPreferencesRoutes(
|
|
app: Express,
|
|
{
|
|
validateHostId,
|
|
fetchHostById,
|
|
parseStatsConfig,
|
|
canAccessHost,
|
|
}: HostMetricsPreferencesRoutesDeps,
|
|
): void {
|
|
/**
|
|
* @openapi
|
|
* /host-metrics/preferences/{id}:
|
|
* get:
|
|
* summary: Get the Host Metrics layout for a host
|
|
* description: Returns the current user's saved card layout for the host, or a default layout derived from the host's enabled widgets when none is saved.
|
|
* tags:
|
|
* - Host Metrics
|
|
* parameters:
|
|
* - in: path
|
|
* name: id
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* responses:
|
|
* 200:
|
|
* description: The layout for this host.
|
|
* 403:
|
|
* description: No access to this host.
|
|
* 404:
|
|
* description: Host not found.
|
|
*/
|
|
app.get("/host-metrics/preferences/:id", validateHostId, async (req, res) => {
|
|
const userId = (req as AuthenticatedRequest).userId;
|
|
const hostId = parseInt(String(req.params.id), 10);
|
|
try {
|
|
if (!(await canAccessHost(userId, hostId, "connect"))) {
|
|
return res.status(403).json({ error: "No access to this host" });
|
|
}
|
|
const host = await fetchHostById(hostId, userId);
|
|
if (!host) {
|
|
return res.status(404).json({ error: "Host not found" });
|
|
}
|
|
|
|
const row =
|
|
await createCurrentHostMetricsPreferenceRepository().findByUserAndHost(
|
|
userId,
|
|
hostId,
|
|
);
|
|
|
|
if (row?.layout) {
|
|
try {
|
|
const parsed = sanitizeLayout(JSON.parse(row.layout));
|
|
if (parsed) return res.json({ layout: parsed });
|
|
} catch {
|
|
// fall through to default
|
|
}
|
|
}
|
|
|
|
const statsConfig = parseStatsConfig(host.statsConfig);
|
|
const layout = defaultLayoutFromWidgets(statsConfig.enabledWidgets ?? []);
|
|
return res.json({ layout });
|
|
} catch (error) {
|
|
statsLogger.error("Failed to fetch host metrics preferences", {
|
|
operation: "host_metrics_prefs_fetch_error",
|
|
hostId,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
return res
|
|
.status(500)
|
|
.json({ error: "Failed to fetch host metrics preferences" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @openapi
|
|
* /host-metrics/preferences/{id}:
|
|
* post:
|
|
* summary: Save the Host Metrics layout for a host
|
|
* description: Persists the current user's card layout for the host and keeps statsConfig.enabledWidgets in sync (for hosts the user owns) so the mobile app keeps working.
|
|
* tags:
|
|
* - Host Metrics
|
|
* parameters:
|
|
* - in: path
|
|
* name: id
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* slots:
|
|
* type: array
|
|
* columns:
|
|
* type: integer
|
|
* responses:
|
|
* 200:
|
|
* description: Layout saved.
|
|
* 400:
|
|
* description: Invalid layout.
|
|
* 403:
|
|
* description: No access to this host.
|
|
*/
|
|
app.post(
|
|
"/host-metrics/preferences/:id",
|
|
validateHostId,
|
|
async (req, res) => {
|
|
const userId = (req as AuthenticatedRequest).userId;
|
|
const hostId = parseInt(String(req.params.id), 10);
|
|
try {
|
|
if (!(await canAccessHost(userId, hostId, "connect"))) {
|
|
return res.status(403).json({ error: "No access to this host" });
|
|
}
|
|
const host = await fetchHostById(hostId, userId);
|
|
if (!host) {
|
|
return res.status(404).json({ error: "Host not found" });
|
|
}
|
|
|
|
const layout = sanitizeLayout(req.body);
|
|
if (!layout) {
|
|
return res.status(400).json({ error: "Invalid layout" });
|
|
}
|
|
|
|
const now = new Date().toISOString();
|
|
const layoutJson = JSON.stringify(layout);
|
|
|
|
await createCurrentHostMetricsPreferenceRepository().upsertLayout(
|
|
userId,
|
|
hostId,
|
|
layoutJson,
|
|
now,
|
|
);
|
|
|
|
// Keep statsConfig.enabledWidgets in sync (mobile contract) for hosts the
|
|
// user owns. stats_config is plain JSON text (not an encrypted field).
|
|
if (host.userId === userId) {
|
|
try {
|
|
const current = parseStatsConfig(host.statsConfig);
|
|
const merged = {
|
|
...current,
|
|
enabledWidgets: deriveEnabledWidgets(layout.slots),
|
|
};
|
|
await createCurrentHostMetricsPreferenceRepository().updateHostStatsConfig(
|
|
userId,
|
|
hostId,
|
|
JSON.stringify(merged),
|
|
);
|
|
} catch (syncErr) {
|
|
statsLogger.warn("Failed to sync enabledWidgets from layout", {
|
|
operation: "host_metrics_prefs_sync_widgets",
|
|
hostId,
|
|
error:
|
|
syncErr instanceof Error ? syncErr.message : String(syncErr),
|
|
});
|
|
}
|
|
}
|
|
|
|
return res.json({ success: true });
|
|
} catch (error) {
|
|
statsLogger.error("Failed to save host metrics preferences", {
|
|
operation: "host_metrics_prefs_save_error",
|
|
hostId,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
return res
|
|
.status(500)
|
|
.json({ error: "Failed to save host metrics preferences" });
|
|
}
|
|
},
|
|
);
|
|
}
|