From 9722e1905dd25b9dd4d602e860ba36586db043b9 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 25 Dec 2025 16:50:55 -0500 Subject: [PATCH] fix(dashboard): support unified config across health checks and settings - health-service.ts: respect isUnifiedMode() in fixHealthIssue() - config-checks.ts: check config.yaml in unified mode - profile-checks.ts: read profiles from unified config - settings-routes.ts: use atomic writes (temp+rename) for presets - profile-registry.ts: add DRY helpers getAllProfilesMerged(), getDefaultResolved() Closes #203 --- src/auth/profile-registry.ts | 36 ++++++++++++++++++++ src/web-server/health-service.ts | 17 ++++++++++ src/web-server/health/config-checks.ts | 43 ++++++++++++++++++++++-- src/web-server/health/profile-checks.ts | 38 ++++++++++++++++++++- src/web-server/routes/settings-routes.ts | 12 +++++-- 5 files changed, 141 insertions(+), 5 deletions(-) diff --git a/src/auth/profile-registry.ts b/src/auth/profile-registry.ts index e0013e9c..151781ee 100644 --- a/src/auth/profile-registry.ts +++ b/src/auth/profile-registry.ts @@ -332,6 +332,42 @@ export class ProfileRegistry { config.accounts[name].last_used = new Date().toISOString(); saveUnifiedConfig(config); } + + // ========================================== + // DRY Helper Methods (consolidated logic) + // ========================================== + + /** + * Get all profiles merged from both legacy and unified config. + * Unified config takes precedence for duplicate names. + * DRY helper to consolidate merge logic used in multiple places. + */ + getAllProfilesMerged(): Record { + const legacyProfiles = this.getAllProfiles(); + const unifiedAccounts = this.getAllAccountsUnified(); + + // Start with legacy profiles + const merged: Record = { ...legacyProfiles }; + + // Override with unified config accounts (takes precedence) + for (const [name, account] of Object.entries(unifiedAccounts)) { + merged[name] = { + type: 'account', + created: account.created, + last_used: account.last_used, + }; + } + + return merged; + } + + /** + * Get resolved default profile from unified config first, fallback to legacy. + * DRY helper to consolidate default resolution logic. + */ + getDefaultResolved(): string | null { + return this.getDefaultUnified() ?? this.getDefaultProfile(); + } } export default ProfileRegistry; diff --git a/src/web-server/health-service.ts b/src/web-server/health-service.ts index 25696d85..21a8723d 100644 --- a/src/web-server/health-service.ts +++ b/src/web-server/health-service.ts @@ -142,6 +142,17 @@ export function fixHealthIssue(checkId: string): { success: boolean; message: st return { success: true, message: 'Created ~/.ccs directory' }; case 'config-file': { + // Use appropriate config based on unified mode + const { isUnifiedMode } = require('../config/unified-config-loader'); + if (isUnifiedMode()) { + const { + loadOrCreateUnifiedConfig, + saveUnifiedConfig, + } = require('../config/unified-config-loader'); + const config = loadOrCreateUnifiedConfig(); + saveUnifiedConfig(config); + return { success: true, message: 'Created/updated config.yaml' }; + } const configPath = getConfigPath(); fs.mkdirSync(path.dirname(configPath), { recursive: true }); fs.writeFileSync(configPath, JSON.stringify({ profiles: {} }, null, 2) + '\n'); @@ -149,6 +160,12 @@ export function fixHealthIssue(checkId: string): { success: boolean; message: st } case 'profiles-file': { + // Use appropriate storage based on unified mode + const { isUnifiedMode: isUnified } = require('../config/unified-config-loader'); + if (isUnified()) { + // In unified mode, accounts are stored in config.yaml + return { success: true, message: 'Accounts stored in config.yaml (unified mode)' }; + } const profilesPath = path.join(ccsDir, 'profiles.json'); fs.mkdirSync(ccsDir, { recursive: true }); fs.writeFileSync(profilesPath, JSON.stringify({ profiles: {} }, null, 2) + '\n'); diff --git a/src/web-server/health/config-checks.ts b/src/web-server/health/config-checks.ts index 30cc881c..45c0c2c8 100644 --- a/src/web-server/health/config-checks.ts +++ b/src/web-server/health/config-checks.ts @@ -1,18 +1,57 @@ /** * Configuration Health Checks * - * Check config.json, settings files, and Claude settings. + * Check config.json, config.yaml, settings files, and Claude settings. + * Supports both legacy (config.json) and unified (config.yaml) modes. */ import * as fs from 'fs'; import * as path from 'path'; import { getConfigPath } from '../../utils/config-manager'; +import { isUnifiedMode, hasUnifiedConfig } from '../../config/unified-config-loader'; import type { HealthCheck } from './types'; /** - * Check config.json file + * Check config file (config.json or config.yaml based on mode) */ export function checkConfigFile(): HealthCheck { + // In unified mode, check config.yaml + if (isUnifiedMode() || hasUnifiedConfig()) { + const ccsDir = path.join(process.env.HOME || '', '.ccs'); + const yamlPath = path.join(ccsDir, 'config.yaml'); + + if (!fs.existsSync(yamlPath)) { + return { + id: 'config-file', + name: 'config.yaml', + status: 'warning', + message: 'Not found (unified mode)', + details: yamlPath, + fixable: true, + }; + } + + try { + fs.readFileSync(yamlPath, 'utf8'); + return { + id: 'config-file', + name: 'config.yaml', + status: 'ok', + message: 'Valid (unified mode)', + details: yamlPath, + }; + } catch { + return { + id: 'config-file', + name: 'config.yaml', + status: 'error', + message: 'Cannot read file', + details: yamlPath, + }; + } + } + + // Legacy mode: check config.json const configPath = getConfigPath(); if (!fs.existsSync(configPath)) { diff --git a/src/web-server/health/profile-checks.ts b/src/web-server/health/profile-checks.ts index 4c2a30e5..83955611 100644 --- a/src/web-server/health/profile-checks.ts +++ b/src/web-server/health/profile-checks.ts @@ -2,16 +2,52 @@ * Profile Health Checks * * Check profiles, instances, and delegation. + * Supports both legacy (config.json) and unified (config.yaml) modes. */ import * as fs from 'fs'; import * as path from 'path'; +import { isUnifiedMode, loadUnifiedConfig } from '../../config/unified-config-loader'; import type { HealthCheck } from './types'; /** - * Check profiles configuration + * Check profiles configuration (API profiles from config.json or config.yaml) */ export function checkProfiles(ccsDir: string): HealthCheck { + // Check unified config first + if (isUnifiedMode()) { + const config = loadUnifiedConfig(); + if (!config) { + return { + id: 'profiles', + name: 'Profiles', + status: 'info', + message: 'config.yaml not loaded', + }; + } + + const profileCount = Object.keys(config.profiles || {}).length; + const profileNames = Object.keys(config.profiles || {}).join(', '); + + if (profileCount === 0) { + return { + id: 'profiles', + name: 'Profiles', + status: 'ok', + message: 'No API profiles (unified mode)', + }; + } + + return { + id: 'profiles', + name: 'Profiles', + status: 'ok', + message: `${profileCount} configured (unified)`, + details: profileNames.length > 40 ? profileNames.substring(0, 37) + '...' : profileNames, + }; + } + + // Legacy mode: check config.json const configPath = path.join(ccsDir, 'config.json'); if (!fs.existsSync(configPath)) { diff --git a/src/web-server/routes/settings-routes.ts b/src/web-server/routes/settings-routes.ts index 04ec189a..555c3bac 100644 --- a/src/web-server/routes/settings-routes.ts +++ b/src/web-server/routes/settings-routes.ts @@ -222,7 +222,11 @@ router.post('/:profile/presets', (req: Request, res: Response): void => { }; settings.presets.push(preset); - fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n'); + + // Atomic write: temp file + rename + const tempPath = settingsPath + '.tmp'; + fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2) + '\n'); + fs.renameSync(tempPath, settingsPath); res.status(201).json({ preset }); } catch (error) { @@ -250,7 +254,11 @@ router.delete('/:profile/presets/:name', (req: Request, res: Response): void => } settings.presets = settings.presets.filter((p) => p.name !== name); - fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n'); + + // Atomic write: temp file + rename + const tempPath = settingsPath + '.tmp'; + fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2) + '\n'); + fs.renameSync(tempPath, settingsPath); res.json({ success: true }); } catch (error) {