Compare commits
17 Commits
b252a0b3bf
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 813f6d4c75 | |||
| 7f1c110f8f | |||
| f83ecf864a | |||
| 2d5ffac56c | |||
| bdec163fb0 | |||
| c50274452c | |||
| 145d37193c | |||
| 20877abbc7 | |||
| 9fcb0447ee | |||
| df3e022049 | |||
| dda335d501 | |||
| 791c99097c | |||
| 06dd1c20d0 | |||
| 54eb14bf20 | |||
| 6985032006 | |||
| 95acf4542b | |||
| d592b58b75 |
31
app/api/client-error/route.ts
Normal file
31
app/api/client-error/route.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
|
interface ClientErrorReport {
|
||||||
|
message: string;
|
||||||
|
stack?: string;
|
||||||
|
componentStack?: string;
|
||||||
|
name?: string;
|
||||||
|
url: string;
|
||||||
|
userAgent: string;
|
||||||
|
timestamp: string;
|
||||||
|
type?: 'error' | 'unhandledrejection';
|
||||||
|
filename?: string;
|
||||||
|
lineno?: number;
|
||||||
|
colno?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const POST = withLogging(async (request: NextRequest) => {
|
||||||
|
const errorReport: ClientErrorReport = await request.json();
|
||||||
|
|
||||||
|
// The withLogging wrapper will automatically log this with the error details
|
||||||
|
// We can return success to acknowledge receipt
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
message: 'Client error logged successfully',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{ status: 200 }
|
||||||
|
);
|
||||||
|
}, 'logClientError');
|
||||||
@@ -1,60 +1,45 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
// POST - Add a process group
|
export const POST = withLogging(async (request: NextRequest) => {
|
||||||
export async function POST(request: NextRequest) {
|
const body = await request.json();
|
||||||
try {
|
const { name } = body;
|
||||||
const body = await request.json();
|
|
||||||
const { name } = body;
|
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Group name is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = createSupervisorClient();
|
|
||||||
const result = await client.addProcessGroup(name);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: result,
|
|
||||||
message: `Process group '${name}' added successfully`,
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Supervisor add process group error:', error);
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: error.message || 'Failed to add process group' },
|
{ error: 'Group name is required' },
|
||||||
{ status: 500 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE - Remove a process group
|
const client = createSupervisorClient();
|
||||||
export async function DELETE(request: NextRequest) {
|
const result = await client.addProcessGroup(name);
|
||||||
try {
|
|
||||||
const body = await request.json();
|
|
||||||
const { name } = body;
|
|
||||||
|
|
||||||
if (!name) {
|
return NextResponse.json({
|
||||||
return NextResponse.json(
|
success: result,
|
||||||
{ error: 'Group name is required' },
|
message: `Process group '${name}' added successfully`,
|
||||||
{ status: 400 }
|
groupName: name,
|
||||||
);
|
});
|
||||||
}
|
}, 'addProcessGroup');
|
||||||
|
|
||||||
const client = createSupervisorClient();
|
export const DELETE = withLogging(async (request: NextRequest) => {
|
||||||
const result = await client.removeProcessGroup(name);
|
const body = await request.json();
|
||||||
|
const { name } = body;
|
||||||
|
|
||||||
return NextResponse.json({
|
if (!name) {
|
||||||
success: result,
|
|
||||||
message: `Process group '${name}' removed successfully`,
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Supervisor remove process group error:', error);
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: error.message || 'Failed to remove process group' },
|
{ error: 'Group name is required' },
|
||||||
{ status: 500 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
const client = createSupervisorClient();
|
||||||
|
const result = await client.removeProcessGroup(name);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: result,
|
||||||
|
message: `Process group '${name}' removed successfully`,
|
||||||
|
groupName: name,
|
||||||
|
});
|
||||||
|
}, 'removeProcessGroup');
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
// POST - Reload configuration
|
export const POST = withLogging(async (request: NextRequest) => {
|
||||||
export async function POST() {
|
const client = createSupervisorClient();
|
||||||
try {
|
const result = await client.reloadConfig();
|
||||||
const client = createSupervisorClient();
|
|
||||||
const result = await client.reloadConfig();
|
return NextResponse.json({
|
||||||
return NextResponse.json({
|
success: true,
|
||||||
success: true,
|
message: 'Configuration reloaded',
|
||||||
message: 'Configuration reloaded',
|
result,
|
||||||
result,
|
});
|
||||||
});
|
}, 'reloadConfig');
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Supervisor reload config error:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to reload configuration' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
// GET - Get all process configurations
|
export const GET = withLogging(async (request: NextRequest) => {
|
||||||
export async function GET() {
|
const client = createSupervisorClient();
|
||||||
try {
|
const configs = await client.getAllConfigInfo();
|
||||||
const client = createSupervisorClient();
|
return NextResponse.json(configs);
|
||||||
const configs = await client.getAllConfigInfo();
|
}, 'getAllConfigInfo');
|
||||||
return NextResponse.json(configs);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Supervisor get config error:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to fetch configuration' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { NextRequest } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { createApiLogger } from '@/lib/utils/api-logger';
|
||||||
|
import { formatError, generateRequestId } from '@/lib/utils/logger';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@@ -8,9 +10,16 @@ export const dynamic = 'force-dynamic';
|
|||||||
* Polls supervisor every 2 seconds and sends state changes to clients
|
* Polls supervisor every 2 seconds and sends state changes to clients
|
||||||
*/
|
*/
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
|
const requestId = generateRequestId();
|
||||||
|
const logger = createApiLogger(request, 'SSE-Events');
|
||||||
|
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
let intervalId: NodeJS.Timeout | null = null;
|
let intervalId: NodeJS.Timeout | null = null;
|
||||||
let previousState: string | null = null;
|
let previousState: string | null = null;
|
||||||
|
let pollCount = 0;
|
||||||
|
let stateChangeCount = 0;
|
||||||
|
|
||||||
|
logger.info({ requestId }, 'SSE connection initiated');
|
||||||
|
|
||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
async start(controller) {
|
async start(controller) {
|
||||||
@@ -22,9 +31,13 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
// Send initial connection message
|
// Send initial connection message
|
||||||
sendEvent('connected', { timestamp: Date.now() });
|
sendEvent('connected', { timestamp: Date.now() });
|
||||||
|
logger.debug({ requestId }, 'SSE connected event sent');
|
||||||
|
|
||||||
// Poll supervisor for state changes
|
// Poll supervisor for state changes
|
||||||
const pollSupervisor = async () => {
|
const pollSupervisor = async () => {
|
||||||
|
pollCount++;
|
||||||
|
const pollStartTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = createSupervisorClient();
|
const client = createSupervisorClient();
|
||||||
const processes = await client.getAllProcessInfo();
|
const processes = await client.getAllProcessInfo();
|
||||||
@@ -41,17 +54,40 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
// Send update if state changed
|
// Send update if state changed
|
||||||
if (currentState !== previousState) {
|
if (currentState !== previousState) {
|
||||||
|
stateChangeCount++;
|
||||||
sendEvent('process-update', {
|
sendEvent('process-update', {
|
||||||
processes,
|
processes,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
|
logger.info({
|
||||||
|
requestId,
|
||||||
|
pollCount,
|
||||||
|
stateChangeCount,
|
||||||
|
processCount: processes.length,
|
||||||
|
duration: Date.now() - pollStartTime,
|
||||||
|
}, `Process state change detected (change #${stateChangeCount})`);
|
||||||
previousState = currentState;
|
previousState = currentState;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send heartbeat every poll
|
// Send heartbeat every poll (reduced logging - only log every 10th heartbeat)
|
||||||
sendEvent('heartbeat', { timestamp: Date.now() });
|
sendEvent('heartbeat', { timestamp: Date.now() });
|
||||||
|
if (pollCount % 10 === 0) {
|
||||||
|
logger.debug({
|
||||||
|
requestId,
|
||||||
|
pollCount,
|
||||||
|
stateChangeCount,
|
||||||
|
duration: Date.now() - pollStartTime,
|
||||||
|
}, `SSE heartbeat #${pollCount}`);
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('SSE polling error:', error);
|
const errorInfo = formatError(error);
|
||||||
|
logger.error({
|
||||||
|
requestId,
|
||||||
|
pollCount,
|
||||||
|
error: errorInfo,
|
||||||
|
duration: Date.now() - pollStartTime,
|
||||||
|
}, `SSE polling error: ${errorInfo.message}`);
|
||||||
|
|
||||||
sendEvent('error', {
|
sendEvent('error', {
|
||||||
message: error.message || 'Failed to fetch process state',
|
message: error.message || 'Failed to fetch process state',
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
@@ -70,6 +106,12 @@ export async function GET(request: NextRequest) {
|
|||||||
if (intervalId) {
|
if (intervalId) {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
}
|
}
|
||||||
|
logger.info({
|
||||||
|
requestId,
|
||||||
|
pollCount,
|
||||||
|
stateChangeCount,
|
||||||
|
duration: Date.now() - Date.now(),
|
||||||
|
}, 'SSE connection closed');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -79,6 +121,7 @@ export async function GET(request: NextRequest) {
|
|||||||
'Cache-Control': 'no-cache, no-transform',
|
'Cache-Control': 'no-cache, no-transform',
|
||||||
'Connection': 'keep-alive',
|
'Connection': 'keep-alive',
|
||||||
'X-Accel-Buffering': 'no', // Disable nginx buffering
|
'X-Accel-Buffering': 'no', // Disable nginx buffering
|
||||||
|
'X-Request-ID': requestId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,29 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
interface RouteParams {
|
interface RouteParams {
|
||||||
params: Promise<{ name: string }>;
|
params: Promise<{ name: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST - Restart all processes in a group (stop then start)
|
export const POST = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||||
export async function POST(request: NextRequest, { params }: RouteParams) {
|
const { name } = await params;
|
||||||
try {
|
const body = await request.json().catch(() => ({}));
|
||||||
const { name } = await params;
|
const wait = body.wait ?? true;
|
||||||
const body = await request.json().catch(() => ({}));
|
|
||||||
const wait = body.wait ?? true;
|
|
||||||
|
|
||||||
const client = createSupervisorClient();
|
const client = createSupervisorClient();
|
||||||
|
|
||||||
// Stop all processes in the group first
|
// Stop all processes in the group first
|
||||||
await client.stopProcessGroup(name, wait);
|
await client.stopProcessGroup(name, wait);
|
||||||
|
|
||||||
// Then start them
|
// Then start them
|
||||||
const results = await client.startProcessGroup(name, wait);
|
const results = await client.startProcessGroup(name, wait);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `Restarted process group: ${name}`,
|
message: `Restarted process group: ${name}`,
|
||||||
results,
|
results,
|
||||||
});
|
groupName: name,
|
||||||
} catch (error: any) {
|
wait,
|
||||||
console.error('Supervisor restart process group error:', error);
|
});
|
||||||
return NextResponse.json(
|
}, 'restartProcessGroup');
|
||||||
{ error: error.message || 'Failed to restart process group' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,37 +1,31 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
interface RouteParams {
|
interface RouteParams {
|
||||||
params: Promise<{ name: string }>;
|
params: Promise<{ name: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST - Send signal to all processes in a group
|
export const POST = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||||
export async function POST(request: NextRequest, { params }: RouteParams) {
|
const { name } = await params;
|
||||||
try {
|
const body = await request.json();
|
||||||
const { name } = await params;
|
const { signal } = body;
|
||||||
const body = await request.json();
|
|
||||||
const { signal } = body;
|
|
||||||
|
|
||||||
if (!signal) {
|
if (!signal) {
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Signal is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = createSupervisorClient();
|
|
||||||
const results = await client.signalProcessGroup(name, signal);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
message: `Signal ${signal} sent to group ${name}`,
|
|
||||||
results,
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Supervisor signal process group error:', error);
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: error.message || 'Failed to send signal to process group' },
|
{ error: 'Signal is required' },
|
||||||
{ status: 500 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
const client = createSupervisorClient();
|
||||||
|
const results = await client.signalProcessGroup(name, signal);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: `Signal ${signal} sent to group ${name}`,
|
||||||
|
results,
|
||||||
|
groupName: name,
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
}, 'signalProcessGroup');
|
||||||
|
|||||||
@@ -1,30 +1,24 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
interface RouteParams {
|
interface RouteParams {
|
||||||
params: Promise<{ name: string }>;
|
params: Promise<{ name: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST - Start all processes in a group
|
export const POST = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||||
export async function POST(request: NextRequest, { params }: RouteParams) {
|
const { name } = await params;
|
||||||
try {
|
const body = await request.json().catch(() => ({}));
|
||||||
const { name } = await params;
|
const wait = body.wait ?? true;
|
||||||
const body = await request.json().catch(() => ({}));
|
|
||||||
const wait = body.wait ?? true;
|
|
||||||
|
|
||||||
const client = createSupervisorClient();
|
const client = createSupervisorClient();
|
||||||
const results = await client.startProcessGroup(name, wait);
|
const results = await client.startProcessGroup(name, wait);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `Started process group: ${name}`,
|
message: `Started process group: ${name}`,
|
||||||
results,
|
results,
|
||||||
});
|
groupName: name,
|
||||||
} catch (error: any) {
|
wait,
|
||||||
console.error('Supervisor start process group error:', error);
|
});
|
||||||
return NextResponse.json(
|
}, 'startProcessGroup');
|
||||||
{ error: error.message || 'Failed to start process group' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,30 +1,24 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
interface RouteParams {
|
interface RouteParams {
|
||||||
params: Promise<{ name: string }>;
|
params: Promise<{ name: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST - Stop all processes in a group
|
export const POST = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||||
export async function POST(request: NextRequest, { params }: RouteParams) {
|
const { name } = await params;
|
||||||
try {
|
const body = await request.json().catch(() => ({}));
|
||||||
const { name } = await params;
|
const wait = body.wait ?? true;
|
||||||
const body = await request.json().catch(() => ({}));
|
|
||||||
const wait = body.wait ?? true;
|
|
||||||
|
|
||||||
const client = createSupervisorClient();
|
const client = createSupervisorClient();
|
||||||
const results = await client.stopProcessGroup(name, wait);
|
const results = await client.stopProcessGroup(name, wait);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `Stopped process group: ${name}`,
|
message: `Stopped process group: ${name}`,
|
||||||
results,
|
results,
|
||||||
});
|
groupName: name,
|
||||||
} catch (error: any) {
|
wait,
|
||||||
console.error('Supervisor stop process group error:', error);
|
});
|
||||||
return NextResponse.json(
|
}, 'stopProcessGroup');
|
||||||
{ error: error.message || 'Failed to stop process group' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,39 +1,23 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
// GET main supervisord log
|
export const GET = withLogging(async (request: NextRequest) => {
|
||||||
export async function GET(request: NextRequest) {
|
const searchParams = request.nextUrl.searchParams;
|
||||||
try {
|
const offset = parseInt(searchParams.get('offset') || '-4096', 10);
|
||||||
const searchParams = request.nextUrl.searchParams;
|
const length = parseInt(searchParams.get('length') || '0', 10);
|
||||||
const offset = parseInt(searchParams.get('offset') || '-4096', 10);
|
|
||||||
const length = parseInt(searchParams.get('length') || '4096', 10);
|
|
||||||
|
|
||||||
const client = createSupervisorClient();
|
const client = createSupervisorClient();
|
||||||
const logs = await client.readLog(offset, length);
|
const logs = await client.readLog(offset, length);
|
||||||
|
|
||||||
return NextResponse.json({ logs, offset, length });
|
return NextResponse.json({ logs, offset, length });
|
||||||
} catch (error: any) {
|
}, 'readMainLog');
|
||||||
console.error('Supervisor main log error:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to fetch main log' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE - Clear main supervisord log
|
export const DELETE = withLogging(async (request: NextRequest) => {
|
||||||
export async function DELETE() {
|
const client = createSupervisorClient();
|
||||||
try {
|
const result = await client.clearLog();
|
||||||
const client = createSupervisorClient();
|
|
||||||
const result = await client.clearLog();
|
return NextResponse.json({ success: result, message: 'Main log cleared' });
|
||||||
return NextResponse.json({ success: result, message: 'Main log cleared' });
|
}, 'clearMainLog');
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Supervisor clear main log error:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to clear main log' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
interface RouteParams {
|
interface RouteParams {
|
||||||
params: Promise<{ name: string }>;
|
params: Promise<{ name: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DELETE - Clear process logs (both stdout and stderr)
|
export const DELETE = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||||
export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
const { name } = await params;
|
||||||
try {
|
|
||||||
const { name } = await params;
|
const client = createSupervisorClient();
|
||||||
const client = createSupervisorClient();
|
const result = await client.clearProcessLogs(name);
|
||||||
const result = await client.clearProcessLogs(name);
|
|
||||||
return NextResponse.json({ success: result, message: `Logs cleared for ${name}` });
|
return NextResponse.json({
|
||||||
} catch (error: any) {
|
success: result,
|
||||||
console.error('Supervisor clear process logs error:', error);
|
message: `Logs cleared for ${name}`,
|
||||||
return NextResponse.json(
|
processName: name,
|
||||||
{ error: error.message || 'Failed to clear process logs' },
|
});
|
||||||
{ status: 500 }
|
}, 'clearProcessLogs');
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@@ -7,21 +8,14 @@ interface RouteParams {
|
|||||||
params: Promise<{ name: string }>;
|
params: Promise<{ name: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(request: NextRequest, { params }: RouteParams) {
|
export const GET = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||||
try {
|
const { name } = await params;
|
||||||
const { name } = await params;
|
const searchParams = request.nextUrl.searchParams;
|
||||||
const searchParams = request.nextUrl.searchParams;
|
const offset = parseInt(searchParams.get('offset') || '-4096', 10);
|
||||||
const offset = parseInt(searchParams.get('offset') || '-4096', 10);
|
const length = parseInt(searchParams.get('length') || '4096', 10);
|
||||||
const length = parseInt(searchParams.get('length') || '4096', 10);
|
|
||||||
|
|
||||||
const client = createSupervisorClient();
|
const client = createSupervisorClient();
|
||||||
const logs = await client.tailProcessStderrLog(name, offset, length);
|
const logs = await client.tailProcessStderrLog(name, offset, length);
|
||||||
return NextResponse.json(logs);
|
|
||||||
} catch (error: any) {
|
return NextResponse.json(logs);
|
||||||
console.error('Supervisor stderr log error:', error);
|
}, 'readProcessStderr');
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to fetch stderr logs' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@@ -7,21 +8,14 @@ interface RouteParams {
|
|||||||
params: Promise<{ name: string }>;
|
params: Promise<{ name: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(request: NextRequest, { params }: RouteParams) {
|
export const GET = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||||
try {
|
const { name } = await params;
|
||||||
const { name } = await params;
|
const searchParams = request.nextUrl.searchParams;
|
||||||
const searchParams = request.nextUrl.searchParams;
|
const offset = parseInt(searchParams.get('offset') || '-4096', 10);
|
||||||
const offset = parseInt(searchParams.get('offset') || '-4096', 10);
|
const length = parseInt(searchParams.get('length') || '4096', 10);
|
||||||
const length = parseInt(searchParams.get('length') || '4096', 10);
|
|
||||||
|
|
||||||
const client = createSupervisorClient();
|
const client = createSupervisorClient();
|
||||||
const logs = await client.tailProcessStdoutLog(name, offset, length);
|
const logs = await client.tailProcessStdoutLog(name, offset, length);
|
||||||
return NextResponse.json(logs);
|
|
||||||
} catch (error: any) {
|
return NextResponse.json(logs);
|
||||||
console.error('Supervisor stdout log error:', error);
|
}, 'readProcessStdout');
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to fetch stdout logs' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
interface RouteParams {
|
interface RouteParams {
|
||||||
params: Promise<{ name: string }>;
|
params: Promise<{ name: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest, { params }: RouteParams) {
|
export const POST = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||||
try {
|
const { name } = await params;
|
||||||
const { name } = await params;
|
|
||||||
const client = createSupervisorClient();
|
const client = createSupervisorClient();
|
||||||
const result = await client.restartProcess(name);
|
const result = await client.restartProcess(name);
|
||||||
return NextResponse.json({ success: result, message: `Process ${name} restarted` });
|
|
||||||
} catch (error: any) {
|
return NextResponse.json({
|
||||||
console.error('Supervisor restart process error:', error);
|
success: result,
|
||||||
return NextResponse.json(
|
message: `Process ${name} restarted`,
|
||||||
{ error: error.message || 'Failed to restart process' },
|
processName: name,
|
||||||
{ status: 500 }
|
});
|
||||||
);
|
}, 'restartProcess');
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@@ -7,17 +8,11 @@ interface RouteParams {
|
|||||||
params: Promise<{ name: string }>;
|
params: Promise<{ name: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(request: NextRequest, { params }: RouteParams) {
|
export const GET = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||||
try {
|
const { name } = await params;
|
||||||
const { name } = await params;
|
|
||||||
const client = createSupervisorClient();
|
const client = createSupervisorClient();
|
||||||
const processInfo = await client.getProcessInfo(name);
|
const processInfo = await client.getProcessInfo(name);
|
||||||
return NextResponse.json(processInfo);
|
|
||||||
} catch (error: any) {
|
return NextResponse.json(processInfo);
|
||||||
console.error('Supervisor process info error:', error);
|
}, 'getProcessInfo');
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to fetch process info' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,36 +1,30 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
interface RouteParams {
|
interface RouteParams {
|
||||||
params: Promise<{ name: string }>;
|
params: Promise<{ name: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST - Send signal to a process
|
export const POST = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||||
export async function POST(request: NextRequest, { params }: RouteParams) {
|
const { name } = await params;
|
||||||
try {
|
const body = await request.json();
|
||||||
const { name } = await params;
|
const { signal } = body;
|
||||||
const body = await request.json();
|
|
||||||
const { signal } = body;
|
|
||||||
|
|
||||||
if (!signal) {
|
if (!signal) {
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Signal is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = createSupervisorClient();
|
|
||||||
const result = await client.signalProcess(name, signal);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: result,
|
|
||||||
message: `Signal ${signal} sent to ${name}`,
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Supervisor signal process error:', error);
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: error.message || 'Failed to send signal to process' },
|
{ error: 'Signal is required' },
|
||||||
{ status: 500 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
const client = createSupervisorClient();
|
||||||
|
const result = await client.signalProcess(name, signal);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: result,
|
||||||
|
message: `Signal ${signal} sent to ${name}`,
|
||||||
|
processName: name,
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
}, 'signalProcess');
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
interface RouteParams {
|
interface RouteParams {
|
||||||
params: Promise<{ name: string }>;
|
params: Promise<{ name: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest, { params }: RouteParams) {
|
export const POST = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||||
try {
|
const { name } = await params;
|
||||||
const { name } = await params;
|
const body = await request.json().catch(() => ({}));
|
||||||
const body = await request.json().catch(() => ({}));
|
const wait = body.wait !== undefined ? body.wait : true;
|
||||||
const wait = body.wait !== undefined ? body.wait : true;
|
|
||||||
|
|
||||||
const client = createSupervisorClient();
|
const client = createSupervisorClient();
|
||||||
const result = await client.startProcess(name, wait);
|
const result = await client.startProcess(name, wait);
|
||||||
return NextResponse.json({ success: result, message: `Process ${name} started` });
|
|
||||||
} catch (error: any) {
|
return NextResponse.json({
|
||||||
console.error('Supervisor start process error:', error);
|
success: result,
|
||||||
return NextResponse.json(
|
message: `Process ${name} started`,
|
||||||
{ error: error.message || 'Failed to start process' },
|
processName: name,
|
||||||
{ status: 500 }
|
wait,
|
||||||
);
|
});
|
||||||
}
|
}, 'startProcess');
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,36 +1,29 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
interface RouteParams {
|
interface RouteParams {
|
||||||
params: Promise<{ name: string }>;
|
params: Promise<{ name: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST - Send input to process stdin
|
export const POST = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||||
export async function POST(request: NextRequest, { params }: RouteParams) {
|
const { name } = await params;
|
||||||
try {
|
const body = await request.json();
|
||||||
const { name } = await params;
|
const { chars } = body;
|
||||||
const body = await request.json();
|
|
||||||
const { chars } = body;
|
|
||||||
|
|
||||||
if (chars === undefined || chars === null) {
|
if (chars === undefined || chars === null) {
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Input characters are required' },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = createSupervisorClient();
|
|
||||||
const result = await client.sendProcessStdin(name, chars);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: result,
|
|
||||||
message: `Input sent to ${name}`,
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Supervisor send stdin error:', error);
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: error.message || 'Failed to send input to process' },
|
{ error: 'Input characters are required' },
|
||||||
{ status: 500 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
const client = createSupervisorClient();
|
||||||
|
const result = await client.sendProcessStdin(name, chars);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: result,
|
||||||
|
message: `Input sent to ${name}`,
|
||||||
|
processName: name,
|
||||||
|
});
|
||||||
|
}, 'sendProcessStdin');
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
interface RouteParams {
|
interface RouteParams {
|
||||||
params: Promise<{ name: string }>;
|
params: Promise<{ name: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest, { params }: RouteParams) {
|
export const POST = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||||
try {
|
const { name } = await params;
|
||||||
const { name } = await params;
|
const body = await request.json().catch(() => ({}));
|
||||||
const body = await request.json().catch(() => ({}));
|
const wait = body.wait !== undefined ? body.wait : true;
|
||||||
const wait = body.wait !== undefined ? body.wait : true;
|
|
||||||
|
|
||||||
const client = createSupervisorClient();
|
const client = createSupervisorClient();
|
||||||
const result = await client.stopProcess(name, wait);
|
const result = await client.stopProcess(name, wait);
|
||||||
return NextResponse.json({ success: result, message: `Process ${name} stopped` });
|
|
||||||
} catch (error: any) {
|
return NextResponse.json({
|
||||||
console.error('Supervisor stop process error:', error);
|
success: result,
|
||||||
return NextResponse.json(
|
message: `Process ${name} stopped`,
|
||||||
{ error: error.message || 'Failed to stop process' },
|
processName: name,
|
||||||
{ status: 500 }
|
wait,
|
||||||
);
|
});
|
||||||
}
|
}, 'stopProcess');
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
// POST - Clear all process logs
|
export const POST = withLogging(async (request: NextRequest) => {
|
||||||
export async function POST() {
|
const client = createSupervisorClient();
|
||||||
try {
|
const results = await client.clearAllProcessLogs();
|
||||||
const client = createSupervisorClient();
|
|
||||||
const results = await client.clearAllProcessLogs();
|
return NextResponse.json({
|
||||||
return NextResponse.json({
|
success: true,
|
||||||
success: true,
|
message: 'All process logs cleared',
|
||||||
message: 'All process logs cleared',
|
results,
|
||||||
results
|
});
|
||||||
});
|
}, 'clearAllProcessLogs');
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Supervisor clear all logs error:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to clear all logs' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,30 +1,23 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
// POST - Restart all processes (stop then start)
|
export const POST = withLogging(async (request: NextRequest) => {
|
||||||
export async function POST(request: NextRequest) {
|
const body = await request.json().catch(() => ({}));
|
||||||
try {
|
const wait = body.wait ?? true;
|
||||||
const body = await request.json().catch(() => ({}));
|
|
||||||
const wait = body.wait ?? true;
|
|
||||||
|
|
||||||
const client = createSupervisorClient();
|
const client = createSupervisorClient();
|
||||||
|
|
||||||
// Stop all processes first
|
// Stop all processes first
|
||||||
await client.stopAllProcesses(wait);
|
await client.stopAllProcesses(wait);
|
||||||
|
|
||||||
// Then start them
|
// Then start them
|
||||||
const results = await client.startAllProcesses(wait);
|
const results = await client.startAllProcesses(wait);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Restarted all processes',
|
message: 'Restarted all processes',
|
||||||
results,
|
results,
|
||||||
});
|
wait,
|
||||||
} catch (error: any) {
|
});
|
||||||
console.error('Supervisor restart all processes error:', error);
|
}, 'restartAllProcesses');
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to restart all processes' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export async function GET() {
|
export const GET = withLogging(async (request: NextRequest) => {
|
||||||
try {
|
const client = createSupervisorClient();
|
||||||
const client = createSupervisorClient();
|
const processes = await client.getAllProcessInfo();
|
||||||
const processes = await client.getAllProcessInfo();
|
|
||||||
return NextResponse.json(processes);
|
return NextResponse.json(processes);
|
||||||
} catch (error: any) {
|
}, 'getAllProcessInfo');
|
||||||
console.error('Supervisor processes error:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to fetch processes' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,32 +1,25 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
// POST - Send signal to all processes
|
export const POST = withLogging(async (request: NextRequest) => {
|
||||||
export async function POST(request: NextRequest) {
|
const body = await request.json();
|
||||||
try {
|
const { signal } = body;
|
||||||
const body = await request.json();
|
|
||||||
const { signal } = body;
|
|
||||||
|
|
||||||
if (!signal) {
|
if (!signal) {
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Signal is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = createSupervisorClient();
|
|
||||||
const results = await client.signalAllProcesses(signal);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
message: `Signal ${signal} sent to all processes`,
|
|
||||||
results,
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Supervisor signal all processes error:', error);
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: error.message || 'Failed to send signal to all processes' },
|
{ error: 'Signal is required' },
|
||||||
{ status: 500 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
const client = createSupervisorClient();
|
||||||
|
const results = await client.signalAllProcesses(signal);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: `Signal ${signal} sent to all processes`,
|
||||||
|
results,
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
}, 'signalAllProcesses');
|
||||||
|
|||||||
@@ -1,25 +1,18 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
// POST - Start all processes
|
export const POST = withLogging(async (request: NextRequest) => {
|
||||||
export async function POST(request: NextRequest) {
|
const body = await request.json().catch(() => ({}));
|
||||||
try {
|
const wait = body.wait ?? true;
|
||||||
const body = await request.json().catch(() => ({}));
|
|
||||||
const wait = body.wait ?? true;
|
|
||||||
|
|
||||||
const client = createSupervisorClient();
|
const client = createSupervisorClient();
|
||||||
const results = await client.startAllProcesses(wait);
|
const results = await client.startAllProcesses(wait);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Started all processes',
|
message: 'Started all processes',
|
||||||
results,
|
results,
|
||||||
});
|
wait,
|
||||||
} catch (error: any) {
|
});
|
||||||
console.error('Supervisor start all processes error:', error);
|
}, 'startAllProcesses');
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to start all processes' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,25 +1,18 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
// POST - Stop all processes
|
export const POST = withLogging(async (request: NextRequest) => {
|
||||||
export async function POST(request: NextRequest) {
|
const body = await request.json().catch(() => ({}));
|
||||||
try {
|
const wait = body.wait ?? true;
|
||||||
const body = await request.json().catch(() => ({}));
|
|
||||||
const wait = body.wait ?? true;
|
|
||||||
|
|
||||||
const client = createSupervisorClient();
|
const client = createSupervisorClient();
|
||||||
const results = await client.stopAllProcesses(wait);
|
const results = await client.stopAllProcesses(wait);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Stopped all processes',
|
message: 'Stopped all processes',
|
||||||
results,
|
results,
|
||||||
});
|
wait,
|
||||||
} catch (error: any) {
|
});
|
||||||
console.error('Supervisor stop all processes error:', error);
|
}, 'stopAllProcesses');
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to stop all processes' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||||
|
import { withLogging } from '@/lib/utils/api-logger';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export async function GET() {
|
export const GET = withLogging(async (request: NextRequest) => {
|
||||||
try {
|
const client = createSupervisorClient();
|
||||||
const client = createSupervisorClient();
|
const systemInfo = await client.getSystemInfo();
|
||||||
const systemInfo = await client.getSystemInfo();
|
|
||||||
return NextResponse.json(systemInfo);
|
return NextResponse.json(systemInfo);
|
||||||
} catch (error: any) {
|
}, 'getSystemInfo');
|
||||||
console.error('Supervisor system info error:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: error.message || 'Failed to fetch system info' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ export default function ConfigPage() {
|
|||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-4 md:space-y-6">
|
||||||
<h1 className="text-3xl font-bold">Configuration</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold">Configuration</h1>
|
||||||
<Card className="border-destructive/50">
|
<Card className="border-destructive/50">
|
||||||
<CardContent className="p-12 text-center">
|
<CardContent className="p-8 md:p-12 text-center">
|
||||||
<AlertCircle className="h-12 w-12 text-destructive mx-auto mb-4" />
|
<AlertCircle className="h-10 w-10 md:h-12 md:w-12 text-destructive mx-auto mb-4" />
|
||||||
<h2 className="text-xl font-semibold mb-2">Failed to load configuration</h2>
|
<h2 className="text-lg md:text-xl font-semibold mb-2">Failed to load configuration</h2>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-sm md:text-base text-muted-foreground">
|
||||||
Could not connect to Supervisor. Please check your configuration.
|
Could not connect to Supervisor. Please check your configuration.
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -28,11 +28,11 @@ export default function ConfigPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-4 md:space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold">Configuration</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold">Configuration</h1>
|
||||||
<p className="text-muted-foreground mt-1">
|
<p className="text-sm md:text-base text-muted-foreground mt-1">
|
||||||
Manage process configurations and supervisor settings
|
Manage process configurations and supervisor settings
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,8 +65,8 @@ export default function ConfigPage() {
|
|||||||
<ConfigTable configs={configs} />
|
<ConfigTable configs={configs} />
|
||||||
) : (
|
) : (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-12 text-center">
|
<CardContent className="p-8 md:p-12 text-center">
|
||||||
<p className="text-muted-foreground">No process configurations found</p>
|
<p className="text-sm md:text-base text-muted-foreground">No process configurations found</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ const inter = Inter({
|
|||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Supervisor UI',
|
title: 'Supervisor UI',
|
||||||
description: 'Modern web interface for Supervisor process management',
|
description: 'Modern web interface for Supervisor process management',
|
||||||
|
viewport: {
|
||||||
|
width: 'device-width',
|
||||||
|
initialScale: 1,
|
||||||
|
maximumScale: 5,
|
||||||
|
userScalable: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Force dynamic rendering
|
// Force dynamic rendering
|
||||||
|
|||||||
@@ -76,13 +76,13 @@ export default function LogsPage() {
|
|||||||
|
|
||||||
if (processesError) {
|
if (processesError) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-4 md:space-y-6">
|
||||||
<h1 className="text-3xl font-bold">Process Logs</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold">Process Logs</h1>
|
||||||
<Card className="border-destructive/50">
|
<Card className="border-destructive/50">
|
||||||
<CardContent className="p-12 text-center">
|
<CardContent className="p-8 md:p-12 text-center">
|
||||||
<AlertCircle className="h-12 w-12 text-destructive mx-auto mb-4" />
|
<AlertCircle className="h-10 w-10 md:h-12 md:w-12 text-destructive mx-auto mb-4" />
|
||||||
<h2 className="text-xl font-semibold mb-2">Failed to load processes</h2>
|
<h2 className="text-lg md:text-xl font-semibold mb-2">Failed to load processes</h2>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-sm md:text-base text-muted-foreground">
|
||||||
Could not connect to Supervisor. Please check your configuration.
|
Could not connect to Supervisor. Please check your configuration.
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -92,20 +92,20 @@ export default function LogsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 h-[calc(100vh-12rem)] flex flex-col">
|
<div className="space-y-4 md:space-y-6 h-[calc(100vh-10rem)] md:h-[calc(100vh-12rem)] flex flex-col">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<h1 className="text-3xl font-bold">Process Logs</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold">Process Logs</h1>
|
||||||
<p className="text-muted-foreground mt-1">Real-time log viewing and management</p>
|
<p className="text-sm md:text-base text-muted-foreground mt-1">Real-time log viewing and management</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Controls */}
|
{/* Controls */}
|
||||||
<Card className="flex-shrink-0">
|
<Card className="flex-shrink-0">
|
||||||
<CardHeader className="pb-4">
|
<CardHeader className="pb-3 md:pb-4">
|
||||||
<CardTitle className="text-lg">Log Controls</CardTitle>
|
<CardTitle className="text-base md:text-lg">Log Controls</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-3 md:space-y-4">
|
||||||
{/* Process Selector */}
|
{/* Process Selector */}
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4">
|
||||||
<div className="flex-1 min-w-[200px]">
|
<div className="flex-1 min-w-[200px]">
|
||||||
<label className="block text-sm font-medium mb-2">Select Process</label>
|
<label className="block text-sm font-medium mb-2">Select Process</label>
|
||||||
<select
|
<select
|
||||||
@@ -131,6 +131,7 @@ export default function LogsPage() {
|
|||||||
variant={logType === 'stdout' ? 'default' : 'outline'}
|
variant={logType === 'stdout' ? 'default' : 'outline'}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setLogType('stdout')}
|
onClick={() => setLogType('stdout')}
|
||||||
|
className="flex-1 sm:flex-initial"
|
||||||
>
|
>
|
||||||
Stdout
|
Stdout
|
||||||
</Button>
|
</Button>
|
||||||
@@ -138,6 +139,7 @@ export default function LogsPage() {
|
|||||||
variant={logType === 'stderr' ? 'default' : 'outline'}
|
variant={logType === 'stderr' ? 'default' : 'outline'}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setLogType('stderr')}
|
onClick={() => setLogType('stderr')}
|
||||||
|
className="flex-1 sm:flex-initial"
|
||||||
>
|
>
|
||||||
Stderr
|
Stderr
|
||||||
</Button>
|
</Button>
|
||||||
@@ -147,7 +149,7 @@ export default function LogsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search and Controls */}
|
{/* Search and Controls */}
|
||||||
<div className="flex flex-wrap gap-4 items-end">
|
<div className="flex flex-col sm:flex-row flex-wrap gap-3 sm:gap-4 items-stretch sm:items-end">
|
||||||
<LogSearch value={searchTerm} onChange={setSearchTerm} />
|
<LogSearch value={searchTerm} onChange={setSearchTerm} />
|
||||||
<LogControls
|
<LogControls
|
||||||
isPlaying={isPlaying}
|
isPlaying={isPlaying}
|
||||||
|
|||||||
@@ -39,12 +39,12 @@ export default function HomePage() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-8 animate-fade-in">
|
<div className="space-y-8 animate-fade-in">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex flex-col sm:flex-row items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
|
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
|
||||||
Supervisor Dashboard
|
Supervisor Dashboard
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground mt-2">
|
<p className="text-sm md:text-base text-muted-foreground mt-2">
|
||||||
Monitor and manage your processes in real-time
|
Monitor and manage your processes in real-time
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -183,9 +183,9 @@ export default function ProcessesPage() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-4 md:space-y-6">
|
||||||
<h1 className="text-3xl font-bold">Processes</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold">Processes</h1>
|
||||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 md:gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||||
<div key={i} className="h-64 bg-muted rounded-lg animate-pulse" />
|
<div key={i} className="h-64 bg-muted rounded-lg animate-pulse" />
|
||||||
))}
|
))}
|
||||||
@@ -196,12 +196,12 @@ export default function ProcessesPage() {
|
|||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-4 md:space-y-6">
|
||||||
<h1 className="text-3xl font-bold">Processes</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold">Processes</h1>
|
||||||
<div className="flex flex-col items-center justify-center p-12 text-center">
|
<div className="flex flex-col items-center justify-center p-8 md:p-12 text-center">
|
||||||
<AlertCircle className="h-12 w-12 text-destructive mb-4" />
|
<AlertCircle className="h-10 w-10 md:h-12 md:w-12 text-destructive mb-4" />
|
||||||
<h2 className="text-xl font-semibold mb-2">Failed to load processes</h2>
|
<h2 className="text-lg md:text-xl font-semibold mb-2">Failed to load processes</h2>
|
||||||
<p className="text-muted-foreground mb-4">
|
<p className="text-sm md:text-base text-muted-foreground mb-4">
|
||||||
Could not connect to Supervisor. Please check your configuration.
|
Could not connect to Supervisor. Please check your configuration.
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()}>
|
||||||
@@ -215,11 +215,12 @@ export default function ProcessesPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 animate-fade-in">
|
<div className="space-y-6 animate-fade-in">
|
||||||
<div className="flex items-center justify-between">
|
{/* Header - responsive layout */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
|
<div className="flex items-start gap-4 flex-wrap">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold">Processes</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold">Processes</h1>
|
||||||
<p className="text-muted-foreground mt-1">
|
<p className="text-sm md:text-base text-muted-foreground mt-1">
|
||||||
{displayedProcesses.length} of {processes?.length ?? 0} processes
|
{displayedProcesses.length} of {processes?.length ?? 0} processes
|
||||||
{displayedProcesses.length !== (processes?.length ?? 0) && ' (filtered)'}
|
{displayedProcesses.length !== (processes?.length ?? 0) && ' (filtered)'}
|
||||||
</p>
|
</p>
|
||||||
@@ -230,7 +231,9 @@ export default function ProcessesPage() {
|
|||||||
onReconnect={reconnect}
|
onReconnect={reconnect}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
|
{/* Controls - stack on mobile, row on desktop */}
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{viewMode === 'flat' && displayedProcesses.length > 0 && (
|
{viewMode === 'flat' && displayedProcesses.length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -239,13 +242,15 @@ export default function ProcessesPage() {
|
|||||||
className="gap-2"
|
className="gap-2"
|
||||||
>
|
>
|
||||||
<CheckSquare className="h-4 w-4" />
|
<CheckSquare className="h-4 w-4" />
|
||||||
{selectedProcesses.size === displayedProcesses.length ? 'Deselect All' : 'Select All'}
|
<span className="hidden sm:inline">
|
||||||
|
{selectedProcesses.size === displayedProcesses.length ? 'Deselect All' : 'Select All'}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<GroupSelector viewMode={viewMode} onViewModeChange={setViewMode} />
|
<GroupSelector viewMode={viewMode} onViewModeChange={setViewMode} />
|
||||||
<Button variant="outline" onClick={() => refetch()}>
|
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||||||
<RefreshCw className="h-4 w-4 mr-2" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
Refresh
|
<span className="hidden sm:inline ml-2">Refresh</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -265,17 +270,17 @@ export default function ProcessesPage() {
|
|||||||
|
|
||||||
{/* Process Display */}
|
{/* Process Display */}
|
||||||
{processes && processes.length === 0 ? (
|
{processes && processes.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center p-12 text-center border-2 border-dashed rounded-lg">
|
<div className="flex flex-col items-center justify-center p-8 md:p-12 text-center border-2 border-dashed rounded-lg">
|
||||||
<p className="text-muted-foreground">No processes configured</p>
|
<p className="text-sm md:text-base text-muted-foreground">No processes configured</p>
|
||||||
</div>
|
</div>
|
||||||
) : displayedProcesses.length === 0 ? (
|
) : displayedProcesses.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center p-12 text-center border-2 border-dashed rounded-lg">
|
<div className="flex flex-col items-center justify-center p-8 md:p-12 text-center border-2 border-dashed rounded-lg">
|
||||||
<p className="text-muted-foreground">No processes match the current filters</p>
|
<p className="text-sm md:text-base text-muted-foreground">No processes match the current filters</p>
|
||||||
</div>
|
</div>
|
||||||
) : viewMode === 'grouped' ? (
|
) : viewMode === 'grouped' ? (
|
||||||
<GroupView processes={displayedProcesses} />
|
<GroupView processes={displayedProcesses} />
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 md:gap-6 md:grid-cols-2 lg:grid-cols-3 auto-rows-fr">
|
||||||
{displayedProcesses.map((process, index) => {
|
{displayedProcesses.map((process, index) => {
|
||||||
const fullName = `${process.group}:${process.name}`;
|
const fullName = `${process.group}:${process.name}`;
|
||||||
const isFocused = index === focusedIndex;
|
const isFocused = index === focusedIndex;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { ProcessInfo, ProcessState } from '@/lib/supervisor/types';
|
import { ProcessInfo, ProcessState } from '@/lib/supervisor/types';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
||||||
|
import { chartColors } from '@/lib/utils/chartColors';
|
||||||
|
|
||||||
interface GroupStatisticsProps {
|
interface GroupStatisticsProps {
|
||||||
processes: ProcessInfo[];
|
processes: ProcessInfo[];
|
||||||
@@ -50,11 +51,19 @@ export function GroupStatistics({ processes }: GroupStatisticsProps) {
|
|||||||
<CartesianGrid strokeDasharray="3 3" />
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
<XAxis dataKey="name" />
|
<XAxis dataKey="name" />
|
||||||
<YAxis />
|
<YAxis />
|
||||||
<Tooltip />
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
color: '#ffffff',
|
||||||
|
}}
|
||||||
|
itemStyle={{ color: '#ffffff' }}
|
||||||
|
/>
|
||||||
<Legend />
|
<Legend />
|
||||||
<Bar dataKey="running" stackId="a" fill="hsl(var(--success))" name="Running" />
|
<Bar dataKey="running" stackId="a" fill={chartColors.running} name="Running" />
|
||||||
<Bar dataKey="stopped" stackId="a" fill="hsl(var(--muted-foreground))" name="Stopped" />
|
<Bar dataKey="stopped" stackId="a" fill={chartColors.stopped} name="Stopped" />
|
||||||
<Bar dataKey="fatal" stackId="a" fill="hsl(var(--destructive))" name="Fatal" />
|
<Bar dataKey="fatal" stackId="a" fill={chartColors.fatal} name="Fatal" />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { ProcessInfo, ProcessState } from '@/lib/supervisor/types';
|
import { ProcessInfo, ProcessState } from '@/lib/supervisor/types';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts';
|
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts';
|
||||||
|
import { chartColors } from '@/lib/utils/chartColors';
|
||||||
|
|
||||||
interface ProcessStateChartProps {
|
interface ProcessStateChartProps {
|
||||||
processes: ProcessInfo[];
|
processes: ProcessInfo[];
|
||||||
@@ -23,12 +24,12 @@ export function ProcessStateChart({ processes }: ProcessStateChartProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const data = [
|
const data = [
|
||||||
{ name: 'Running', value: stateCounts.running, color: 'hsl(var(--success))' },
|
{ name: 'Running', value: stateCounts.running, color: chartColors.running },
|
||||||
{ name: 'Stopped', value: stateCounts.stopped, color: 'hsl(var(--muted-foreground))' },
|
{ name: 'Stopped', value: stateCounts.stopped, color: chartColors.stopped },
|
||||||
{ name: 'Fatal', value: stateCounts.fatal, color: 'hsl(var(--destructive))' },
|
{ name: 'Fatal', value: stateCounts.fatal, color: chartColors.fatal },
|
||||||
{ name: 'Starting', value: stateCounts.starting, color: 'hsl(var(--warning))' },
|
{ name: 'Starting', value: stateCounts.starting, color: chartColors.starting },
|
||||||
{ name: 'Stopping', value: stateCounts.stopping, color: 'hsl(var(--accent))' },
|
{ name: 'Stopping', value: stateCounts.stopping, color: chartColors.stopping },
|
||||||
{ name: 'Other', value: stateCounts.other, color: 'hsl(var(--muted))' },
|
{ name: 'Other', value: stateCounts.other, color: chartColors.muted },
|
||||||
].filter((item) => item.value > 0);
|
].filter((item) => item.value > 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -53,7 +54,15 @@ export function ProcessStateChart({ processes }: ProcessStateChartProps) {
|
|||||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
))}
|
))}
|
||||||
</Pie>
|
</Pie>
|
||||||
<Tooltip />
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
color: '#ffffff',
|
||||||
|
}}
|
||||||
|
itemStyle={{ color: '#ffffff' }}
|
||||||
|
/>
|
||||||
<Legend />
|
<Legend />
|
||||||
</PieChart>
|
</PieChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { ProcessInfo, ProcessState } from '@/lib/supervisor/types';
|
import { ProcessInfo, ProcessState } from '@/lib/supervisor/types';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
||||||
|
import { chartColors } from '@/lib/utils/chartColors';
|
||||||
|
|
||||||
interface ProcessUptimeChartProps {
|
interface ProcessUptimeChartProps {
|
||||||
processes: ProcessInfo[];
|
processes: ProcessInfo[];
|
||||||
@@ -56,9 +57,16 @@ export function ProcessUptimeChart({ processes }: ProcessUptimeChartProps) {
|
|||||||
const minutes = Math.floor((value - hours) * 60);
|
const minutes = Math.floor((value - hours) * 60);
|
||||||
return `${hours}h ${minutes}m`;
|
return `${hours}h ${minutes}m`;
|
||||||
}}
|
}}
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
color: '#ffffff',
|
||||||
|
}}
|
||||||
|
itemStyle={{ color: '#ffffff' }}
|
||||||
/>
|
/>
|
||||||
<Legend />
|
<Legend />
|
||||||
<Bar dataKey="uptime" fill="hsl(var(--success))" name="Uptime (hours)" />
|
<Bar dataKey="uptime" fill={chartColors.success} name="Uptime (hours)" />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -57,72 +57,115 @@ export function ConfigTable({ configs }: ConfigTableProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<>
|
||||||
<CardContent className="p-0">
|
{/* Desktop Table View - hidden on mobile */}
|
||||||
<div className="overflow-x-auto">
|
<Card className="hidden md:block">
|
||||||
<table className="w-full">
|
<CardContent className="p-0">
|
||||||
<thead className="bg-muted/50 border-b">
|
<div className="overflow-x-auto">
|
||||||
<tr>
|
<table className="w-full">
|
||||||
<th
|
<thead className="bg-muted/50 border-b">
|
||||||
className="text-left p-3 cursor-pointer hover:bg-muted transition-colors"
|
<tr>
|
||||||
onClick={() => handleSort('group')}
|
<th
|
||||||
>
|
className="text-left p-3 cursor-pointer hover:bg-muted transition-colors"
|
||||||
Group <SortIcon field="group" />
|
onClick={() => handleSort('group')}
|
||||||
</th>
|
>
|
||||||
<th
|
Group <SortIcon field="group" />
|
||||||
className="text-left p-3 cursor-pointer hover:bg-muted transition-colors"
|
</th>
|
||||||
onClick={() => handleSort('name')}
|
<th
|
||||||
>
|
className="text-left p-3 cursor-pointer hover:bg-muted transition-colors"
|
||||||
Name <SortIcon field="name" />
|
onClick={() => handleSort('name')}
|
||||||
</th>
|
>
|
||||||
<th
|
Name <SortIcon field="name" />
|
||||||
className="text-left p-3 cursor-pointer hover:bg-muted transition-colors"
|
</th>
|
||||||
onClick={() => handleSort('command')}
|
<th
|
||||||
>
|
className="text-left p-3 cursor-pointer hover:bg-muted transition-colors"
|
||||||
Command <SortIcon field="command" />
|
onClick={() => handleSort('command')}
|
||||||
</th>
|
>
|
||||||
<th className="text-left p-3">Directory</th>
|
Command <SortIcon field="command" />
|
||||||
<th
|
</th>
|
||||||
className="text-center p-3 cursor-pointer hover:bg-muted transition-colors"
|
<th className="text-left p-3">Directory</th>
|
||||||
onClick={() => handleSort('autostart')}
|
<th
|
||||||
>
|
className="text-center p-3 cursor-pointer hover:bg-muted transition-colors"
|
||||||
Autostart <SortIcon field="autostart" />
|
onClick={() => handleSort('autostart')}
|
||||||
</th>
|
>
|
||||||
<th className="text-center p-3">Priority</th>
|
Autostart <SortIcon field="autostart" />
|
||||||
<th className="text-center p-3">Processes</th>
|
</th>
|
||||||
</tr>
|
<th className="text-center p-3">Priority</th>
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{sortedConfigs.map((config, index) => (
|
|
||||||
<tr
|
|
||||||
key={`${config.group}:${config.name}`}
|
|
||||||
className={cn(
|
|
||||||
'border-b hover:bg-muted/20 transition-colors',
|
|
||||||
index % 2 === 0 && 'bg-muted/5'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<td className="p-3 font-medium">{config.group}</td>
|
|
||||||
<td className="p-3">{config.name}</td>
|
|
||||||
<td className="p-3">
|
|
||||||
<code className="text-xs bg-muted px-2 py-1 rounded">{config.command}</code>
|
|
||||||
</td>
|
|
||||||
<td className="p-3 text-sm text-muted-foreground">{config.directory}</td>
|
|
||||||
<td className="p-3 text-center">
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'inline-block w-3 h-3 rounded-full',
|
|
||||||
config.autostart ? 'bg-success' : 'bg-muted'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td className="p-3 text-center text-sm">{config.priority}</td>
|
|
||||||
<td className="p-3 text-center text-sm">{config.numprocs}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{sortedConfigs.map((config, index) => (
|
||||||
</div>
|
<tr
|
||||||
</CardContent>
|
key={`${config.group}:${config.name}`}
|
||||||
</Card>
|
className={cn(
|
||||||
|
'border-b hover:bg-muted/20 transition-colors',
|
||||||
|
index % 2 === 0 && 'bg-muted/5'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<td className="p-3 font-medium">{config.group}</td>
|
||||||
|
<td className="p-3">{config.name}</td>
|
||||||
|
<td className="p-3">
|
||||||
|
<code className="text-xs bg-muted px-2 py-1 rounded">{config.command}</code>
|
||||||
|
</td>
|
||||||
|
<td className="p-3 text-sm text-muted-foreground">{config.directory}</td>
|
||||||
|
<td className="p-3 text-center">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'inline-block w-3 h-3 rounded-full',
|
||||||
|
config.autostart ? 'bg-success' : 'bg-muted'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="p-3 text-center text-sm">{config.process_prio}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Mobile Card View - visible on mobile only */}
|
||||||
|
<div className="md:hidden space-y-4">
|
||||||
|
{sortedConfigs.map((config) => (
|
||||||
|
<Card key={`${config.group}:${config.name}`}>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-lg">{config.name}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">{config.group}</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'inline-block w-3 h-3 rounded-full mt-1',
|
||||||
|
config.autostart ? 'bg-success' : 'bg-muted'
|
||||||
|
)}
|
||||||
|
title={config.autostart ? 'Autostart enabled' : 'Autostart disabled'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Command:</span>
|
||||||
|
<code className="block mt-1 text-xs bg-muted px-2 py-1 rounded break-all">
|
||||||
|
{config.command}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Directory:</span>
|
||||||
|
<div className="mt-1 text-xs break-all">{config.directory}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2">
|
||||||
|
<span className="text-muted-foreground">Priority:</span>
|
||||||
|
<span className="ml-2 font-mono">{config.process_prio}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function GroupCard({ groupName, processes }: GroupCardProps) {
|
|||||||
return (
|
return (
|
||||||
<Card className="overflow-hidden">
|
<Card className="overflow-hidden">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col md:flex-row items-start md:items-center gap-3 md:justify-between">
|
||||||
<div className="flex items-center gap-3 flex-1">
|
<div className="flex items-center gap-3 flex-1">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -80,13 +80,13 @@ export function GroupCard({ groupName, processes }: GroupCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex flex-wrap gap-2 w-full md:w-auto">
|
||||||
<Button
|
<Button
|
||||||
variant="success"
|
variant="success"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleStart}
|
onClick={handleStart}
|
||||||
disabled={isLoading || stats.stopped === 0}
|
disabled={isLoading || stats.stopped === 0}
|
||||||
className="gap-2"
|
className="gap-2 flex-1 sm:flex-initial"
|
||||||
>
|
>
|
||||||
<Play className="h-4 w-4" />
|
<Play className="h-4 w-4" />
|
||||||
Start All
|
Start All
|
||||||
@@ -96,7 +96,7 @@ export function GroupCard({ groupName, processes }: GroupCardProps) {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleStop}
|
onClick={handleStop}
|
||||||
disabled={isLoading || stats.running === 0}
|
disabled={isLoading || stats.running === 0}
|
||||||
className="gap-2"
|
className="gap-2 flex-1 sm:flex-initial"
|
||||||
>
|
>
|
||||||
<Square className="h-4 w-4" />
|
<Square className="h-4 w-4" />
|
||||||
Stop All
|
Stop All
|
||||||
@@ -106,7 +106,7 @@ export function GroupCard({ groupName, processes }: GroupCardProps) {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleRestart}
|
onClick={handleRestart}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="gap-2"
|
className="gap-2 flex-1 sm:flex-initial"
|
||||||
>
|
>
|
||||||
<RotateCw className={cn('h-4 w-4', isLoading && 'animate-spin')} />
|
<RotateCw className={cn('h-4 w-4', isLoading && 'animate-spin')} />
|
||||||
Restart All
|
Restart All
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import { Navbar } from './Navbar';
|
|||||||
|
|
||||||
export function AppLayout({ children }: { children: ReactNode }) {
|
export function AppLayout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen">
|
<div className="flex flex-col min-h-screen">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<main className="flex-1 container mx-auto px-4 py-8">{children}</main>
|
<main className="flex-1 container mx-auto px-4 md:px-6 lg:px-8 py-4 md:py-6 lg:py-8">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { Moon, Sun, Activity } from 'lucide-react';
|
import { Moon, Sun, Activity, Menu, X } from 'lucide-react';
|
||||||
import { useTheme } from '@/components/providers/ThemeProvider';
|
import { useTheme } from '@/components/providers/ThemeProvider';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { cn } from '@/lib/utils/cn';
|
import { cn } from '@/lib/utils/cn';
|
||||||
@@ -19,12 +19,30 @@ const navItems = [
|
|||||||
export function Navbar() {
|
export function Navbar() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
const themeContext = useTheme();
|
const themeContext = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Close mobile menu when route changes
|
||||||
|
setMobileMenuOpen(false);
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Prevent scroll when mobile menu is open
|
||||||
|
if (mobileMenuOpen) {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = 'unset';
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = 'unset';
|
||||||
|
};
|
||||||
|
}, [mobileMenuOpen]);
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
if (themeContext) {
|
if (themeContext) {
|
||||||
themeContext.setTheme(themeContext.resolvedTheme === 'dark' ? 'light' : 'dark');
|
themeContext.setTheme(themeContext.resolvedTheme === 'dark' ? 'light' : 'dark');
|
||||||
@@ -33,17 +51,17 @@ export function Navbar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
<nav className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||||
<div className="container flex h-16 items-center px-4">
|
<div className="container mx-auto flex h-16 items-center px-4 md:px-6">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link href="/" className="flex items-center gap-2 mr-8">
|
<Link href="/" className="flex items-center gap-2 mr-4 md:mr-8">
|
||||||
<Activity className="h-6 w-6 text-primary" />
|
<Activity className="h-6 w-6 text-primary" />
|
||||||
<span className="font-bold text-xl bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
|
<span className="font-bold text-lg md:text-xl bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
|
||||||
Supervisor
|
Supervisor
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Navigation Links */}
|
{/* Desktop Navigation Links */}
|
||||||
<div className="flex gap-1 flex-1">
|
<div className="hidden md:flex gap-1 flex-1">
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<Link key={item.href} href={item.href}>
|
<Link key={item.href} href={item.href}>
|
||||||
<Button
|
<Button
|
||||||
@@ -60,22 +78,65 @@ export function Navbar() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Theme Toggle */}
|
{/* Right side buttons */}
|
||||||
{mounted && themeContext && (
|
<div className="flex items-center gap-2 ml-auto">
|
||||||
|
{/* Theme Toggle */}
|
||||||
|
{mounted && themeContext && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={toggleTheme}
|
||||||
|
aria-label="Toggle theme"
|
||||||
|
className="h-10 w-10"
|
||||||
|
>
|
||||||
|
{themeContext.resolvedTheme === 'dark' ? (
|
||||||
|
<Sun className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<Moon className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Mobile Menu Button */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={toggleTheme}
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||||
aria-label="Toggle theme"
|
aria-label="Toggle menu"
|
||||||
|
className="md:hidden h-10 w-10"
|
||||||
>
|
>
|
||||||
{themeContext.resolvedTheme === 'dark' ? (
|
{mobileMenuOpen ? (
|
||||||
<Sun className="h-5 w-5" />
|
<X className="h-6 w-6" />
|
||||||
) : (
|
) : (
|
||||||
<Moon className="h-5 w-5" />
|
<Menu className="h-6 w-6" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Menu Drawer */}
|
||||||
|
{mobileMenuOpen && (
|
||||||
|
<div className="md:hidden fixed inset-0 top-16 z-50">
|
||||||
|
<div className="container">
|
||||||
|
<nav className="flex flex-col gap-2 bg-background/95 backdrop-blur-md rounded-lg p-4">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<Link key={item.href} href={item.href}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="lg"
|
||||||
|
className={cn(
|
||||||
|
'w-full justify-start text-lg transition-colors',
|
||||||
|
pathname === item.href && 'bg-accent text-accent-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function LogControls({
|
|||||||
isClearing = false,
|
isClearing = false,
|
||||||
}: LogControlsProps) {
|
}: LogControlsProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant={isPlaying ? 'destructive' : 'success'}
|
variant={isPlaying ? 'destructive' : 'success'}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface LogSearchProps {
|
|||||||
|
|
||||||
export function LogSearch({ value, onChange, placeholder = 'Search logs...' }: LogSearchProps) {
|
export function LogSearch({ value, onChange, placeholder = 'Search logs...' }: LogSearchProps) {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex-1 max-w-md">
|
<div className="relative w-full sm:flex-1 sm:max-w-md">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -46,60 +46,74 @@ export function BatchActions({ selectedProcesses, processes, onClearSelection }:
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 animate-slide-up">
|
<div className="fixed bottom-4 left-4 right-4 md:left-1/2 md:right-auto md:-translate-x-1/2 z-50 animate-slide-up">
|
||||||
<Card className="shadow-2xl border-2">
|
<Card className="shadow-2xl border-2">
|
||||||
<div className="flex items-center gap-4 px-6 py-4">
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 sm:gap-4 p-4">
|
||||||
<div className="flex items-center gap-2">
|
{/* Selection count */}
|
||||||
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center">
|
<div className="flex items-center justify-between sm:justify-start gap-2">
|
||||||
<span className="text-sm font-bold text-primary">{selectedCount}</span>
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center">
|
||||||
|
<span className="text-sm font-bold text-primary">{selectedCount}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{selectedCount} {selectedCount === 1 ? 'process' : 'processes'} selected
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium">
|
{/* Close button on mobile - inline with count */}
|
||||||
{selectedCount} {selectedCount === 1 ? 'process' : 'processes'} selected
|
<Button
|
||||||
</span>
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={onClearSelection}
|
||||||
|
className="h-8 w-8 sm:hidden"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-6 w-px bg-border" />
|
<div className="hidden sm:block h-6 w-px bg-border" />
|
||||||
|
|
||||||
<div className="flex gap-2">
|
{/* Action buttons - full width on mobile */}
|
||||||
|
<div className="flex gap-2 flex-1">
|
||||||
<Button
|
<Button
|
||||||
variant="success"
|
variant="success"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleStartSelected}
|
onClick={handleStartSelected}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="gap-2"
|
className="gap-2 flex-1 sm:flex-initial"
|
||||||
>
|
>
|
||||||
<Play className="h-4 w-4" />
|
<Play className="h-4 w-4" />
|
||||||
Start Selected
|
<span>Start</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleStopSelected}
|
onClick={handleStopSelected}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="gap-2"
|
className="gap-2 flex-1 sm:flex-initial"
|
||||||
>
|
>
|
||||||
<Square className="h-4 w-4" />
|
<Square className="h-4 w-4" />
|
||||||
Stop Selected
|
<span>Stop</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleRestartSelected}
|
onClick={handleRestartSelected}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="gap-2"
|
className="gap-2 flex-1 sm:flex-initial"
|
||||||
>
|
>
|
||||||
<RotateCw className={cn('h-4 w-4', isLoading && 'animate-spin')} />
|
<RotateCw className={cn('h-4 w-4', isLoading && 'animate-spin')} />
|
||||||
Restart Selected
|
<span>Restart</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-6 w-px bg-border" />
|
<div className="hidden sm:block h-6 w-px bg-border" />
|
||||||
|
|
||||||
|
{/* Close button on desktop */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={onClearSelection}
|
onClick={onClearSelection}
|
||||||
className="h-8 w-8"
|
className="hidden sm:flex h-8 w-8"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function ProcessCard({ process, isSelected = false, isFocused = false, on
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={cn(
|
className={cn(
|
||||||
'transition-all hover:shadow-lg animate-fade-in',
|
'transition-all hover:shadow-lg animate-fade-in h-full flex flex-col',
|
||||||
onSelectionChange && 'cursor-pointer',
|
onSelectionChange && 'cursor-pointer',
|
||||||
isSelected && 'ring-2 ring-primary ring-offset-2',
|
isSelected && 'ring-2 ring-primary ring-offset-2',
|
||||||
isFocused && 'ring-2 ring-accent ring-offset-2 shadow-xl'
|
isFocused && 'ring-2 ring-accent ring-offset-2 shadow-xl'
|
||||||
@@ -82,7 +82,7 @@ export function ProcessCard({ process, isSelected = false, isFocused = false, on
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4 flex-1 flex flex-col justify-between">
|
||||||
{/* Metrics */}
|
{/* Metrics */}
|
||||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
@@ -109,34 +109,37 @@ export function ProcessCard({ process, isSelected = false, isFocused = false, on
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-2" onClick={(e) => e.stopPropagation()}>
|
<div className="flex flex-wrap gap-2" onClick={(e) => e.stopPropagation()}>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="success"
|
variant="success"
|
||||||
onClick={handleStart}
|
onClick={handleStart}
|
||||||
disabled={!canStartProcess(process.state as ProcessStateCode) || isLoading}
|
disabled={!canStartProcess(process.state as ProcessStateCode) || isLoading}
|
||||||
className="flex-1"
|
className="flex-1 min-w-[100px]"
|
||||||
>
|
>
|
||||||
<Play className="h-4 w-4" />
|
<Play className="h-4 w-4" />
|
||||||
Start
|
<span className="hidden sm:inline">Start</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="warning"
|
variant="warning"
|
||||||
onClick={handleStop}
|
onClick={handleStop}
|
||||||
disabled={!canStopProcess(process.state as ProcessStateCode) || isLoading}
|
disabled={!canStopProcess(process.state as ProcessStateCode) || isLoading}
|
||||||
className="flex-1"
|
className="flex-1 min-w-[100px]"
|
||||||
>
|
>
|
||||||
<Square className="h-4 w-4" />
|
<Square className="h-4 w-4" />
|
||||||
Stop
|
<span className="hidden sm:inline">Stop</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleRestart}
|
onClick={handleRestart}
|
||||||
disabled={process.state === 0 || isLoading}
|
disabled={process.state === 0 || isLoading}
|
||||||
|
title="Restart"
|
||||||
|
className="flex-1 sm:flex-initial"
|
||||||
>
|
>
|
||||||
<RotateCw className={cn('h-4 w-4', isLoading && 'animate-spin-slow')} />
|
<RotateCw className={cn('h-4 w-4', isLoading && 'animate-spin-slow')} />
|
||||||
|
<span className="sm:hidden">Restart</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -144,8 +147,10 @@ export function ProcessCard({ process, isSelected = false, isFocused = false, on
|
|||||||
onClick={() => setShowSignalModal(true)}
|
onClick={() => setShowSignalModal(true)}
|
||||||
disabled={process.state === 0}
|
disabled={process.state === 0}
|
||||||
title="Send Signal"
|
title="Send Signal"
|
||||||
|
className="flex-1 sm:flex-initial"
|
||||||
>
|
>
|
||||||
<Zap className="h-4 w-4" />
|
<Zap className="h-4 w-4" />
|
||||||
|
<span className="sm:hidden">Signal</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -153,8 +158,10 @@ export function ProcessCard({ process, isSelected = false, isFocused = false, on
|
|||||||
onClick={() => setShowStdinModal(true)}
|
onClick={() => setShowStdinModal(true)}
|
||||||
disabled={process.state !== 20}
|
disabled={process.state !== 20}
|
||||||
title="Send Stdin"
|
title="Send Stdin"
|
||||||
|
className="flex-1 sm:flex-initial"
|
||||||
>
|
>
|
||||||
<Terminal className="h-4 w-4" />
|
<Terminal className="h-4 w-4" />
|
||||||
|
<span className="sm:hidden">Stdin</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export function SignalSender({ processName, onClose }: SignalSenderProps) {
|
|||||||
const isDangerous = signal && ['TERM', 'KILL', 'QUIT'].includes(signal.toUpperCase());
|
const isDangerous = signal && ['TERM', 'KILL', 'QUIT'].includes(signal.toUpperCase());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm p-4">
|
||||||
<Card className="w-full max-w-lg shadow-2xl">
|
<Card className="w-full max-w-lg shadow-2xl">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function StdinInput({ processName, onClose }: StdinInputProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm p-4">
|
||||||
<Card className="w-full max-w-2xl shadow-2xl">
|
<Card className="w-full max-w-2xl shadow-2xl">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
163
components/providers/ErrorBoundary.tsx
Normal file
163
components/providers/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Component, ErrorInfo, ReactNode } from 'react';
|
||||||
|
import { clientLogger } from '@/lib/utils/client-logger';
|
||||||
|
|
||||||
|
interface ErrorBoundaryProps {
|
||||||
|
children: ReactNode;
|
||||||
|
fallback?: (error: Error, errorInfo: ErrorInfo, reset: () => void) => ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorBoundaryState {
|
||||||
|
hasError: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
errorInfo: ErrorInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error Boundary component that catches React errors and logs them
|
||||||
|
* Provides a fallback UI when errors occur
|
||||||
|
*/
|
||||||
|
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||||
|
constructor(props: ErrorBoundaryProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
hasError: false,
|
||||||
|
error: null,
|
||||||
|
errorInfo: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
|
||||||
|
return {
|
||||||
|
hasError: true,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
||||||
|
// Log error to client logger
|
||||||
|
clientLogger.error('React Error Boundary caught error', error, {
|
||||||
|
componentStack: errorInfo.componentStack,
|
||||||
|
errorBoundary: 'ErrorBoundary',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store error info in state
|
||||||
|
this.setState({
|
||||||
|
errorInfo,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send error to server for logging (async, non-blocking)
|
||||||
|
this.reportErrorToServer(error, errorInfo).catch((reportError) => {
|
||||||
|
clientLogger.error('Failed to report error to server', reportError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async reportErrorToServer(error: Error, errorInfo: ErrorInfo): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fetch('/api/client-error', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
componentStack: errorInfo.componentStack,
|
||||||
|
name: error.name,
|
||||||
|
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown',
|
||||||
|
url: typeof window !== 'undefined' ? window.location.href : 'unknown',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// Silently fail - we don't want error reporting to cause more errors
|
||||||
|
console.error('Error reporting failed:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleReset = (): void => {
|
||||||
|
clientLogger.info('Error boundary reset requested');
|
||||||
|
this.setState({
|
||||||
|
hasError: false,
|
||||||
|
error: null,
|
||||||
|
errorInfo: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render(): ReactNode {
|
||||||
|
if (this.state.hasError && this.state.error) {
|
||||||
|
// Use custom fallback if provided
|
||||||
|
if (this.props.fallback) {
|
||||||
|
return this.props.fallback(
|
||||||
|
this.state.error,
|
||||||
|
this.state.errorInfo!,
|
||||||
|
this.handleReset
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default fallback UI
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 px-4">
|
||||||
|
<div className="max-w-md w-full bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
|
||||||
|
<div className="flex items-center justify-center w-12 h-12 mx-auto bg-red-100 dark:bg-red-900/20 rounded-full mb-4">
|
||||||
|
<svg
|
||||||
|
className="w-6 h-6 text-red-600 dark:text-red-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white text-center mb-2">
|
||||||
|
Something went wrong
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 text-center mb-4">
|
||||||
|
An unexpected error occurred. The error has been logged and we'll look into it.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{process.env.NODE_ENV === 'development' && this.state.error && (
|
||||||
|
<div className="mb-4 p-4 bg-gray-100 dark:bg-gray-900 rounded-md">
|
||||||
|
<p className="text-sm font-mono text-red-600 dark:text-red-400 break-all">
|
||||||
|
{this.state.error.message}
|
||||||
|
</p>
|
||||||
|
{this.state.error.stack && (
|
||||||
|
<details className="mt-2">
|
||||||
|
<summary className="text-xs text-gray-600 dark:text-gray-400 cursor-pointer">
|
||||||
|
Stack trace
|
||||||
|
</summary>
|
||||||
|
<pre className="mt-2 text-xs text-gray-600 dark:text-gray-400 overflow-x-auto">
|
||||||
|
{this.state.error.stack}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={this.handleReset}
|
||||||
|
className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="w-full mt-2 px-4 py-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-900 dark:text-white rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
Reload page
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { useState, ReactNode } from 'react';
|
import { useState, ReactNode, useEffect } from 'react';
|
||||||
import { Toaster } from 'sonner';
|
import { Toaster } from 'sonner';
|
||||||
import { ThemeProvider } from './ThemeProvider';
|
import { ThemeProvider } from './ThemeProvider';
|
||||||
|
import { ErrorBoundary } from './ErrorBoundary';
|
||||||
|
import { clientLogger } from '@/lib/utils/client-logger';
|
||||||
|
import { initGlobalErrorHandlers } from '@/lib/utils/global-error-handler';
|
||||||
|
|
||||||
export function Providers({ children }: { children: ReactNode }) {
|
export function Providers({ children }: { children: ReactNode }) {
|
||||||
const [queryClient] = useState(
|
const [queryClient] = useState(
|
||||||
@@ -15,16 +18,26 @@ export function Providers({ children }: { children: ReactNode }) {
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
retry: 2,
|
retry: 2,
|
||||||
},
|
},
|
||||||
|
mutations: {
|
||||||
|
retry: 1,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Initialize global error handlers once
|
||||||
|
useEffect(() => {
|
||||||
|
initGlobalErrorHandlers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ErrorBoundary>
|
||||||
<QueryClientProvider client={queryClient}>
|
<ThemeProvider>
|
||||||
{children}
|
<QueryClientProvider client={queryClient}>
|
||||||
<Toaster position="top-right" richColors closeButton />
|
{children}
|
||||||
</QueryClientProvider>
|
<Toaster position="top-right" richColors closeButton />
|
||||||
</ThemeProvider>
|
</QueryClientProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const SHORTCUT_GROUPS: ShortcutGroup[] = [
|
|||||||
|
|
||||||
export function KeyboardShortcutsHelp({ onClose }: KeyboardShortcutsHelpProps) {
|
export function KeyboardShortcutsHelp({ onClose }: KeyboardShortcutsHelpProps) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm p-4">
|
||||||
<Card className="w-full max-w-2xl shadow-2xl max-h-[80vh] overflow-y-auto">
|
<Card className="w-full max-w-2xl shadow-2xl max-h-[80vh] overflow-y-auto">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
variant === 'outline',
|
variant === 'outline',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'h-8 px-3 text-sm': size === 'sm',
|
'h-9 md:h-8 px-3 text-sm': size === 'sm',
|
||||||
'h-10 px-4': size === 'md',
|
'h-11 md:h-10 px-4': size === 'md',
|
||||||
'h-12 px-6 text-lg': size === 'lg',
|
'h-12 px-6 text-lg': size === 'lg',
|
||||||
'h-10 w-10': size === 'icon',
|
'h-11 w-11 md:h-10 md:w-10': size === 'icon',
|
||||||
},
|
},
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|||||||
({ className, ...props }, ref) => (
|
({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
className={cn('flex flex-col space-y-1.5 p-4 md:p-6', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -30,7 +30,7 @@ const CardTitle = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHeadingEle
|
|||||||
({ className, ...props }, ref) => (
|
({ className, ...props }, ref) => (
|
||||||
<h3
|
<h3
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('text-2xl font-semibold leading-none tracking-tight', className)}
|
className={cn('text-xl md:text-2xl font-semibold leading-none tracking-tight', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -50,7 +50,7 @@ CardDescription.displayName = 'CardDescription';
|
|||||||
|
|
||||||
const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||||
({ className, ...props }, ref) => (
|
({ className, ...props }, ref) => (
|
||||||
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
<div ref={ref} className={cn('p-4 md:p-6 pt-0', className)} {...props} />
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
CardContent.displayName = 'CardContent';
|
CardContent.displayName = 'CardContent';
|
||||||
@@ -59,7 +59,7 @@ const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|||||||
({ className, ...props }, ref) => (
|
({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('flex items-center p-6 pt-0', className)}
|
className={cn('flex items-center p-4 md:p-6 pt-0', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
|
import { createHookLogger } from '@/lib/utils/client-logger';
|
||||||
|
|
||||||
|
const logger = createHookLogger('useEventSource');
|
||||||
|
|
||||||
export type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
|
export type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
|
||||||
|
|
||||||
@@ -36,6 +39,7 @@ export function useEventSource(url: string, options: UseEventSourceOptions = {})
|
|||||||
const connect = useCallback(() => {
|
const connect = useCallback(() => {
|
||||||
if (!enabled || eventSourceRef.current) return;
|
if (!enabled || eventSourceRef.current) return;
|
||||||
|
|
||||||
|
logger.info('Connecting to SSE', { url });
|
||||||
setStatus('connecting');
|
setStatus('connecting');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -43,6 +47,7 @@ export function useEventSource(url: string, options: UseEventSourceOptions = {})
|
|||||||
eventSourceRef.current = eventSource;
|
eventSourceRef.current = eventSource;
|
||||||
|
|
||||||
eventSource.addEventListener('connected', () => {
|
eventSource.addEventListener('connected', () => {
|
||||||
|
logger.info('SSE connected successfully', { url });
|
||||||
setStatus('connected');
|
setStatus('connected');
|
||||||
setReconnectAttempts(0);
|
setReconnectAttempts(0);
|
||||||
onConnect?.();
|
onConnect?.();
|
||||||
@@ -51,6 +56,7 @@ export function useEventSource(url: string, options: UseEventSourceOptions = {})
|
|||||||
eventSource.addEventListener('heartbeat', (event) => {
|
eventSource.addEventListener('heartbeat', (event) => {
|
||||||
// Keep connection alive
|
// Keep connection alive
|
||||||
if (status !== 'connected') {
|
if (status !== 'connected') {
|
||||||
|
logger.debug('SSE heartbeat received, updating status to connected');
|
||||||
setStatus('connected');
|
setStatus('connected');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -58,15 +64,20 @@ export function useEventSource(url: string, options: UseEventSourceOptions = {})
|
|||||||
eventSource.addEventListener('process-update', (event) => {
|
eventSource.addEventListener('process-update', (event) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
logger.debug('Process update received', {
|
||||||
|
processCount: data.processes?.length,
|
||||||
|
timestamp: data.timestamp,
|
||||||
|
});
|
||||||
onMessage?.({ event: 'process-update', data });
|
onMessage?.({ event: 'process-update', data });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to parse SSE message:', error);
|
logger.error('Failed to parse SSE message', error, { event: event.data });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
eventSource.addEventListener('error', (event) => {
|
eventSource.addEventListener('error', (event) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse((event as MessageEvent).data);
|
const data = JSON.parse((event as MessageEvent).data);
|
||||||
|
logger.warn('SSE error event received', { error: data });
|
||||||
onMessage?.({ event: 'error', data });
|
onMessage?.({ event: 'error', data });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Not a message error, connection error
|
// Not a message error, connection error
|
||||||
@@ -74,7 +85,7 @@ export function useEventSource(url: string, options: UseEventSourceOptions = {})
|
|||||||
});
|
});
|
||||||
|
|
||||||
eventSource.onerror = (event) => {
|
eventSource.onerror = (event) => {
|
||||||
console.error('EventSource error:', event);
|
logger.error('EventSource connection error', event);
|
||||||
setStatus('error');
|
setStatus('error');
|
||||||
onError?.(event);
|
onError?.(event);
|
||||||
|
|
||||||
@@ -85,24 +96,33 @@ export function useEventSource(url: string, options: UseEventSourceOptions = {})
|
|||||||
// Attempt reconnection with exponential backoff
|
// Attempt reconnection with exponential backoff
|
||||||
if (reconnectAttempts < maxReconnectAttempts) {
|
if (reconnectAttempts < maxReconnectAttempts) {
|
||||||
const delay = Math.min(reconnectInterval * Math.pow(2, reconnectAttempts), 30000);
|
const delay = Math.min(reconnectInterval * Math.pow(2, reconnectAttempts), 30000);
|
||||||
console.log(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts + 1}/${maxReconnectAttempts})`);
|
logger.info('Scheduling reconnection', {
|
||||||
|
delay,
|
||||||
|
attempt: reconnectAttempts + 1,
|
||||||
|
maxAttempts: maxReconnectAttempts,
|
||||||
|
});
|
||||||
|
|
||||||
reconnectTimeoutRef.current = setTimeout(() => {
|
reconnectTimeoutRef.current = setTimeout(() => {
|
||||||
setReconnectAttempts((prev) => prev + 1);
|
setReconnectAttempts((prev) => prev + 1);
|
||||||
connect();
|
connect();
|
||||||
}, delay);
|
}, delay);
|
||||||
} else {
|
} else {
|
||||||
|
logger.warn('Max reconnection attempts reached, disconnecting', {
|
||||||
|
maxAttempts: maxReconnectAttempts,
|
||||||
|
});
|
||||||
setStatus('disconnected');
|
setStatus('disconnected');
|
||||||
onDisconnect?.();
|
onDisconnect?.();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create EventSource:', error);
|
logger.error('Failed to create EventSource', error, { url });
|
||||||
setStatus('error');
|
setStatus('error');
|
||||||
}
|
}
|
||||||
}, [url, enabled, status, reconnectAttempts, maxReconnectAttempts, reconnectInterval, onMessage, onError, onConnect, onDisconnect]);
|
}, [url, enabled, status, reconnectAttempts, maxReconnectAttempts, reconnectInterval, onMessage, onError, onConnect, onDisconnect]);
|
||||||
|
|
||||||
const disconnect = useCallback(() => {
|
const disconnect = useCallback(() => {
|
||||||
|
logger.info('Disconnecting from SSE');
|
||||||
|
|
||||||
if (reconnectTimeoutRef.current) {
|
if (reconnectTimeoutRef.current) {
|
||||||
clearTimeout(reconnectTimeoutRef.current);
|
clearTimeout(reconnectTimeoutRef.current);
|
||||||
reconnectTimeoutRef.current = null;
|
reconnectTimeoutRef.current = null;
|
||||||
@@ -118,6 +138,7 @@ export function useEventSource(url: string, options: UseEventSourceOptions = {})
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const reconnect = useCallback(() => {
|
const reconnect = useCallback(() => {
|
||||||
|
logger.info('Manual reconnection requested');
|
||||||
disconnect();
|
disconnect();
|
||||||
setReconnectAttempts(0);
|
setReconnectAttempts(0);
|
||||||
connect();
|
connect();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import type { ProcessInfo, SystemInfo, LogTailResult, ConfigInfo } from '@/lib/supervisor/types';
|
import type { ProcessInfo, SystemInfo, LogTailResult, ConfigInfo } from '@/lib/supervisor/types';
|
||||||
|
import { createMutationLogger } from '@/lib/utils/client-logger';
|
||||||
|
|
||||||
// Query Keys
|
// Query Keys
|
||||||
export const supervisorKeys = {
|
export const supervisorKeys = {
|
||||||
@@ -143,16 +144,21 @@ export function useProcessLogs(
|
|||||||
|
|
||||||
export function useStartProcess() {
|
export function useStartProcess() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const logger = createMutationLogger('startProcess');
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ name, wait }: { name: string; wait?: boolean }) => startProcess(name, wait),
|
mutationFn: ({ name, wait }: { name: string; wait?: boolean }) => {
|
||||||
|
logger.info('Starting process', { name, wait });
|
||||||
|
return startProcess(name, wait);
|
||||||
|
},
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
|
logger.info('Process started successfully', { name: variables.name });
|
||||||
toast.success(data.message);
|
toast.success(data.message);
|
||||||
// Invalidate and refetch
|
|
||||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
||||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.process(variables.name) });
|
queryClient.invalidateQueries({ queryKey: supervisorKeys.process(variables.name) });
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error, variables) => {
|
||||||
|
logger.error('Failed to start process', error, { name: variables.name });
|
||||||
toast.error(`Failed to start process: ${error.message}`);
|
toast.error(`Failed to start process: ${error.message}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -160,15 +166,21 @@ export function useStartProcess() {
|
|||||||
|
|
||||||
export function useStopProcess() {
|
export function useStopProcess() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const logger = createMutationLogger('stopProcess');
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ name, wait }: { name: string; wait?: boolean }) => stopProcess(name, wait),
|
mutationFn: ({ name, wait }: { name: string; wait?: boolean }) => {
|
||||||
|
logger.info('Stopping process', { name, wait });
|
||||||
|
return stopProcess(name, wait);
|
||||||
|
},
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
|
logger.info('Process stopped successfully', { name: variables.name });
|
||||||
toast.success(data.message);
|
toast.success(data.message);
|
||||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
||||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.process(variables.name) });
|
queryClient.invalidateQueries({ queryKey: supervisorKeys.process(variables.name) });
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error, variables) => {
|
||||||
|
logger.error('Failed to stop process', error, { name: variables.name });
|
||||||
toast.error(`Failed to stop process: ${error.message}`);
|
toast.error(`Failed to stop process: ${error.message}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -176,15 +188,21 @@ export function useStopProcess() {
|
|||||||
|
|
||||||
export function useRestartProcess() {
|
export function useRestartProcess() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const logger = createMutationLogger('restartProcess');
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (name: string) => restartProcess(name),
|
mutationFn: (name: string) => {
|
||||||
|
logger.info('Restarting process', { name });
|
||||||
|
return restartProcess(name);
|
||||||
|
},
|
||||||
onSuccess: (data, name) => {
|
onSuccess: (data, name) => {
|
||||||
|
logger.info('Process restarted successfully', { name });
|
||||||
toast.success(data.message);
|
toast.success(data.message);
|
||||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
||||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.process(name) });
|
queryClient.invalidateQueries({ queryKey: supervisorKeys.process(name) });
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error, name) => {
|
||||||
|
logger.error('Failed to restart process', error, { name });
|
||||||
toast.error(`Failed to restart process: ${error.message}`);
|
toast.error(`Failed to restart process: ${error.message}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -192,7 +210,7 @@ export function useRestartProcess() {
|
|||||||
|
|
||||||
// Log Management
|
// Log Management
|
||||||
|
|
||||||
async function fetchMainLog(offset: number = -4096, length: number = 4096): Promise<{ logs: string }> {
|
async function fetchMainLog(offset: number = -4096, length: number = 0): Promise<{ logs: string }> {
|
||||||
const response = await fetch(`/api/supervisor/logs?offset=${offset}&length=${length}`);
|
const response = await fetch(`/api/supervisor/logs?offset=${offset}&length=${length}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
@@ -415,14 +433,22 @@ async function restartAllProcesses(wait: boolean = true): Promise<{ success: boo
|
|||||||
|
|
||||||
export function useStartAllProcesses() {
|
export function useStartAllProcesses() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const logger = createMutationLogger('startAllProcesses');
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (wait: boolean = true) => startAllProcesses(wait),
|
mutationFn: (wait: boolean = true) => {
|
||||||
|
logger.info('Starting all processes', { wait });
|
||||||
|
return startAllProcesses(wait);
|
||||||
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
|
logger.info('All processes started successfully', {
|
||||||
|
resultCount: data.results?.length,
|
||||||
|
});
|
||||||
toast.success(data.message);
|
toast.success(data.message);
|
||||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
|
logger.error('Failed to start all processes', error);
|
||||||
toast.error(`Failed to start all processes: ${error.message}`);
|
toast.error(`Failed to start all processes: ${error.message}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -430,14 +456,22 @@ export function useStartAllProcesses() {
|
|||||||
|
|
||||||
export function useStopAllProcesses() {
|
export function useStopAllProcesses() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const logger = createMutationLogger('stopAllProcesses');
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (wait: boolean = true) => stopAllProcesses(wait),
|
mutationFn: (wait: boolean = true) => {
|
||||||
|
logger.info('Stopping all processes', { wait });
|
||||||
|
return stopAllProcesses(wait);
|
||||||
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
|
logger.info('All processes stopped successfully', {
|
||||||
|
resultCount: data.results?.length,
|
||||||
|
});
|
||||||
toast.success(data.message);
|
toast.success(data.message);
|
||||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
|
logger.error('Failed to stop all processes', error);
|
||||||
toast.error(`Failed to stop all processes: ${error.message}`);
|
toast.error(`Failed to stop all processes: ${error.message}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -445,14 +479,22 @@ export function useStopAllProcesses() {
|
|||||||
|
|
||||||
export function useRestartAllProcesses() {
|
export function useRestartAllProcesses() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const logger = createMutationLogger('restartAllProcesses');
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (wait: boolean = true) => restartAllProcesses(wait),
|
mutationFn: (wait: boolean = true) => {
|
||||||
|
logger.info('Restarting all processes', { wait });
|
||||||
|
return restartAllProcesses(wait);
|
||||||
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
|
logger.info('All processes restarted successfully', {
|
||||||
|
resultCount: data.results?.length,
|
||||||
|
});
|
||||||
toast.success(data.message);
|
toast.success(data.message);
|
||||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
|
logger.error('Failed to restart all processes', error);
|
||||||
toast.error(`Failed to restart all processes: ${error.message}`);
|
toast.error(`Failed to restart all processes: ${error.message}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -516,14 +558,20 @@ export function useConfig() {
|
|||||||
|
|
||||||
export function useReloadConfig() {
|
export function useReloadConfig() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const logger = createMutationLogger('reloadConfig');
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: reloadConfig,
|
mutationFn: () => {
|
||||||
|
logger.info('Reloading supervisor configuration');
|
||||||
|
return reloadConfig();
|
||||||
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
|
logger.info('Configuration reloaded successfully');
|
||||||
toast.success(data.message);
|
toast.success(data.message);
|
||||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.all });
|
queryClient.invalidateQueries({ queryKey: supervisorKeys.all });
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
|
logger.error('Failed to reload configuration', error);
|
||||||
toast.error(`Failed to reload configuration: ${error.message}`);
|
toast.error(`Failed to reload configuration: ${error.message}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -602,15 +650,24 @@ async function signalAllProcesses(signal: string): Promise<{ success: boolean; m
|
|||||||
|
|
||||||
export function useSignalProcess() {
|
export function useSignalProcess() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const logger = createMutationLogger('signalProcess');
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ name, signal }: { name: string; signal: string }) => signalProcess(name, signal),
|
mutationFn: ({ name, signal }: { name: string; signal: string }) => {
|
||||||
|
logger.info('Sending signal to process', { name, signal });
|
||||||
|
return signalProcess(name, signal);
|
||||||
|
},
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
|
logger.info('Signal sent successfully', { name: variables.name, signal: variables.signal });
|
||||||
toast.success(data.message);
|
toast.success(data.message);
|
||||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.process(variables.name) });
|
queryClient.invalidateQueries({ queryKey: supervisorKeys.process(variables.name) });
|
||||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error, variables) => {
|
||||||
|
logger.error('Failed to send signal', error, {
|
||||||
|
name: variables.name,
|
||||||
|
signal: variables.signal,
|
||||||
|
});
|
||||||
toast.error(`Failed to send signal: ${error.message}`);
|
toast.error(`Failed to send signal: ${error.message}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -62,38 +62,39 @@ export const ConfigInfoSchema = z.object({
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
group: z.string(),
|
group: z.string(),
|
||||||
autostart: z.boolean(),
|
autostart: z.boolean(),
|
||||||
directory: z.union([z.string(), z.null()]),
|
autorestart: z.string().optional(), // "auto", "none", or "unexpected"
|
||||||
|
directory: z.string(),
|
||||||
command: z.string(),
|
command: z.string(),
|
||||||
environment: z.union([z.string(), z.null()]),
|
|
||||||
exitcodes: z.array(z.number()),
|
exitcodes: z.array(z.number()),
|
||||||
|
group_prio: z.number(),
|
||||||
|
inuse: z.boolean(),
|
||||||
|
killasgroup: z.boolean(),
|
||||||
|
process_prio: z.number(),
|
||||||
redirect_stderr: z.boolean(),
|
redirect_stderr: z.boolean(),
|
||||||
|
serverurl: z.string(),
|
||||||
|
startretries: z.number(),
|
||||||
|
startsecs: z.number(),
|
||||||
stderr_capture_maxbytes: z.number(),
|
stderr_capture_maxbytes: z.number(),
|
||||||
stderr_events_enabled: z.boolean(),
|
stderr_events_enabled: z.boolean(),
|
||||||
stderr_logfile: z.string(),
|
stderr_logfile: z.string(),
|
||||||
stderr_logfile_backups: z.number(),
|
stderr_logfile_backups: z.number(),
|
||||||
stderr_logfile_maxbytes: z.number(),
|
stderr_logfile_maxbytes: z.number(),
|
||||||
|
stderr_syslog: z.boolean(),
|
||||||
|
stopsignal: z.number(), // Signal number (e.g., 15 for SIGTERM)
|
||||||
|
stopwaitsecs: z.number(),
|
||||||
stdout_capture_maxbytes: z.number(),
|
stdout_capture_maxbytes: z.number(),
|
||||||
stdout_events_enabled: z.boolean(),
|
stdout_events_enabled: z.boolean(),
|
||||||
stdout_logfile: z.string(),
|
stdout_logfile: z.string(),
|
||||||
stdout_logfile_backups: z.number(),
|
stdout_logfile_backups: z.number(),
|
||||||
stdout_logfile_maxbytes: z.number(),
|
stdout_logfile_maxbytes: z.number(),
|
||||||
stopsignal: z.string(),
|
stdout_syslog: z.boolean(),
|
||||||
stopwaitsecs: z.number(),
|
uid: z.string(), // Username string
|
||||||
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({
|
export const ReloadConfigResultSchema = z.object({
|
||||||
added: z.array(z.array(z.string())),
|
added: z.array(z.array(z.string())).optional().default([]),
|
||||||
changed: z.array(z.array(z.string())),
|
changed: z.array(z.array(z.string())).optional().default([]),
|
||||||
removed: z.array(z.array(z.string())),
|
removed: z.array(z.array(z.string())).optional().default([]),
|
||||||
});
|
});
|
||||||
|
|
||||||
// TypeScript Types
|
// TypeScript Types
|
||||||
|
|||||||
53
lib/utils/chartColors.ts
Normal file
53
lib/utils/chartColors.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Chart color utilities for Recharts compatibility
|
||||||
|
*
|
||||||
|
* Recharts doesn't properly parse CSS custom properties or OKLCH colors,
|
||||||
|
* so we provide static hex colors that work reliably across themes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const chartColors = {
|
||||||
|
primary: '#3b82f6', // Blue
|
||||||
|
success: '#22c55e', // Green
|
||||||
|
warning: '#eab308', // Yellow
|
||||||
|
destructive: '#ef4444', // Red
|
||||||
|
accent: '#06b6d4', // Cyan
|
||||||
|
muted: '#9ca3af', // Gray
|
||||||
|
running: '#22c55e', // Same as success
|
||||||
|
stopped: '#6b7280', // Darker gray
|
||||||
|
fatal: '#ef4444', // Same as destructive
|
||||||
|
starting: '#eab308', // Same as warning
|
||||||
|
backoff: '#f97316', // Orange
|
||||||
|
stopping: '#fb923c', // Light orange
|
||||||
|
exited: '#9ca3af', // Same as muted
|
||||||
|
unknown: '#64748b', // Slate
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get color for process state
|
||||||
|
*/
|
||||||
|
export function getStateColor(state: string): string {
|
||||||
|
const stateMap: Record<string, string> = {
|
||||||
|
running: chartColors.running,
|
||||||
|
stopped: chartColors.stopped,
|
||||||
|
fatal: chartColors.fatal,
|
||||||
|
starting: chartColors.starting,
|
||||||
|
backoff: chartColors.backoff,
|
||||||
|
stopping: chartColors.stopping,
|
||||||
|
exited: chartColors.exited,
|
||||||
|
unknown: chartColors.unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
return stateMap[state.toLowerCase()] || chartColors.muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color palette for multiple data series
|
||||||
|
*/
|
||||||
|
export const colorPalette = [
|
||||||
|
chartColors.primary,
|
||||||
|
chartColors.success,
|
||||||
|
chartColors.warning,
|
||||||
|
chartColors.accent,
|
||||||
|
chartColors.destructive,
|
||||||
|
chartColors.muted,
|
||||||
|
];
|
||||||
137
lib/utils/client-logger.ts
Normal file
137
lib/utils/client-logger.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client-side logging utility
|
||||||
|
* Logs to console in development, can be extended to send to server in production
|
||||||
|
*/
|
||||||
|
|
||||||
|
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||||
|
|
||||||
|
interface LogContext {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ClientLogger {
|
||||||
|
private isDevelopment: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.isDevelopment = process.env.NODE_ENV === 'development';
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatMessage(level: LogLevel, message: string, context?: LogContext): string {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const contextStr = context ? ` ${JSON.stringify(context)}` : '';
|
||||||
|
return `[${timestamp}] [${level.toUpperCase()}] ${message}${contextStr}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldLog(level: LogLevel): boolean {
|
||||||
|
// In production, only log warnings and errors
|
||||||
|
if (!this.isDevelopment && (level === 'debug' || level === 'info')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(message: string, context?: LogContext): void {
|
||||||
|
if (this.shouldLog('debug')) {
|
||||||
|
console.debug(this.formatMessage('debug', message, context));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info(message: string, context?: LogContext): void {
|
||||||
|
if (this.shouldLog('info')) {
|
||||||
|
console.info(this.formatMessage('info', message, context));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(message: string, context?: LogContext): void {
|
||||||
|
if (this.shouldLog('warn')) {
|
||||||
|
console.warn(this.formatMessage('warn', message, context));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message: string, error?: Error | unknown, context?: LogContext): void {
|
||||||
|
if (this.shouldLog('error')) {
|
||||||
|
const errorContext = {
|
||||||
|
...context,
|
||||||
|
error: error instanceof Error ? {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
name: error.name,
|
||||||
|
} : error,
|
||||||
|
};
|
||||||
|
console.error(this.formatMessage('error', message, errorContext));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performance timing helper
|
||||||
|
time(label: string): void {
|
||||||
|
if (this.isDevelopment) {
|
||||||
|
console.time(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timeEnd(label: string): void {
|
||||||
|
if (this.isDevelopment) {
|
||||||
|
console.timeEnd(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group logging for related operations
|
||||||
|
group(label: string): void {
|
||||||
|
if (this.isDevelopment) {
|
||||||
|
console.group(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
groupEnd(): void {
|
||||||
|
if (this.isDevelopment) {
|
||||||
|
console.groupEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
export const clientLogger = new ClientLogger();
|
||||||
|
|
||||||
|
// Hook-specific logger factory
|
||||||
|
export function createHookLogger(hookName: string) {
|
||||||
|
return {
|
||||||
|
debug: (message: string, context?: LogContext) =>
|
||||||
|
clientLogger.debug(`[${hookName}] ${message}`, context),
|
||||||
|
info: (message: string, context?: LogContext) =>
|
||||||
|
clientLogger.info(`[${hookName}] ${message}`, context),
|
||||||
|
warn: (message: string, context?: LogContext) =>
|
||||||
|
clientLogger.warn(`[${hookName}] ${message}`, context),
|
||||||
|
error: (message: string, error?: Error | unknown, context?: LogContext) =>
|
||||||
|
clientLogger.error(`[${hookName}] ${message}`, error, context),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query/Mutation logger factory
|
||||||
|
export function createQueryLogger(queryKey: readonly unknown[]) {
|
||||||
|
const key = JSON.stringify(queryKey);
|
||||||
|
return {
|
||||||
|
debug: (message: string, context?: LogContext) =>
|
||||||
|
clientLogger.debug(`[Query: ${key}] ${message}`, context),
|
||||||
|
info: (message: string, context?: LogContext) =>
|
||||||
|
clientLogger.info(`[Query: ${key}] ${message}`, context),
|
||||||
|
warn: (message: string, context?: LogContext) =>
|
||||||
|
clientLogger.warn(`[Query: ${key}] ${message}`, context),
|
||||||
|
error: (message: string, error?: Error | unknown, context?: LogContext) =>
|
||||||
|
clientLogger.error(`[Query: ${key}] ${message}`, error, context),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMutationLogger(mutationKey: string) {
|
||||||
|
return {
|
||||||
|
debug: (message: string, context?: LogContext) =>
|
||||||
|
clientLogger.debug(`[Mutation: ${mutationKey}] ${message}`, context),
|
||||||
|
info: (message: string, context?: LogContext) =>
|
||||||
|
clientLogger.info(`[Mutation: ${mutationKey}] ${message}`, context),
|
||||||
|
warn: (message: string, context?: LogContext) =>
|
||||||
|
clientLogger.warn(`[Mutation: ${mutationKey}] ${message}`, context),
|
||||||
|
error: (message: string, error?: Error | unknown, context?: LogContext) =>
|
||||||
|
clientLogger.error(`[Mutation: ${mutationKey}] ${message}`, error, context),
|
||||||
|
};
|
||||||
|
}
|
||||||
108
lib/utils/global-error-handler.ts
Normal file
108
lib/utils/global-error-handler.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { clientLogger } from './client-logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global error handler for unhandled errors and promise rejections
|
||||||
|
* Sets up event listeners for window errors and unhandled promise rejections
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface ClientErrorReport {
|
||||||
|
message: string;
|
||||||
|
stack?: string;
|
||||||
|
url: string;
|
||||||
|
userAgent: string;
|
||||||
|
timestamp: string;
|
||||||
|
type: 'error' | 'unhandledrejection';
|
||||||
|
filename?: string;
|
||||||
|
lineno?: number;
|
||||||
|
colno?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reportErrorToServer(errorReport: ClientErrorReport): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fetch('/api/client-error', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(errorReport),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// Silently fail - don't want error reporting to cause more errors
|
||||||
|
console.error('Failed to report error to server:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize global error handlers
|
||||||
|
* Should be called once when the app starts
|
||||||
|
*/
|
||||||
|
export function initGlobalErrorHandlers(): void {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return; // Only run in browser
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle uncaught errors
|
||||||
|
window.addEventListener('error', (event: ErrorEvent) => {
|
||||||
|
clientLogger.error('Uncaught error', event.error, {
|
||||||
|
message: event.message,
|
||||||
|
filename: event.filename,
|
||||||
|
lineno: event.lineno,
|
||||||
|
colno: event.colno,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Report to server
|
||||||
|
reportErrorToServer({
|
||||||
|
message: event.message,
|
||||||
|
stack: event.error?.stack,
|
||||||
|
url: window.location.href,
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
type: 'error',
|
||||||
|
filename: event.filename,
|
||||||
|
lineno: event.lineno,
|
||||||
|
colno: event.colno,
|
||||||
|
}).catch(() => {
|
||||||
|
// Ignore reporting errors
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle unhandled promise rejections
|
||||||
|
window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {
|
||||||
|
const reason = event.reason;
|
||||||
|
const message = reason instanceof Error ? reason.message : String(reason);
|
||||||
|
const stack = reason instanceof Error ? reason.stack : undefined;
|
||||||
|
|
||||||
|
clientLogger.error('Unhandled promise rejection', reason, {
|
||||||
|
message,
|
||||||
|
stack,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Report to server
|
||||||
|
reportErrorToServer({
|
||||||
|
message: `Unhandled Promise Rejection: ${message}`,
|
||||||
|
stack,
|
||||||
|
url: window.location.href,
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
type: 'unhandledrejection',
|
||||||
|
}).catch(() => {
|
||||||
|
// Ignore reporting errors
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
clientLogger.info('Global error handlers initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup global error handlers
|
||||||
|
*/
|
||||||
|
export function cleanupGlobalErrorHandlers(): void {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove event listeners
|
||||||
|
// Note: We can't remove the exact listeners without keeping references
|
||||||
|
// This is mainly for completeness, in practice these stay for the lifetime of the app
|
||||||
|
clientLogger.info('Global error handlers cleanup requested');
|
||||||
|
}
|
||||||
@@ -5,9 +5,7 @@ const nextConfig: NextConfig = {
|
|||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
// Turbopack configuration (Next.js 16+)
|
// Turbopack configuration (Next.js 16+)
|
||||||
turbopack: {},
|
turbopack: {},
|
||||||
experimental: {
|
serverExternalPackages: ['pino', 'pino-pretty'],
|
||||||
serverComponentsExternalPackages: ['pino', 'pino-pretty'],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
Reference in New Issue
Block a user