8 Commits

Author SHA1 Message Date
fccview
fc5cabb342 Merge branch 'main' into bugfix/replace-docker-crontab-functionality 2025-08-26 09:42:52 +01:00
fccview
032c63cfbd build legacy branches 2025-08-26 08:04:39 +01:00
fccview
08d37154b4 update readme, clean up code, add contributions 2025-08-26 07:59:08 +01:00
fccview
888297c56a final product for testers 2025-08-26 07:10:19 +01:00
fccview
165f625c65 final product for testers 2025-08-25 21:11:55 +01:00
fccview
40e8f44564 Fix major issue with cron logic 2025-08-25 20:13:05 +01:00
fccview
89fed1f1b4 Merge branch 'feature/ARM64' 2025-08-25 17:43:43 +01:00
fccview
1d882c6caa Merge pull request #2 from fccview/BUG-1
Fix dockerized issue with scripts on build
2025-08-21 09:12:53 +01:00
17 changed files with 462 additions and 1115 deletions

View File

@@ -2,7 +2,7 @@ name: Docker
on:
push:
branches: ["main", "feature/*"]
branches: ["main", "legacy", "feature/*", "bugfix/*"]
tags: ["v*.*.*"]
pull_request:
branches: ["main"]

View File

@@ -1,17 +1,15 @@
FROM node:20-slim AS base
# Install system utilities for system information
RUN apt-get update && apt-get install -y \
pciutils \
curl \
iputils-ping \
util-linux \
&& rm -rf /var/lib/apt/lists/*
# Install dependencies only when needed
FROM base AS deps
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
@@ -20,20 +18,15 @@ RUN \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
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
RUN yarn build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
@@ -43,33 +36,20 @@ ENV NEXT_TELEMETRY_DISABLED=1
RUN groupadd --system --gid 1001 nodejs
RUN useradd --system --uid 1001 nextjs
# Create directories for mounted volumes with proper permissions
RUN mkdir -p /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 the entire .next directory
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 package.json and yarn.lock for yarn start
COPY --from=builder /app/package.json ./package.json
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
# Don't set default user - let docker-compose decide
# USER nextjs
EXPOSE 3000
ENV PORT=3000

View File

@@ -2,6 +2,18 @@
<img src="public/heading.png" width="400px">
</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
- **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:
cronjob-manager:
image: ghcr.io/fccview/cronmaster:main
container_name: cronmaster
container_name: cronmaster-test
user: "root"
ports:
# Feel free to change port, 3000 is very common so I like to map it to something else
- "40123:3000"
- "40124:3000"
environment:
- NODE_ENV=production
- DOCKER=true
- NEXT_PUBLIC_CLOCK_UPDATE_INTERVAL=30000
- 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:
# --- CRONTAB MANAGEMENT ---
# We're mounting /etc/crontab to /host/crontab in read-only mode.
# 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
# Mount Docker socket to execute commands on host
- /var/run/docker.sock:/var/run/docker.sock
# --- 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.
# 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)
- ./scripts:/app/scripts
- ./data:/app/data
- ./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
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
@@ -206,6 +212,32 @@ The application uses standard cron format: `* * * * *`
4. Add tests if applicable
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
This project is licensed under the MIT License.

View File

@@ -1,22 +1,60 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "./ui/Card";
import { MetricCard } from "./ui/MetricCard";
import { SystemStatus } from "./ui/SystemStatus";
import { PerformanceSummary } from "./ui/PerformanceSummary";
import { Sidebar } from "./ui/Sidebar";
import {
Monitor,
Globe,
Clock,
HardDrive,
Cpu,
Server,
Monitor,
Wifi,
} 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 { fetchSystemInfo } from "@/app/_server/actions/cronjobs";
interface SystemInfoCardProps {
systemInfo: SystemInfoType;
@@ -30,10 +68,16 @@ export function SystemInfoCard({
useState<SystemInfoType>(initialSystemInfo);
const [isUpdating, setIsUpdating] = useState(false);
const updateSystemInfo = async () => {
try {
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);
} catch (error) {
console.error("Failed to update system info:", error);
@@ -47,49 +91,39 @@ export function SystemInfoCard({
setCurrentTime(new Date().toLocaleTimeString());
};
const updateStats = () => {
updateSystemInfo();
};
updateTime();
updateStats();
updateSystemInfo();
const updateInterval = parseInt(
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 = {
cpu: systemInfo.cpu.usage,
memory: systemInfo.memory.usage,
network: `${systemInfo.network.latency}ms`,
network: systemInfo.network ? `${systemInfo.network.latency}ms` : "N/A",
};
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,
label: "Uptime",
@@ -129,14 +163,14 @@ export function SystemInfoCard({
status: systemInfo.gpu.status,
color: "text-indigo-500",
},
{
...(systemInfo.network ? [{
icon: Wifi,
label: "Network",
value: `${systemInfo.network.latency}ms`,
detail: `${systemInfo.network.latency}ms latency • ${systemInfo.network.speed}`,
status: systemInfo.network.status,
color: "text-teal-500",
},
}] : []),
];
const performanceMetrics = [
@@ -150,11 +184,11 @@ export function SystemInfoCard({
value: `${systemInfo.memory.usage}%`,
status: systemInfo.memory.status,
},
{
...(systemInfo.network ? [{
label: "Network Latency",
value: `${systemInfo.network.latency}ms`,
status: systemInfo.network.status,
},
}] : []),
];
return (
@@ -216,7 +250,7 @@ export function SystemInfoCard({
💡 Stats update every{" "}
{Math.round(
parseInt(process.env.NEXT_PUBLIC_CLOCK_UPDATE_INTERVAL || "30000") /
1000
1000
)}
s Network speed estimated from latency
{isUpdating && (

View File

@@ -5,9 +5,7 @@ import {
addCronJob,
deleteCronJob,
updateCronJob,
getSystemInfo,
type CronJob,
type SystemInfo,
} from "@/app/_utils/system";
import { revalidatePath } from "next/cache";
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(
formData: FormData

View File

@@ -1,4 +1,3 @@
export { getSystemInfo, type SystemInfo } from "./system/info";
export {
getCronJobs,
addCronJob,

View File

@@ -1,6 +1,6 @@
import { exec } from "child_process";
import { promisify } from "util";
import { isDocker, readCronFilesDocker, writeCronFilesDocker } from "./docker";
import { readHostCrontab, writeHostCrontab } from "./hostCrontab";
const execAsync = promisify(exec);
@@ -12,6 +12,8 @@ export interface CronJob {
}
async function readCronFiles(): Promise<string> {
const isDocker = process.env.DOCKER === "true";
if (!isDocker) {
try {
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> {
const isDocker = process.env.DOCKER === "true";
if (!isDocker) {
try {
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[]> {
@@ -105,52 +109,19 @@ export async function addCronJob(
try {
const cronContent = await readCronFiles();
if (isDocker) {
const lines = cronContent.split("\n");
let hasUserSection = false;
let userSectionEnd = -1;
const newEntry = comment
? `# ${comment}\n${schedule} ${command}`
: `${schedule} ${command}`;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith("# User: ")) {
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);
}
let newCron;
if (cronContent.trim() === "") {
newCron = newEntry;
} else {
const newEntry = comment
? `# ${comment}\n${schedule} ${command}`
: `${schedule} ${command}`;
const newCron = cronContent + "\n" + newEntry;
await writeCronFiles(newCron);
const existingContent = cronContent.endsWith('\n') ? cronContent : cronContent + '\n';
newCron = existingContent + newEntry;
}
return true;
return await writeCronFiles(newCron);
} catch (error) {
console.error("Error adding cron job:", error);
return false;

View File

@@ -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 };

View 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;
}
}

View File

@@ -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",
},
};
}
}

View 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 }
);
}
}

View File

@@ -1,18 +1,51 @@
import { SystemInfoCard } from "./_components/SystemInfo";
import { TabbedInterface } from "./_components/TabbedInterface";
import { getSystemInfo, getCronJobs } from "./_utils/system";
import { getCronJobs } from "./_utils/system";
import { fetchScripts } from "./_server/actions/scripts";
import { ThemeToggle } from "./_components/ui/ThemeToggle";
import { ToastContainer } from "./_components/ui/Toast";
export const dynamic = "force-dynamic";
export default async function Home() {
const [systemInfo, cronJobs, scripts] = await Promise.all([
getSystemInfo(),
const [cronJobs, scripts] = await Promise.all([
getCronJobs(),
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 (
<div className="min-h-screen relative">
<div className="hero-gradient absolute inset-0 -z-10"></div>
@@ -38,7 +71,7 @@ export default async function Home() {
</div>
</header>
<SystemInfoCard systemInfo={systemInfo} />
<SystemInfoCard systemInfo={initialSystemInfo} />
<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">

View File

@@ -1,42 +1,36 @@
services:
cronjob-manager:
image: ghcr.io/fccview/cronmaster:main
container_name: cronmaster
container_name: cronmaster-test
user: "root"
ports:
# Feel free to change port, 3000 is very common so I like to map it to something else
- "40123:3000"
- "40124:3000"
environment:
- NODE_ENV=production
- DOCKER=true
- NEXT_PUBLIC_CLOCK_UPDATE_INTERVAL=30000
- 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:
# --- CRONTAB MANAGEMENT ---
# We're mounting /etc/crontab to /host/crontab in read-only mode.
# 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
# Mount Docker socket to execute commands on host
- /var/run/docker.sock:/var/run/docker.sock
# --- 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.
# 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)
- ./scripts:/app/scripts
- ./data:/app/data
- ./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
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

View File

@@ -1,6 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
// Removed standalone output for traditional Next.js deployment
}
module.exports = nextConfig

View File

@@ -34,6 +34,7 @@
"react": "^18",
"react-dom": "^18",
"react-syntax-highlighter": "^15.6.1",
"systeminformation": "^5.27.8",
"tailwind-merge": "^2.0.0",
"tailwindcss": "^3.3.0",
"typescript": "^5"

View File

@@ -1,6 +1,6 @@
# @id: demo-script
# @title: Hi, this is a demo script
# @description: This script logs a "hello world" to teach you how scripts work.
# @id: demo-script
# @title: Hi, this is a demo script
# @description: This script logs a "hello world" to teach you how scripts work.
#!/bin/bash
echo 'Hello World' > hello.txt

View File

@@ -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"
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:
version "2.6.0"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz#ac5fb7e227910c038d458f396b7400d93a3142d5"