Compare commits

...

12 Commits

Author SHA1 Message Date
813f6d4c75 fix: make ReloadConfigResult fields optional with default empty arrays
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 1m33s
The Supervisor API doesn't always return 'changed' and 'removed' arrays
when reloading configuration, causing Zod validation errors.

Made all three fields (added, changed, removed) optional with default
empty arrays to handle cases where the API omits them.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 05:32:58 +01:00
7f1c110f8f style: center header content with mx-auto
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 1m13s
Added mx-auto to navbar container to center the header content.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 00:10:11 +01:00
f83ecf864a fix: make autorestart field optional in ConfigInfo schema
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 1m10s
The autorestart field is not always present in the Supervisor API
response for getAllConfigInfo(), causing Zod validation errors.

Changed autorestart from required to optional field using .optional().

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 23:04:04 +01:00
2d5ffac56c fix: correct readLog parameters for Supervisor XML-RPC API
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 1m13s
Fixed the readLog() call to use the correct parameters for reading
from the end of the log file. When using a negative offset to read
the last N bytes, the length parameter must be 0, not a positive number.

Changes:
- Updated fetchMainLog default length from 4096 to 0
- Updated API route default length from '4096' to '0'

Correct usage:
- readLog(-4096, 0) - Read last 4096 bytes from end of file
- readLog(0, 4096) - Read 4096 bytes from start of file

This fixes the INCORRECT_PARAMETERS error when fetching the main
supervisord log.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 22:59:22 +01:00
bdec163fb0 refactor: simplify readLog to use standard Supervisor XML-RPC API
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 1m11s
Removed SUPERVISOR_LOGFILE environment variable and simplified readLog()
to use the standard 2-parameter XML-RPC API that relies on supervisord's
configured logfile path.

Changes:
- Removed SUPERVISOR_LOGFILE from .env.example
- Simplified SupervisorClient.readLog() to accept only offset and length
- Removed logfile path parameter and all environment variable logic
- Fixed mobile nav container padding (removed px-4 py-6)

The readLog() method now uses the standard supervisor.readLog(offset, length)
XML-RPC call, which automatically reads from the logfile path configured
in supervisord.conf.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 22:58:03 +01:00
c50274452c feat: make Supervisor logfile path configurable via environment variable
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 1m12s
Added SUPERVISOR_LOGFILE environment variable to configure the path to
the Supervisor main logfile for reading via XML-RPC.

Changes:
- Added SUPERVISOR_LOGFILE to .env.example with default path
- Removed getLogfilePath() method and cachedLogfilePath field from SupervisorClient
- Updated readLog() to use environment variable with fallback chain:
  1. Explicitly provided logfilePath parameter
  2. SUPERVISOR_LOGFILE environment variable
  3. Default: /var/log/supervisor/supervisord.log
- Added debug logging to readLog() for troubleshooting

This allows users to configure the correct logfile path for their
Supervisor installation when using the custom 3-parameter XML-RPC
readLog implementation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 22:46:53 +01:00
145d37193c fix: update ConfigInfo schema to match Supervisor API response
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 1m11s
Updated the ConfigInfoSchema to accurately reflect the data structure
returned by Supervisor's getAllConfigInfo() XML-RPC method, fixing Zod
validation errors on the /config page.

Schema changes:
- Removed fields not in API: environment, priority, process_name, numprocs, numprocs_start, username
- Added missing fields: autorestart, killasgroup, process_prio, group_prio, stdout_syslog, stderr_syslog, serverurl
- Fixed type mismatches:
  - stopsignal: string → number (API returns signal numbers like 15)
  - uid: number|null → string (API returns username strings)
  - directory: string|null → string

Updated ConfigTable.tsx to use process_prio instead of priority and
removed the non-existent numprocs column.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 22:39:33 +01:00
20877abbc7 fix: update supervisor.readLog to accept 3 parameters with logfile path
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 1m10s
Update the readLog method to match custom Supervisor API requirements:
- Add third parameter (logfile_path) to supervisor.readLog call
- Retrieve logfile path from supervisor.getAPIVersion() metadata
- Cache logfile path after first retrieval to avoid repeated API calls
- Support optional explicit logfile_path parameter
- Fallback to default path if metadata extraction fails

Implementation details:
- Added cachedLogfilePath private field to SupervisorClient
- Added private getLogfilePath() method to extract path from API version
- Updated readLog signature: (offset, length, logfilePath?)
- Automatic path retrieval when logfilePath not provided
- Supports multiple metadata formats (logfile, logfile_path properties)
- Logs warnings if path extraction fails, uses sensible default

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 22:27:53 +01:00
9fcb0447ee fix: apply background and blur to nav element for mobile menu
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 1m12s
Move background styling to the nav element itself with rounded corners.
- Add bg-background/95 backdrop-blur-md to nav element
- Add rounded-lg and p-4 for better visual appearance
- Creates frosted glass card effect for the mobile menu

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 22:18:24 +01:00
df3e022049 fix: correct mobile menu background placement with blur effect
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 1m10s
Move background styling from outer overlay to inner container for proper visibility.
- Remove bg-background from outer fixed div
- Add bg-background/95 backdrop-blur-md h-full to inner container
- Creates frosted glass effect with proper blur

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 22:04:24 +01:00
dda335d501 fix: resolve 7 critical UI issues - charts, layouts, and mobile responsiveness
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 1m8s
This commit fixes all reported UI issues across the dashboard:

## Issue 1: Chart Colors and Tooltips 
- Create chartColors utility with static hex colors for Recharts compatibility
- Replace CSS variable colors (hsl(var(--))) with hex colors in all charts
- Add custom tooltip styling with dark background and white text for readability
- Fixes: ProcessStateChart, ProcessUptimeChart, GroupStatistics

## Issue 2: Process Card Heights 
- Add h-full and flex flex-col to ProcessCard component
- Add auto-rows-fr to process grid layout
- Ensures all cards have consistent heights regardless of content

## Issue 3: Batch Actions Button Labels 
- Simplify button labels from "Start Selected" to "Start"
- Remove "Stop Selected" to "Stop", "Restart Selected" to "Restart"
- Labels now always visible on all screen sizes

## Issue 4: Mobile Menu Background 
- Change mobile menu from semi-transparent (bg-background/95) to solid (bg-background)
- Removes backdrop blur for better visibility

## Issue 5: Group Header Button Overflow 
- Add flex-wrap to button container in GroupCard
- Stack buttons vertically on mobile (flex-col md:flex-row)
- Buttons take full width on mobile, auto width on desktop

## Issue 6: Logs Search Input Overflow 
- Change LogSearch from max-w-md to w-full sm:flex-1 sm:max-w-md
- Search input now full width on mobile, constrained on desktop

## Issue 7: Logs Action Button Overflow 
- Add flex-wrap to LogControls button container
- Buttons wrap to new row when space is limited

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 21:52:35 +01:00
791c99097c fix: resolve build errors in api-logger imports and React Query config
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 1m10s
- Fix import of generateRequestId in events/route.ts (import from logger instead of api-logger)
- Remove deprecated logger config from QueryClient (no longer supported in latest React Query)

These changes resolve TypeScript compilation errors and allow the build to succeed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 21:25:15 +01:00
17 changed files with 133 additions and 66 deletions

View File

@@ -1,7 +1,7 @@
import { NextRequest } from 'next/server'; import { NextRequest } from 'next/server';
import { createSupervisorClient } from '@/lib/supervisor/client'; import { createSupervisorClient } from '@/lib/supervisor/client';
import { createApiLogger, generateRequestId } from '@/lib/utils/api-logger'; import { createApiLogger } from '@/lib/utils/api-logger';
import { formatError } from '@/lib/utils/logger'; import { formatError, generateRequestId } from '@/lib/utils/logger';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';

View File

@@ -7,7 +7,7 @@ export const dynamic = 'force-dynamic';
export const GET = withLogging(async (request: NextRequest) => { export const GET = withLogging(async (request: NextRequest) => {
const searchParams = request.nextUrl.searchParams; const searchParams = request.nextUrl.searchParams;
const offset = parseInt(searchParams.get('offset') || '-4096', 10); const offset = parseInt(searchParams.get('offset') || '-4096', 10);
const length = parseInt(searchParams.get('length') || '4096', 10); const length = parseInt(searchParams.get('length') || '0', 10);
const client = createSupervisorClient(); const client = createSupervisorClient();
const logs = await client.readLog(offset, length); const logs = await client.readLog(offset, length);

View File

@@ -280,7 +280,7 @@ export default function ProcessesPage() {
) : viewMode === 'grouped' ? ( ) : viewMode === 'grouped' ? (
<GroupView processes={displayedProcesses} /> <GroupView processes={displayedProcesses} />
) : ( ) : (
<div className="grid gap-4 md:gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 md:gap-6 md:grid-cols-2 lg:grid-cols-3 auto-rows-fr">
{displayedProcesses.map((process, index) => { {displayedProcesses.map((process, index) => {
const fullName = `${process.group}:${process.name}`; const fullName = `${process.group}:${process.name}`;
const isFocused = index === focusedIndex; const isFocused = index === focusedIndex;

View File

@@ -3,6 +3,7 @@
import { ProcessInfo, ProcessState } from '@/lib/supervisor/types'; import { ProcessInfo, ProcessState } from '@/lib/supervisor/types';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { chartColors } from '@/lib/utils/chartColors';
interface GroupStatisticsProps { interface GroupStatisticsProps {
processes: ProcessInfo[]; processes: ProcessInfo[];
@@ -50,11 +51,19 @@ export function GroupStatistics({ processes }: GroupStatisticsProps) {
<CartesianGrid strokeDasharray="3 3" /> <CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" /> <XAxis dataKey="name" />
<YAxis /> <YAxis />
<Tooltip /> <Tooltip
contentStyle={{
backgroundColor: 'rgba(0, 0, 0, 0.9)',
border: 'none',
borderRadius: '8px',
color: '#ffffff',
}}
itemStyle={{ color: '#ffffff' }}
/>
<Legend /> <Legend />
<Bar dataKey="running" stackId="a" fill="hsl(var(--success))" name="Running" /> <Bar dataKey="running" stackId="a" fill={chartColors.running} name="Running" />
<Bar dataKey="stopped" stackId="a" fill="hsl(var(--muted-foreground))" name="Stopped" /> <Bar dataKey="stopped" stackId="a" fill={chartColors.stopped} name="Stopped" />
<Bar dataKey="fatal" stackId="a" fill="hsl(var(--destructive))" name="Fatal" /> <Bar dataKey="fatal" stackId="a" fill={chartColors.fatal} name="Fatal" />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</CardContent> </CardContent>

View File

@@ -3,6 +3,7 @@
import { ProcessInfo, ProcessState } from '@/lib/supervisor/types'; import { ProcessInfo, ProcessState } from '@/lib/supervisor/types';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts'; import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts';
import { chartColors } from '@/lib/utils/chartColors';
interface ProcessStateChartProps { interface ProcessStateChartProps {
processes: ProcessInfo[]; processes: ProcessInfo[];
@@ -23,12 +24,12 @@ export function ProcessStateChart({ processes }: ProcessStateChartProps) {
); );
const data = [ const data = [
{ name: 'Running', value: stateCounts.running, color: 'hsl(var(--success))' }, { name: 'Running', value: stateCounts.running, color: chartColors.running },
{ name: 'Stopped', value: stateCounts.stopped, color: 'hsl(var(--muted-foreground))' }, { name: 'Stopped', value: stateCounts.stopped, color: chartColors.stopped },
{ name: 'Fatal', value: stateCounts.fatal, color: 'hsl(var(--destructive))' }, { name: 'Fatal', value: stateCounts.fatal, color: chartColors.fatal },
{ name: 'Starting', value: stateCounts.starting, color: 'hsl(var(--warning))' }, { name: 'Starting', value: stateCounts.starting, color: chartColors.starting },
{ name: 'Stopping', value: stateCounts.stopping, color: 'hsl(var(--accent))' }, { name: 'Stopping', value: stateCounts.stopping, color: chartColors.stopping },
{ name: 'Other', value: stateCounts.other, color: 'hsl(var(--muted))' }, { name: 'Other', value: stateCounts.other, color: chartColors.muted },
].filter((item) => item.value > 0); ].filter((item) => item.value > 0);
return ( return (
@@ -53,7 +54,15 @@ export function ProcessStateChart({ processes }: ProcessStateChartProps) {
<Cell key={`cell-${index}`} fill={entry.color} /> <Cell key={`cell-${index}`} fill={entry.color} />
))} ))}
</Pie> </Pie>
<Tooltip /> <Tooltip
contentStyle={{
backgroundColor: 'rgba(0, 0, 0, 0.9)',
border: 'none',
borderRadius: '8px',
color: '#ffffff',
}}
itemStyle={{ color: '#ffffff' }}
/>
<Legend /> <Legend />
</PieChart> </PieChart>
</ResponsiveContainer> </ResponsiveContainer>

View File

@@ -3,6 +3,7 @@
import { ProcessInfo, ProcessState } from '@/lib/supervisor/types'; import { ProcessInfo, ProcessState } from '@/lib/supervisor/types';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { chartColors } from '@/lib/utils/chartColors';
interface ProcessUptimeChartProps { interface ProcessUptimeChartProps {
processes: ProcessInfo[]; processes: ProcessInfo[];
@@ -56,9 +57,16 @@ export function ProcessUptimeChart({ processes }: ProcessUptimeChartProps) {
const minutes = Math.floor((value - hours) * 60); const minutes = Math.floor((value - hours) * 60);
return `${hours}h ${minutes}m`; return `${hours}h ${minutes}m`;
}} }}
contentStyle={{
backgroundColor: 'rgba(0, 0, 0, 0.9)',
border: 'none',
borderRadius: '8px',
color: '#ffffff',
}}
itemStyle={{ color: '#ffffff' }}
/> />
<Legend /> <Legend />
<Bar dataKey="uptime" fill="hsl(var(--success))" name="Uptime (hours)" /> <Bar dataKey="uptime" fill={chartColors.success} name="Uptime (hours)" />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</CardContent> </CardContent>

View File

@@ -91,7 +91,6 @@ export function ConfigTable({ configs }: ConfigTableProps) {
Autostart <SortIcon field="autostart" /> Autostart <SortIcon field="autostart" />
</th> </th>
<th className="text-center p-3">Priority</th> <th className="text-center p-3">Priority</th>
<th className="text-center p-3">Processes</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -117,8 +116,7 @@ export function ConfigTable({ configs }: ConfigTableProps) {
)} )}
/> />
</td> </td>
<td className="p-3 text-center text-sm">{config.priority}</td> <td className="p-3 text-center text-sm">{config.process_prio}</td>
<td className="p-3 text-center text-sm">{config.numprocs}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@@ -159,15 +157,9 @@ export function ConfigTable({ configs }: ConfigTableProps) {
<div className="mt-1 text-xs break-all">{config.directory}</div> <div className="mt-1 text-xs break-all">{config.directory}</div>
</div> </div>
<div className="flex gap-4 pt-2"> <div className="pt-2">
<div> <span className="text-muted-foreground">Priority:</span>
<span className="text-muted-foreground">Priority:</span> <span className="ml-2 font-mono">{config.process_prio}</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>
</div> </div>
</CardContent> </CardContent>

View File

@@ -49,7 +49,7 @@ export function GroupCard({ groupName, processes }: GroupCardProps) {
return ( return (
<Card className="overflow-hidden"> <Card className="overflow-hidden">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center justify-between"> <div className="flex flex-col md:flex-row items-start md:items-center gap-3 md:justify-between">
<div className="flex items-center gap-3 flex-1"> <div className="flex items-center gap-3 flex-1">
<Button <Button
variant="ghost" variant="ghost"
@@ -80,13 +80,13 @@ export function GroupCard({ groupName, processes }: GroupCardProps) {
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex flex-wrap gap-2 w-full md:w-auto">
<Button <Button
variant="success" variant="success"
size="sm" size="sm"
onClick={handleStart} onClick={handleStart}
disabled={isLoading || stats.stopped === 0} disabled={isLoading || stats.stopped === 0}
className="gap-2" className="gap-2 flex-1 sm:flex-initial"
> >
<Play className="h-4 w-4" /> <Play className="h-4 w-4" />
Start All Start All
@@ -96,7 +96,7 @@ export function GroupCard({ groupName, processes }: GroupCardProps) {
size="sm" size="sm"
onClick={handleStop} onClick={handleStop}
disabled={isLoading || stats.running === 0} disabled={isLoading || stats.running === 0}
className="gap-2" className="gap-2 flex-1 sm:flex-initial"
> >
<Square className="h-4 w-4" /> <Square className="h-4 w-4" />
Stop All Stop All
@@ -106,7 +106,7 @@ export function GroupCard({ groupName, processes }: GroupCardProps) {
size="sm" size="sm"
onClick={handleRestart} onClick={handleRestart}
disabled={isLoading} disabled={isLoading}
className="gap-2" className="gap-2 flex-1 sm:flex-initial"
> >
<RotateCw className={cn('h-4 w-4', isLoading && 'animate-spin')} /> <RotateCw className={cn('h-4 w-4', isLoading && 'animate-spin')} />
Restart All Restart All

View File

@@ -51,7 +51,7 @@ export function Navbar() {
return ( return (
<nav className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"> <nav className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-16 items-center px-4 md:px-6"> <div className="container mx-auto flex h-16 items-center px-4 md:px-6">
{/* Logo */} {/* Logo */}
<Link href="/" className="flex items-center gap-2 mr-4 md:mr-8"> <Link href="/" className="flex items-center gap-2 mr-4 md:mr-8">
<Activity className="h-6 w-6 text-primary" /> <Activity className="h-6 w-6 text-primary" />
@@ -116,9 +116,9 @@ export function Navbar() {
{/* Mobile Menu Drawer */} {/* Mobile Menu Drawer */}
{mobileMenuOpen && ( {mobileMenuOpen && (
<div className="md:hidden fixed inset-0 top-16 z-50 bg-background/95 backdrop-blur-sm"> <div className="md:hidden fixed inset-0 top-16 z-50">
<div className="container px-4 py-6"> <div className="container">
<nav className="flex flex-col gap-2"> <nav className="flex flex-col gap-2 bg-background/95 backdrop-blur-md rounded-lg p-4">
{navItems.map((item) => ( {navItems.map((item) => (
<Link key={item.href} href={item.href}> <Link key={item.href} href={item.href}>
<Button <Button

View File

@@ -26,7 +26,7 @@ export function LogControls({
isClearing = false, isClearing = false,
}: LogControlsProps) { }: LogControlsProps) {
return ( return (
<div className="flex items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<Button <Button
variant={isPlaying ? 'destructive' : 'success'} variant={isPlaying ? 'destructive' : 'success'}
size="sm" size="sm"

View File

@@ -12,7 +12,7 @@ interface LogSearchProps {
export function LogSearch({ value, onChange, placeholder = 'Search logs...' }: LogSearchProps) { export function LogSearch({ value, onChange, placeholder = 'Search logs...' }: LogSearchProps) {
return ( return (
<div className="relative flex-1 max-w-md"> <div className="relative w-full sm:flex-1 sm:max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input <Input
type="text" type="text"

View File

@@ -82,7 +82,7 @@ export function BatchActions({ selectedProcesses, processes, onClearSelection }:
className="gap-2 flex-1 sm:flex-initial" className="gap-2 flex-1 sm:flex-initial"
> >
<Play className="h-4 w-4" /> <Play className="h-4 w-4" />
<span className="hidden sm:inline">Start Selected</span> <span>Start</span>
</Button> </Button>
<Button <Button
variant="destructive" variant="destructive"
@@ -92,7 +92,7 @@ export function BatchActions({ selectedProcesses, processes, onClearSelection }:
className="gap-2 flex-1 sm:flex-initial" className="gap-2 flex-1 sm:flex-initial"
> >
<Square className="h-4 w-4" /> <Square className="h-4 w-4" />
<span className="hidden sm:inline">Stop Selected</span> <span>Stop</span>
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
@@ -102,7 +102,7 @@ export function BatchActions({ selectedProcesses, processes, onClearSelection }:
className="gap-2 flex-1 sm:flex-initial" className="gap-2 flex-1 sm:flex-initial"
> >
<RotateCw className={cn('h-4 w-4', isLoading && 'animate-spin')} /> <RotateCw className={cn('h-4 w-4', isLoading && 'animate-spin')} />
<span className="hidden sm:inline">Restart Selected</span> <span>Restart</span>
</Button> </Button>
</div> </div>

View File

@@ -42,7 +42,7 @@ export function ProcessCard({ process, isSelected = false, isFocused = false, on
return ( return (
<Card <Card
className={cn( className={cn(
'transition-all hover:shadow-lg animate-fade-in', 'transition-all hover:shadow-lg animate-fade-in h-full flex flex-col',
onSelectionChange && 'cursor-pointer', onSelectionChange && 'cursor-pointer',
isSelected && 'ring-2 ring-primary ring-offset-2', isSelected && 'ring-2 ring-primary ring-offset-2',
isFocused && 'ring-2 ring-accent ring-offset-2 shadow-xl' isFocused && 'ring-2 ring-accent ring-offset-2 shadow-xl'
@@ -82,7 +82,7 @@ export function ProcessCard({ process, isSelected = false, isFocused = false, on
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4 flex-1 flex flex-col justify-between">
{/* Metrics */} {/* Metrics */}
<div className="grid grid-cols-2 gap-3 text-sm"> <div className="grid grid-cols-2 gap-3 text-sm">
<div> <div>

View File

@@ -22,11 +22,6 @@ export function Providers({ children }: { children: ReactNode }) {
retry: 1, retry: 1,
}, },
}, },
logger: {
log: (message) => clientLogger.debug(message),
warn: (message) => clientLogger.warn(message),
error: (error) => clientLogger.error('React Query error', error),
},
}) })
); );

View File

@@ -210,7 +210,7 @@ export function useRestartProcess() {
// Log Management // Log Management
async function fetchMainLog(offset: number = -4096, length: number = 4096): Promise<{ logs: string }> { async function fetchMainLog(offset: number = -4096, length: number = 0): Promise<{ logs: string }> {
const response = await fetch(`/api/supervisor/logs?offset=${offset}&length=${length}`); const response = await fetch(`/api/supervisor/logs?offset=${offset}&length=${length}`);
if (!response.ok) { if (!response.ok) {
const error = await response.json(); const error = await response.json();

View File

@@ -62,38 +62,39 @@ export const ConfigInfoSchema = z.object({
name: z.string(), name: z.string(),
group: z.string(), group: z.string(),
autostart: z.boolean(), autostart: z.boolean(),
directory: z.union([z.string(), z.null()]), autorestart: z.string().optional(), // "auto", "none", or "unexpected"
directory: z.string(),
command: z.string(), command: z.string(),
environment: z.union([z.string(), z.null()]),
exitcodes: z.array(z.number()), exitcodes: z.array(z.number()),
group_prio: z.number(),
inuse: z.boolean(),
killasgroup: z.boolean(),
process_prio: z.number(),
redirect_stderr: z.boolean(), redirect_stderr: z.boolean(),
serverurl: z.string(),
startretries: z.number(),
startsecs: z.number(),
stderr_capture_maxbytes: z.number(), stderr_capture_maxbytes: z.number(),
stderr_events_enabled: z.boolean(), stderr_events_enabled: z.boolean(),
stderr_logfile: z.string(), stderr_logfile: z.string(),
stderr_logfile_backups: z.number(), stderr_logfile_backups: z.number(),
stderr_logfile_maxbytes: z.number(), stderr_logfile_maxbytes: z.number(),
stderr_syslog: z.boolean(),
stopsignal: z.number(), // Signal number (e.g., 15 for SIGTERM)
stopwaitsecs: z.number(),
stdout_capture_maxbytes: z.number(), stdout_capture_maxbytes: z.number(),
stdout_events_enabled: z.boolean(), stdout_events_enabled: z.boolean(),
stdout_logfile: z.string(), stdout_logfile: z.string(),
stdout_logfile_backups: z.number(), stdout_logfile_backups: z.number(),
stdout_logfile_maxbytes: z.number(), stdout_logfile_maxbytes: z.number(),
stopsignal: z.string(), stdout_syslog: z.boolean(),
stopwaitsecs: z.number(), uid: z.string(), // Username string
priority: z.number(),
startretries: z.number(),
startsecs: z.number(),
process_name: z.string(),
numprocs: z.number(),
numprocs_start: z.number(),
uid: z.union([z.number(), z.null()]),
username: z.union([z.string(), z.null()]),
inuse: z.boolean(),
}); });
export const ReloadConfigResultSchema = z.object({ export const ReloadConfigResultSchema = z.object({
added: z.array(z.array(z.string())), added: z.array(z.array(z.string())).optional().default([]),
changed: z.array(z.array(z.string())), changed: z.array(z.array(z.string())).optional().default([]),
removed: z.array(z.array(z.string())), removed: z.array(z.array(z.string())).optional().default([]),
}); });
// TypeScript Types // TypeScript Types

53
lib/utils/chartColors.ts Normal file
View File

@@ -0,0 +1,53 @@
/**
* Chart color utilities for Recharts compatibility
*
* Recharts doesn't properly parse CSS custom properties or OKLCH colors,
* so we provide static hex colors that work reliably across themes.
*/
export const chartColors = {
primary: '#3b82f6', // Blue
success: '#22c55e', // Green
warning: '#eab308', // Yellow
destructive: '#ef4444', // Red
accent: '#06b6d4', // Cyan
muted: '#9ca3af', // Gray
running: '#22c55e', // Same as success
stopped: '#6b7280', // Darker gray
fatal: '#ef4444', // Same as destructive
starting: '#eab308', // Same as warning
backoff: '#f97316', // Orange
stopping: '#fb923c', // Light orange
exited: '#9ca3af', // Same as muted
unknown: '#64748b', // Slate
};
/**
* Get color for process state
*/
export function getStateColor(state: string): string {
const stateMap: Record<string, string> = {
running: chartColors.running,
stopped: chartColors.stopped,
fatal: chartColors.fatal,
starting: chartColors.starting,
backoff: chartColors.backoff,
stopping: chartColors.stopping,
exited: chartColors.exited,
unknown: chartColors.unknown,
};
return stateMap[state.toLowerCase()] || chartColors.muted;
}
/**
* Color palette for multiple data series
*/
export const colorPalette = [
chartColors.primary,
chartColors.success,
chartColors.warning,
chartColors.accent,
chartColors.destructive,
chartColors.muted,
];