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:
190
lib/hooks/useSupervisor.ts
Normal file
190
lib/hooks/useSupervisor.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
'use client';
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import type { ProcessInfo, SystemInfo, LogTailResult } from '@/lib/supervisor/types';
|
||||
|
||||
// Query Keys
|
||||
export const supervisorKeys = {
|
||||
all: ['supervisor'] as const,
|
||||
system: () => [...supervisorKeys.all, 'system'] as const,
|
||||
processes: () => [...supervisorKeys.all, 'processes'] as const,
|
||||
process: (name: string) => [...supervisorKeys.processes(), name] as const,
|
||||
logs: (name: string, type: 'stdout' | 'stderr') =>
|
||||
[...supervisorKeys.process(name), 'logs', type] as const,
|
||||
};
|
||||
|
||||
// API Client Functions
|
||||
async function fetchSystemInfo(): Promise<SystemInfo> {
|
||||
const response = await fetch('/api/supervisor/system');
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to fetch system info');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function fetchProcesses(): Promise<ProcessInfo[]> {
|
||||
const response = await fetch('/api/supervisor/processes');
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to fetch processes');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function fetchProcessInfo(name: string): Promise<ProcessInfo> {
|
||||
const response = await fetch(`/api/supervisor/processes/${encodeURIComponent(name)}`);
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to fetch process info');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function fetchProcessLogs(
|
||||
name: string,
|
||||
type: 'stdout' | 'stderr',
|
||||
offset: number = -4096,
|
||||
length: number = 4096
|
||||
): Promise<LogTailResult> {
|
||||
const response = await fetch(
|
||||
`/api/supervisor/processes/${encodeURIComponent(name)}/logs/${type}?offset=${offset}&length=${length}`
|
||||
);
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || `Failed to fetch ${type} logs`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function startProcess(name: string, wait: boolean = true): Promise<{ success: boolean; message: string }> {
|
||||
const response = await fetch(`/api/supervisor/processes/${encodeURIComponent(name)}/start`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ wait }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to start process');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function stopProcess(name: string, wait: boolean = true): Promise<{ success: boolean; message: string }> {
|
||||
const response = await fetch(`/api/supervisor/processes/${encodeURIComponent(name)}/stop`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ wait }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to stop process');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function restartProcess(name: string): Promise<{ success: boolean; message: string }> {
|
||||
const response = await fetch(`/api/supervisor/processes/${encodeURIComponent(name)}/restart`, {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to restart process');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// Custom Hooks
|
||||
|
||||
export function useSystemInfo() {
|
||||
return useQuery({
|
||||
queryKey: supervisorKeys.system(),
|
||||
queryFn: fetchSystemInfo,
|
||||
refetchInterval: 5000, // Refetch every 5 seconds
|
||||
});
|
||||
}
|
||||
|
||||
export function useProcesses(options?: { refetchInterval?: number }) {
|
||||
return useQuery({
|
||||
queryKey: supervisorKeys.processes(),
|
||||
queryFn: fetchProcesses,
|
||||
refetchInterval: options?.refetchInterval ?? 3000, // Default 3 seconds
|
||||
});
|
||||
}
|
||||
|
||||
export function useProcessInfo(name: string, enabled: boolean = true) {
|
||||
return useQuery({
|
||||
queryKey: supervisorKeys.process(name),
|
||||
queryFn: () => fetchProcessInfo(name),
|
||||
enabled,
|
||||
refetchInterval: 3000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useProcessLogs(
|
||||
name: string,
|
||||
type: 'stdout' | 'stderr',
|
||||
options?: {
|
||||
offset?: number;
|
||||
length?: number;
|
||||
enabled?: boolean;
|
||||
refetchInterval?: number;
|
||||
}
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...supervisorKeys.logs(name, type), options?.offset, options?.length],
|
||||
queryFn: () => fetchProcessLogs(name, type, options?.offset, options?.length),
|
||||
enabled: options?.enabled ?? true,
|
||||
refetchInterval: options?.refetchInterval ?? 2000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useStartProcess() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ name, wait }: { name: string; wait?: boolean }) => startProcess(name, wait),
|
||||
onSuccess: (data, variables) => {
|
||||
toast.success(data.message);
|
||||
// Invalidate and refetch
|
||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.process(variables.name) });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(`Failed to start process: ${error.message}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useStopProcess() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ name, wait }: { name: string; wait?: boolean }) => stopProcess(name, wait),
|
||||
onSuccess: (data, variables) => {
|
||||
toast.success(data.message);
|
||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.process(variables.name) });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(`Failed to stop process: ${error.message}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useRestartProcess() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (name: string) => restartProcess(name),
|
||||
onSuccess: (data, name) => {
|
||||
toast.success(data.message);
|
||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.process(name) });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(`Failed to restart process: ${error.message}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
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 });
|
||||
}
|
||||
196
lib/supervisor/types.ts
Normal file
196
lib/supervisor/types.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Process States from Supervisor
|
||||
export const ProcessState = {
|
||||
STOPPED: 0,
|
||||
STARTING: 10,
|
||||
RUNNING: 20,
|
||||
BACKOFF: 30,
|
||||
STOPPING: 40,
|
||||
EXITED: 100,
|
||||
FATAL: 200,
|
||||
UNKNOWN: 1000,
|
||||
} as const;
|
||||
|
||||
export type ProcessStateCode = (typeof ProcessState)[keyof typeof ProcessState];
|
||||
|
||||
export const ProcessStateNames: Record<ProcessStateCode, string> = {
|
||||
[ProcessState.STOPPED]: 'STOPPED',
|
||||
[ProcessState.STARTING]: 'STARTING',
|
||||
[ProcessState.RUNNING]: 'RUNNING',
|
||||
[ProcessState.BACKOFF]: 'BACKOFF',
|
||||
[ProcessState.STOPPING]: 'STOPPING',
|
||||
[ProcessState.EXITED]: 'EXITED',
|
||||
[ProcessState.FATAL]: 'FATAL',
|
||||
[ProcessState.UNKNOWN]: 'UNKNOWN',
|
||||
};
|
||||
|
||||
// Supervisor States
|
||||
export const SupervisorState = {
|
||||
FATAL: 2,
|
||||
RUNNING: 1,
|
||||
RESTARTING: 0,
|
||||
SHUTDOWN: -1,
|
||||
} as const;
|
||||
|
||||
export type SupervisorStateCode = (typeof SupervisorState)[keyof typeof SupervisorState];
|
||||
|
||||
// Zod Schemas for API Responses
|
||||
export const ProcessInfoSchema = z.object({
|
||||
name: z.string(),
|
||||
group: z.string(),
|
||||
description: z.string(),
|
||||
start: z.number(),
|
||||
stop: z.number(),
|
||||
now: z.number(),
|
||||
state: z.number(),
|
||||
statename: z.string(),
|
||||
spawnerr: z.string(),
|
||||
exitstatus: z.number(),
|
||||
logfile: z.string(),
|
||||
stdout_logfile: z.string(),
|
||||
stderr_logfile: z.string(),
|
||||
pid: z.number(),
|
||||
});
|
||||
|
||||
export const SupervisorStateInfoSchema = z.object({
|
||||
statecode: z.number(),
|
||||
statename: z.string(),
|
||||
});
|
||||
|
||||
export const ConfigInfoSchema = z.object({
|
||||
name: z.string(),
|
||||
group: z.string(),
|
||||
autostart: z.boolean(),
|
||||
directory: z.union([z.string(), z.null()]),
|
||||
command: z.string(),
|
||||
environment: z.union([z.string(), z.null()]),
|
||||
exitcodes: z.array(z.number()),
|
||||
redirect_stderr: z.boolean(),
|
||||
stderr_capture_maxbytes: z.number(),
|
||||
stderr_events_enabled: z.boolean(),
|
||||
stderr_logfile: z.string(),
|
||||
stderr_logfile_backups: z.number(),
|
||||
stderr_logfile_maxbytes: z.number(),
|
||||
stdout_capture_maxbytes: z.number(),
|
||||
stdout_events_enabled: z.boolean(),
|
||||
stdout_logfile: z.string(),
|
||||
stdout_logfile_backups: z.number(),
|
||||
stdout_logfile_maxbytes: z.number(),
|
||||
stopsignal: z.string(),
|
||||
stopwaitsecs: z.number(),
|
||||
priority: z.number(),
|
||||
startretries: z.number(),
|
||||
startsecs: z.number(),
|
||||
process_name: z.string(),
|
||||
numprocs: z.number(),
|
||||
numprocs_start: z.number(),
|
||||
uid: z.union([z.number(), z.null()]),
|
||||
username: z.union([z.string(), z.null()]),
|
||||
inuse: z.boolean(),
|
||||
});
|
||||
|
||||
export const ReloadConfigResultSchema = z.object({
|
||||
added: z.array(z.array(z.string())),
|
||||
changed: z.array(z.array(z.string())),
|
||||
removed: z.array(z.array(z.string())),
|
||||
});
|
||||
|
||||
// TypeScript Types
|
||||
export type ProcessInfo = z.infer<typeof ProcessInfoSchema>;
|
||||
export type SupervisorStateInfo = z.infer<typeof SupervisorStateInfoSchema>;
|
||||
export type ConfigInfo = z.infer<typeof ConfigInfoSchema>;
|
||||
export type ReloadConfigResult = z.infer<typeof ReloadConfigResultSchema>;
|
||||
|
||||
// Fault Codes
|
||||
export const FaultCodes = {
|
||||
UNKNOWN_METHOD: 1,
|
||||
INCORRECT_PARAMETERS: 2,
|
||||
BAD_ARGUMENTS: 3,
|
||||
SIGNATURE_UNSUPPORTED: 4,
|
||||
SHUTDOWN_STATE: 6,
|
||||
BAD_NAME: 10,
|
||||
BAD_SIGNAL: 11,
|
||||
NO_FILE: 20,
|
||||
NOT_EXECUTABLE: 21,
|
||||
FAILED: 30,
|
||||
ABNORMAL_TERMINATION: 40,
|
||||
SPAWN_ERROR: 50,
|
||||
ALREADY_STARTED: 60,
|
||||
NOT_RUNNING: 70,
|
||||
SUCCESS: 80,
|
||||
ALREADY_ADDED: 90,
|
||||
STILL_RUNNING: 91,
|
||||
CANT_REREAD: 92,
|
||||
} as const;
|
||||
|
||||
// API Request/Response Types
|
||||
export interface ProcessActionResult {
|
||||
name: string;
|
||||
group: string;
|
||||
status: number;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface LogTailResult {
|
||||
bytes: string;
|
||||
offset: number;
|
||||
overflow: boolean;
|
||||
}
|
||||
|
||||
export interface SystemInfo {
|
||||
apiVersion: string;
|
||||
supervisorVersion: string;
|
||||
identification: string;
|
||||
state: SupervisorStateInfo;
|
||||
pid: number;
|
||||
}
|
||||
|
||||
// Helper function to get state class name for styling
|
||||
export function getProcessStateClass(state: ProcessStateCode): string {
|
||||
switch (state) {
|
||||
case ProcessState.RUNNING:
|
||||
return 'process-running';
|
||||
case ProcessState.STOPPED:
|
||||
case ProcessState.EXITED:
|
||||
return 'process-stopped';
|
||||
case ProcessState.STARTING:
|
||||
case ProcessState.BACKOFF:
|
||||
return 'process-starting';
|
||||
case ProcessState.STOPPING:
|
||||
return 'process-stopping';
|
||||
case ProcessState.FATAL:
|
||||
return 'process-fatal';
|
||||
default:
|
||||
return 'process-unknown';
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to determine if a process can be started
|
||||
export function canStartProcess(state: ProcessStateCode): boolean {
|
||||
return [ProcessState.STOPPED, ProcessState.EXITED, ProcessState.FATAL].includes(state);
|
||||
}
|
||||
|
||||
// Helper function to determine if a process can be stopped
|
||||
export function canStopProcess(state: ProcessStateCode): boolean {
|
||||
return [ProcessState.RUNNING, ProcessState.STARTING].includes(state);
|
||||
}
|
||||
|
||||
// Helper function to format uptime
|
||||
export function formatUptime(startTime: number, currentTime: number): string {
|
||||
const uptimeSeconds = currentTime - startTime;
|
||||
if (uptimeSeconds <= 0) return 'Not running';
|
||||
|
||||
const days = Math.floor(uptimeSeconds / 86400);
|
||||
const hours = Math.floor((uptimeSeconds % 86400) / 3600);
|
||||
const minutes = Math.floor((uptimeSeconds % 3600) / 60);
|
||||
const seconds = Math.floor(uptimeSeconds % 60);
|
||||
|
||||
const parts: string[] = [];
|
||||
if (days > 0) parts.push(`${days}d`);
|
||||
if (hours > 0) parts.push(`${hours}h`);
|
||||
if (minutes > 0) parts.push(`${minutes}m`);
|
||||
if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`);
|
||||
|
||||
return parts.join(' ');
|
||||
}
|
||||
10
lib/utils/cn.ts
Normal file
10
lib/utils/cn.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
/**
|
||||
* Utility function to merge class names with Tailwind CSS classes
|
||||
* Combines clsx for conditional classes and tailwind-merge for deduplication
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
Reference in New Issue
Block a user