From 8d7845d67fb156671713888726d631415d0f4f9c Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 25 Dec 2025 14:25:40 -0500 Subject: [PATCH 1/3] fix(dashboard): read accounts from unified config Dashboard accounts page was only reading from profiles.json, ignoring accounts stored in config.yaml when in unified mode. This caused accounts created via CLI to be invisible in the dashboard. - Use ProfileRegistry instead of direct file access - Merge legacy and unified accounts (unified takes precedence) - Set default via unified config when in unified mode Fixes #203 --- src/web-server/routes/account-routes.ts | 69 +++++++++++++++---------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/src/web-server/routes/account-routes.ts b/src/web-server/routes/account-routes.ts index 848383f5..6b512b3f 100644 --- a/src/web-server/routes/account-routes.ts +++ b/src/web-server/routes/account-routes.ts @@ -1,38 +1,57 @@ /** - * Account Routes - CRUD operations for Claude accounts (profiles.json) + * Account Routes - CRUD operations for Claude accounts * - * Separated from profile-routes.ts to avoid dual-mounting conflicts. + * Uses ProfileRegistry to read from both legacy (profiles.json) + * and unified config (config.yaml) for consistent data with CLI. */ import { Router, Request, Response } from 'express'; -import * as fs from 'fs'; -import * as path from 'path'; -import { getCcsDir } from '../../utils/config-manager'; +import ProfileRegistry from '../../auth/profile-registry'; +import { isUnifiedMode } from '../../config/unified-config-loader'; const router = Router(); +const registry = new ProfileRegistry(); /** - * GET /api/accounts - List accounts from profiles.json + * GET /api/accounts - List accounts from both profiles.json and config.yaml */ router.get('/', (_req: Request, res: Response): void => { try { - const profilesPath = path.join(getCcsDir(), 'profiles.json'); + // Get profiles from both legacy and unified config (same logic as CLI) + const legacyProfiles = registry.getAllProfiles(); + const unifiedAccounts = registry.getAllAccountsUnified(); - if (!fs.existsSync(profilesPath)) { - res.json({ accounts: [], default: null }); - return; + // Merge profiles: unified config takes precedence + const merged: Record = {}; + + // Add legacy profiles first + for (const [name, meta] of Object.entries(legacyProfiles)) { + merged[name] = { + type: meta.type || 'account', + created: meta.created, + last_used: meta.last_used || null, + }; } - const data = JSON.parse(fs.readFileSync(profilesPath, 'utf8')); - const accounts = Object.entries(data.profiles || {}).map(([name, meta]) => { - const metadata = meta as Record; - return { - name, - ...metadata, + // 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, }; - }); + } - res.json({ accounts, default: data.default || null }); + // Convert to array format + const accounts = Object.entries(merged).map(([name, meta]) => ({ + name, + ...meta, + })); + + // Get default from unified config first, fallback to legacy + const defaultProfile = registry.getDefaultUnified() ?? registry.getDefaultProfile() ?? null; + + res.json({ accounts, default: defaultProfile }); } catch (error) { res.status(500).json({ error: (error as Error).message }); } @@ -50,14 +69,12 @@ router.post('/default', (req: Request, res: Response): void => { return; } - const profilesPath = path.join(getCcsDir(), 'profiles.json'); - - const data = fs.existsSync(profilesPath) - ? JSON.parse(fs.readFileSync(profilesPath, 'utf8')) - : { profiles: {} }; - - data.default = name; - fs.writeFileSync(profilesPath, JSON.stringify(data, null, 2) + '\n'); + // Use unified config if in unified mode, otherwise use legacy + if (isUnifiedMode()) { + registry.setDefaultUnified(name); + } else { + registry.setDefaultProfile(name); + } res.json({ default: name }); } catch (error) { From 25f0ddb9ddb19f9eb75c880b7c878d840cb2a494 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 25 Dec 2025 14:35:31 -0500 Subject: [PATCH 2/3] fix(dashboard): support unified config in overview and file watcher - overview-routes: Use ProfileRegistry for account count, merge both legacy and unified sources - file-watcher: Add config.yaml to watch list for real-time updates in unified mode Related to #203 --- src/web-server/file-watcher.ts | 5 +++-- src/web-server/overview-routes.ts | 17 ++++++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/web-server/file-watcher.ts b/src/web-server/file-watcher.ts index a57bce39..90de9027 100644 --- a/src/web-server/file-watcher.ts +++ b/src/web-server/file-watcher.ts @@ -22,7 +22,8 @@ export function createFileWatcher(onChange: FileChangeCallback): FSWatcher { const watcher = chokidar.watch( [ - path.join(ccsDir, 'config.json'), + path.join(ccsDir, 'config.json'), // Legacy config + path.join(ccsDir, 'config.yaml'), // Unified config path.join(ccsDir, '*.settings.json'), path.join(ccsDir, 'profiles.json'), path.join(ccsDir, 'cliproxy', 'sessions.json'), // Proxy session tracking @@ -41,7 +42,7 @@ export function createFileWatcher(onChange: FileChangeCallback): FSWatcher { const basename = path.basename(filePath); let type: FileChangeEvent['type']; - if (basename === 'config.json') { + if (basename === 'config.json' || basename === 'config.yaml') { type = 'config-changed'; } else if (basename === 'profiles.json') { type = 'profiles-changed'; diff --git a/src/web-server/overview-routes.ts b/src/web-server/overview-routes.ts index 0ddaf1da..b2cd6525 100644 --- a/src/web-server/overview-routes.ts +++ b/src/web-server/overview-routes.ts @@ -5,12 +5,11 @@ */ import { Router, Request, Response } from 'express'; -import * as fs from 'fs'; -import * as path from 'path'; -import { getCcsDir, loadConfig } from '../utils/config-manager'; +import { loadConfig } from '../utils/config-manager'; import { runHealthChecks } from './health-service'; import { getAllAuthStatus, initializeAccounts } from '../cliproxy/auth-handler'; import { getVersion } from '../utils/version'; +import ProfileRegistry from '../auth/profile-registry'; export const overviewRoutes = Router(); @@ -64,12 +63,16 @@ overviewRoutes.get('/', async (_req: Request, res: Response) => { function getAccountCount(): number { try { - const profilesPath = path.join(getCcsDir(), 'profiles.json'); + const registry = new ProfileRegistry(); - if (!fs.existsSync(profilesPath)) return 0; + // Merge legacy (profiles.json) and unified (config.yaml) accounts + const legacyProfiles = registry.getAllProfiles(); + const unifiedAccounts = registry.getAllAccountsUnified(); - const data = JSON.parse(fs.readFileSync(profilesPath, 'utf8')); - return Object.keys(data.profiles || {}).length; + // Count unique accounts (unified takes precedence for duplicates) + const allNames = new Set([...Object.keys(legacyProfiles), ...Object.keys(unifiedAccounts)]); + + return allNames.size; } catch { return 0; } From 9722e1905dd25b9dd4d602e860ba36586db043b9 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 25 Dec 2025 16:50:55 -0500 Subject: [PATCH 3/3] 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) {