diff --git a/app/layout.tsx b/app/layout.tsx
index c9401d9..b482475 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -14,6 +14,9 @@ export const metadata: Metadata = {
description: 'Modern web interface for Supervisor process management',
};
+// Force dynamic rendering
+export const dynamic = 'force-dynamic';
+
export default function RootLayout({
children,
}: Readonly<{
diff --git a/app/not-found.tsx b/app/not-found.tsx
new file mode 100644
index 0000000..7fe2d4e
--- /dev/null
+++ b/app/not-found.tsx
@@ -0,0 +1,17 @@
+import Link from 'next/link';
+import { Button } from '@/components/ui/button';
+
+export default function NotFound() {
+ return (
+
+
404
+
Page Not Found
+
+ The page you're looking for doesn't exist.
+
+
+
+
+
+ );
+}
diff --git a/components/layout/Navbar.tsx b/components/layout/Navbar.tsx
index 6192d92..4ce33b5 100644
--- a/components/layout/Navbar.tsx
+++ b/components/layout/Navbar.tsx
@@ -1,5 +1,6 @@
'use client';
+import { useState, useEffect } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Moon, Sun, Activity } from 'lucide-react';
@@ -16,8 +17,13 @@ const navItems = [
export function Navbar() {
const pathname = usePathname();
+ const [mounted, setMounted] = useState(false);
const { theme, setTheme, resolvedTheme } = useTheme();
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
const toggleTheme = () => {
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
};
@@ -52,18 +58,20 @@ export function Navbar() {
{/* Theme Toggle */}
-
+ {mounted && (
+
+ )}
);
diff --git a/components/logs/LogControls.tsx b/components/logs/LogControls.tsx
new file mode 100644
index 0000000..d3cc2bd
--- /dev/null
+++ b/components/logs/LogControls.tsx
@@ -0,0 +1,75 @@
+'use client';
+
+import { Play, Pause, RotateCcw, Download, Trash2 } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { cn } from '@/lib/utils/cn';
+
+interface LogControlsProps {
+ isPlaying: boolean;
+ onPlayPause: () => void;
+ autoScroll: boolean;
+ onToggleAutoScroll: () => void;
+ onRefresh: () => void;
+ onClear?: () => void;
+ onDownload?: () => void;
+ isClearing?: boolean;
+}
+
+export function LogControls({
+ isPlaying,
+ onPlayPause,
+ autoScroll,
+ onToggleAutoScroll,
+ onRefresh,
+ onClear,
+ onDownload,
+ isClearing = false,
+}: LogControlsProps) {
+ return (
+
+
+
+
+
+
+
+ {onDownload && (
+
+ )}
+
+ {onClear && (
+
+ )}
+
+ );
+}
diff --git a/components/logs/LogSearch.tsx b/components/logs/LogSearch.tsx
new file mode 100644
index 0000000..a6fe66d
--- /dev/null
+++ b/components/logs/LogSearch.tsx
@@ -0,0 +1,36 @@
+'use client';
+
+import { Search, X } from 'lucide-react';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button';
+
+interface LogSearchProps {
+ value: string;
+ onChange: (value: string) => void;
+ placeholder?: string;
+}
+
+export function LogSearch({ value, onChange, placeholder = 'Search logs...' }: LogSearchProps) {
+ return (
+
+
+ onChange(e.target.value)}
+ className="pl-9 pr-9"
+ />
+ {value && (
+
+ )}
+
+ );
+}
diff --git a/components/logs/LogViewer.tsx b/components/logs/LogViewer.tsx
new file mode 100644
index 0000000..9a0c8a4
--- /dev/null
+++ b/components/logs/LogViewer.tsx
@@ -0,0 +1,119 @@
+'use client';
+
+import { useEffect, useRef, useState } from 'react';
+import { Card, CardContent } from '@/components/ui/card';
+import { cn } from '@/lib/utils/cn';
+
+interface LogViewerProps {
+ logs: string;
+ isLoading?: boolean;
+ autoScroll?: boolean;
+ searchTerm?: string;
+}
+
+export function LogViewer({ logs, isLoading, autoScroll = true, searchTerm = '' }: LogViewerProps) {
+ const scrollRef = useRef(null);
+ const [userHasScrolled, setUserHasScrolled] = useState(false);
+
+ // Auto-scroll to bottom when logs update (if enabled and user hasn't manually scrolled)
+ useEffect(() => {
+ if (autoScroll && !userHasScrolled && scrollRef.current) {
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
+ }
+ }, [logs, autoScroll, userHasScrolled]);
+
+ // Detect user scroll
+ const handleScroll = () => {
+ if (!scrollRef.current) return;
+
+ const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
+ const isAtBottom = Math.abs(scrollHeight - clientHeight - scrollTop) < 10;
+
+ setUserHasScrolled(!isAtBottom);
+ };
+
+ // Highlight search term
+ const highlightSearchTerm = (text: string) => {
+ if (!searchTerm) return text;
+
+ const regex = new RegExp(`(${searchTerm})`, 'gi');
+ const parts = text.split(regex);
+
+ return parts.map((part, i) =>
+ regex.test(part) ? (
+
+ {part}
+
+ ) : (
+ part
+ )
+ );
+ };
+
+ // Syntax highlighting for log levels
+ const formatLogLine = (line: string, index: number) => {
+ const errorRegex = /(ERROR|FATAL|CRITICAL)/i;
+ const warnRegex = /(WARN|WARNING)/i;
+ const infoRegex = /(INFO|DEBUG)/i;
+
+ let className = '';
+ if (errorRegex.test(line)) {
+ className = 'text-destructive';
+ } else if (warnRegex.test(line)) {
+ className = 'text-warning';
+ } else if (infoRegex.test(line)) {
+ className = 'text-accent';
+ }
+
+ return (
+
+ {highlightSearchTerm(line)}
+
+ );
+ };
+
+ const logLines = logs.split('\n').filter((line) => {
+ if (!searchTerm) return true;
+ return line.toLowerCase().includes(searchTerm.toLowerCase());
+ });
+
+ return (
+
+
+
+ {isLoading ? (
+
+ ) : logLines.length === 0 ? (
+
+ {searchTerm ? 'No logs matching search term' : 'No logs available'}
+
+ ) : (
+
+ {logLines.map((line, index) => formatLogLine(line, index))}
+
+ )}
+
+ {!autoScroll && userHasScrolled && (
+
+ )}
+
+
+
+ );
+}
diff --git a/components/process/ProcessCard.tsx b/components/process/ProcessCard.tsx
index 0be7563..bfa3f1e 100644
--- a/components/process/ProcessCard.tsx
+++ b/components/process/ProcessCard.tsx
@@ -4,7 +4,7 @@ import { Play, Square, RotateCw, Activity } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
-import { ProcessInfo, getProcessStateClass, formatUptime, canStartProcess, canStopProcess } from '@/lib/supervisor/types';
+import { ProcessInfo, ProcessStateCode, getProcessStateClass, formatUptime, canStartProcess, canStopProcess } from '@/lib/supervisor/types';
import { useStartProcess, useStopProcess, useRestartProcess } from '@/lib/hooks/useSupervisor';
import { cn } from '@/lib/utils/cn';
@@ -36,7 +36,7 @@ export function ProcessCard({ process }: ProcessCardProps) {
@@ -77,7 +77,7 @@ export function ProcessCard({ process }: ProcessCardProps) {
size="sm"
variant="success"
onClick={handleStart}
- disabled={!canStartProcess(process.state) || isLoading}
+ disabled={!canStartProcess(process.state as ProcessStateCode) || isLoading}
className="flex-1"
>
@@ -87,7 +87,7 @@ export function ProcessCard({ process }: ProcessCardProps) {
size="sm"
variant="warning"
onClick={handleStop}
- disabled={!canStopProcess(process.state) || isLoading}
+ disabled={!canStopProcess(process.state as ProcessStateCode) || isLoading}
className="flex-1"
>
diff --git a/components/ui/input.tsx b/components/ui/input.tsx
new file mode 100644
index 0000000..91bb359
--- /dev/null
+++ b/components/ui/input.tsx
@@ -0,0 +1,27 @@
+import { InputHTMLAttributes, forwardRef } from 'react';
+import { cn } from '@/lib/utils/cn';
+
+export interface InputProps extends InputHTMLAttributes {}
+
+const Input = forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ );
+ }
+);
+Input.displayName = 'Input';
+
+export { Input };
diff --git a/lib/supervisor/client.ts b/lib/supervisor/client.ts
index 210867f..04b0299 100644
--- a/lib/supervisor/client.ts
+++ b/lib/supervisor/client.ts
@@ -27,7 +27,7 @@ export class SupervisorClient {
constructor(config: SupervisorClientConfig) {
this.config = config;
- const clientOptions: xmlrpc.ClientOptions = {
+ const clientOptions: any = {
host: config.host,
port: config.port,
path: '/RPC2',
@@ -49,9 +49,9 @@ export class SupervisorClient {
*/
private async call(method: string, params: any[] = []): Promise {
return new Promise((resolve, reject) => {
- this.client.methodCall(method, params, (error, value) => {
+ this.client.methodCall(method, params, (error: any, value: any) => {
if (error) {
- reject(new Error(`XML-RPC Error: ${error.message}`));
+ reject(new Error(`XML-RPC Error: ${error?.message || 'Unknown error'}`));
} else {
resolve(value);
}
diff --git a/lib/supervisor/types.ts b/lib/supervisor/types.ts
index 712c964..d9872da 100644
--- a/lib/supervisor/types.ts
+++ b/lib/supervisor/types.ts
@@ -168,12 +168,12 @@ export function getProcessStateClass(state: ProcessStateCode): string {
// Helper function to determine if a process can be started
export function canStartProcess(state: ProcessStateCode): boolean {
- return [ProcessState.STOPPED, ProcessState.EXITED, ProcessState.FATAL].includes(state);
+ return [ProcessState.STOPPED as ProcessStateCode, ProcessState.EXITED as ProcessStateCode, ProcessState.FATAL as ProcessStateCode].includes(state);
}
// Helper function to determine if a process can be stopped
export function canStopProcess(state: ProcessStateCode): boolean {
- return [ProcessState.RUNNING, ProcessState.STARTING].includes(state);
+ return [ProcessState.RUNNING as ProcessStateCode, ProcessState.STARTING as ProcessStateCode].includes(state);
}
// Helper function to format uptime
diff --git a/next.config.ts b/next.config.ts
index 6a8ff92..31bb80d 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -2,11 +2,8 @@ import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactStrictMode: true,
- // Standalone output for Docker deployment
- output: 'standalone',
- // For static export (nginx), uncomment:
- // output: 'export',
- // images: { unoptimized: true },
+ // Note: Using default mode (not standalone) to avoid SSR issues with client components
+ // For Docker, we'll use regular server mode which works fine
};
export default nextConfig;