feat: initial commit - Supervisor UI with Next.js 16 and Tailwind CSS 4
Some checks failed
Build and Push Docker Image to Gitea / build-and-push (push) Failing after 1m22s
Some checks failed
Build and Push Docker Image to Gitea / build-and-push (push) Failing after 1m22s
- Modern web interface for Supervisor process management - Built with Next.js 16 (App Router) and Tailwind CSS 4 - Full XML-RPC client implementation for Supervisor API - Real-time process monitoring with auto-refresh - Process control: start, stop, restart operations - Modern dashboard with system status and statistics - Dark/light theme with OKLCH color system - Docker multi-stage build with runtime env var configuration - Gitea CI/CD workflow for automated builds - Comprehensive documentation (README, IMPLEMENTATION, DEPLOYMENT) Features: - Backend proxy pattern for secure API communication - React Query for state management and caching - TypeScript strict mode with Zod validation - Responsive design with mobile support - Health check endpoint for monitoring - Non-root user security in Docker Environment Variables: - SUPERVISOR_HOST, SUPERVISOR_PORT - SUPERVISOR_USERNAME, SUPERVISOR_PASSWORD (optional) - Configurable at build-time and runtime 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
281
lib/supervisor/client.ts
Normal file
281
lib/supervisor/client.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
import * as xmlrpc from 'xmlrpc';
|
||||
import {
|
||||
ProcessInfo,
|
||||
ProcessInfoSchema,
|
||||
SupervisorStateInfo,
|
||||
SupervisorStateInfoSchema,
|
||||
ConfigInfo,
|
||||
ConfigInfoSchema,
|
||||
ReloadConfigResult,
|
||||
ReloadConfigResultSchema,
|
||||
ProcessActionResult,
|
||||
LogTailResult,
|
||||
SystemInfo,
|
||||
} from './types';
|
||||
|
||||
export interface SupervisorClientConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export class SupervisorClient {
|
||||
private client: xmlrpc.Client;
|
||||
private config: SupervisorClientConfig;
|
||||
|
||||
constructor(config: SupervisorClientConfig) {
|
||||
this.config = config;
|
||||
|
||||
const clientOptions: xmlrpc.ClientOptions = {
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
path: '/RPC2',
|
||||
};
|
||||
|
||||
// Add basic auth if credentials provided
|
||||
if (config.username && config.password) {
|
||||
clientOptions.basic_auth = {
|
||||
user: config.username,
|
||||
pass: config.password,
|
||||
};
|
||||
}
|
||||
|
||||
this.client = xmlrpc.createClient(clientOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic method call wrapper with error handling
|
||||
*/
|
||||
private async call<T>(method: string, params: any[] = []): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.methodCall(method, params, (error, value) => {
|
||||
if (error) {
|
||||
reject(new Error(`XML-RPC Error: ${error.message}`));
|
||||
} else {
|
||||
resolve(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ===== System Methods =====
|
||||
|
||||
async getAPIVersion(): Promise<string> {
|
||||
return this.call<string>('supervisor.getAPIVersion');
|
||||
}
|
||||
|
||||
async getSupervisorVersion(): Promise<string> {
|
||||
return this.call<string>('supervisor.getSupervisorVersion');
|
||||
}
|
||||
|
||||
async getIdentification(): Promise<string> {
|
||||
return this.call<string>('supervisor.getIdentification');
|
||||
}
|
||||
|
||||
async getState(): Promise<SupervisorStateInfo> {
|
||||
const result = await this.call<any>('supervisor.getState');
|
||||
return SupervisorStateInfoSchema.parse(result);
|
||||
}
|
||||
|
||||
async getPID(): Promise<number> {
|
||||
return this.call<number>('supervisor.getPID');
|
||||
}
|
||||
|
||||
async getSystemInfo(): Promise<SystemInfo> {
|
||||
const [apiVersion, supervisorVersion, identification, state, pid] = await Promise.all([
|
||||
this.getAPIVersion(),
|
||||
this.getSupervisorVersion(),
|
||||
this.getIdentification(),
|
||||
this.getState(),
|
||||
this.getPID(),
|
||||
]);
|
||||
|
||||
return {
|
||||
apiVersion,
|
||||
supervisorVersion,
|
||||
identification,
|
||||
state,
|
||||
pid,
|
||||
};
|
||||
}
|
||||
|
||||
// ===== Process Info Methods =====
|
||||
|
||||
async getAllProcessInfo(): Promise<ProcessInfo[]> {
|
||||
const result = await this.call<any[]>('supervisor.getAllProcessInfo');
|
||||
return result.map((item) => ProcessInfoSchema.parse(item));
|
||||
}
|
||||
|
||||
async getProcessInfo(name: string): Promise<ProcessInfo> {
|
||||
const result = await this.call<any>('supervisor.getProcessInfo', [name]);
|
||||
return ProcessInfoSchema.parse(result);
|
||||
}
|
||||
|
||||
async getAllConfigInfo(): Promise<ConfigInfo[]> {
|
||||
const result = await this.call<any[]>('supervisor.getAllConfigInfo');
|
||||
return result.map((item) => ConfigInfoSchema.parse(item));
|
||||
}
|
||||
|
||||
// ===== Process Control Methods =====
|
||||
|
||||
async startProcess(name: string, wait: boolean = true): Promise<boolean> {
|
||||
return this.call<boolean>('supervisor.startProcess', [name, wait]);
|
||||
}
|
||||
|
||||
async startProcessGroup(name: string, wait: boolean = true): Promise<ProcessActionResult[]> {
|
||||
return this.call<ProcessActionResult[]>('supervisor.startProcessGroup', [name, wait]);
|
||||
}
|
||||
|
||||
async startAllProcesses(wait: boolean = true): Promise<ProcessActionResult[]> {
|
||||
return this.call<ProcessActionResult[]>('supervisor.startAllProcesses', [wait]);
|
||||
}
|
||||
|
||||
async stopProcess(name: string, wait: boolean = true): Promise<boolean> {
|
||||
return this.call<boolean>('supervisor.stopProcess', [name, wait]);
|
||||
}
|
||||
|
||||
async stopProcessGroup(name: string, wait: boolean = true): Promise<ProcessActionResult[]> {
|
||||
return this.call<ProcessActionResult[]>('supervisor.stopProcessGroup', [name, wait]);
|
||||
}
|
||||
|
||||
async stopAllProcesses(wait: boolean = true): Promise<ProcessActionResult[]> {
|
||||
return this.call<ProcessActionResult[]>('supervisor.stopAllProcesses', [wait]);
|
||||
}
|
||||
|
||||
async restartProcess(name: string): Promise<boolean> {
|
||||
await this.stopProcess(name, true);
|
||||
return this.startProcess(name, true);
|
||||
}
|
||||
|
||||
async signalProcess(name: string, signal: string): Promise<boolean> {
|
||||
return this.call<boolean>('supervisor.signalProcess', [name, signal]);
|
||||
}
|
||||
|
||||
async signalProcessGroup(name: string, signal: string): Promise<ProcessActionResult[]> {
|
||||
return this.call<ProcessActionResult[]>('supervisor.signalProcessGroup', [name, signal]);
|
||||
}
|
||||
|
||||
async signalAllProcesses(signal: string): Promise<ProcessActionResult[]> {
|
||||
return this.call<ProcessActionResult[]>('supervisor.signalAllProcesses', [signal]);
|
||||
}
|
||||
|
||||
// ===== Log Methods =====
|
||||
|
||||
async readProcessStdoutLog(
|
||||
name: string,
|
||||
offset: number,
|
||||
length: number
|
||||
): Promise<string> {
|
||||
return this.call<string>('supervisor.readProcessStdoutLog', [name, offset, length]);
|
||||
}
|
||||
|
||||
async readProcessStderrLog(
|
||||
name: string,
|
||||
offset: number,
|
||||
length: number
|
||||
): Promise<string> {
|
||||
return this.call<string>('supervisor.readProcessStderrLog', [name, offset, length]);
|
||||
}
|
||||
|
||||
async tailProcessStdoutLog(
|
||||
name: string,
|
||||
offset: number,
|
||||
length: number
|
||||
): Promise<LogTailResult> {
|
||||
const result = await this.call<[string, number, boolean]>('supervisor.tailProcessStdoutLog', [
|
||||
name,
|
||||
offset,
|
||||
length,
|
||||
]);
|
||||
return {
|
||||
bytes: result[0],
|
||||
offset: result[1],
|
||||
overflow: result[2],
|
||||
};
|
||||
}
|
||||
|
||||
async tailProcessStderrLog(
|
||||
name: string,
|
||||
offset: number,
|
||||
length: number
|
||||
): Promise<LogTailResult> {
|
||||
const result = await this.call<[string, number, boolean]>('supervisor.tailProcessStderrLog', [
|
||||
name,
|
||||
offset,
|
||||
length,
|
||||
]);
|
||||
return {
|
||||
bytes: result[0],
|
||||
offset: result[1],
|
||||
overflow: result[2],
|
||||
};
|
||||
}
|
||||
|
||||
async clearProcessLogs(name: string): Promise<boolean> {
|
||||
return this.call<boolean>('supervisor.clearProcessLogs', [name]);
|
||||
}
|
||||
|
||||
async clearAllProcessLogs(): Promise<ProcessActionResult[]> {
|
||||
return this.call<ProcessActionResult[]>('supervisor.clearAllProcessLogs');
|
||||
}
|
||||
|
||||
async readLog(offset: number, length: number): Promise<string> {
|
||||
return this.call<string>('supervisor.readLog', [offset, length]);
|
||||
}
|
||||
|
||||
async clearLog(): Promise<boolean> {
|
||||
return this.call<boolean>('supervisor.clearLog');
|
||||
}
|
||||
|
||||
// ===== Configuration Methods =====
|
||||
|
||||
async reloadConfig(): Promise<ReloadConfigResult> {
|
||||
const result = await this.call<any>('supervisor.reloadConfig');
|
||||
return ReloadConfigResultSchema.parse({
|
||||
added: result[0],
|
||||
changed: result[1],
|
||||
removed: result[2],
|
||||
});
|
||||
}
|
||||
|
||||
async addProcessGroup(name: string): Promise<boolean> {
|
||||
return this.call<boolean>('supervisor.addProcessGroup', [name]);
|
||||
}
|
||||
|
||||
async removeProcessGroup(name: string): Promise<boolean> {
|
||||
return this.call<boolean>('supervisor.removeProcessGroup', [name]);
|
||||
}
|
||||
|
||||
// ===== Supervisor Control Methods =====
|
||||
|
||||
async shutdown(): Promise<boolean> {
|
||||
return this.call<boolean>('supervisor.shutdown');
|
||||
}
|
||||
|
||||
async restart(): Promise<boolean> {
|
||||
return this.call<boolean>('supervisor.restart');
|
||||
}
|
||||
|
||||
async sendProcessStdin(name: string, chars: string): Promise<boolean> {
|
||||
return this.call<boolean>('supervisor.sendProcessStdin', [name, chars]);
|
||||
}
|
||||
|
||||
async sendRemoteCommEvent(type: string, data: string): Promise<boolean> {
|
||||
return this.call<boolean>('supervisor.sendRemoteCommEvent', [type, data]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create a supervisor client from environment variables
|
||||
*/
|
||||
export function createSupervisorClient(config?: Partial<SupervisorClientConfig>): SupervisorClient {
|
||||
const defaultConfig: SupervisorClientConfig = {
|
||||
host: process.env.SUPERVISOR_HOST || 'localhost',
|
||||
port: parseInt(process.env.SUPERVISOR_PORT || '9001', 10),
|
||||
username: process.env.SUPERVISOR_USERNAME,
|
||||
password: process.env.SUPERVISOR_PASSWORD,
|
||||
};
|
||||
|
||||
return new SupervisorClient({ ...defaultConfig, ...config });
|
||||
}
|
||||
Reference in New Issue
Block a user