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
This commit is contained in:
kaitranntt
2025-12-25 16:50:55 -05:00
parent 25f0ddb9dd
commit 9722e1905d
5 changed files with 141 additions and 5 deletions
+36
View File
@@ -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<string, ProfileMetadata> {
const legacyProfiles = this.getAllProfiles();
const unifiedAccounts = this.getAllAccountsUnified();
// Start with legacy profiles
const merged: Record<string, ProfileMetadata> = { ...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;
+17
View File
@@ -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');
+41 -2
View File
@@ -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)) {
+37 -1
View File
@@ -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)) {
+10 -2
View File
@@ -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) {