/**
* Iris MCP Dashboard - Config API Routes
* GET/PUT endpoints for configuration management
*/
import { Router } from "express";
import { readFileSync, writeFileSync } from "fs";
import { z } from "zod";
import { parseDocument, stringify } from "yaml";
import type { DashboardStateBridge } from "../state-bridge.js";
import { getChildLogger } from "../../../utils/logger.js";
import { getConfigPath } from "../../../utils/paths.js";
const logger = getChildLogger("dashboard:routes:config");
const router = Router();
// Zod schema for config validation (same as TeamsConfigSchema)
const IrisConfigSchema = z.object({
path: z.string().min(1, "Path cannot be empty"),
description: z.string(),
idleTimeout: z.number().positive().optional(),
sessionInitTimeout: z.number().positive().optional(),
color: z
.string()
.regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color")
.optional(),
});
const ConfigSchema = z.object({
settings: z.object({
idleTimeout: z.number().positive(),
maxProcesses: z.number().int().min(1).max(50),
healthCheckInterval: z.number().positive(),
sessionInitTimeout: z.number().positive(),
httpPort: z.number().int().min(1).max(65535).optional(),
defaultTransport: z.enum(["stdio", "http"]).optional(),
}),
dashboard: z
.object({
enabled: z.boolean(),
port: z.number().int().min(1).max(65535),
host: z.string(),
})
.optional(),
teams: z.record(z.string(), IrisConfigSchema),
});
export function createConfigRouter(bridge: DashboardStateBridge): Router {
/**
* GET /api/config
* Returns current configuration
*/
router.get("/", (req, res) => {
try {
const config = bridge.getConfig();
res.json({
success: true,
config,
});
} catch (error: any) {
logger.error(
{
err: error instanceof Error ? error : new Error(String(error)),
},
"Failed to get config",
);
res.status(500).json({
success: false,
error: error.message || "Failed to retrieve configuration",
});
}
});
/**
* PUT /api/config
* Saves new configuration to disk
* Does NOT apply changes (requires restart)
*/
router.put("/", (req, res) => {
try {
// Validate request body
const validation = ConfigSchema.safeParse(req.body);
if (!validation.success) {
const errors = validation.error.errors.map((e) => ({
path: e.path.join("."),
message: e.message,
}));
logger.warn({ errors }, "Config validation failed");
return res.status(400).json({
success: false,
error: "Configuration validation failed",
details: errors,
});
}
const newConfig = validation.data;
// Get config file path
const configPath = getConfigPath();
// Read existing config to preserve comments
const existingContent = readFileSync(configPath, "utf8");
const doc = parseDocument(existingContent);
// Update document with new values (preserving comments)
// Note: This is a simple implementation - in production we'd want to
// surgically update only changed values to preserve all formatting
const yamlContent = stringify(newConfig, {
defaultStringType: "QUOTE_DOUBLE",
defaultKeyType: "PLAIN",
});
// For now, just write the new YAML (will lose comments)
// TODO Phase 2: Implement surgical update to preserve all comments
writeFileSync(configPath, yamlContent, "utf8");
logger.info({ configPath }, "Configuration saved to disk");
// Emit event for WebSocket clients
bridge.emit("ws:config-saved", {
timestamp: Date.now(),
configPath,
});
res.json({
success: true,
message: "Configuration saved. Restart Iris MCP to apply changes.",
configPath,
});
} catch (error: any) {
logger.error(
{
err: error instanceof Error ? error : new Error(String(error)),
},
"Failed to save config",
);
res.status(500).json({
success: false,
error: error.message || "Failed to save configuration",
});
}
});
return router;
}