Compare commits
5 Commits
b252a0b3bf
...
06dd1c20d0
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
// POST - Add a process group
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { name } = body;
|
||||
export const POST = withLogging(async (request: NextRequest) => {
|
||||
const body = await request.json();
|
||||
const { name } = body;
|
||||
|
||||
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);
|
||||
if (!name) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to add process group' },
|
||||
{ status: 500 }
|
||||
{ error: 'Group name is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE - Remove a process group
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { name } = body;
|
||||
const client = createSupervisorClient();
|
||||
const result = await client.addProcessGroup(name);
|
||||
|
||||
if (!name) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Group name is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json({
|
||||
success: result,
|
||||
message: `Process group '${name}' added successfully`,
|
||||
groupName: name,
|
||||
});
|
||||
}, 'addProcessGroup');
|
||||
|
||||
const client = createSupervisorClient();
|
||||
const result = await client.removeProcessGroup(name);
|
||||
export const DELETE = withLogging(async (request: NextRequest) => {
|
||||
const body = await request.json();
|
||||
const { name } = body;
|
||||
|
||||
return NextResponse.json({
|
||||
success: result,
|
||||
message: `Process group '${name}' removed successfully`,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Supervisor remove process group error:', error);
|
||||
if (!name) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to remove process group' },
|
||||
{ status: 500 }
|
||||
{ error: 'Group name is required' },
|
||||
{ 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 { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
// POST - Reload configuration
|
||||
export async function POST() {
|
||||
try {
|
||||
const client = createSupervisorClient();
|
||||
const result = await client.reloadConfig();
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Configuration reloaded',
|
||||
result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Supervisor reload config error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to reload configuration' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
export const POST = withLogging(async (request: NextRequest) => {
|
||||
const client = createSupervisorClient();
|
||||
const result = await client.reloadConfig();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Configuration reloaded',
|
||||
result,
|
||||
});
|
||||
}, 'reloadConfig');
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
// GET - Get all process configurations
|
||||
export async function GET() {
|
||||
try {
|
||||
const client = createSupervisorClient();
|
||||
const configs = await client.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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
export const GET = withLogging(async (request: NextRequest) => {
|
||||
const client = createSupervisorClient();
|
||||
const configs = await client.getAllConfigInfo();
|
||||
return NextResponse.json(configs);
|
||||
}, 'getAllConfigInfo');
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { createApiLogger, generateRequestId } from '@/lib/utils/api-logger';
|
||||
import { formatError } from '@/lib/utils/logger';
|
||||
|
||||
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
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const requestId = generateRequestId();
|
||||
const logger = createApiLogger(request, 'SSE-Events');
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
let intervalId: NodeJS.Timeout | null = null;
|
||||
let previousState: string | null = null;
|
||||
let pollCount = 0;
|
||||
let stateChangeCount = 0;
|
||||
|
||||
logger.info({ requestId }, 'SSE connection initiated');
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
@@ -22,9 +31,13 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
// Send initial connection message
|
||||
sendEvent('connected', { timestamp: Date.now() });
|
||||
logger.debug({ requestId }, 'SSE connected event sent');
|
||||
|
||||
// Poll supervisor for state changes
|
||||
const pollSupervisor = async () => {
|
||||
pollCount++;
|
||||
const pollStartTime = Date.now();
|
||||
|
||||
try {
|
||||
const client = createSupervisorClient();
|
||||
const processes = await client.getAllProcessInfo();
|
||||
@@ -41,17 +54,40 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
// Send update if state changed
|
||||
if (currentState !== previousState) {
|
||||
stateChangeCount++;
|
||||
sendEvent('process-update', {
|
||||
processes,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
logger.info({
|
||||
requestId,
|
||||
pollCount,
|
||||
stateChangeCount,
|
||||
processCount: processes.length,
|
||||
duration: Date.now() - pollStartTime,
|
||||
}, `Process state change detected (change #${stateChangeCount})`);
|
||||
previousState = currentState;
|
||||
}
|
||||
|
||||
// Send heartbeat every poll
|
||||
// Send heartbeat every poll (reduced logging - only log every 10th heartbeat)
|
||||
sendEvent('heartbeat', { timestamp: Date.now() });
|
||||
if (pollCount % 10 === 0) {
|
||||
logger.debug({
|
||||
requestId,
|
||||
pollCount,
|
||||
stateChangeCount,
|
||||
duration: Date.now() - pollStartTime,
|
||||
}, `SSE heartbeat #${pollCount}`);
|
||||
}
|
||||
} 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', {
|
||||
message: error.message || 'Failed to fetch process state',
|
||||
timestamp: Date.now(),
|
||||
@@ -70,6 +106,12 @@ export async function GET(request: NextRequest) {
|
||||
if (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',
|
||||
'Connection': 'keep-alive',
|
||||
'X-Accel-Buffering': 'no', // Disable nginx buffering
|
||||
'X-Request-ID': requestId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,35 +1,29 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{ name: string }>;
|
||||
}
|
||||
|
||||
// POST - Restart all processes in a group (stop then start)
|
||||
export async function POST(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
const { name } = await params;
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const wait = body.wait ?? true;
|
||||
export const POST = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||
const { name } = await params;
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const wait = body.wait ?? true;
|
||||
|
||||
const client = createSupervisorClient();
|
||||
const client = createSupervisorClient();
|
||||
|
||||
// Stop all processes in the group first
|
||||
await client.stopProcessGroup(name, wait);
|
||||
// Stop all processes in the group first
|
||||
await client.stopProcessGroup(name, wait);
|
||||
|
||||
// Then start them
|
||||
const results = await client.startProcessGroup(name, wait);
|
||||
// Then start them
|
||||
const results = await client.startProcessGroup(name, wait);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Restarted process group: ${name}`,
|
||||
results,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Supervisor restart process group error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to restart process group' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Restarted process group: ${name}`,
|
||||
results,
|
||||
groupName: name,
|
||||
wait,
|
||||
});
|
||||
}, 'restartProcessGroup');
|
||||
|
||||
@@ -1,37 +1,31 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{ name: string }>;
|
||||
}
|
||||
|
||||
// POST - Send signal to all processes in a group
|
||||
export async function POST(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
const { name } = await params;
|
||||
const body = await request.json();
|
||||
const { signal } = body;
|
||||
export const POST = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||
const { name } = await params;
|
||||
const body = await request.json();
|
||||
const { signal } = body;
|
||||
|
||||
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);
|
||||
if (!signal) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to send signal to process group' },
|
||||
{ status: 500 }
|
||||
{ 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,
|
||||
groupName: name,
|
||||
signal,
|
||||
});
|
||||
}, 'signalProcessGroup');
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{ name: string }>;
|
||||
}
|
||||
|
||||
// POST - Start all processes in a group
|
||||
export async function POST(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
const { name } = await params;
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const wait = body.wait ?? true;
|
||||
export const POST = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||
const { name } = await params;
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const wait = body.wait ?? true;
|
||||
|
||||
const client = createSupervisorClient();
|
||||
const results = await client.startProcessGroup(name, wait);
|
||||
const client = createSupervisorClient();
|
||||
const results = await client.startProcessGroup(name, wait);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Started process group: ${name}`,
|
||||
results,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Supervisor start process group error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to start process group' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Started process group: ${name}`,
|
||||
results,
|
||||
groupName: name,
|
||||
wait,
|
||||
});
|
||||
}, 'startProcessGroup');
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{ name: string }>;
|
||||
}
|
||||
|
||||
// POST - Stop all processes in a group
|
||||
export async function POST(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
const { name } = await params;
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const wait = body.wait ?? true;
|
||||
export const POST = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||
const { name } = await params;
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const wait = body.wait ?? true;
|
||||
|
||||
const client = createSupervisorClient();
|
||||
const results = await client.stopProcessGroup(name, wait);
|
||||
const client = createSupervisorClient();
|
||||
const results = await client.stopProcessGroup(name, wait);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Stopped process group: ${name}`,
|
||||
results,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Supervisor stop process group error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to stop process group' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Stopped process group: ${name}`,
|
||||
results,
|
||||
groupName: name,
|
||||
wait,
|
||||
});
|
||||
}, 'stopProcessGroup');
|
||||
|
||||
@@ -1,39 +1,23 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
// GET main supervisord log
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const offset = parseInt(searchParams.get('offset') || '-4096', 10);
|
||||
const length = parseInt(searchParams.get('length') || '4096', 10);
|
||||
export const GET = withLogging(async (request: NextRequest) => {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const offset = parseInt(searchParams.get('offset') || '-4096', 10);
|
||||
const length = parseInt(searchParams.get('length') || '4096', 10);
|
||||
|
||||
const client = createSupervisorClient();
|
||||
const logs = await client.readLog(offset, length);
|
||||
const client = createSupervisorClient();
|
||||
const logs = await client.readLog(offset, length);
|
||||
|
||||
return NextResponse.json({ logs, offset, length });
|
||||
} catch (error: any) {
|
||||
console.error('Supervisor main log error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to fetch main log' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
return NextResponse.json({ logs, offset, length });
|
||||
}, 'readMainLog');
|
||||
|
||||
// DELETE - Clear main supervisord log
|
||||
export async function DELETE() {
|
||||
try {
|
||||
const client = createSupervisorClient();
|
||||
const result = await client.clearLog();
|
||||
return NextResponse.json({ success: result, message: 'Main log cleared' });
|
||||
} catch (error: any) {
|
||||
console.error('Supervisor clear main log error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to clear main log' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
export const DELETE = withLogging(async (request: NextRequest) => {
|
||||
const client = createSupervisorClient();
|
||||
const result = await client.clearLog();
|
||||
|
||||
return NextResponse.json({ success: result, message: 'Main log cleared' });
|
||||
}, 'clearMainLog');
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{ name: string }>;
|
||||
}
|
||||
|
||||
// DELETE - Clear process logs (both stdout and stderr)
|
||||
export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
const { name } = await params;
|
||||
const client = createSupervisorClient();
|
||||
const result = await client.clearProcessLogs(name);
|
||||
return NextResponse.json({ success: result, message: `Logs cleared for ${name}` });
|
||||
} catch (error: any) {
|
||||
console.error('Supervisor clear process logs error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to clear process logs' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
export const DELETE = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||
const { name } = await params;
|
||||
|
||||
const client = createSupervisorClient();
|
||||
const result = await client.clearProcessLogs(name);
|
||||
|
||||
return NextResponse.json({
|
||||
success: result,
|
||||
message: `Logs cleared for ${name}`,
|
||||
processName: name,
|
||||
});
|
||||
}, 'clearProcessLogs');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -7,21 +8,14 @@ interface RouteParams {
|
||||
params: Promise<{ name: string }>;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
const { name } = await params;
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const offset = parseInt(searchParams.get('offset') || '-4096', 10);
|
||||
const length = parseInt(searchParams.get('length') || '4096', 10);
|
||||
export const GET = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||
const { name } = await params;
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const offset = parseInt(searchParams.get('offset') || '-4096', 10);
|
||||
const length = parseInt(searchParams.get('length') || '4096', 10);
|
||||
|
||||
const client = createSupervisorClient();
|
||||
const logs = await client.tailProcessStderrLog(name, offset, length);
|
||||
return NextResponse.json(logs);
|
||||
} catch (error: any) {
|
||||
console.error('Supervisor stderr log error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to fetch stderr logs' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
const client = createSupervisorClient();
|
||||
const logs = await client.tailProcessStderrLog(name, offset, length);
|
||||
|
||||
return NextResponse.json(logs);
|
||||
}, 'readProcessStderr');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -7,21 +8,14 @@ interface RouteParams {
|
||||
params: Promise<{ name: string }>;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
const { name } = await params;
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const offset = parseInt(searchParams.get('offset') || '-4096', 10);
|
||||
const length = parseInt(searchParams.get('length') || '4096', 10);
|
||||
export const GET = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||
const { name } = await params;
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const offset = parseInt(searchParams.get('offset') || '-4096', 10);
|
||||
const length = parseInt(searchParams.get('length') || '4096', 10);
|
||||
|
||||
const client = createSupervisorClient();
|
||||
const logs = await client.tailProcessStdoutLog(name, offset, length);
|
||||
return NextResponse.json(logs);
|
||||
} catch (error: any) {
|
||||
console.error('Supervisor stdout log error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to fetch stdout logs' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
const client = createSupervisorClient();
|
||||
const logs = await client.tailProcessStdoutLog(name, offset, length);
|
||||
|
||||
return NextResponse.json(logs);
|
||||
}, 'readProcessStdout');
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{ name: string }>;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
const { name } = await params;
|
||||
const client = createSupervisorClient();
|
||||
const result = await client.restartProcess(name);
|
||||
return NextResponse.json({ success: result, message: `Process ${name} restarted` });
|
||||
} catch (error: any) {
|
||||
console.error('Supervisor restart process error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to restart process' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
export const POST = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||
const { name } = await params;
|
||||
|
||||
const client = createSupervisorClient();
|
||||
const result = await client.restartProcess(name);
|
||||
|
||||
return NextResponse.json({
|
||||
success: result,
|
||||
message: `Process ${name} restarted`,
|
||||
processName: name,
|
||||
});
|
||||
}, 'restartProcess');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -7,17 +8,11 @@ interface RouteParams {
|
||||
params: Promise<{ name: string }>;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
const { name } = await params;
|
||||
const client = createSupervisorClient();
|
||||
const processInfo = await client.getProcessInfo(name);
|
||||
return NextResponse.json(processInfo);
|
||||
} catch (error: any) {
|
||||
console.error('Supervisor process info error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to fetch process info' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
export const GET = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||
const { name } = await params;
|
||||
|
||||
const client = createSupervisorClient();
|
||||
const processInfo = await client.getProcessInfo(name);
|
||||
|
||||
return NextResponse.json(processInfo);
|
||||
}, 'getProcessInfo');
|
||||
|
||||
@@ -1,36 +1,30 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{ name: string }>;
|
||||
}
|
||||
|
||||
// POST - Send signal to a process
|
||||
export async function POST(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
const { name } = await params;
|
||||
const body = await request.json();
|
||||
const { signal } = body;
|
||||
export const POST = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||
const { name } = await params;
|
||||
const body = await request.json();
|
||||
const { signal } = body;
|
||||
|
||||
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);
|
||||
if (!signal) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to send signal to process' },
|
||||
{ status: 500 }
|
||||
{ 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}`,
|
||||
processName: name,
|
||||
signal,
|
||||
});
|
||||
}, 'signalProcess');
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{ name: string }>;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
const { name } = await params;
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const wait = body.wait !== undefined ? body.wait : true;
|
||||
export const POST = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||
const { name } = await params;
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const wait = body.wait !== undefined ? body.wait : true;
|
||||
|
||||
const client = createSupervisorClient();
|
||||
const result = await client.startProcess(name, wait);
|
||||
return NextResponse.json({ success: result, message: `Process ${name} started` });
|
||||
} catch (error: any) {
|
||||
console.error('Supervisor start process error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to start process' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
const client = createSupervisorClient();
|
||||
const result = await client.startProcess(name, wait);
|
||||
|
||||
return NextResponse.json({
|
||||
success: result,
|
||||
message: `Process ${name} started`,
|
||||
processName: name,
|
||||
wait,
|
||||
});
|
||||
}, 'startProcess');
|
||||
|
||||
@@ -1,36 +1,29 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{ name: string }>;
|
||||
}
|
||||
|
||||
// POST - Send input to process stdin
|
||||
export async function POST(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
const { name } = await params;
|
||||
const body = await request.json();
|
||||
const { chars } = body;
|
||||
export const POST = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||
const { name } = await params;
|
||||
const body = await request.json();
|
||||
const { chars } = body;
|
||||
|
||||
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);
|
||||
if (chars === undefined || chars === null) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to send input to process' },
|
||||
{ status: 500 }
|
||||
{ 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}`,
|
||||
processName: name,
|
||||
});
|
||||
}, 'sendProcessStdin');
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
interface RouteParams {
|
||||
params: Promise<{ name: string }>;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
const { name } = await params;
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const wait = body.wait !== undefined ? body.wait : true;
|
||||
export const POST = withLogging(async (request: NextRequest, { params }: RouteParams) => {
|
||||
const { name } = await params;
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const wait = body.wait !== undefined ? body.wait : true;
|
||||
|
||||
const client = createSupervisorClient();
|
||||
const result = await client.stopProcess(name, wait);
|
||||
return NextResponse.json({ success: result, message: `Process ${name} stopped` });
|
||||
} catch (error: any) {
|
||||
console.error('Supervisor stop process error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to stop process' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
const client = createSupervisorClient();
|
||||
const result = await client.stopProcess(name, wait);
|
||||
|
||||
return NextResponse.json({
|
||||
success: result,
|
||||
message: `Process ${name} stopped`,
|
||||
processName: name,
|
||||
wait,
|
||||
});
|
||||
}, 'stopProcess');
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
// POST - Clear all process logs
|
||||
export async function POST() {
|
||||
try {
|
||||
const client = createSupervisorClient();
|
||||
const results = await client.clearAllProcessLogs();
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'All process logs cleared',
|
||||
results
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Supervisor clear all logs error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to clear all logs' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
export const POST = withLogging(async (request: NextRequest) => {
|
||||
const client = createSupervisorClient();
|
||||
const results = await client.clearAllProcessLogs();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'All process logs cleared',
|
||||
results,
|
||||
});
|
||||
}, 'clearAllProcessLogs');
|
||||
|
||||
@@ -1,30 +1,23 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
// POST - Restart all processes (stop then start)
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const wait = body.wait ?? true;
|
||||
export const POST = withLogging(async (request: NextRequest) => {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const wait = body.wait ?? true;
|
||||
|
||||
const client = createSupervisorClient();
|
||||
const client = createSupervisorClient();
|
||||
|
||||
// Stop all processes first
|
||||
await client.stopAllProcesses(wait);
|
||||
// Stop all processes first
|
||||
await client.stopAllProcesses(wait);
|
||||
|
||||
// Then start them
|
||||
const results = await client.startAllProcesses(wait);
|
||||
// Then start them
|
||||
const results = await client.startAllProcesses(wait);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Restarted all processes',
|
||||
results,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Supervisor restart all processes error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to restart all processes' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Restarted all processes',
|
||||
results,
|
||||
wait,
|
||||
});
|
||||
}, 'restartAllProcesses');
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const client = createSupervisorClient();
|
||||
const processes = await client.getAllProcessInfo();
|
||||
return NextResponse.json(processes);
|
||||
} catch (error: any) {
|
||||
console.error('Supervisor processes error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to fetch processes' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
export const GET = withLogging(async (request: NextRequest) => {
|
||||
const client = createSupervisorClient();
|
||||
const processes = await client.getAllProcessInfo();
|
||||
|
||||
return NextResponse.json(processes);
|
||||
}, 'getAllProcessInfo');
|
||||
|
||||
@@ -1,32 +1,25 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
// POST - Send signal to all processes
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { signal } = body;
|
||||
export const POST = withLogging(async (request: NextRequest) => {
|
||||
const body = await request.json();
|
||||
const { signal } = body;
|
||||
|
||||
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);
|
||||
if (!signal) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to send signal to all processes' },
|
||||
{ status: 500 }
|
||||
{ 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,
|
||||
signal,
|
||||
});
|
||||
}, 'signalAllProcesses');
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
// POST - Start all processes
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const wait = body.wait ?? true;
|
||||
export const POST = withLogging(async (request: NextRequest) => {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const wait = body.wait ?? true;
|
||||
|
||||
const client = createSupervisorClient();
|
||||
const results = await client.startAllProcesses(wait);
|
||||
const client = createSupervisorClient();
|
||||
const results = await client.startAllProcesses(wait);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Started all processes',
|
||||
results,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Supervisor start all processes error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to start all processes' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Started all processes',
|
||||
results,
|
||||
wait,
|
||||
});
|
||||
}, 'startAllProcesses');
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
// POST - Stop all processes
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const wait = body.wait ?? true;
|
||||
export const POST = withLogging(async (request: NextRequest) => {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const wait = body.wait ?? true;
|
||||
|
||||
const client = createSupervisorClient();
|
||||
const results = await client.stopAllProcesses(wait);
|
||||
const client = createSupervisorClient();
|
||||
const results = await client.stopAllProcesses(wait);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Stopped all processes',
|
||||
results,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Supervisor stop all processes error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to stop all processes' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Stopped all processes',
|
||||
results,
|
||||
wait,
|
||||
});
|
||||
}, 'stopAllProcesses');
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
import { withLogging } from '@/lib/utils/api-logger';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const client = createSupervisorClient();
|
||||
const systemInfo = await client.getSystemInfo();
|
||||
return NextResponse.json(systemInfo);
|
||||
} catch (error: any) {
|
||||
console.error('Supervisor system info error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to fetch system info' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
export const GET = withLogging(async (request: NextRequest) => {
|
||||
const client = createSupervisorClient();
|
||||
const systemInfo = await client.getSystemInfo();
|
||||
|
||||
return NextResponse.json(systemInfo);
|
||||
}, 'getSystemInfo');
|
||||
|
||||
@@ -12,13 +12,13 @@ export default function ConfigPage() {
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-3xl font-bold">Configuration</h1>
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold">Configuration</h1>
|
||||
<Card className="border-destructive/50">
|
||||
<CardContent className="p-12 text-center">
|
||||
<AlertCircle className="h-12 w-12 text-destructive mx-auto mb-4" />
|
||||
<h2 className="text-xl font-semibold mb-2">Failed to load configuration</h2>
|
||||
<p className="text-muted-foreground">
|
||||
<CardContent className="p-8 md:p-12 text-center">
|
||||
<AlertCircle className="h-10 w-10 md:h-12 md:w-12 text-destructive mx-auto mb-4" />
|
||||
<h2 className="text-lg md:text-xl font-semibold mb-2">Failed to load configuration</h2>
|
||||
<p className="text-sm md:text-base text-muted-foreground">
|
||||
Could not connect to Supervisor. Please check your configuration.
|
||||
</p>
|
||||
</CardContent>
|
||||
@@ -28,11 +28,11 @@ export default function ConfigPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Configuration</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold">Configuration</h1>
|
||||
<p className="text-sm md:text-base text-muted-foreground mt-1">
|
||||
Manage process configurations and supervisor settings
|
||||
</p>
|
||||
</div>
|
||||
@@ -65,8 +65,8 @@ export default function ConfigPage() {
|
||||
<ConfigTable configs={configs} />
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="p-12 text-center">
|
||||
<p className="text-muted-foreground">No process configurations found</p>
|
||||
<CardContent className="p-8 md:p-12 text-center">
|
||||
<p className="text-sm md:text-base text-muted-foreground">No process configurations found</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
@@ -12,6 +12,12 @@ const inter = Inter({
|
||||
export const metadata: Metadata = {
|
||||
title: 'Supervisor UI',
|
||||
description: 'Modern web interface for Supervisor process management',
|
||||
viewport: {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
maximumScale: 5,
|
||||
userScalable: true,
|
||||
},
|
||||
};
|
||||
|
||||
// Force dynamic rendering
|
||||
|
||||
@@ -76,13 +76,13 @@ export default function LogsPage() {
|
||||
|
||||
if (processesError) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-3xl font-bold">Process Logs</h1>
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold">Process Logs</h1>
|
||||
<Card className="border-destructive/50">
|
||||
<CardContent className="p-12 text-center">
|
||||
<AlertCircle className="h-12 w-12 text-destructive mx-auto mb-4" />
|
||||
<h2 className="text-xl font-semibold mb-2">Failed to load processes</h2>
|
||||
<p className="text-muted-foreground">
|
||||
<CardContent className="p-8 md:p-12 text-center">
|
||||
<AlertCircle className="h-10 w-10 md:h-12 md:w-12 text-destructive mx-auto mb-4" />
|
||||
<h2 className="text-lg md:text-xl font-semibold mb-2">Failed to load processes</h2>
|
||||
<p className="text-sm md:text-base text-muted-foreground">
|
||||
Could not connect to Supervisor. Please check your configuration.
|
||||
</p>
|
||||
</CardContent>
|
||||
@@ -92,20 +92,20 @@ export default function LogsPage() {
|
||||
}
|
||||
|
||||
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">
|
||||
<h1 className="text-3xl font-bold">Process Logs</h1>
|
||||
<p className="text-muted-foreground mt-1">Real-time log viewing and management</p>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold">Process Logs</h1>
|
||||
<p className="text-sm md:text-base text-muted-foreground mt-1">Real-time log viewing and management</p>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<Card className="flex-shrink-0">
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="text-lg">Log Controls</CardTitle>
|
||||
<CardHeader className="pb-3 md:pb-4">
|
||||
<CardTitle className="text-base md:text-lg">Log Controls</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<CardContent className="space-y-3 md:space-y-4">
|
||||
{/* 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]">
|
||||
<label className="block text-sm font-medium mb-2">Select Process</label>
|
||||
<select
|
||||
@@ -131,6 +131,7 @@ export default function LogsPage() {
|
||||
variant={logType === 'stdout' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setLogType('stdout')}
|
||||
className="flex-1 sm:flex-initial"
|
||||
>
|
||||
Stdout
|
||||
</Button>
|
||||
@@ -138,6 +139,7 @@ export default function LogsPage() {
|
||||
variant={logType === 'stderr' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setLogType('stderr')}
|
||||
className="flex-1 sm:flex-initial"
|
||||
>
|
||||
Stderr
|
||||
</Button>
|
||||
@@ -147,7 +149,7 @@ export default function LogsPage() {
|
||||
</div>
|
||||
|
||||
{/* 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} />
|
||||
<LogControls
|
||||
isPlaying={isPlaying}
|
||||
|
||||
@@ -39,12 +39,12 @@ export default function HomePage() {
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex flex-col sm:flex-row items-start justify-between gap-4">
|
||||
<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
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -183,9 +183,9 @@ export default function ProcessesPage() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-3xl font-bold">Processes</h1>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold">Processes</h1>
|
||||
<div className="grid gap-4 md:gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||
<div key={i} className="h-64 bg-muted rounded-lg animate-pulse" />
|
||||
))}
|
||||
@@ -196,12 +196,12 @@ export default function ProcessesPage() {
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-3xl font-bold">Processes</h1>
|
||||
<div className="flex flex-col items-center justify-center p-12 text-center">
|
||||
<AlertCircle className="h-12 w-12 text-destructive mb-4" />
|
||||
<h2 className="text-xl font-semibold mb-2">Failed to load processes</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold">Processes</h1>
|
||||
<div className="flex flex-col items-center justify-center p-8 md:p-12 text-center">
|
||||
<AlertCircle className="h-10 w-10 md:h-12 md:w-12 text-destructive mb-4" />
|
||||
<h2 className="text-lg md:text-xl font-semibold mb-2">Failed to load processes</h2>
|
||||
<p className="text-sm md:text-base text-muted-foreground mb-4">
|
||||
Could not connect to Supervisor. Please check your configuration.
|
||||
</p>
|
||||
<Button onClick={() => refetch()}>
|
||||
@@ -215,11 +215,12 @@ export default function ProcessesPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Header - responsive layout */}
|
||||
<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>
|
||||
<h1 className="text-3xl font-bold">Processes</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold">Processes</h1>
|
||||
<p className="text-sm md:text-base text-muted-foreground mt-1">
|
||||
{displayedProcesses.length} of {processes?.length ?? 0} processes
|
||||
{displayedProcesses.length !== (processes?.length ?? 0) && ' (filtered)'}
|
||||
</p>
|
||||
@@ -230,7 +231,9 @@ export default function ProcessesPage() {
|
||||
onReconnect={reconnect}
|
||||
/>
|
||||
</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 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -239,13 +242,15 @@ export default function ProcessesPage() {
|
||||
className="gap-2"
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
<GroupSelector viewMode={viewMode} onViewModeChange={setViewMode} />
|
||||
<Button variant="outline" onClick={() => refetch()}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
Refresh
|
||||
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
<span className="hidden sm:inline ml-2">Refresh</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -265,17 +270,17 @@ export default function ProcessesPage() {
|
||||
|
||||
{/* Process Display */}
|
||||
{processes && processes.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center p-12 text-center border-2 border-dashed rounded-lg">
|
||||
<p className="text-muted-foreground">No processes configured</p>
|
||||
<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-sm md:text-base text-muted-foreground">No processes configured</p>
|
||||
</div>
|
||||
) : displayedProcesses.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center p-12 text-center border-2 border-dashed rounded-lg">
|
||||
<p className="text-muted-foreground">No processes match the current filters</p>
|
||||
<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-sm md:text-base text-muted-foreground">No processes match the current filters</p>
|
||||
</div>
|
||||
) : viewMode === 'grouped' ? (
|
||||
<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">
|
||||
{displayedProcesses.map((process, index) => {
|
||||
const fullName = `${process.group}:${process.name}`;
|
||||
const isFocused = index === focusedIndex;
|
||||
|
||||
@@ -57,72 +57,123 @@ export function ConfigTable({ configs }: ConfigTableProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-muted/50 border-b">
|
||||
<tr>
|
||||
<th
|
||||
className="text-left p-3 cursor-pointer hover:bg-muted transition-colors"
|
||||
onClick={() => handleSort('group')}
|
||||
>
|
||||
Group <SortIcon field="group" />
|
||||
</th>
|
||||
<th
|
||||
className="text-left p-3 cursor-pointer hover:bg-muted transition-colors"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
Name <SortIcon field="name" />
|
||||
</th>
|
||||
<th
|
||||
className="text-left p-3 cursor-pointer hover:bg-muted transition-colors"
|
||||
onClick={() => handleSort('command')}
|
||||
>
|
||||
Command <SortIcon field="command" />
|
||||
</th>
|
||||
<th className="text-left p-3">Directory</th>
|
||||
<th
|
||||
className="text-center p-3 cursor-pointer hover:bg-muted transition-colors"
|
||||
onClick={() => handleSort('autostart')}
|
||||
>
|
||||
Autostart <SortIcon field="autostart" />
|
||||
</th>
|
||||
<th className="text-center p-3">Priority</th>
|
||||
<th className="text-center p-3">Processes</th>
|
||||
</tr>
|
||||
</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>
|
||||
<>
|
||||
{/* Desktop Table View - hidden on mobile */}
|
||||
<Card className="hidden md:block">
|
||||
<CardContent className="p-0">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-muted/50 border-b">
|
||||
<tr>
|
||||
<th
|
||||
className="text-left p-3 cursor-pointer hover:bg-muted transition-colors"
|
||||
onClick={() => handleSort('group')}
|
||||
>
|
||||
Group <SortIcon field="group" />
|
||||
</th>
|
||||
<th
|
||||
className="text-left p-3 cursor-pointer hover:bg-muted transition-colors"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
Name <SortIcon field="name" />
|
||||
</th>
|
||||
<th
|
||||
className="text-left p-3 cursor-pointer hover:bg-muted transition-colors"
|
||||
onClick={() => handleSort('command')}
|
||||
>
|
||||
Command <SortIcon field="command" />
|
||||
</th>
|
||||
<th className="text-left p-3">Directory</th>
|
||||
<th
|
||||
className="text-center p-3 cursor-pointer hover:bg-muted transition-colors"
|
||||
onClick={() => handleSort('autostart')}
|
||||
>
|
||||
Autostart <SortIcon field="autostart" />
|
||||
</th>
|
||||
<th className="text-center p-3">Priority</th>
|
||||
<th className="text-center p-3">Processes</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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>
|
||||
))}
|
||||
</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="flex gap-4 pt-2">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Priority:</span>
|
||||
<span className="ml-2 font-mono">{config.priority}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Processes:</span>
|
||||
<span className="ml-2 font-mono">{config.numprocs}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ import { Navbar } from './Navbar';
|
||||
|
||||
export function AppLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
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 { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
@@ -19,12 +19,30 @@ const navItems = [
|
||||
export function Navbar() {
|
||||
const pathname = usePathname();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const themeContext = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
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 = () => {
|
||||
if (themeContext) {
|
||||
themeContext.setTheme(themeContext.resolvedTheme === 'dark' ? 'light' : 'dark');
|
||||
@@ -33,17 +51,17 @@ export function Navbar() {
|
||||
|
||||
return (
|
||||
<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 flex h-16 items-center px-4 md:px-6">
|
||||
{/* 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" />
|
||||
<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
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<div className="flex gap-1 flex-1">
|
||||
{/* Desktop Navigation Links */}
|
||||
<div className="hidden md:flex gap-1 flex-1">
|
||||
{navItems.map((item) => (
|
||||
<Link key={item.href} href={item.href}>
|
||||
<Button
|
||||
@@ -60,22 +78,65 @@ export function Navbar() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Theme Toggle */}
|
||||
{mounted && themeContext && (
|
||||
{/* Right side buttons */}
|
||||
<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
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleTheme}
|
||||
aria-label="Toggle theme"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
aria-label="Toggle menu"
|
||||
className="md:hidden h-10 w-10"
|
||||
>
|
||||
{themeContext.resolvedTheme === 'dark' ? (
|
||||
<Sun className="h-5 w-5" />
|
||||
{mobileMenuOpen ? (
|
||||
<X className="h-6 w-6" />
|
||||
) : (
|
||||
<Moon className="h-5 w-5" />
|
||||
<Menu className="h-6 w-6" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Drawer */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="md:hidden fixed inset-0 top-16 z-50 bg-background/95 backdrop-blur-sm">
|
||||
<div className="container px-4 py-6">
|
||||
<nav className="flex flex-col gap-2">
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,60 +46,74 @@ export function BatchActions({ selectedProcesses, processes, onClearSelection }:
|
||||
};
|
||||
|
||||
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">
|
||||
<div className="flex items-center gap-4 px-6 py-4">
|
||||
<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 className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 sm:gap-4 p-4">
|
||||
{/* Selection count */}
|
||||
<div className="flex items-center justify-between sm:justify-start gap-2">
|
||||
<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>
|
||||
<span className="text-sm font-medium">
|
||||
{selectedCount} {selectedCount === 1 ? 'process' : 'processes'} selected
|
||||
</span>
|
||||
{/* Close button on mobile - inline with count */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClearSelection}
|
||||
className="h-8 w-8 sm:hidden"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</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
|
||||
variant="success"
|
||||
size="sm"
|
||||
onClick={handleStartSelected}
|
||||
disabled={isLoading}
|
||||
className="gap-2"
|
||||
className="gap-2 flex-1 sm:flex-initial"
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
Start Selected
|
||||
<span className="hidden sm:inline">Start Selected</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={handleStopSelected}
|
||||
disabled={isLoading}
|
||||
className="gap-2"
|
||||
className="gap-2 flex-1 sm:flex-initial"
|
||||
>
|
||||
<Square className="h-4 w-4" />
|
||||
Stop Selected
|
||||
<span className="hidden sm:inline">Stop Selected</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRestartSelected}
|
||||
disabled={isLoading}
|
||||
className="gap-2"
|
||||
className="gap-2 flex-1 sm:flex-initial"
|
||||
>
|
||||
<RotateCw className={cn('h-4 w-4', isLoading && 'animate-spin')} />
|
||||
Restart Selected
|
||||
<span className="hidden sm:inline">Restart Selected</span>
|
||||
</Button>
|
||||
</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
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClearSelection}
|
||||
className="h-8 w-8"
|
||||
className="hidden sm:flex h-8 w-8"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
@@ -109,34 +109,37 @@ export function ProcessCard({ process, isSelected = false, isFocused = false, on
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex flex-wrap gap-2" onClick={(e) => e.stopPropagation()}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="success"
|
||||
onClick={handleStart}
|
||||
disabled={!canStartProcess(process.state as ProcessStateCode) || isLoading}
|
||||
className="flex-1"
|
||||
className="flex-1 min-w-[100px]"
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
Start
|
||||
<span className="hidden sm:inline">Start</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="warning"
|
||||
onClick={handleStop}
|
||||
disabled={!canStopProcess(process.state as ProcessStateCode) || isLoading}
|
||||
className="flex-1"
|
||||
className="flex-1 min-w-[100px]"
|
||||
>
|
||||
<Square className="h-4 w-4" />
|
||||
Stop
|
||||
<span className="hidden sm:inline">Stop</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleRestart}
|
||||
disabled={process.state === 0 || isLoading}
|
||||
title="Restart"
|
||||
className="flex-1 sm:flex-initial"
|
||||
>
|
||||
<RotateCw className={cn('h-4 w-4', isLoading && 'animate-spin-slow')} />
|
||||
<span className="sm:hidden">Restart</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -144,8 +147,10 @@ export function ProcessCard({ process, isSelected = false, isFocused = false, on
|
||||
onClick={() => setShowSignalModal(true)}
|
||||
disabled={process.state === 0}
|
||||
title="Send Signal"
|
||||
className="flex-1 sm:flex-initial"
|
||||
>
|
||||
<Zap className="h-4 w-4" />
|
||||
<span className="sm:hidden">Signal</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -153,8 +158,10 @@ export function ProcessCard({ process, isSelected = false, isFocused = false, on
|
||||
onClick={() => setShowStdinModal(true)}
|
||||
disabled={process.state !== 20}
|
||||
title="Send Stdin"
|
||||
className="flex-1 sm:flex-initial"
|
||||
>
|
||||
<Terminal className="h-4 w-4" />
|
||||
<span className="sm:hidden">Stdin</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ export function SignalSender({ processName, onClose }: SignalSenderProps) {
|
||||
const isDangerous = signal && ['TERM', 'KILL', 'QUIT'].includes(signal.toUpperCase());
|
||||
|
||||
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">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -42,7 +42,7 @@ export function StdinInput({ processName, onClose }: StdinInputProps) {
|
||||
};
|
||||
|
||||
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">
|
||||
<CardHeader>
|
||||
<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';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { useState, ReactNode } from 'react';
|
||||
import { useState, ReactNode, useEffect } from 'react';
|
||||
import { Toaster } from 'sonner';
|
||||
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 }) {
|
||||
const [queryClient] = useState(
|
||||
@@ -15,16 +18,31 @@ export function Providers({ children }: { children: ReactNode }) {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 2,
|
||||
},
|
||||
mutations: {
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
logger: {
|
||||
log: (message) => clientLogger.debug(message),
|
||||
warn: (message) => clientLogger.warn(message),
|
||||
error: (error) => clientLogger.error('React Query error', error),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize global error handlers once
|
||||
useEffect(() => {
|
||||
initGlobalErrorHandlers();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<Toaster position="top-right" richColors closeButton />
|
||||
</QueryClientProvider>
|
||||
</ThemeProvider>
|
||||
<ErrorBoundary>
|
||||
<ThemeProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<Toaster position="top-right" richColors closeButton />
|
||||
</QueryClientProvider>
|
||||
</ThemeProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ const SHORTCUT_GROUPS: ShortcutGroup[] = [
|
||||
|
||||
export function KeyboardShortcutsHelp({ onClose }: KeyboardShortcutsHelpProps) {
|
||||
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">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -26,10 +26,10 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
variant === 'outline',
|
||||
},
|
||||
{
|
||||
'h-8 px-3 text-sm': size === 'sm',
|
||||
'h-10 px-4': size === 'md',
|
||||
'h-9 md:h-8 px-3 text-sm': size === 'sm',
|
||||
'h-11 md:h-10 px-4': size === 'md',
|
||||
'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
|
||||
)}
|
||||
|
||||
@@ -19,7 +19,7 @@ const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
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}
|
||||
/>
|
||||
)
|
||||
@@ -30,7 +30,7 @@ const CardTitle = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHeadingEle
|
||||
({ className, ...props }, ref) => (
|
||||
<h3
|
||||
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}
|
||||
/>
|
||||
)
|
||||
@@ -50,7 +50,7 @@ CardDescription.displayName = 'CardDescription';
|
||||
|
||||
const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ 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';
|
||||
@@ -59,7 +59,7 @@ const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
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}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
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';
|
||||
|
||||
@@ -36,6 +39,7 @@ export function useEventSource(url: string, options: UseEventSourceOptions = {})
|
||||
const connect = useCallback(() => {
|
||||
if (!enabled || eventSourceRef.current) return;
|
||||
|
||||
logger.info('Connecting to SSE', { url });
|
||||
setStatus('connecting');
|
||||
|
||||
try {
|
||||
@@ -43,6 +47,7 @@ export function useEventSource(url: string, options: UseEventSourceOptions = {})
|
||||
eventSourceRef.current = eventSource;
|
||||
|
||||
eventSource.addEventListener('connected', () => {
|
||||
logger.info('SSE connected successfully', { url });
|
||||
setStatus('connected');
|
||||
setReconnectAttempts(0);
|
||||
onConnect?.();
|
||||
@@ -51,6 +56,7 @@ export function useEventSource(url: string, options: UseEventSourceOptions = {})
|
||||
eventSource.addEventListener('heartbeat', (event) => {
|
||||
// Keep connection alive
|
||||
if (status !== 'connected') {
|
||||
logger.debug('SSE heartbeat received, updating status to connected');
|
||||
setStatus('connected');
|
||||
}
|
||||
});
|
||||
@@ -58,15 +64,20 @@ export function useEventSource(url: string, options: UseEventSourceOptions = {})
|
||||
eventSource.addEventListener('process-update', (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
logger.debug('Process update received', {
|
||||
processCount: data.processes?.length,
|
||||
timestamp: data.timestamp,
|
||||
});
|
||||
onMessage?.({ event: 'process-update', data });
|
||||
} 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) => {
|
||||
try {
|
||||
const data = JSON.parse((event as MessageEvent).data);
|
||||
logger.warn('SSE error event received', { error: data });
|
||||
onMessage?.({ event: 'error', data });
|
||||
} catch (error) {
|
||||
// Not a message error, connection error
|
||||
@@ -74,7 +85,7 @@ export function useEventSource(url: string, options: UseEventSourceOptions = {})
|
||||
});
|
||||
|
||||
eventSource.onerror = (event) => {
|
||||
console.error('EventSource error:', event);
|
||||
logger.error('EventSource connection error', event);
|
||||
setStatus('error');
|
||||
onError?.(event);
|
||||
|
||||
@@ -85,24 +96,33 @@ export function useEventSource(url: string, options: UseEventSourceOptions = {})
|
||||
// Attempt reconnection with exponential backoff
|
||||
if (reconnectAttempts < maxReconnectAttempts) {
|
||||
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(() => {
|
||||
setReconnectAttempts((prev) => prev + 1);
|
||||
connect();
|
||||
}, delay);
|
||||
} else {
|
||||
logger.warn('Max reconnection attempts reached, disconnecting', {
|
||||
maxAttempts: maxReconnectAttempts,
|
||||
});
|
||||
setStatus('disconnected');
|
||||
onDisconnect?.();
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to create EventSource:', error);
|
||||
logger.error('Failed to create EventSource', error, { url });
|
||||
setStatus('error');
|
||||
}
|
||||
}, [url, enabled, status, reconnectAttempts, maxReconnectAttempts, reconnectInterval, onMessage, onError, onConnect, onDisconnect]);
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
logger.info('Disconnecting from SSE');
|
||||
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current);
|
||||
reconnectTimeoutRef.current = null;
|
||||
@@ -118,6 +138,7 @@ export function useEventSource(url: string, options: UseEventSourceOptions = {})
|
||||
}, []);
|
||||
|
||||
const reconnect = useCallback(() => {
|
||||
logger.info('Manual reconnection requested');
|
||||
disconnect();
|
||||
setReconnectAttempts(0);
|
||||
connect();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import type { ProcessInfo, SystemInfo, LogTailResult, ConfigInfo } from '@/lib/supervisor/types';
|
||||
import { createMutationLogger } from '@/lib/utils/client-logger';
|
||||
|
||||
// Query Keys
|
||||
export const supervisorKeys = {
|
||||
@@ -143,16 +144,21 @@ export function useProcessLogs(
|
||||
|
||||
export function useStartProcess() {
|
||||
const queryClient = useQueryClient();
|
||||
const logger = createMutationLogger('startProcess');
|
||||
|
||||
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) => {
|
||||
logger.info('Process started successfully', { name: variables.name });
|
||||
toast.success(data.message);
|
||||
// Invalidate and refetch
|
||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
||||
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}`);
|
||||
},
|
||||
});
|
||||
@@ -160,15 +166,21 @@ export function useStartProcess() {
|
||||
|
||||
export function useStopProcess() {
|
||||
const queryClient = useQueryClient();
|
||||
const logger = createMutationLogger('stopProcess');
|
||||
|
||||
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) => {
|
||||
logger.info('Process stopped successfully', { name: variables.name });
|
||||
toast.success(data.message);
|
||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
||||
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}`);
|
||||
},
|
||||
});
|
||||
@@ -176,15 +188,21 @@ export function useStopProcess() {
|
||||
|
||||
export function useRestartProcess() {
|
||||
const queryClient = useQueryClient();
|
||||
const logger = createMutationLogger('restartProcess');
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (name: string) => restartProcess(name),
|
||||
mutationFn: (name: string) => {
|
||||
logger.info('Restarting process', { name });
|
||||
return restartProcess(name);
|
||||
},
|
||||
onSuccess: (data, name) => {
|
||||
logger.info('Process restarted successfully', { name });
|
||||
toast.success(data.message);
|
||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
||||
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}`);
|
||||
},
|
||||
});
|
||||
@@ -415,14 +433,22 @@ async function restartAllProcesses(wait: boolean = true): Promise<{ success: boo
|
||||
|
||||
export function useStartAllProcesses() {
|
||||
const queryClient = useQueryClient();
|
||||
const logger = createMutationLogger('startAllProcesses');
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (wait: boolean = true) => startAllProcesses(wait),
|
||||
mutationFn: (wait: boolean = true) => {
|
||||
logger.info('Starting all processes', { wait });
|
||||
return startAllProcesses(wait);
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
logger.info('All processes started successfully', {
|
||||
resultCount: data.results?.length,
|
||||
});
|
||||
toast.success(data.message);
|
||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
logger.error('Failed to start all processes', error);
|
||||
toast.error(`Failed to start all processes: ${error.message}`);
|
||||
},
|
||||
});
|
||||
@@ -430,14 +456,22 @@ export function useStartAllProcesses() {
|
||||
|
||||
export function useStopAllProcesses() {
|
||||
const queryClient = useQueryClient();
|
||||
const logger = createMutationLogger('stopAllProcesses');
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (wait: boolean = true) => stopAllProcesses(wait),
|
||||
mutationFn: (wait: boolean = true) => {
|
||||
logger.info('Stopping all processes', { wait });
|
||||
return stopAllProcesses(wait);
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
logger.info('All processes stopped successfully', {
|
||||
resultCount: data.results?.length,
|
||||
});
|
||||
toast.success(data.message);
|
||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
logger.error('Failed to stop all processes', error);
|
||||
toast.error(`Failed to stop all processes: ${error.message}`);
|
||||
},
|
||||
});
|
||||
@@ -445,14 +479,22 @@ export function useStopAllProcesses() {
|
||||
|
||||
export function useRestartAllProcesses() {
|
||||
const queryClient = useQueryClient();
|
||||
const logger = createMutationLogger('restartAllProcesses');
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (wait: boolean = true) => restartAllProcesses(wait),
|
||||
mutationFn: (wait: boolean = true) => {
|
||||
logger.info('Restarting all processes', { wait });
|
||||
return restartAllProcesses(wait);
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
logger.info('All processes restarted successfully', {
|
||||
resultCount: data.results?.length,
|
||||
});
|
||||
toast.success(data.message);
|
||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
logger.error('Failed to restart all processes', error);
|
||||
toast.error(`Failed to restart all processes: ${error.message}`);
|
||||
},
|
||||
});
|
||||
@@ -516,14 +558,20 @@ export function useConfig() {
|
||||
|
||||
export function useReloadConfig() {
|
||||
const queryClient = useQueryClient();
|
||||
const logger = createMutationLogger('reloadConfig');
|
||||
|
||||
return useMutation({
|
||||
mutationFn: reloadConfig,
|
||||
mutationFn: () => {
|
||||
logger.info('Reloading supervisor configuration');
|
||||
return reloadConfig();
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
logger.info('Configuration reloaded successfully');
|
||||
toast.success(data.message);
|
||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.all });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
logger.error('Failed to reload configuration', error);
|
||||
toast.error(`Failed to reload configuration: ${error.message}`);
|
||||
},
|
||||
});
|
||||
@@ -602,15 +650,24 @@ async function signalAllProcesses(signal: string): Promise<{ success: boolean; m
|
||||
|
||||
export function useSignalProcess() {
|
||||
const queryClient = useQueryClient();
|
||||
const logger = createMutationLogger('signalProcess');
|
||||
|
||||
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) => {
|
||||
logger.info('Signal sent successfully', { name: variables.name, signal: variables.signal });
|
||||
toast.success(data.message);
|
||||
queryClient.invalidateQueries({ queryKey: supervisorKeys.process(variables.name) });
|
||||
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}`);
|
||||
},
|
||||
});
|
||||
|
||||
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',
|
||||
// Turbopack configuration (Next.js 16+)
|
||||
turbopack: {},
|
||||
experimental: {
|
||||
serverComponentsExternalPackages: ['pino', 'pino-pretty'],
|
||||
},
|
||||
serverExternalPackages: ['pino', 'pino-pretty'],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
Reference in New Issue
Block a user