mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-14 12:25:27 +00:00
feat(analytics): add 24H hourly chart with caching and UI improvements
- Add GitHub link button next to connection status for quick issue reporting - Add 24H button on analytics page with hourly granularity chart - Add /api/usage/hourly endpoint with date range filtering - Add hourly data aggregation and caching (disk + memory) - Fix timezone display: convert UTC hours to local time in chart - Fix CLIProxy Stats card loading state synchronization - Bump disk cache version to 3 (includes hourly data)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { initUI, header, color, dim, errorBox } from './ui';
|
||||
import { ERROR_CODES, getErrorDocUrl, ErrorCode } from './error-codes';
|
||||
import { getPortCheckCommand, getKillPidCommand } from './platform-commands';
|
||||
|
||||
/**
|
||||
* Error types with structured messages (Legacy - kept for compatibility)
|
||||
@@ -214,6 +215,7 @@ export class ErrorManager {
|
||||
*/
|
||||
static async showPortConflict(port: number): Promise<void> {
|
||||
await initUI();
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
console.error('');
|
||||
console.error(
|
||||
@@ -224,13 +226,19 @@ export class ErrorManager {
|
||||
console.error(header('SOLUTIONS'));
|
||||
console.error('');
|
||||
console.error(' 1. Find process using port:');
|
||||
console.error(` ${color(`lsof -i :${port}`, 'command')} (macOS/Linux)`);
|
||||
console.error(` ${color(`netstat -ano | findstr ${port}`, 'command')} (Windows)`);
|
||||
console.error(` ${color(getPortCheckCommand(port), 'command')}`);
|
||||
console.error('');
|
||||
console.error(' 2. Kill the process:');
|
||||
console.error(` ${color(`lsof -ti:${port} | xargs kill -9`, 'command')}`);
|
||||
if (isWindows) {
|
||||
console.error(
|
||||
` ${color(`taskkill /F /PID <PID>`, 'command')} (replace <PID> with actual ID)`
|
||||
);
|
||||
} else {
|
||||
console.error(` ${color(getKillPidCommand(12345).replace('12345', '<PID>'), 'command')}`);
|
||||
}
|
||||
console.error('');
|
||||
console.error(' 3. Wait and retry (process may exit on its own)');
|
||||
console.error(' 3. Auto-fix: Run:');
|
||||
console.error(` ${color('ccs doctor --fix', 'command')}`);
|
||||
console.error('');
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Platform-aware command suggestions and utilities
|
||||
*
|
||||
* Provides OS-specific commands for troubleshooting messages
|
||||
* to help non-technical users on Windows, macOS, and Linux.
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
const isMac = process.platform === 'darwin';
|
||||
|
||||
/**
|
||||
* Get platform-specific command to check what's using a port
|
||||
*/
|
||||
export function getPortCheckCommand(port: number): string {
|
||||
if (isWindows) {
|
||||
return `netstat -ano | findstr :${port}`;
|
||||
}
|
||||
return `lsof -i :${port}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platform-specific command to view file contents
|
||||
*/
|
||||
export function getCatCommand(filePath: string): string {
|
||||
if (isWindows) {
|
||||
// Use type for CMD, Get-Content for PowerShell
|
||||
return `type "${filePath}"`;
|
||||
}
|
||||
return `cat "${filePath}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platform-specific command to kill CLIProxy processes
|
||||
*/
|
||||
export function getKillCLIProxyCommand(): string {
|
||||
if (isWindows) {
|
||||
return 'taskkill /F /IM cli-proxy-api.exe';
|
||||
}
|
||||
return 'pkill -f cli-proxy-api';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platform-specific command to kill a process by PID
|
||||
*/
|
||||
export function getKillPidCommand(pid: number): string {
|
||||
if (isWindows) {
|
||||
return `taskkill /F /PID ${pid}`;
|
||||
}
|
||||
return `kill -9 ${pid}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-friendly platform name
|
||||
*/
|
||||
export function getPlatformName(): string {
|
||||
if (isWindows) return 'Windows';
|
||||
if (isMac) return 'macOS';
|
||||
return 'Linux';
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill process by PID (cross-platform)
|
||||
* @returns true if killed successfully, false otherwise
|
||||
*/
|
||||
export function killProcessByPid(pid: number, verbose = false): boolean {
|
||||
try {
|
||||
if (isWindows) {
|
||||
execSync(`taskkill /F /PID ${pid}`, { stdio: 'pipe' });
|
||||
} else {
|
||||
execSync(`kill -9 ${pid}`, { stdio: 'pipe' });
|
||||
}
|
||||
if (verbose) {
|
||||
console.error(`[cleanup] Killed process ${pid}`);
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill all CLIProxy processes (cross-platform)
|
||||
* @returns number of processes killed
|
||||
*/
|
||||
export function killAllCLIProxyProcesses(verbose = false): number {
|
||||
let killed = 0;
|
||||
|
||||
try {
|
||||
if (isWindows) {
|
||||
// Windows: taskkill by image name
|
||||
// Use /T to kill child processes too
|
||||
execSync('taskkill /F /IM cli-proxy-api.exe /T 2>nul', { stdio: 'pipe' });
|
||||
killed++;
|
||||
} else {
|
||||
// Unix: pkill with pattern matching
|
||||
try {
|
||||
execSync('pkill -9 -f cli-proxy-api', { stdio: 'pipe' });
|
||||
killed++;
|
||||
} catch {
|
||||
// pkill returns non-zero if no processes matched - that's OK
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// No processes to kill or command failed
|
||||
}
|
||||
|
||||
if (verbose && killed > 0) {
|
||||
console.error(`[cleanup] Killed ${killed} CLIProxy process(es)`);
|
||||
}
|
||||
|
||||
return killed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill process on specific port (cross-platform)
|
||||
* @returns true if a process was killed, false otherwise
|
||||
*/
|
||||
export function killProcessOnPort(port: number, verbose = false): boolean {
|
||||
try {
|
||||
if (isWindows) {
|
||||
// Windows: netstat + taskkill
|
||||
const result = execSync(`netstat -ano | findstr :${port}`, {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
const lines = result.trim().split('\n');
|
||||
let killed = false;
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
const pid = parts[parts.length - 1];
|
||||
if (pid && /^\d+$/.test(pid)) {
|
||||
try {
|
||||
execSync(`taskkill /F /PID ${pid}`, { stdio: 'pipe' });
|
||||
if (verbose) {
|
||||
console.error(`[cleanup] Killed process ${pid} on port ${port}`);
|
||||
}
|
||||
killed = true;
|
||||
} catch {
|
||||
// Process may have already exited
|
||||
}
|
||||
}
|
||||
}
|
||||
return killed;
|
||||
} else {
|
||||
// Unix: lsof + kill
|
||||
const result = execSync(`lsof -ti:${port}`, { encoding: 'utf-8', stdio: 'pipe' });
|
||||
const pids = result
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((p) => p);
|
||||
for (const pid of pids) {
|
||||
try {
|
||||
execSync(`kill -9 ${pid}`, { stdio: 'pipe' });
|
||||
if (verbose) {
|
||||
console.error(`[cleanup] Killed process ${pid} on port ${port}`);
|
||||
}
|
||||
} catch {
|
||||
// Process may have already exited
|
||||
}
|
||||
}
|
||||
return pids.length > 0;
|
||||
}
|
||||
} catch {
|
||||
// No process on port or command failed
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getPortCheckCommand,
|
||||
getCatCommand,
|
||||
getKillCLIProxyCommand,
|
||||
getKillPidCommand,
|
||||
getPlatformName,
|
||||
killProcessByPid,
|
||||
killAllCLIProxyProcesses,
|
||||
killProcessOnPort,
|
||||
};
|
||||
Reference in New Issue
Block a user