feat: initial commit - Supervisor UI with Next.js 16 and Tailwind CSS 4
Some checks failed
Build and Push Docker Image to Gitea / build-and-push (push) Failing after 1m22s
Some checks failed
Build and Push Docker Image to Gitea / build-and-push (push) Failing after 1m22s
- Modern web interface for Supervisor process management - Built with Next.js 16 (App Router) and Tailwind CSS 4 - Full XML-RPC client implementation for Supervisor API - Real-time process monitoring with auto-refresh - Process control: start, stop, restart operations - Modern dashboard with system status and statistics - Dark/light theme with OKLCH color system - Docker multi-stage build with runtime env var configuration - Gitea CI/CD workflow for automated builds - Comprehensive documentation (README, IMPLEMENTATION, DEPLOYMENT) Features: - Backend proxy pattern for secure API communication - React Query for state management and caching - TypeScript strict mode with Zod validation - Responsive design with mobile support - Health check endpoint for monitoring - Non-root user security in Docker Environment Variables: - SUPERVISOR_HOST, SUPERVISOR_PORT - SUPERVISOR_USERNAME, SUPERVISOR_PASSWORD (optional) - Configurable at build-time and runtime 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
5
app/api/health/route.ts
Normal file
5
app/api/health/route.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ status: 'healthy', timestamp: new Date().toISOString() });
|
||||
}
|
||||
27
app/api/supervisor/processes/[name]/logs/stderr/route.ts
Normal file
27
app/api/supervisor/processes/[name]/logs/stderr/route.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
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);
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
27
app/api/supervisor/processes/[name]/logs/stdout/route.ts
Normal file
27
app/api/supervisor/processes/[name]/logs/stdout/route.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
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);
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
21
app/api/supervisor/processes/[name]/restart/route.ts
Normal file
21
app/api/supervisor/processes/[name]/restart/route.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
23
app/api/supervisor/processes/[name]/route.ts
Normal file
23
app/api/supervisor/processes/[name]/route.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
24
app/api/supervisor/processes/[name]/start/route.ts
Normal file
24
app/api/supervisor/processes/[name]/start/route.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
|
||||
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;
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
24
app/api/supervisor/processes/[name]/stop/route.ts
Normal file
24
app/api/supervisor/processes/[name]/stop/route.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
|
||||
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;
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
18
app/api/supervisor/processes/route.ts
Normal file
18
app/api/supervisor/processes/route.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
18
app/api/supervisor/system/route.ts
Normal file
18
app/api/supervisor/system/route.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { createSupervisorClient } from '@/lib/supervisor/client';
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
25
app/config/page.tsx
Normal file
25
app/config/page.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { Settings } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
|
||||
export default function ConfigPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Configuration</h1>
|
||||
<p className="text-muted-foreground mt-1">Manage Supervisor settings</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-12 text-center">
|
||||
<Settings className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||
<h2 className="text-xl font-semibold mb-2">Configuration Coming Soon</h2>
|
||||
<p className="text-muted-foreground">
|
||||
This feature will allow you to reload configuration, add/remove process groups, and manage settings.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
329
app/globals.css
Normal file
329
app/globals.css
Normal file
@@ -0,0 +1,329 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
@plugin "@tailwindcss/forms";
|
||||
|
||||
/* Content source definitions for Tailwind v4 */
|
||||
@source "../components/**/*.{js,ts,jsx,tsx}";
|
||||
@source "../lib/**/*.{js,ts,jsx,tsx}";
|
||||
@source "./**/*.{js,ts,jsx,tsx}";
|
||||
|
||||
/* Custom dark mode variant */
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/* Color System - Supervisor Theme */
|
||||
:root {
|
||||
/* Base colors using OKLCH for perceptual uniformity */
|
||||
--background: oklch(98% 0 0);
|
||||
--foreground: oklch(20% 0 0);
|
||||
|
||||
/* Cards and panels */
|
||||
--card: oklch(100% 0 0);
|
||||
--card-foreground: oklch(20% 0 0);
|
||||
|
||||
/* Primary - Process management (blue-green) */
|
||||
--primary: oklch(55% 0.15 220);
|
||||
--primary-foreground: oklch(98% 0 0);
|
||||
|
||||
/* Secondary - System status (slate) */
|
||||
--secondary: oklch(75% 0.03 250);
|
||||
--secondary-foreground: oklch(20% 0 0);
|
||||
|
||||
/* Accent - Highlights (cyan) */
|
||||
--accent: oklch(65% 0.12 200);
|
||||
--accent-foreground: oklch(98% 0 0);
|
||||
|
||||
/* Success - Running state (green) */
|
||||
--success: oklch(60% 0.15 140);
|
||||
--success-foreground: oklch(98% 0 0);
|
||||
|
||||
/* Warning - Stopped/paused (yellow) */
|
||||
--warning: oklch(75% 0.15 90);
|
||||
--warning-foreground: oklch(20% 0 0);
|
||||
|
||||
/* Destructive - Fatal/error (red) */
|
||||
--destructive: oklch(55% 0.20 25);
|
||||
--destructive-foreground: oklch(98% 0 0);
|
||||
|
||||
/* Muted - Disabled states */
|
||||
--muted: oklch(92% 0.01 250);
|
||||
--muted-foreground: oklch(55% 0.01 250);
|
||||
|
||||
/* Borders and inputs */
|
||||
--border: oklch(88% 0.01 250);
|
||||
--input: oklch(88% 0.01 250);
|
||||
--ring: oklch(55% 0.15 220);
|
||||
|
||||
/* Chart colors */
|
||||
--chart-1: oklch(55% 0.15 220);
|
||||
--chart-2: oklch(60% 0.15 140);
|
||||
--chart-3: oklch(75% 0.15 90);
|
||||
--chart-4: oklch(55% 0.20 25);
|
||||
--chart-5: oklch(65% 0.12 200);
|
||||
|
||||
/* Radius */
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(15% 0.01 250);
|
||||
--foreground: oklch(95% 0 0);
|
||||
|
||||
--card: oklch(18% 0.01 250);
|
||||
--card-foreground: oklch(95% 0 0);
|
||||
|
||||
--primary: oklch(65% 0.15 220);
|
||||
--primary-foreground: oklch(15% 0 0);
|
||||
|
||||
--secondary: oklch(25% 0.03 250);
|
||||
--secondary-foreground: oklch(95% 0 0);
|
||||
|
||||
--accent: oklch(55% 0.12 200);
|
||||
--accent-foreground: oklch(15% 0 0);
|
||||
|
||||
--success: oklch(55% 0.15 140);
|
||||
--success-foreground: oklch(15% 0 0);
|
||||
|
||||
--warning: oklch(65% 0.15 90);
|
||||
--warning-foreground: oklch(15% 0 0);
|
||||
|
||||
--destructive: oklch(60% 0.20 25);
|
||||
--destructive-foreground: oklch(98% 0 0);
|
||||
|
||||
--muted: oklch(20% 0.01 250);
|
||||
--muted-foreground: oklch(60% 0.01 250);
|
||||
|
||||
--border: oklch(25% 0.01 250);
|
||||
--input: oklch(25% 0.01 250);
|
||||
--ring: oklch(65% 0.15 220);
|
||||
|
||||
--chart-1: oklch(65% 0.15 220);
|
||||
--chart-2: oklch(55% 0.15 140);
|
||||
--chart-3: oklch(65% 0.15 90);
|
||||
--chart-4: oklch(60% 0.20 25);
|
||||
--chart-5: oklch(55% 0.12 200);
|
||||
}
|
||||
|
||||
/* Map colors to Tailwind utilities */
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-success: var(--success);
|
||||
--color-success-foreground: var(--success-foreground);
|
||||
--color-warning: var(--warning);
|
||||
--color-warning-foreground: var(--warning-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
|
||||
/* Custom animations */
|
||||
--animate-fade-in: fadeIn 0.3s ease-in-out;
|
||||
--animate-slide-up: slideUp 0.4s ease-out;
|
||||
--animate-slide-down: slideDown 0.4s ease-out;
|
||||
--animate-slide-in-right: slideInRight 0.3s ease-out;
|
||||
--animate-slide-in-left: slideInLeft 0.3s ease-out;
|
||||
--animate-scale-in: scaleIn 0.2s ease-out;
|
||||
--animate-bounce-gentle: bounceGentle 0.5s ease-in-out;
|
||||
--animate-shimmer: shimmer 2s infinite;
|
||||
--animate-pulse-slow: pulseSlow 3s ease-in-out infinite;
|
||||
--animate-spin-slow: spinSlow 3s linear infinite;
|
||||
}
|
||||
|
||||
/* Global Styles */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-muted;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-muted-foreground/30 rounded;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-muted-foreground/50;
|
||||
}
|
||||
|
||||
/* Screen reader only */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
/* Disable transitions during theme change */
|
||||
.disable-transitions * {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
/* Animation Keyframes */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(10px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
transform: translateY(-10px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(-10px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
transform: translateX(10px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
transform: scale(0.95);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounceGentle {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -1000px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 1000px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulseSlow {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinSlow {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Prose styles for log viewers */
|
||||
.prose-log {
|
||||
@apply font-mono text-sm;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Process state indicators */
|
||||
.process-running {
|
||||
@apply bg-success/10 text-success border-success/20;
|
||||
}
|
||||
|
||||
.process-stopped {
|
||||
@apply bg-muted text-muted-foreground border-border;
|
||||
}
|
||||
|
||||
.process-starting {
|
||||
@apply bg-warning/10 text-warning border-warning/20;
|
||||
}
|
||||
|
||||
.process-stopping {
|
||||
@apply bg-warning/10 text-warning border-warning/20;
|
||||
}
|
||||
|
||||
.process-fatal {
|
||||
@apply bg-destructive/10 text-destructive border-destructive/20;
|
||||
}
|
||||
|
||||
.process-unknown {
|
||||
@apply bg-muted text-muted-foreground border-border;
|
||||
}
|
||||
50
app/layout.tsx
Normal file
50
app/layout.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
import './globals.css';
|
||||
import { Providers } from '@/components/providers/Providers';
|
||||
import { Navbar } from '@/components/layout/Navbar';
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-inter',
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Supervisor UI',
|
||||
description: 'Modern web interface for Supervisor process management',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
try {
|
||||
const theme = localStorage.getItem('theme') || 'system';
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
const effectiveTheme = theme === 'system' ? systemTheme : theme;
|
||||
if (effectiveTheme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body className={`${inter.variable} antialiased`}>
|
||||
<Providers>
|
||||
<Navbar />
|
||||
<main className="container mx-auto px-4 py-8">{children}</main>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
25
app/logs/page.tsx
Normal file
25
app/logs/page.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
|
||||
export default function LogsPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Process Logs</h1>
|
||||
<p className="text-muted-foreground mt-1">Real-time log viewing</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-12 text-center">
|
||||
<AlertCircle className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||
<h2 className="text-xl font-semibold mb-2">Logs View Coming Soon</h2>
|
||||
<p className="text-muted-foreground">
|
||||
This feature will display real-time logs from your processes with filtering and search capabilities.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
135
app/page.tsx
Normal file
135
app/page.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
'use client';
|
||||
|
||||
import { Activity, Server, FileText, Settings } from 'lucide-react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { SystemStatus } from '@/components/process/SystemStatus';
|
||||
import { useProcesses } from '@/lib/hooks/useSupervisor';
|
||||
import { ProcessState } from '@/lib/supervisor/types';
|
||||
|
||||
export default function HomePage() {
|
||||
const { data: processes, isLoading } = useProcesses();
|
||||
|
||||
const stats = {
|
||||
total: processes?.length ?? 0,
|
||||
running: processes?.filter((p) => p.state === ProcessState.RUNNING).length ?? 0,
|
||||
stopped: processes?.filter((p) => p.state === ProcessState.STOPPED || p.state === ProcessState.EXITED).length ?? 0,
|
||||
fatal: processes?.filter((p) => p.state === ProcessState.FATAL).length ?? 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="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">
|
||||
Monitor and manage your processes in real-time
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* System Status */}
|
||||
<SystemStatus />
|
||||
|
||||
{/* Process Statistics */}
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Processes</CardTitle>
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{isLoading ? '...' : stats.total}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
All configured processes
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Running</CardTitle>
|
||||
<div className="h-3 w-3 rounded-full bg-success animate-pulse-slow" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-success">{isLoading ? '...' : stats.running}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Active processes
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Stopped</CardTitle>
|
||||
<div className="h-3 w-3 rounded-full bg-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-muted-foreground">{isLoading ? '...' : stats.stopped}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Inactive processes
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Fatal</CardTitle>
|
||||
<div className="h-3 w-3 rounded-full bg-destructive animate-pulse-slow" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-destructive">{isLoading ? '...' : stats.fatal}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Failed processes
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<Card className="hover:shadow-lg transition-shadow cursor-pointer" onClick={() => window.location.href = '/processes'}>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-3 rounded-lg bg-primary/10">
|
||||
<Server className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Manage Processes</CardTitle>
|
||||
<CardDescription>Start, stop, and restart</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:shadow-lg transition-shadow cursor-pointer" onClick={() => window.location.href = '/logs'}>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-3 rounded-lg bg-accent/10">
|
||||
<FileText className="h-6 w-6 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>View Logs</CardTitle>
|
||||
<CardDescription>Monitor process output</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<Card className="hover:shadow-lg transition-shadow cursor-pointer" onClick={() => window.location.href = '/config'}>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-3 rounded-lg bg-success/10">
|
||||
<Settings className="h-6 w-6 text-success" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Configuration</CardTitle>
|
||||
<CardDescription>Manage settings</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
app/processes/page.tsx
Normal file
71
app/processes/page.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
|
||||
import { useProcesses } from '@/lib/hooks/useSupervisor';
|
||||
import { ProcessCard } from '@/components/process/ProcessCard';
|
||||
import { RefreshCw, AlertCircle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export default function ProcessesPage() {
|
||||
const { data: processes, isLoading, isError, refetch } = useProcesses();
|
||||
|
||||
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">
|
||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||
<div key={i} className="h-64 bg-muted rounded-lg animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
Could not connect to Supervisor. Please check your configuration.
|
||||
</p>
|
||||
<Button onClick={() => refetch()}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Processes</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{processes?.length ?? 0} processes configured
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => refetch()}>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
) : (
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{processes?.map((process) => (
|
||||
<ProcessCard key={`${process.group}:${process.name}`} process={process} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user