mirror of
https://github.com/fccview/cronmaster.git
synced 2026-01-01 10:29:02 -05:00
Compare commits
5 Commits
BUG-2
...
bugfix/rep
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc5cabb342 | ||
|
|
08d37154b4 | ||
|
|
888297c56a | ||
|
|
165f625c65 | ||
|
|
40e8f44564 |
22
Dockerfile
22
Dockerfile
@@ -1,17 +1,15 @@
|
|||||||
FROM node:20-slim AS base
|
FROM node:20-slim AS base
|
||||||
|
|
||||||
# Install system utilities for system information
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
pciutils \
|
pciutils \
|
||||||
curl \
|
curl \
|
||||||
iputils-ping \
|
iputils-ping \
|
||||||
|
util-linux \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install dependencies only when needed
|
|
||||||
FROM base AS deps
|
FROM base AS deps
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install dependencies based on the preferred package manager
|
|
||||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
|
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
|
||||||
RUN \
|
RUN \
|
||||||
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
||||||
@@ -20,20 +18,15 @@ RUN \
|
|||||||
else echo "Lockfile not found." && exit 1; \
|
else echo "Lockfile not found." && exit 1; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Rebuild the source code only when needed
|
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Next.js collects completely anonymous telemetry data about general usage.
|
|
||||||
# Learn more here: https://nextjs.org/telemetry
|
|
||||||
# Uncomment the following line in case you want to disable telemetry during the build.
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
RUN yarn build
|
RUN yarn build
|
||||||
|
|
||||||
# Production image, copy all the files and run next
|
|
||||||
FROM base AS runner
|
FROM base AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -43,33 +36,20 @@ ENV NEXT_TELEMETRY_DISABLED=1
|
|||||||
RUN groupadd --system --gid 1001 nodejs
|
RUN groupadd --system --gid 1001 nodejs
|
||||||
RUN useradd --system --uid 1001 nextjs
|
RUN useradd --system --uid 1001 nextjs
|
||||||
|
|
||||||
# Create directories for mounted volumes with proper permissions
|
|
||||||
RUN mkdir -p /app/scripts /app/data /app/snippets && \
|
RUN mkdir -p /app/scripts /app/data /app/snippets && \
|
||||||
chown -R nextjs:nodejs /app/scripts /app/data /app/snippets
|
chown -R nextjs:nodejs /app/scripts /app/data /app/snippets
|
||||||
|
|
||||||
# Create cron directories that will be mounted (this is the key fix!)
|
|
||||||
RUN mkdir -p /var/spool/cron/crontabs /etc/crontab && \
|
|
||||||
chown -R root:root /var/spool/cron/crontabs /etc/crontab
|
|
||||||
|
|
||||||
# Copy public directory
|
|
||||||
COPY --from=builder /app/public ./public
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
# Copy the entire .next directory
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
|
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
|
||||||
|
|
||||||
# Copy app directory for builtin snippets and other app files
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/app ./app
|
COPY --from=builder --chown=nextjs:nodejs /app/app ./app
|
||||||
|
|
||||||
# Copy package.json and yarn.lock for yarn start
|
|
||||||
COPY --from=builder /app/package.json ./package.json
|
COPY --from=builder /app/package.json ./package.json
|
||||||
COPY --from=builder /app/yarn.lock ./yarn.lock
|
COPY --from=builder /app/yarn.lock ./yarn.lock
|
||||||
|
|
||||||
# Copy node_modules for production dependencies
|
|
||||||
COPY --from=deps --chown=nextjs:nodejs /app/node_modules ./node_modules
|
COPY --from=deps --chown=nextjs:nodejs /app/node_modules ./node_modules
|
||||||
|
|
||||||
# Don't set default user - let docker-compose decide
|
|
||||||
# USER nextjs
|
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
|
|||||||
74
README.md
74
README.md
@@ -2,6 +2,18 @@
|
|||||||
<img src="public/heading.png" width="400px">
|
<img src="public/heading.png" width="400px">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
# ATTENTION BREAKING UPDATE!!
|
||||||
|
|
||||||
|
> The latest `main` branch has completely changed the way this app used to run.
|
||||||
|
> The main reason being trying to address some security concerns and make the whole application work
|
||||||
|
> across multiple platform without too much trouble.
|
||||||
|
>
|
||||||
|
> If you came here due to this change trying to figure out why your app stopped working you have two options:
|
||||||
|
>
|
||||||
|
> 1 - Update your `docker-compose.yml` with the new one provided within this readme (or just copy [docker-compose.yml](docker-compose.yml))
|
||||||
|
>
|
||||||
|
> 2 - Keep your `docker-compose.yml` file as it is and use the legacy tag in the image `image: ghcr.io/fccview/cronmaster:legacy`. However bear in mind this will not be supported going forward, any issue regarding the legacy tag will be ignored and I will only support the main branch. Feel free to fork that specific branch in case you want to work on it yourself :)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Modern UI**: Beautiful, responsive interface with dark/light mode.
|
- **Modern UI**: Beautiful, responsive interface with dark/light mode.
|
||||||
@@ -38,45 +50,39 @@ If you find my projects helpful and want to fuel my late-night coding sessions w
|
|||||||
services:
|
services:
|
||||||
cronjob-manager:
|
cronjob-manager:
|
||||||
image: ghcr.io/fccview/cronmaster:main
|
image: ghcr.io/fccview/cronmaster:main
|
||||||
container_name: cronmaster
|
container_name: cronmaster-test
|
||||||
user: "root"
|
user: "root"
|
||||||
ports:
|
ports:
|
||||||
# Feel free to change port, 3000 is very common so I like to map it to something else
|
# Feel free to change port, 3000 is very common so I like to map it to something else
|
||||||
- "40123:3000"
|
- "40124:3000"
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- DOCKER=true
|
- DOCKER=true
|
||||||
- NEXT_PUBLIC_CLOCK_UPDATE_INTERVAL=30000
|
- NEXT_PUBLIC_CLOCK_UPDATE_INTERVAL=30000
|
||||||
- NEXT_PUBLIC_HOST_PROJECT_DIR=/path/to/cronmaster/directory
|
- NEXT_PUBLIC_HOST_PROJECT_DIR=/path/to/cronmaster/directory
|
||||||
|
# If docker struggles to find your crontab user, update this variable with it.
|
||||||
|
# Obviously replace fccview with your user - find it with: ls -asl /var/spool/cron/crontabs/
|
||||||
|
# - HOST_CRONTAB_USER=fccview
|
||||||
volumes:
|
volumes:
|
||||||
# --- CRONTAB MANAGEMENT ---
|
# Mount Docker socket to execute commands on host
|
||||||
# We're mounting /etc/crontab to /host/crontab in read-only mode.
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
# We are thenmounting /var/spool/cron/crontabs with read-write permissions to allow the application
|
|
||||||
# to manipulate the crontab file - docker does not have access to the crontab command, it's the only
|
|
||||||
# workaround I could think of.
|
|
||||||
- /var/spool/cron/crontabs:/host/cron/crontabs
|
|
||||||
- /etc/crontab:/host/crontab:ro
|
|
||||||
|
|
||||||
# --- HOST SYSTEM STATS ---
|
|
||||||
# Mounting system specific folders to their /host/ equivalent folders.
|
|
||||||
# Similar story, we don't want to override docker system folders.
|
|
||||||
# These are all mounted read-only for security.
|
|
||||||
- /proc:/host/proc:ro
|
|
||||||
- /sys:/host/sys:ro
|
|
||||||
- /etc:/host/etc:ro
|
|
||||||
- /usr:/host/usr:ro
|
|
||||||
|
|
||||||
# --- APPLICATION-SPECIFIC MOUNTS ---
|
|
||||||
# These are needed if you want to keep your data on the host machine and not wihin the docker volume.
|
# These are needed if you want to keep your data on the host machine and not wihin the docker volume.
|
||||||
# DO NOT change the location of ./scripts as all cronjobs that use custom scripts created via the app
|
# DO NOT change the location of ./scripts as all cronjobs that use custom scripts created via the app
|
||||||
# will target this foler (thanks to the NEXT_PUBLIC_HOST_PROJECT_DIR variable set above)
|
# will target this foler (thanks to the NEXT_PUBLIC_HOST_PROJECT_DIR variable set above)
|
||||||
- ./scripts:/app/scripts
|
- ./scripts:/app/scripts
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ./snippets:/app/snippets
|
- ./snippets:/app/snippets
|
||||||
|
|
||||||
|
# Use host PID namespace for host command execution
|
||||||
|
# Run in privileged mode for nsenter access
|
||||||
|
pid: "host"
|
||||||
|
privileged: true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
init: true
|
init: true
|
||||||
# Default platform is set to amd64, can be overridden by using arm64.
|
|
||||||
#platform: linux/arm64
|
# Default platform is set to amd64, uncomment to use arm64.
|
||||||
|
#platform: linux/arm64
|
||||||
```
|
```
|
||||||
|
|
||||||
### ARM64 Support
|
### ARM64 Support
|
||||||
@@ -206,6 +212,32 @@ The application uses standard cron format: `* * * * *`
|
|||||||
4. Add tests if applicable
|
4. Add tests if applicable
|
||||||
5. Submit a pull request
|
5. Submit a pull request
|
||||||
|
|
||||||
|
## Community shouts
|
||||||
|
|
||||||
|
I would like to thank the following members for raising issues and help test/debug them!
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="20%">
|
||||||
|
<a href="https://github.com/hermannx5"><img width="100" height="100" alt="hermannx5" src="https://avatars.githubusercontent.com/u/46320338?v=4&s=100"><br/>hermannx5</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="top" width="20%">
|
||||||
|
<a href="https://github.com/edersong"><img width="100" height="100" src="https://avatars.githubusercontent.com/u/64137913?v=4&s=100"><br />edersong</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="top" width="20%">
|
||||||
|
<a href="https://github.com/corasaniti"><img width="100" height="100" src="https://avatars.githubusercontent.com/u/5001932?u=2e8bc25b74eb11f7675d38c8e312374794a7b6e0&v=4&s=100"><br />corasaniti</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="top" width="20%">
|
||||||
|
<a href="https://github.com/abhisheknair"><img width="100" height="100" src="https://avatars.githubusercontent.com/u/5221047?u=313beaabbb4a8e82fe07a2523076b4dafdc0bfec&v=4&s=100"><br />abhisheknair</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="top" width="20%">
|
||||||
|
<a href="https://github.com/mariushosting"><img width="100" height="100" src="https://avatars.githubusercontent.com/u/37554361?u=9007d0600680ac2b267bde2d8c19b05c06285a34&v=4&s=100"><br />mariushosting</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the MIT License.
|
This project is licensed under the MIT License.
|
||||||
|
|||||||
@@ -1,22 +1,60 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "./ui/Card";
|
|
||||||
import { MetricCard } from "./ui/MetricCard";
|
import { MetricCard } from "./ui/MetricCard";
|
||||||
import { SystemStatus } from "./ui/SystemStatus";
|
import { SystemStatus } from "./ui/SystemStatus";
|
||||||
import { PerformanceSummary } from "./ui/PerformanceSummary";
|
import { PerformanceSummary } from "./ui/PerformanceSummary";
|
||||||
import { Sidebar } from "./ui/Sidebar";
|
import { Sidebar } from "./ui/Sidebar";
|
||||||
import {
|
import {
|
||||||
Monitor,
|
|
||||||
Globe,
|
|
||||||
Clock,
|
Clock,
|
||||||
HardDrive,
|
HardDrive,
|
||||||
Cpu,
|
Cpu,
|
||||||
Server,
|
Monitor,
|
||||||
Wifi,
|
Wifi,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { SystemInfo as SystemInfoType } from "@/app/_utils/system";
|
|
||||||
|
interface SystemInfoType {
|
||||||
|
hostname: string;
|
||||||
|
platform: string;
|
||||||
|
ip?: string;
|
||||||
|
uptime: string;
|
||||||
|
memory: {
|
||||||
|
total: string;
|
||||||
|
used: string;
|
||||||
|
free: string;
|
||||||
|
usage: number;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
cpu: {
|
||||||
|
model: string;
|
||||||
|
cores: number;
|
||||||
|
usage: number;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
gpu: {
|
||||||
|
model: string;
|
||||||
|
memory?: string;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
network?: {
|
||||||
|
speed: string;
|
||||||
|
latency: number;
|
||||||
|
downloadSpeed: number;
|
||||||
|
uploadSpeed: number;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
disk: {
|
||||||
|
total: string;
|
||||||
|
used: string;
|
||||||
|
free: string;
|
||||||
|
usage: number;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
systemStatus: {
|
||||||
|
overall: string;
|
||||||
|
details: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { fetchSystemInfo } from "@/app/_server/actions/cronjobs";
|
|
||||||
|
|
||||||
interface SystemInfoCardProps {
|
interface SystemInfoCardProps {
|
||||||
systemInfo: SystemInfoType;
|
systemInfo: SystemInfoType;
|
||||||
@@ -30,10 +68,16 @@ export function SystemInfoCard({
|
|||||||
useState<SystemInfoType>(initialSystemInfo);
|
useState<SystemInfoType>(initialSystemInfo);
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const updateSystemInfo = async () => {
|
const updateSystemInfo = async () => {
|
||||||
try {
|
try {
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
const freshData = await fetchSystemInfo();
|
const response = await fetch('/api/system-stats');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch system stats');
|
||||||
|
}
|
||||||
|
const freshData = await response.json();
|
||||||
setSystemInfo(freshData);
|
setSystemInfo(freshData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update system info:", error);
|
console.error("Failed to update system info:", error);
|
||||||
@@ -47,49 +91,39 @@ export function SystemInfoCard({
|
|||||||
setCurrentTime(new Date().toLocaleTimeString());
|
setCurrentTime(new Date().toLocaleTimeString());
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateStats = () => {
|
|
||||||
updateSystemInfo();
|
|
||||||
};
|
|
||||||
|
|
||||||
updateTime();
|
updateTime();
|
||||||
updateStats();
|
updateSystemInfo();
|
||||||
|
|
||||||
const updateInterval = parseInt(
|
const updateInterval = parseInt(
|
||||||
process.env.NEXT_PUBLIC_CLOCK_UPDATE_INTERVAL || "30000"
|
process.env.NEXT_PUBLIC_CLOCK_UPDATE_INTERVAL || "30000"
|
||||||
);
|
);
|
||||||
const interval = setInterval(() => {
|
|
||||||
updateTime();
|
|
||||||
updateStats();
|
|
||||||
}, updateInterval);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
let mounted = true;
|
||||||
|
|
||||||
|
const doUpdate = () => {
|
||||||
|
if (!mounted) return;
|
||||||
|
updateTime();
|
||||||
|
updateSystemInfo().finally(() => {
|
||||||
|
if (mounted) {
|
||||||
|
setTimeout(doUpdate, updateInterval);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(doUpdate, updateInterval);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const quickStats = {
|
const quickStats = {
|
||||||
cpu: systemInfo.cpu.usage,
|
cpu: systemInfo.cpu.usage,
|
||||||
memory: systemInfo.memory.usage,
|
memory: systemInfo.memory.usage,
|
||||||
network: `${systemInfo.network.latency}ms`,
|
network: systemInfo.network ? `${systemInfo.network.latency}ms` : "N/A",
|
||||||
};
|
};
|
||||||
|
|
||||||
const basicInfoItems = [
|
const basicInfoItems = [
|
||||||
{
|
|
||||||
icon: Monitor,
|
|
||||||
label: "Operating System",
|
|
||||||
value: systemInfo.platform,
|
|
||||||
color: "text-blue-500",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Server,
|
|
||||||
label: "Hostname",
|
|
||||||
value: systemInfo.hostname,
|
|
||||||
color: "text-green-500",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Globe,
|
|
||||||
label: "IP Address",
|
|
||||||
value: systemInfo.ip,
|
|
||||||
color: "text-purple-500",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
label: "Uptime",
|
label: "Uptime",
|
||||||
@@ -129,14 +163,14 @@ export function SystemInfoCard({
|
|||||||
status: systemInfo.gpu.status,
|
status: systemInfo.gpu.status,
|
||||||
color: "text-indigo-500",
|
color: "text-indigo-500",
|
||||||
},
|
},
|
||||||
{
|
...(systemInfo.network ? [{
|
||||||
icon: Wifi,
|
icon: Wifi,
|
||||||
label: "Network",
|
label: "Network",
|
||||||
value: `${systemInfo.network.latency}ms`,
|
value: `${systemInfo.network.latency}ms`,
|
||||||
detail: `${systemInfo.network.latency}ms latency • ${systemInfo.network.speed}`,
|
detail: `${systemInfo.network.latency}ms latency • ${systemInfo.network.speed}`,
|
||||||
status: systemInfo.network.status,
|
status: systemInfo.network.status,
|
||||||
color: "text-teal-500",
|
color: "text-teal-500",
|
||||||
},
|
}] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const performanceMetrics = [
|
const performanceMetrics = [
|
||||||
@@ -150,11 +184,11 @@ export function SystemInfoCard({
|
|||||||
value: `${systemInfo.memory.usage}%`,
|
value: `${systemInfo.memory.usage}%`,
|
||||||
status: systemInfo.memory.status,
|
status: systemInfo.memory.status,
|
||||||
},
|
},
|
||||||
{
|
...(systemInfo.network ? [{
|
||||||
label: "Network Latency",
|
label: "Network Latency",
|
||||||
value: `${systemInfo.network.latency}ms`,
|
value: `${systemInfo.network.latency}ms`,
|
||||||
status: systemInfo.network.status,
|
status: systemInfo.network.status,
|
||||||
},
|
}] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -216,7 +250,7 @@ export function SystemInfoCard({
|
|||||||
💡 Stats update every{" "}
|
💡 Stats update every{" "}
|
||||||
{Math.round(
|
{Math.round(
|
||||||
parseInt(process.env.NEXT_PUBLIC_CLOCK_UPDATE_INTERVAL || "30000") /
|
parseInt(process.env.NEXT_PUBLIC_CLOCK_UPDATE_INTERVAL || "30000") /
|
||||||
1000
|
1000
|
||||||
)}
|
)}
|
||||||
s • Network speed estimated from latency
|
s • Network speed estimated from latency
|
||||||
{isUpdating && (
|
{isUpdating && (
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import {
|
|||||||
addCronJob,
|
addCronJob,
|
||||||
deleteCronJob,
|
deleteCronJob,
|
||||||
updateCronJob,
|
updateCronJob,
|
||||||
getSystemInfo,
|
|
||||||
type CronJob,
|
type CronJob,
|
||||||
type SystemInfo,
|
|
||||||
} from "@/app/_utils/system";
|
} from "@/app/_utils/system";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { getScriptPath } from "@/app/_utils/scripts";
|
import { getScriptPath } from "@/app/_utils/scripts";
|
||||||
@@ -21,47 +19,7 @@ export async function fetchCronJobs(): Promise<CronJob[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchSystemInfo(): Promise<SystemInfo> {
|
|
||||||
try {
|
|
||||||
return await getSystemInfo();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching system info:", error);
|
|
||||||
return {
|
|
||||||
hostname: "Unknown",
|
|
||||||
platform: "Unknown",
|
|
||||||
ip: "Unknown",
|
|
||||||
uptime: "Unknown",
|
|
||||||
memory: {
|
|
||||||
total: "Unknown",
|
|
||||||
used: "Unknown",
|
|
||||||
free: "Unknown",
|
|
||||||
usage: 0,
|
|
||||||
status: "Unknown",
|
|
||||||
},
|
|
||||||
cpu: {
|
|
||||||
model: "Unknown",
|
|
||||||
cores: 0,
|
|
||||||
usage: 0,
|
|
||||||
status: "Unknown",
|
|
||||||
},
|
|
||||||
gpu: {
|
|
||||||
model: "Unknown",
|
|
||||||
status: "Unknown",
|
|
||||||
},
|
|
||||||
network: {
|
|
||||||
latency: 0,
|
|
||||||
speed: "Unknown",
|
|
||||||
downloadSpeed: 0,
|
|
||||||
uploadSpeed: 0,
|
|
||||||
status: "Unknown",
|
|
||||||
},
|
|
||||||
systemStatus: {
|
|
||||||
overall: "Unknown",
|
|
||||||
details: "Unable to retrieve system information",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createCronJob(
|
export async function createCronJob(
|
||||||
formData: FormData
|
formData: FormData
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
export { getSystemInfo, type SystemInfo } from "./system/info";
|
|
||||||
export {
|
export {
|
||||||
getCronJobs,
|
getCronJobs,
|
||||||
addCronJob,
|
addCronJob,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { exec } from "child_process";
|
import { exec } from "child_process";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
import { isDocker, readCronFilesDocker, writeCronFilesDocker } from "./docker";
|
import { readHostCrontab, writeHostCrontab } from "./hostCrontab";
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
@@ -12,6 +12,8 @@ export interface CronJob {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function readCronFiles(): Promise<string> {
|
async function readCronFiles(): Promise<string> {
|
||||||
|
const isDocker = process.env.DOCKER === "true";
|
||||||
|
|
||||||
if (!isDocker) {
|
if (!isDocker) {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execAsync('crontab -l 2>/dev/null || echo ""');
|
const { stdout } = await execAsync('crontab -l 2>/dev/null || echo ""');
|
||||||
@@ -22,10 +24,12 @@ async function readCronFiles(): Promise<string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await readCronFilesDocker();
|
return await readHostCrontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeCronFiles(content: string): Promise<boolean> {
|
async function writeCronFiles(content: string): Promise<boolean> {
|
||||||
|
const isDocker = process.env.DOCKER === "true";
|
||||||
|
|
||||||
if (!isDocker) {
|
if (!isDocker) {
|
||||||
try {
|
try {
|
||||||
await execAsync('echo "' + content + '" | crontab -');
|
await execAsync('echo "' + content + '" | crontab -');
|
||||||
@@ -36,7 +40,7 @@ async function writeCronFiles(content: string): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await writeCronFilesDocker(content);
|
return await writeHostCrontab(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCronJobs(): Promise<CronJob[]> {
|
export async function getCronJobs(): Promise<CronJob[]> {
|
||||||
@@ -105,52 +109,19 @@ export async function addCronJob(
|
|||||||
try {
|
try {
|
||||||
const cronContent = await readCronFiles();
|
const cronContent = await readCronFiles();
|
||||||
|
|
||||||
if (isDocker) {
|
const newEntry = comment
|
||||||
const lines = cronContent.split("\n");
|
? `# ${comment}\n${schedule} ${command}`
|
||||||
let hasUserSection = false;
|
: `${schedule} ${command}`;
|
||||||
let userSectionEnd = -1;
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
let newCron;
|
||||||
const line = lines[i];
|
if (cronContent.trim() === "") {
|
||||||
if (line.startsWith("# User: ")) {
|
newCron = newEntry;
|
||||||
hasUserSection = true;
|
|
||||||
userSectionEnd = i;
|
|
||||||
for (let j = i + 1; j < lines.length; j++) {
|
|
||||||
if (lines[j].startsWith("# User: ") || lines[j].startsWith("# System Crontab")) {
|
|
||||||
userSectionEnd = j - 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
userSectionEnd = j;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasUserSection) {
|
|
||||||
const newEntry = comment
|
|
||||||
? `# User: root\n# ${comment}\n${schedule} ${command}`
|
|
||||||
: `# User: root\n${schedule} ${command}`;
|
|
||||||
const newCron = cronContent + "\n" + newEntry;
|
|
||||||
await writeCronFiles(newCron);
|
|
||||||
} else {
|
|
||||||
const newEntry = comment
|
|
||||||
? `# ${comment}\n${schedule} ${command}`
|
|
||||||
: `${schedule} ${command}`;
|
|
||||||
|
|
||||||
const beforeSection = lines.slice(0, userSectionEnd + 1).join("\n");
|
|
||||||
const afterSection = lines.slice(userSectionEnd + 1).join("\n");
|
|
||||||
const newCron = beforeSection + "\n" + newEntry + "\n" + afterSection;
|
|
||||||
await writeCronFiles(newCron);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const newEntry = comment
|
const existingContent = cronContent.endsWith('\n') ? cronContent : cronContent + '\n';
|
||||||
? `# ${comment}\n${schedule} ${command}`
|
newCron = existingContent + newEntry;
|
||||||
: `${schedule} ${command}`;
|
|
||||||
const newCron = cronContent + "\n" + newEntry;
|
|
||||||
await writeCronFiles(newCron);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return await writeCronFiles(newCron);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error adding cron job:", error);
|
console.error("Error adding cron job:", error);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,432 +0,0 @@
|
|||||||
import { exec } from "child_process";
|
|
||||||
import { promisify } from "util";
|
|
||||||
import fs from "fs/promises";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
|
||||||
|
|
||||||
const isDocker = process.env.DOCKER === "true";
|
|
||||||
|
|
||||||
export async function getHostInfo(): Promise<{ hostname: string; ip: string; uptime: string }> {
|
|
||||||
if (isDocker) {
|
|
||||||
try {
|
|
||||||
const hostname = await fs.readFile("/host/etc/hostname", "utf-8");
|
|
||||||
|
|
||||||
let ipOutput = "";
|
|
||||||
try {
|
|
||||||
const { stdout } = await execAsync("hostname -I | awk '{print $1}'");
|
|
||||||
ipOutput = stdout;
|
|
||||||
} catch (error) {
|
|
||||||
try {
|
|
||||||
const fibInfo = await fs.readFile("/host/proc/net/fib_trie", "utf-8");
|
|
||||||
const lines = fibInfo.split("\n");
|
|
||||||
for (const line of lines) {
|
|
||||||
const match = line.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/);
|
|
||||||
if (match && !match[1].startsWith("127.") && !match[1].startsWith("0.")) {
|
|
||||||
ipOutput = match[1];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (fibError) {
|
|
||||||
console.error("Could not determine IP address:", fibError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const uptimeContent = await fs.readFile("/host/proc/uptime", "utf-8");
|
|
||||||
const uptimeSeconds = parseFloat(uptimeContent.split(" ")[0]);
|
|
||||||
const uptime = formatUptime(uptimeSeconds);
|
|
||||||
|
|
||||||
return {
|
|
||||||
hostname: hostname.trim(),
|
|
||||||
ip: ipOutput.trim(),
|
|
||||||
uptime: uptime
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error reading host info:", error);
|
|
||||||
const { stdout: hostname } = await execAsync("hostname");
|
|
||||||
const { stdout: ip } = await execAsync("hostname -I | awk '{print $1}'");
|
|
||||||
const { stdout: uptime } = await execAsync("uptime");
|
|
||||||
return {
|
|
||||||
hostname: hostname.trim(),
|
|
||||||
ip: ip.trim(),
|
|
||||||
uptime: parseUptimeOutput(uptime)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const { stdout: hostname } = await execAsync("hostname");
|
|
||||||
const { stdout: ip } = await execAsync("hostname -I | awk '{print $1}'");
|
|
||||||
const { stdout: uptime } = await execAsync("uptime");
|
|
||||||
return {
|
|
||||||
hostname: hostname.trim(),
|
|
||||||
ip: ip.trim(),
|
|
||||||
uptime: parseUptimeOutput(uptime)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatUptime(seconds: number): string {
|
|
||||||
const days = Math.floor(seconds / 86400);
|
|
||||||
const hours = Math.floor((seconds % 86400) / 3600);
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
|
||||||
|
|
||||||
if (days > 0) {
|
|
||||||
return `${days} days, ${hours} hours`;
|
|
||||||
} else if (hours > 0) {
|
|
||||||
return `${hours} hours, ${minutes} minutes`;
|
|
||||||
} else {
|
|
||||||
return `${minutes} minutes`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseUptimeOutput(uptimeOutput: string): string {
|
|
||||||
const cleanOutput = uptimeOutput.trim();
|
|
||||||
|
|
||||||
const match = cleanOutput.match(/up\s+([^,]+)/);
|
|
||||||
if (!match) {
|
|
||||||
return "Unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
const timePart = match[1].trim();
|
|
||||||
|
|
||||||
const timeMatch = timePart.match(/^(\d+):(\d+)$/);
|
|
||||||
if (timeMatch) {
|
|
||||||
const hours = parseInt(timeMatch[1]);
|
|
||||||
const minutes = parseInt(timeMatch[2]);
|
|
||||||
|
|
||||||
if (hours > 24) {
|
|
||||||
const days = Math.floor(hours / 24);
|
|
||||||
const remainingHours = hours % 24;
|
|
||||||
if (remainingHours > 0) {
|
|
||||||
return `${days} days, ${remainingHours} hours`;
|
|
||||||
} else {
|
|
||||||
return `${days} days`;
|
|
||||||
}
|
|
||||||
} else if (hours > 0) {
|
|
||||||
return `${hours} hours, ${minutes} minutes`;
|
|
||||||
} else {
|
|
||||||
return `${minutes} minutes`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return timePart;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSystemPath(originalPath: string): string {
|
|
||||||
if (isDocker) {
|
|
||||||
switch (originalPath) {
|
|
||||||
case "/etc/os-release":
|
|
||||||
return "/host/etc/os-release";
|
|
||||||
case "/proc/stat":
|
|
||||||
return "/host/proc/stat";
|
|
||||||
default:
|
|
||||||
return originalPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return originalPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readCronFilesDocker(): Promise<string> {
|
|
||||||
try {
|
|
||||||
const crontabDir = "/host/cron/crontabs";
|
|
||||||
const files = await fs.readdir(crontabDir);
|
|
||||||
let allCronContent = "";
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
if (file === "." || file === "..") continue;
|
|
||||||
|
|
||||||
if (file.includes("docker") || file.includes("container") || file === "root") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const filePath = path.join(crontabDir, file);
|
|
||||||
const content = await fs.readFile(filePath, "utf-8");
|
|
||||||
allCronContent += `# User: ${file}\n`;
|
|
||||||
allCronContent += content;
|
|
||||||
allCronContent += "\n\n";
|
|
||||||
} catch (fileError) {
|
|
||||||
console.error(`Error reading crontab for user ${file}:`, fileError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return allCronContent;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error reading host crontab files:", error);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function writeCronFilesDocker(cronContent: string): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const lines = cronContent.split("\n");
|
|
||||||
const userCrontabs: { [key: string]: string[] } = {};
|
|
||||||
let currentUser = "root";
|
|
||||||
let currentContent: string[] = [];
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.startsWith("# User:")) {
|
|
||||||
if (currentUser && currentContent.length > 0) {
|
|
||||||
userCrontabs[currentUser] = [...currentContent];
|
|
||||||
}
|
|
||||||
currentUser = line.substring(8).trim();
|
|
||||||
currentContent = [];
|
|
||||||
} else if (line.startsWith("# System Crontab")) {
|
|
||||||
if (currentUser && currentContent.length > 0) {
|
|
||||||
userCrontabs[currentUser] = [...currentContent];
|
|
||||||
}
|
|
||||||
currentUser = "system";
|
|
||||||
currentContent = [];
|
|
||||||
} else if (currentUser && line.trim()) {
|
|
||||||
currentContent.push(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentUser && currentContent.length > 0) {
|
|
||||||
userCrontabs[currentUser] = [...currentContent];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [username, cronJobs] of Object.entries(userCrontabs)) {
|
|
||||||
if (username === "system") {
|
|
||||||
const systemContent = cronJobs.join("\n") + "\n";
|
|
||||||
try {
|
|
||||||
await fs.writeFile("/host/crontab", systemContent);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to write system crontab:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const userCrontabPath = `/host/cron/crontabs/${username}`;
|
|
||||||
const userContent = cronJobs.join("\n") + "\n";
|
|
||||||
try {
|
|
||||||
await execAsync(`chown root:root ${userCrontabPath}`);
|
|
||||||
await execAsync(`chmod 666 ${userCrontabPath}`);
|
|
||||||
await fs.writeFile(userCrontabPath, userContent);
|
|
||||||
await execAsync(`chown 1000:105 ${userCrontabPath}`);
|
|
||||||
await execAsync(`chmod 600 ${userCrontabPath}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to write crontab for user ${username}:`, error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error writing cron files:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getMemoryInfoDocker() {
|
|
||||||
try {
|
|
||||||
const meminfo = await fs.readFile("/host/proc/meminfo", "utf-8");
|
|
||||||
const lines = meminfo.split("\n");
|
|
||||||
|
|
||||||
let total = 0;
|
|
||||||
let available = 0;
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.startsWith("MemTotal:")) {
|
|
||||||
total = parseInt(line.split(/\s+/)[1]) * 1024;
|
|
||||||
} else if (line.startsWith("MemAvailable:")) {
|
|
||||||
available = parseInt(line.split(/\s+/)[1]) * 1024;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (total === 0) {
|
|
||||||
throw new Error("Could not read memory info from /proc/meminfo");
|
|
||||||
}
|
|
||||||
|
|
||||||
const actualUsed = total - available;
|
|
||||||
const usage = (actualUsed / total) * 100;
|
|
||||||
|
|
||||||
const formatBytes = (bytes: number) => {
|
|
||||||
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
|
||||||
if (bytes === 0) return "0 B";
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
||||||
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
let status = "Optimal";
|
|
||||||
if (usage > 90) status = "Critical";
|
|
||||||
else if (usage > 80) status = "High";
|
|
||||||
else if (usage > 70) status = "Moderate";
|
|
||||||
|
|
||||||
return {
|
|
||||||
total: formatBytes(total),
|
|
||||||
used: formatBytes(actualUsed),
|
|
||||||
free: formatBytes(available),
|
|
||||||
usage: Math.round(usage),
|
|
||||||
status,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error reading host memory info:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getCPUInfoDocker() {
|
|
||||||
try {
|
|
||||||
const cpuinfo = await fs.readFile("/host/proc/cpuinfo", "utf-8");
|
|
||||||
const lines = cpuinfo.split("\n");
|
|
||||||
|
|
||||||
let model = "Unknown";
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.startsWith("model name")) {
|
|
||||||
model = line.split(":")[1]?.trim() || "Unknown";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cores = lines.filter(line => line.startsWith("processor")).length;
|
|
||||||
|
|
||||||
return { model, cores };
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error reading host CPU info:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getGPUInfoDocker() {
|
|
||||||
try {
|
|
||||||
let gpuInfo = "Unknown GPU";
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { stdout } = await execAsync("lspci | grep -i vga");
|
|
||||||
const gpuLines = stdout.split("\n").filter((line) => line.trim());
|
|
||||||
if (gpuLines.length > 0) {
|
|
||||||
gpuInfo = gpuLines[0].split(":")[2]?.trim() || "Unknown GPU";
|
|
||||||
}
|
|
||||||
} catch (lspciError) {
|
|
||||||
try {
|
|
||||||
const { stdout } = await execAsync("find /host/sys/devices -name 'card*' -type d | head -1");
|
|
||||||
if (stdout.trim()) {
|
|
||||||
const cardPath = stdout.trim();
|
|
||||||
const { stdout: nameOutput } = await execAsync(`cat ${cardPath}/name 2>/dev/null || echo "Unknown GPU"`);
|
|
||||||
gpuInfo = nameOutput.trim();
|
|
||||||
}
|
|
||||||
} catch (sysfsError) {
|
|
||||||
try {
|
|
||||||
const pciInfo = await fs.readFile("/host/proc/bus/pci/devices", "utf-8");
|
|
||||||
const lines = pciInfo.split("\n");
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.includes("0300")) {
|
|
||||||
const parts = line.split(/\s+/);
|
|
||||||
if (parts.length > 1) {
|
|
||||||
gpuInfo = `PCI Device ${parts[0]}`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (pciError) {
|
|
||||||
console.log("Could not read GPU info from PCI devices:", pciError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gpuInfo === "Unknown GPU") {
|
|
||||||
return {
|
|
||||||
model: "No dedicated GPU detected",
|
|
||||||
status: "Integrated",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
model: gpuInfo,
|
|
||||||
status: "Available",
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
model: "Unknown",
|
|
||||||
status: "Unknown",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getNetworkInfoDocker() {
|
|
||||||
try {
|
|
||||||
let latency = 0;
|
|
||||||
let pingOutput = "";
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { stdout } = await execAsync(
|
|
||||||
'ping -c 1 -W 1 8.8.8.8 2>/dev/null || echo "timeout"'
|
|
||||||
);
|
|
||||||
pingOutput = stdout;
|
|
||||||
|
|
||||||
const lines = pingOutput.split("\n");
|
|
||||||
const timeLine = lines.find((line) => line.includes("time="));
|
|
||||||
|
|
||||||
if (timeLine) {
|
|
||||||
const match = timeLine.match(/time=(\d+\.?\d*)/);
|
|
||||||
if (match) {
|
|
||||||
latency = parseFloat(match[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (pingError) {
|
|
||||||
console.log("Ping failed:", pingError);
|
|
||||||
pingOutput = "timeout";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pingOutput.includes("timeout") || pingOutput.includes("100% packet loss")) {
|
|
||||||
return {
|
|
||||||
speed: "No connection",
|
|
||||||
latency: 0,
|
|
||||||
downloadSpeed: 0,
|
|
||||||
uploadSpeed: 0,
|
|
||||||
status: "Offline",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (latency > 0) {
|
|
||||||
let downloadSpeed = 0;
|
|
||||||
let speed = "Unknown";
|
|
||||||
let status = "Stable";
|
|
||||||
|
|
||||||
if (latency < 10) {
|
|
||||||
downloadSpeed = 50;
|
|
||||||
speed = "Excellent";
|
|
||||||
status = "Optimal";
|
|
||||||
} else if (latency < 30) {
|
|
||||||
downloadSpeed = 25;
|
|
||||||
speed = "Good";
|
|
||||||
status = "Stable";
|
|
||||||
} else if (latency < 100) {
|
|
||||||
downloadSpeed = 10;
|
|
||||||
speed = "Fair";
|
|
||||||
status = "Slow";
|
|
||||||
} else {
|
|
||||||
downloadSpeed = 2;
|
|
||||||
speed = "Poor";
|
|
||||||
status = "Poor";
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
speed,
|
|
||||||
latency: Math.round(latency),
|
|
||||||
downloadSpeed: Math.round(downloadSpeed * 100) / 100,
|
|
||||||
uploadSpeed: 0,
|
|
||||||
status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
speed: "Unknown",
|
|
||||||
latency: 0,
|
|
||||||
downloadSpeed: 0,
|
|
||||||
uploadSpeed: 0,
|
|
||||||
status: "Unknown",
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Network error:", error);
|
|
||||||
return {
|
|
||||||
speed: "Unknown",
|
|
||||||
latency: 0,
|
|
||||||
downloadSpeed: 0,
|
|
||||||
uploadSpeed: 0,
|
|
||||||
status: "Unknown",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { isDocker };
|
|
||||||
83
app/_utils/system/hostCrontab.ts
Normal file
83
app/_utils/system/hostCrontab.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { exec } from "child_process";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
async function execHostCrontab(command: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync(
|
||||||
|
`nsenter -t 1 -m -u -i -n -p sh -c "${command}"`
|
||||||
|
);
|
||||||
|
return stdout;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error executing host crontab command:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTargetUser(): Promise<string> {
|
||||||
|
try {
|
||||||
|
if (process.env.HOST_CRONTAB_USER) {
|
||||||
|
return process.env.HOST_CRONTAB_USER;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { stdout } = await execAsync('stat -c "%U" /var/run/docker.sock');
|
||||||
|
const dockerSocketOwner = stdout.trim();
|
||||||
|
|
||||||
|
if (dockerSocketOwner === 'root') {
|
||||||
|
try {
|
||||||
|
const projectDir = process.env.NEXT_PUBLIC_HOST_PROJECT_DIR;
|
||||||
|
if (projectDir) {
|
||||||
|
const dirOwner = await execHostCrontab(`stat -c "%U" "${projectDir}"`);
|
||||||
|
return dirOwner.trim();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Could not detect user from project directory:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const users = await execHostCrontab('getent passwd | grep ":/home/" | head -1 | cut -d: -f1');
|
||||||
|
const firstUser = users.trim();
|
||||||
|
if (firstUser) {
|
||||||
|
return firstUser;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Could not detect user from passwd:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'root';
|
||||||
|
}
|
||||||
|
|
||||||
|
return dockerSocketOwner;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error detecting target user:", error);
|
||||||
|
return 'root';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readHostCrontab(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const user = await getTargetUser();
|
||||||
|
return await execHostCrontab(`crontab -l -u ${user} 2>/dev/null || echo ""`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error reading host crontab:", error);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writeHostCrontab(content: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const user = await getTargetUser();
|
||||||
|
let finalContent = content;
|
||||||
|
if (!finalContent.endsWith('\n')) {
|
||||||
|
finalContent += '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
const base64Content = Buffer.from(finalContent).toString('base64');
|
||||||
|
await execHostCrontab(`echo '${base64Content}' | base64 -d | crontab -u ${user} -`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error writing host crontab:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,480 +0,0 @@
|
|||||||
import { exec } from "child_process";
|
|
||||||
import { promisify } from "util";
|
|
||||||
import { readFileSync } from "fs";
|
|
||||||
import { isDocker, getSystemPath, getMemoryInfoDocker, getCPUInfoDocker, getGPUInfoDocker, getNetworkInfoDocker, getHostInfo } from "./docker";
|
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
|
||||||
|
|
||||||
function getCachedData<T>(key: string): T | null {
|
|
||||||
if (typeof window === 'undefined') return null;
|
|
||||||
try {
|
|
||||||
const cached = localStorage.getItem(key);
|
|
||||||
if (!cached) return null;
|
|
||||||
const { data, timestamp } = JSON.parse(cached);
|
|
||||||
if (Date.now() - timestamp > 24 * 60 * 60 * 1000) {
|
|
||||||
localStorage.removeItem(key);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCachedData<T>(key: string, data: T): void {
|
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
try {
|
|
||||||
localStorage.setItem(key, JSON.stringify({
|
|
||||||
data,
|
|
||||||
timestamp: Date.now()
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error setting cached data:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SystemInfo {
|
|
||||||
platform: string;
|
|
||||||
hostname: string;
|
|
||||||
ip: string;
|
|
||||||
uptime: string;
|
|
||||||
memory: {
|
|
||||||
total: string;
|
|
||||||
used: string;
|
|
||||||
free: string;
|
|
||||||
usage: number;
|
|
||||||
status: string;
|
|
||||||
};
|
|
||||||
cpu: {
|
|
||||||
model: string;
|
|
||||||
cores: number;
|
|
||||||
usage: number;
|
|
||||||
status: string;
|
|
||||||
};
|
|
||||||
gpu: {
|
|
||||||
model: string;
|
|
||||||
memory?: string;
|
|
||||||
status: string;
|
|
||||||
};
|
|
||||||
network: {
|
|
||||||
speed: string;
|
|
||||||
latency: number;
|
|
||||||
downloadSpeed: number;
|
|
||||||
uploadSpeed: number;
|
|
||||||
status: string;
|
|
||||||
};
|
|
||||||
systemStatus: {
|
|
||||||
overall: string;
|
|
||||||
details: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getOSInfo(): Promise<string> {
|
|
||||||
try {
|
|
||||||
const cachedOS = getCachedData<string>('os_info');
|
|
||||||
if (cachedOS) {
|
|
||||||
return cachedOS;
|
|
||||||
}
|
|
||||||
|
|
||||||
const osReleasePath = getSystemPath("/etc/os-release");
|
|
||||||
const osRelease = readFileSync(osReleasePath, "utf8");
|
|
||||||
const lines = osRelease.split("\n");
|
|
||||||
let name = "";
|
|
||||||
let version = "";
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.startsWith("PRETTY_NAME=")) {
|
|
||||||
const osInfo = line.split("=")[1].replace(/"/g, "");
|
|
||||||
setCachedData('os_info', osInfo);
|
|
||||||
return osInfo;
|
|
||||||
}
|
|
||||||
if (line.startsWith("NAME=") && !name) {
|
|
||||||
name = line.split("=")[1].replace(/"/g, "");
|
|
||||||
}
|
|
||||||
if (line.startsWith("VERSION=") && !version) {
|
|
||||||
version = line.split("=")[1].replace(/"/g, "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name && version) {
|
|
||||||
const osInfo = `${name} ${version}`;
|
|
||||||
setCachedData('os_info', osInfo);
|
|
||||||
return osInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { stdout } = await execAsync("uname -a");
|
|
||||||
const osInfo = stdout.trim();
|
|
||||||
setCachedData('os_info', osInfo);
|
|
||||||
return osInfo;
|
|
||||||
} catch (error) {
|
|
||||||
const { stdout } = await execAsync("uname -s -r");
|
|
||||||
const osInfo = stdout.trim();
|
|
||||||
setCachedData('os_info', osInfo);
|
|
||||||
return osInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMemoryInfo() {
|
|
||||||
try {
|
|
||||||
const memPath = isDocker ? "/host/proc/meminfo" : null;
|
|
||||||
|
|
||||||
if (isDocker && memPath) {
|
|
||||||
try {
|
|
||||||
return await getMemoryInfoDocker();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error reading host memory info:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { stdout } = await execAsync("free -b");
|
|
||||||
const lines = stdout.split("\n");
|
|
||||||
|
|
||||||
const memLine = lines.find((line) => line.trim().startsWith("Mem:"));
|
|
||||||
if (!memLine) {
|
|
||||||
throw new Error("Could not find memory line in free output");
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = memLine.trim().split(/\s+/);
|
|
||||||
|
|
||||||
const total = parseInt(parts[1]);
|
|
||||||
const available = parseInt(parts[6]);
|
|
||||||
|
|
||||||
const actualUsed = total - available;
|
|
||||||
const usage = (actualUsed / total) * 100;
|
|
||||||
|
|
||||||
const formatBytes = (bytes: number) => {
|
|
||||||
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
|
||||||
if (bytes === 0) return "0 B";
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
||||||
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
let status = "Optimal";
|
|
||||||
if (usage > 90) status = "Critical";
|
|
||||||
else if (usage > 80) status = "High";
|
|
||||||
else if (usage > 70) status = "Moderate";
|
|
||||||
|
|
||||||
return {
|
|
||||||
total: formatBytes(total),
|
|
||||||
used: formatBytes(actualUsed),
|
|
||||||
free: formatBytes(available),
|
|
||||||
usage: Math.round(usage),
|
|
||||||
status,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error parsing memory info:", error);
|
|
||||||
return {
|
|
||||||
total: "Unknown",
|
|
||||||
used: "Unknown",
|
|
||||||
free: "Unknown",
|
|
||||||
usage: 0,
|
|
||||||
status: "Unknown",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCPUInfo() {
|
|
||||||
try {
|
|
||||||
const cachedStatic = getCachedData<{ model: string, cores: number }>('cpu_static');
|
|
||||||
let model = "Unknown";
|
|
||||||
let cores = 0;
|
|
||||||
|
|
||||||
if (cachedStatic) {
|
|
||||||
model = cachedStatic.model;
|
|
||||||
cores = cachedStatic.cores;
|
|
||||||
} else {
|
|
||||||
if (isDocker) {
|
|
||||||
try {
|
|
||||||
const cpuInfo = await getCPUInfoDocker();
|
|
||||||
model = cpuInfo.model;
|
|
||||||
cores = cpuInfo.cores;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error reading host CPU info:", error);
|
|
||||||
const { stdout: modelOutput } = await execAsync(
|
|
||||||
"lscpu | grep 'Model name' | cut -f 2 -d ':'"
|
|
||||||
);
|
|
||||||
model = modelOutput.trim();
|
|
||||||
|
|
||||||
const { stdout: coresOutput } = await execAsync("nproc");
|
|
||||||
cores = parseInt(coresOutput.trim());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const { stdout: modelOutput } = await execAsync(
|
|
||||||
"lscpu | grep 'Model name' | cut -f 2 -d ':'"
|
|
||||||
);
|
|
||||||
model = modelOutput.trim();
|
|
||||||
|
|
||||||
const { stdout: coresOutput } = await execAsync("nproc");
|
|
||||||
cores = parseInt(coresOutput.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
setCachedData('cpu_static', { model, cores });
|
|
||||||
}
|
|
||||||
|
|
||||||
const statPath = getSystemPath("/proc/stat");
|
|
||||||
const stat1 = readFileSync(statPath, "utf8").split("\n")[0];
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
||||||
const stat2 = readFileSync(statPath, "utf8").split("\n")[0];
|
|
||||||
|
|
||||||
const parseCPU = (line: string) => {
|
|
||||||
const parts = line.split(/\s+/);
|
|
||||||
return {
|
|
||||||
user: parseInt(parts[1]),
|
|
||||||
nice: parseInt(parts[2]),
|
|
||||||
system: parseInt(parts[3]),
|
|
||||||
idle: parseInt(parts[4]),
|
|
||||||
iowait: parseInt(parts[5]),
|
|
||||||
irq: parseInt(parts[6]),
|
|
||||||
softirq: parseInt(parts[7]),
|
|
||||||
steal: parseInt(parts[8]),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const cpu1 = parseCPU(stat1);
|
|
||||||
const cpu2 = parseCPU(stat2);
|
|
||||||
|
|
||||||
const total1 = Object.values(cpu1).reduce((a, b) => a + b, 0);
|
|
||||||
const total2 = Object.values(cpu2).reduce((a, b) => a + b, 0);
|
|
||||||
const idle1 = cpu1.idle + cpu1.iowait;
|
|
||||||
const idle2 = cpu2.idle + cpu2.iowait;
|
|
||||||
|
|
||||||
const totalDiff = total2 - total1;
|
|
||||||
const idleDiff = idle2 - idle1;
|
|
||||||
const usage = ((totalDiff - idleDiff) / totalDiff) * 100;
|
|
||||||
|
|
||||||
let status = "Optimal";
|
|
||||||
if (usage > 90) status = "Critical";
|
|
||||||
else if (usage > 80) status = "High";
|
|
||||||
else if (usage > 70) status = "Moderate";
|
|
||||||
|
|
||||||
return {
|
|
||||||
model,
|
|
||||||
cores,
|
|
||||||
usage: Math.round(usage),
|
|
||||||
status,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
model: "Unknown",
|
|
||||||
cores: 0,
|
|
||||||
usage: 0,
|
|
||||||
status: "Unknown",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getGPUInfo() {
|
|
||||||
try {
|
|
||||||
const cachedGPU = getCachedData<{ model: string, memory?: string }>('gpu_static');
|
|
||||||
let model = "Unknown";
|
|
||||||
let memory = "";
|
|
||||||
|
|
||||||
if (cachedGPU) {
|
|
||||||
model = cachedGPU.model;
|
|
||||||
memory = cachedGPU.memory || "";
|
|
||||||
} else {
|
|
||||||
if (isDocker) {
|
|
||||||
const gpuInfo = await getGPUInfoDocker();
|
|
||||||
model = gpuInfo.model;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const { stdout } = await execAsync("lspci | grep -i vga");
|
|
||||||
const gpuLines = stdout.split("\n").filter((line) => line.trim());
|
|
||||||
if (gpuLines.length > 0) {
|
|
||||||
model = gpuLines[0].split(":")[2]?.trim() || "Unknown GPU";
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("lspci not available, using fallback methods");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { stdout: nvidiaOutput } = await execAsync(
|
|
||||||
"nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null"
|
|
||||||
);
|
|
||||||
if (nvidiaOutput.trim()) {
|
|
||||||
const memMB = parseInt(nvidiaOutput.trim());
|
|
||||||
memory = `${Math.round(memMB / 1024)} GB`;
|
|
||||||
}
|
|
||||||
} catch (e) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
setCachedData('gpu_static', { model, memory });
|
|
||||||
}
|
|
||||||
|
|
||||||
let status = "Unknown";
|
|
||||||
if (model !== "Unknown" && model !== "No dedicated GPU detected") {
|
|
||||||
status = "Available";
|
|
||||||
} else if (model === "No dedicated GPU detected") {
|
|
||||||
status = "Integrated";
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
model,
|
|
||||||
memory,
|
|
||||||
status,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
model: "Unknown",
|
|
||||||
status: "Unknown",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getNetworkInfo() {
|
|
||||||
try {
|
|
||||||
if (isDocker) {
|
|
||||||
return await getNetworkInfoDocker();
|
|
||||||
} else {
|
|
||||||
const { stdout: pingOutput } = await execAsync(
|
|
||||||
'ping -c 1 -W 1 8.8.8.8 2>/dev/null || echo "timeout"'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
pingOutput.includes("timeout") ||
|
|
||||||
pingOutput.includes("100% packet loss")
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
speed: "No connection",
|
|
||||||
latency: 0,
|
|
||||||
downloadSpeed: 0,
|
|
||||||
uploadSpeed: 0,
|
|
||||||
status: "Offline",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = pingOutput.split("\n");
|
|
||||||
const timeLine = lines.find((line) => line.includes("time="));
|
|
||||||
let latency = 0;
|
|
||||||
|
|
||||||
if (timeLine) {
|
|
||||||
const match = timeLine.match(/time=(\d+\.?\d*)/);
|
|
||||||
if (match) {
|
|
||||||
latency = parseFloat(match[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let downloadSpeed = 0;
|
|
||||||
let speed = "Unknown";
|
|
||||||
let status = "Stable";
|
|
||||||
|
|
||||||
if (latency < 10) {
|
|
||||||
downloadSpeed = 50;
|
|
||||||
speed = "Excellent";
|
|
||||||
status = "Optimal";
|
|
||||||
} else if (latency < 30) {
|
|
||||||
downloadSpeed = 25;
|
|
||||||
speed = "Good";
|
|
||||||
status = "Stable";
|
|
||||||
} else if (latency < 100) {
|
|
||||||
downloadSpeed = 10;
|
|
||||||
speed = "Fair";
|
|
||||||
status = "Slow";
|
|
||||||
} else {
|
|
||||||
downloadSpeed = 2;
|
|
||||||
speed = "Poor";
|
|
||||||
status = "Poor";
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
speed,
|
|
||||||
latency: Math.round(latency),
|
|
||||||
downloadSpeed: Math.round(downloadSpeed * 100) / 100,
|
|
||||||
uploadSpeed: 0,
|
|
||||||
status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
speed: "Unknown",
|
|
||||||
latency: 0,
|
|
||||||
downloadSpeed: 0,
|
|
||||||
uploadSpeed: 0,
|
|
||||||
status: "Unknown",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSystemStatus(memory: any, cpu: any, network: any) {
|
|
||||||
const statuses = [memory.status, cpu.status, network.status];
|
|
||||||
const criticalCount = statuses.filter((s) => s === "Critical").length;
|
|
||||||
const highCount = statuses.filter((s) => s === "High").length;
|
|
||||||
|
|
||||||
let overall = "Operational";
|
|
||||||
let details = "All systems running smoothly";
|
|
||||||
|
|
||||||
if (criticalCount > 0) {
|
|
||||||
overall = "Critical";
|
|
||||||
details = "System performance issues detected";
|
|
||||||
} else if (highCount > 0) {
|
|
||||||
overall = "Warning";
|
|
||||||
details = "Some systems showing high usage";
|
|
||||||
} else if (statuses.some((s) => s === "Moderate")) {
|
|
||||||
overall = "Stable";
|
|
||||||
details = "System performance is stable";
|
|
||||||
}
|
|
||||||
|
|
||||||
return { overall, details };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getSystemInfo(): Promise<SystemInfo> {
|
|
||||||
try {
|
|
||||||
const [
|
|
||||||
hostInfo,
|
|
||||||
platform,
|
|
||||||
memory,
|
|
||||||
cpu,
|
|
||||||
gpu,
|
|
||||||
network,
|
|
||||||
] = await Promise.all([
|
|
||||||
getHostInfo(),
|
|
||||||
getOSInfo(),
|
|
||||||
getMemoryInfo(),
|
|
||||||
getCPUInfo(),
|
|
||||||
getGPUInfo(),
|
|
||||||
getNetworkInfo(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const systemStatus = getSystemStatus(memory, cpu, network);
|
|
||||||
|
|
||||||
return {
|
|
||||||
platform,
|
|
||||||
hostname: hostInfo.hostname,
|
|
||||||
ip: hostInfo.ip || "Unknown",
|
|
||||||
uptime: hostInfo.uptime,
|
|
||||||
memory,
|
|
||||||
cpu,
|
|
||||||
gpu,
|
|
||||||
network,
|
|
||||||
systemStatus,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error getting system info:", error);
|
|
||||||
return {
|
|
||||||
platform: "Unknown",
|
|
||||||
hostname: "Unknown",
|
|
||||||
ip: "Unknown",
|
|
||||||
uptime: "Unknown",
|
|
||||||
memory: {
|
|
||||||
total: "Unknown",
|
|
||||||
used: "Unknown",
|
|
||||||
free: "Unknown",
|
|
||||||
usage: 0,
|
|
||||||
status: "Unknown",
|
|
||||||
},
|
|
||||||
cpu: { model: "Unknown", cores: 0, usage: 0, status: "Unknown" },
|
|
||||||
gpu: { model: "Unknown", status: "Unknown" },
|
|
||||||
network: {
|
|
||||||
speed: "Unknown",
|
|
||||||
latency: 0,
|
|
||||||
downloadSpeed: 0,
|
|
||||||
uploadSpeed: 0,
|
|
||||||
status: "Unknown",
|
|
||||||
},
|
|
||||||
systemStatus: {
|
|
||||||
overall: "Unknown",
|
|
||||||
details: "Unable to retrieve system information",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
169
app/api/system-stats/route.ts
Normal file
169
app/api/system-stats/route.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import * as si from 'systeminformation';
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const [
|
||||||
|
osInfo,
|
||||||
|
memInfo,
|
||||||
|
cpuInfo,
|
||||||
|
diskInfo,
|
||||||
|
loadInfo,
|
||||||
|
uptimeInfo,
|
||||||
|
networkInfo
|
||||||
|
] = await Promise.all([
|
||||||
|
si.osInfo(),
|
||||||
|
si.mem(),
|
||||||
|
si.cpu(),
|
||||||
|
si.fsSize(),
|
||||||
|
si.currentLoad(),
|
||||||
|
si.time(),
|
||||||
|
si.networkStats()
|
||||||
|
]);
|
||||||
|
|
||||||
|
const formatBytes = (bytes: number) => {
|
||||||
|
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||||
|
if (bytes === 0) return "0 B";
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatUptime = (seconds: number): string => {
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
return `${days} days, ${hours} hours`;
|
||||||
|
} else if (hours > 0) {
|
||||||
|
return `${hours} hours, ${minutes} minutes`;
|
||||||
|
} else {
|
||||||
|
return `${minutes} minutes`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const actualUsed = memInfo.active || memInfo.used;
|
||||||
|
const actualFree = memInfo.available || memInfo.free;
|
||||||
|
const memUsage = ((actualUsed / memInfo.total) * 100);
|
||||||
|
let memStatus = "Optimal";
|
||||||
|
if (memUsage > 90) memStatus = "Critical";
|
||||||
|
else if (memUsage > 80) memStatus = "High";
|
||||||
|
else if (memUsage > 70) memStatus = "Moderate";
|
||||||
|
|
||||||
|
const rootDisk = diskInfo.find(disk => disk.mount === '/') || diskInfo[0];
|
||||||
|
const diskUsage = rootDisk ? ((rootDisk.used / rootDisk.size) * 100) : 0;
|
||||||
|
let diskStatus = "Optimal";
|
||||||
|
if (diskUsage > 90) diskStatus = "Critical";
|
||||||
|
else if (diskUsage > 80) diskStatus = "High";
|
||||||
|
else if (diskUsage > 70) diskStatus = "Moderate";
|
||||||
|
|
||||||
|
const cpuStatus = loadInfo.currentLoad > 80 ? "High" :
|
||||||
|
loadInfo.currentLoad > 60 ? "Moderate" : "Optimal";
|
||||||
|
|
||||||
|
const criticalThreshold = 90;
|
||||||
|
const warningThreshold = 80;
|
||||||
|
let overallStatus = "Optimal";
|
||||||
|
let statusDetails = "All systems running normally";
|
||||||
|
|
||||||
|
if (memUsage > criticalThreshold || loadInfo.currentLoad > criticalThreshold || diskUsage > criticalThreshold) {
|
||||||
|
overallStatus = "Critical";
|
||||||
|
statusDetails = "High resource usage detected - immediate attention required";
|
||||||
|
} else if (memUsage > warningThreshold || loadInfo.currentLoad > warningThreshold || diskUsage > warningThreshold) {
|
||||||
|
overallStatus = "Warning";
|
||||||
|
statusDetails = "Moderate resource usage - monitoring recommended";
|
||||||
|
}
|
||||||
|
|
||||||
|
let mainInterface = null;
|
||||||
|
if (Array.isArray(networkInfo) && networkInfo.length > 0) {
|
||||||
|
mainInterface = networkInfo.find(net =>
|
||||||
|
net.iface && !net.iface.includes('lo') && net.operstate === 'up'
|
||||||
|
) || networkInfo.find(net =>
|
||||||
|
net.iface && !net.iface.includes('lo')
|
||||||
|
) || networkInfo[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const networkSpeed = mainInterface && 'rx_sec' in mainInterface && 'tx_sec' in mainInterface
|
||||||
|
? `${Math.round(((mainInterface.rx_sec || 0) + (mainInterface.tx_sec || 0)) / 1024 / 1024)} Mbps`
|
||||||
|
: "Unknown";
|
||||||
|
|
||||||
|
let latency = 0;
|
||||||
|
try {
|
||||||
|
const { exec } = require('child_process');
|
||||||
|
const { promisify } = require('util');
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
const { stdout } = await execAsync('ping -c 1 -W 1000 8.8.8.8 2>/dev/null || echo "timeout"');
|
||||||
|
const match = stdout.match(/time=(\d+\.?\d*)/);
|
||||||
|
if (match) {
|
||||||
|
latency = Math.round(parseFloat(match[1]));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
latency = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemStats: any = {
|
||||||
|
uptime: formatUptime(uptimeInfo.uptime),
|
||||||
|
memory: {
|
||||||
|
total: formatBytes(memInfo.total),
|
||||||
|
used: formatBytes(actualUsed),
|
||||||
|
free: formatBytes(actualFree),
|
||||||
|
usage: Math.round(memUsage),
|
||||||
|
status: memStatus,
|
||||||
|
},
|
||||||
|
cpu: {
|
||||||
|
model: `${cpuInfo.manufacturer} ${cpuInfo.brand}`,
|
||||||
|
cores: cpuInfo.cores,
|
||||||
|
usage: Math.round(loadInfo.currentLoad),
|
||||||
|
status: cpuStatus,
|
||||||
|
},
|
||||||
|
disk: {
|
||||||
|
total: rootDisk ? formatBytes(rootDisk.size) : "Unknown",
|
||||||
|
used: rootDisk ? formatBytes(rootDisk.used) : "Unknown",
|
||||||
|
free: rootDisk ? formatBytes(rootDisk.available) : "Unknown",
|
||||||
|
usage: Math.round(diskUsage),
|
||||||
|
status: diskStatus,
|
||||||
|
},
|
||||||
|
network: {
|
||||||
|
speed: networkSpeed,
|
||||||
|
latency: latency,
|
||||||
|
downloadSpeed: mainInterface && 'rx_sec' in mainInterface ? Math.round((mainInterface.rx_sec || 0) / 1024 / 1024) : 0,
|
||||||
|
uploadSpeed: mainInterface && 'tx_sec' in mainInterface ? Math.round((mainInterface.tx_sec || 0) / 1024 / 1024) : 0,
|
||||||
|
status: mainInterface && 'operstate' in mainInterface && mainInterface.operstate === 'up' ? "Connected" : "Unknown",
|
||||||
|
},
|
||||||
|
systemStatus: {
|
||||||
|
overall: overallStatus,
|
||||||
|
details: statusDetails,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const graphics = await si.graphics();
|
||||||
|
if (graphics.controllers && graphics.controllers.length > 0) {
|
||||||
|
const gpu = graphics.controllers[0];
|
||||||
|
systemStats.gpu = {
|
||||||
|
model: gpu.model || "Unknown GPU",
|
||||||
|
memory: gpu.vram ? `${gpu.vram} MB` : undefined,
|
||||||
|
status: "Available",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
systemStats.gpu = {
|
||||||
|
model: "No GPU detected",
|
||||||
|
status: "Unknown",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
systemStats.gpu = {
|
||||||
|
model: "GPU detection failed",
|
||||||
|
status: "Unknown",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(systemStats);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching system stats:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to fetch system stats' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
app/page.tsx
41
app/page.tsx
@@ -1,18 +1,51 @@
|
|||||||
import { SystemInfoCard } from "./_components/SystemInfo";
|
import { SystemInfoCard } from "./_components/SystemInfo";
|
||||||
import { TabbedInterface } from "./_components/TabbedInterface";
|
import { TabbedInterface } from "./_components/TabbedInterface";
|
||||||
import { getSystemInfo, getCronJobs } from "./_utils/system";
|
import { getCronJobs } from "./_utils/system";
|
||||||
import { fetchScripts } from "./_server/actions/scripts";
|
import { fetchScripts } from "./_server/actions/scripts";
|
||||||
import { ThemeToggle } from "./_components/ui/ThemeToggle";
|
import { ThemeToggle } from "./_components/ui/ThemeToggle";
|
||||||
import { ToastContainer } from "./_components/ui/Toast";
|
import { ToastContainer } from "./_components/ui/Toast";
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const [systemInfo, cronJobs, scripts] = await Promise.all([
|
const [cronJobs, scripts] = await Promise.all([
|
||||||
getSystemInfo(),
|
|
||||||
getCronJobs(),
|
getCronJobs(),
|
||||||
fetchScripts(),
|
fetchScripts(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const initialSystemInfo = {
|
||||||
|
hostname: "Loading...",
|
||||||
|
platform: "Loading...",
|
||||||
|
uptime: "Loading...",
|
||||||
|
memory: {
|
||||||
|
total: "0 B",
|
||||||
|
used: "0 B",
|
||||||
|
free: "0 B",
|
||||||
|
usage: 0,
|
||||||
|
status: "Loading",
|
||||||
|
},
|
||||||
|
cpu: {
|
||||||
|
model: "Loading...",
|
||||||
|
cores: 0,
|
||||||
|
usage: 0,
|
||||||
|
status: "Loading",
|
||||||
|
},
|
||||||
|
gpu: {
|
||||||
|
model: "Loading...",
|
||||||
|
status: "Loading",
|
||||||
|
},
|
||||||
|
disk: {
|
||||||
|
total: "0 B",
|
||||||
|
used: "0 B",
|
||||||
|
free: "0 B",
|
||||||
|
usage: 0,
|
||||||
|
status: "Loading",
|
||||||
|
},
|
||||||
|
systemStatus: {
|
||||||
|
overall: "Loading",
|
||||||
|
details: "Fetching system information...",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen relative">
|
<div className="min-h-screen relative">
|
||||||
<div className="hero-gradient absolute inset-0 -z-10"></div>
|
<div className="hero-gradient absolute inset-0 -z-10"></div>
|
||||||
@@ -38,7 +71,7 @@ export default async function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<SystemInfoCard systemInfo={systemInfo} />
|
<SystemInfoCard systemInfo={initialSystemInfo} />
|
||||||
|
|
||||||
<main className="lg:ml-80 transition-all duration-300 ml-0 sidebar-collapsed:lg:ml-16">
|
<main className="lg:ml-80 transition-all duration-300 ml-0 sidebar-collapsed:lg:ml-16">
|
||||||
<div className="container mx-auto px-4 py-8 lg:px-8">
|
<div className="container mx-auto px-4 py-8 lg:px-8">
|
||||||
|
|||||||
@@ -1,42 +1,36 @@
|
|||||||
services:
|
services:
|
||||||
cronjob-manager:
|
cronjob-manager:
|
||||||
image: ghcr.io/fccview/cronmaster:main
|
image: ghcr.io/fccview/cronmaster:main
|
||||||
container_name: cronmaster
|
container_name: cronmaster-test
|
||||||
user: "root"
|
user: "root"
|
||||||
ports:
|
ports:
|
||||||
# Feel free to change port, 3000 is very common so I like to map it to something else
|
# Feel free to change port, 3000 is very common so I like to map it to something else
|
||||||
- "40123:3000"
|
- "40124:3000"
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- DOCKER=true
|
- DOCKER=true
|
||||||
- NEXT_PUBLIC_CLOCK_UPDATE_INTERVAL=30000
|
- NEXT_PUBLIC_CLOCK_UPDATE_INTERVAL=30000
|
||||||
- NEXT_PUBLIC_HOST_PROJECT_DIR=/path/to/cronmaster/directory
|
- NEXT_PUBLIC_HOST_PROJECT_DIR=/path/to/cronmaster/directory
|
||||||
|
# If docker struggles to find your crontab user, update this variable with it.
|
||||||
|
# Obviously replace fccview with your user - find it with: ls -asl /var/spool/cron/crontabs/
|
||||||
|
# - HOST_CRONTAB_USER=fccview
|
||||||
volumes:
|
volumes:
|
||||||
# --- CRONTAB MANAGEMENT ---
|
# Mount Docker socket to execute commands on host
|
||||||
# We're mounting /etc/crontab to /host/crontab in read-only mode.
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
# We are thenmounting /var/spool/cron/crontabs with read-write permissions to allow the application
|
|
||||||
# to manipulate the crontab file - docker does not have access to the crontab command, it's the only
|
|
||||||
# workaround I could think of.
|
|
||||||
- /var/spool/cron/crontabs:/host/cron/crontabs
|
|
||||||
- /etc/crontab:/host/crontab:ro
|
|
||||||
|
|
||||||
# --- HOST SYSTEM STATS ---
|
|
||||||
# Mounting system specific folders to their /host/ equivalent folders.
|
|
||||||
# Similar story, we don't want to override docker system folders.
|
|
||||||
# These are all mounted read-only for security.
|
|
||||||
- /proc:/host/proc:ro
|
|
||||||
- /sys:/host/sys:ro
|
|
||||||
- /etc:/host/etc:ro
|
|
||||||
- /usr:/host/usr:ro
|
|
||||||
|
|
||||||
# --- APPLICATION-SPECIFIC MOUNTS ---
|
|
||||||
# These are needed if you want to keep your data on the host machine and not wihin the docker volume.
|
# These are needed if you want to keep your data on the host machine and not wihin the docker volume.
|
||||||
# DO NOT change the location of ./scripts as all cronjobs that use custom scripts created via the app
|
# DO NOT change the location of ./scripts as all cronjobs that use custom scripts created via the app
|
||||||
# will target this foler (thanks to the NEXT_PUBLIC_HOST_PROJECT_DIR variable set above)
|
# will target this foler (thanks to the NEXT_PUBLIC_HOST_PROJECT_DIR variable set above)
|
||||||
- ./scripts:/app/scripts
|
- ./scripts:/app/scripts
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ./snippets:/app/snippets
|
- ./snippets:/app/snippets
|
||||||
|
|
||||||
|
# Use host PID namespace for host command execution
|
||||||
|
# Run in privileged mode for nsenter access
|
||||||
|
pid: "host"
|
||||||
|
privileged: true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
init: true
|
init: true
|
||||||
# Default platform is set to amd64, can be overridden by using arm64.
|
|
||||||
#platform: linux/arm64
|
# Default platform is set to amd64, uncomment to use arm64.
|
||||||
|
#platform: linux/arm64
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
// Removed standalone output for traditional Next.js deployment
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = nextConfig
|
module.exports = nextConfig
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-syntax-highlighter": "^15.6.1",
|
"react-syntax-highlighter": "^15.6.1",
|
||||||
|
"systeminformation": "^5.27.8",
|
||||||
"tailwind-merge": "^2.0.0",
|
"tailwind-merge": "^2.0.0",
|
||||||
"tailwindcss": "^3.3.0",
|
"tailwindcss": "^3.3.0",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# @id: demo-script
|
# @id: demo-script
|
||||||
# @title: Hi, this is a demo script
|
# @title: Hi, this is a demo script
|
||||||
# @description: This script logs a "hello world" to teach you how scripts work.
|
# @description: This script logs a "hello world" to teach you how scripts work.
|
||||||
|
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
echo 'Hello World' > hello.txt
|
echo 'Hello World' > hello.txt
|
||||||
@@ -3117,6 +3117,11 @@ supports-preserve-symlinks-flag@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
||||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||||
|
|
||||||
|
systeminformation@^5.27.8:
|
||||||
|
version "5.27.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.27.8.tgz#f13d180104a0df2e7222c5d4aa85aea147428de5"
|
||||||
|
integrity sha512-d3Z0gaQO1MlUxzDUKsmXz5y4TOBCMZ8IyijzaYOykV3AcNOTQ7mT+tpndUOXYNSxzLK3la8G32xiUFvZ0/s6PA==
|
||||||
|
|
||||||
tailwind-merge@^2.0.0:
|
tailwind-merge@^2.0.0:
|
||||||
version "2.6.0"
|
version "2.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz#ac5fb7e227910c038d458f396b7400d93a3142d5"
|
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz#ac5fb7e227910c038d458f396b7400d93a3142d5"
|
||||||
|
|||||||
Reference in New Issue
Block a user