Skip to content

Commit 1487f5f

Browse files
authored
Merge pull request #84 from kaitranntt/dev
feat: Introduce Web Dashboard, Analytics, and Multi-Account Support
2 parents ec1ac2e + f8f7ea7 commit 1487f5f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+6656
-463
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
5.15.0-dev.2
1+
5.15.0-dev.4

logs/main.log

Lines changed: 175 additions & 0 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@kaitranntt/ccs",
3-
"version": "5.15.0-dev.2",
3+
"version": "5.15.0-dev.4",
44
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
55
"keywords": [
66
"cli",

scripts/verify-bundle.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
#!/usr/bin/env node
22

33
/**
4-
* Verify UI bundle size is under 1MB gzipped
5-
* React + shadcn/ui dashboard typically ranges 600-900KB
4+
* Verify UI bundle size stays reasonable
5+
*
6+
* Current stack: React + TanStack Query + Radix UI + shadcn/ui + Recharts
7+
* Expected range: 800KB - 1.5MB gzipped for full-featured dashboard
8+
*
9+
* This is a developer tool, not a public-facing site, so we optimize for
10+
* features over minimal bundle size. The limit is a sanity check to catch
11+
* accidental large dependencies, not a hard performance target.
612
*/
713

814
const fs = require('fs');
915
const path = require('path');
1016
const zlib = require('zlib');
1117

1218
const UI_DIR = path.join(__dirname, '../dist/ui');
13-
const MAX_SIZE = 1024 * 1024; // 1MB
19+
const MAX_SIZE = 1.5 * 1024 * 1024; // 1.5MB - reasonable for full-featured React dashboard
1420

1521
function getGzipSize(filePath) {
1622
const content = fs.readFileSync(filePath);
@@ -40,9 +46,10 @@ if (!fs.existsSync(UI_DIR)) {
4046

4147
const totalSize = walkDir(UI_DIR);
4248
const sizeKB = (totalSize / 1024).toFixed(1);
49+
const maxKB = (MAX_SIZE / 1024).toFixed(0);
4350

4451
if (totalSize > MAX_SIZE) {
45-
console.log(`[X] Bundle too large: ${sizeKB}KB gzipped (max: 1024KB)`);
52+
console.log(`[X] Bundle too large: ${sizeKB}KB gzipped (max: ${maxKB}KB)`);
4653
process.exit(1);
4754
} else {
4855
console.log(`[OK] Bundle size: ${sizeKB}KB gzipped`);

src/cliproxy/account-manager.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,54 @@ export function saveAccountsRegistry(registry: AccountsRegistry): void {
130130
});
131131
}
132132

133+
/**
134+
* Sync registry with actual token files
135+
* Removes stale entries where token file no longer exists
136+
* Called automatically when loading accounts
137+
*/
138+
function syncRegistryWithTokenFiles(registry: AccountsRegistry): boolean {
139+
const authDir = getAuthDir();
140+
let modified = false;
141+
142+
for (const [_providerName, providerAccounts] of Object.entries(registry.providers)) {
143+
if (!providerAccounts) continue;
144+
145+
const staleIds: string[] = [];
146+
147+
for (const [accountId, meta] of Object.entries(providerAccounts.accounts)) {
148+
const tokenPath = path.join(authDir, meta.tokenFile);
149+
if (!fs.existsSync(tokenPath)) {
150+
staleIds.push(accountId);
151+
}
152+
}
153+
154+
// Remove stale accounts
155+
for (const id of staleIds) {
156+
delete providerAccounts.accounts[id];
157+
modified = true;
158+
159+
// Update default if deleted
160+
if (providerAccounts.default === id) {
161+
const remainingIds = Object.keys(providerAccounts.accounts);
162+
providerAccounts.default = remainingIds[0] || 'default';
163+
}
164+
}
165+
}
166+
167+
return modified;
168+
}
169+
133170
/**
134171
* Get all accounts for a provider
135172
*/
136173
export function getProviderAccounts(provider: CLIProxyProvider): AccountInfo[] {
137174
const registry = loadAccountsRegistry();
175+
176+
// Sync with actual token files (removes stale entries)
177+
if (syncRegistryWithTokenFiles(registry)) {
178+
saveAccountsRegistry(registry);
179+
}
180+
138181
const providerAccounts = registry.providers[provider];
139182

140183
if (!providerAccounts) {

src/cliproxy/config-generator.ts

Lines changed: 126 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,17 @@ interface ProviderSettings {
2323
export const CLIPROXY_DEFAULT_PORT = 8317;
2424

2525
/** Internal API key for CCS-managed requests */
26-
const CCS_INTERNAL_API_KEY = 'ccs-internal-managed';
26+
export const CCS_INTERNAL_API_KEY = 'ccs-internal-managed';
27+
28+
/** Simple secret key for Control Panel login (user-facing) */
29+
export const CCS_CONTROL_PANEL_SECRET = 'ccs';
30+
31+
/**
32+
* Config version - bump when config format changes to trigger regeneration
33+
* v1: Initial config (port, auth-dir, api-keys only)
34+
* v2: Full-featured config with dashboard, quota mgmt, simplified key
35+
*/
36+
export const CLIPROXY_CONFIG_VERSION = 2;
2737

2838
/** Provider display names (static metadata) */
2939
const PROVIDER_DISPLAY_NAMES: Record<CLIProxyProvider, string> = {
@@ -112,26 +122,68 @@ export function getBinDir(): string {
112122
function generateUnifiedConfigContent(port: number = CLIPROXY_DEFAULT_PORT): string {
113123
const authDir = getAuthDir(); // Base auth dir - CLIProxyAPI scans subdirectories
114124

115-
// Unified config with all providers
116-
const config = `# CLIProxyAPI unified config generated by CCS
117-
# Supports: gemini, codex, agy, qwen (concurrent usage)
125+
// Unified config with enhanced CLIProxyAPI features
126+
const config = `# CLIProxyAPI config generated by CCS v${CLIPROXY_CONFIG_VERSION}
127+
# Supports: gemini, codex, agy, qwen, iflow (concurrent usage)
118128
# Generated: ${new Date().toISOString()}
129+
#
130+
# This config is auto-managed by CCS. Manual edits may be overwritten.
131+
# Use 'ccs doctor' to regenerate with latest settings.
132+
133+
# =============================================================================
134+
# Server Settings
135+
# =============================================================================
119136
120137
port: ${port}
121138
debug: false
122-
logging-to-file: false
123-
usage-statistics-enabled: false
124139
125-
# CCS internal authentication
140+
# =============================================================================
141+
# Logging
142+
# =============================================================================
143+
144+
# Write logs to file (stored in ~/.ccs/cliproxy/logs/)
145+
logging-to-file: true
146+
147+
# Log individual API requests for debugging/analytics
148+
request-log: true
149+
150+
# =============================================================================
151+
# Dashboard & Management
152+
# =============================================================================
153+
154+
# Enable usage statistics for CCS dashboard analytics
155+
usage-statistics-enabled: true
156+
157+
# Remote management API for CCS dashboard integration
158+
remote-management:
159+
allow-remote: true
160+
secret-key: "${CCS_CONTROL_PANEL_SECRET}"
161+
disable-control-panel: false
162+
163+
# =============================================================================
164+
# Reliability & Quota Management
165+
# =============================================================================
166+
167+
# Auto-retry on transient errors (403, 408, 500, 502, 503, 504)
168+
request-retry: 0
169+
max-retry-interval: 0
170+
171+
# Auto-switch accounts on quota exceeded (429)
172+
# This enables seamless multi-account rotation when rate limited
173+
quota-exceeded:
174+
switch-project: true
175+
switch-preview-model: true
176+
177+
# =============================================================================
178+
# Authentication
179+
# =============================================================================
180+
181+
# API keys for CCS internal requests
126182
api-keys:
127183
- "${CCS_INTERNAL_API_KEY}"
128184
129-
# OAuth tokens stored in auth/ directory
130-
# CLIProxyAPI auto-discovers auth files in subdirectories
131-
auth-dir: "${authDir.replace(/\\/g, '/')}"
132-
133-
# All providers configured - routes by model name
134-
# No provider-specific sections needed - OAuth auth files provide credentials
185+
# OAuth tokens directory (auto-discovered by CLIProxyAPI)
186+
auth-dir: "${authDir.replace(/\\\\/g, '/')}"
135187
`;
136188

137189
return config;
@@ -162,6 +214,67 @@ export function generateConfig(
162214
return configPath;
163215
}
164216

217+
/**
218+
* Force regenerate config.yaml with latest settings
219+
* Deletes existing config and creates fresh one with current port
220+
* @returns Path to new config file
221+
*/
222+
export function regenerateConfig(port: number = CLIPROXY_DEFAULT_PORT): string {
223+
const configPath = getConfigPath();
224+
225+
// Read existing port if config exists (preserve user's port choice)
226+
let effectivePort = port;
227+
if (fs.existsSync(configPath)) {
228+
try {
229+
const content = fs.readFileSync(configPath, 'utf-8');
230+
const portMatch = content.match(/^port:\s*(\d+)/m);
231+
if (portMatch) {
232+
effectivePort = parseInt(portMatch[1], 10);
233+
}
234+
} catch {
235+
// Use default port if reading fails
236+
}
237+
// Delete existing config
238+
fs.unlinkSync(configPath);
239+
}
240+
241+
// Ensure directories exist
242+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
243+
fs.mkdirSync(getAuthDir(), { recursive: true, mode: 0o700 });
244+
245+
// Generate fresh config
246+
const configContent = generateUnifiedConfigContent(effectivePort);
247+
fs.writeFileSync(configPath, configContent, { mode: 0o600 });
248+
249+
return configPath;
250+
}
251+
252+
/**
253+
* Check if config needs regeneration (version mismatch)
254+
* @returns true if config should be regenerated
255+
*/
256+
export function configNeedsRegeneration(): boolean {
257+
const configPath = getConfigPath();
258+
if (!fs.existsSync(configPath)) {
259+
return false; // Will be created on first use
260+
}
261+
262+
try {
263+
const content = fs.readFileSync(configPath, 'utf-8');
264+
265+
// Check for version marker
266+
const versionMatch = content.match(/CCS v(\d+)/);
267+
if (!versionMatch) {
268+
return true; // No version marker = old config
269+
}
270+
271+
const configVersion = parseInt(versionMatch[1], 10);
272+
return configVersion < CLIPROXY_CONFIG_VERSION;
273+
} catch {
274+
return true; // Error reading = regenerate
275+
}
276+
}
277+
165278
/**
166279
* Check if config exists for provider
167280
*/

src/cliproxy/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export {
4848
// Config generation
4949
export {
5050
generateConfig,
51+
regenerateConfig,
52+
configNeedsRegeneration,
5153
getClaudeEnvVars,
5254
getEffectiveEnvVars,
5355
getProviderSettingsPath,
@@ -62,6 +64,7 @@ export {
6264
configExists,
6365
deleteConfig,
6466
CLIPROXY_DEFAULT_PORT,
67+
CLIPROXY_CONFIG_VERSION,
6568
} from './config-generator';
6669

6770
// Base config loader (for reading config/base-*.settings.json)
@@ -98,3 +101,23 @@ export {
98101
getProviderTokenDir,
99102
displayAuthStatus,
100103
} from './auth-handler';
104+
105+
// Stats fetcher
106+
export type { CliproxyStats } from './stats-fetcher';
107+
export { fetchCliproxyStats, isCliproxyRunning } from './stats-fetcher';
108+
109+
// OpenAI compatibility layer
110+
export type { OpenAICompatProvider, OpenAICompatModel } from './openai-compat-manager';
111+
export {
112+
listOpenAICompatProviders,
113+
getOpenAICompatProvider,
114+
addOpenAICompatProvider,
115+
updateOpenAICompatProvider,
116+
removeOpenAICompatProvider,
117+
OPENROUTER_TEMPLATE,
118+
TOGETHER_TEMPLATE,
119+
} from './openai-compat-manager';
120+
121+
// Service manager (background CLIProxy for dashboard)
122+
export type { ServiceStartResult } from './service-manager';
123+
export { ensureCliproxyService, stopCliproxyService, getServiceStatus } from './service-manager';

0 commit comments

Comments
 (0)