diff --git a/Dockerfile b/Dockerfile index 3846f42..1da4ae8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,7 @@ -FROM node:20-slim AS base - -RUN apt-get update && apt-get install -y \ - pciutils \ - curl \ - iputils-ping \ - util-linux \ - ca-certificates \ - gnupg \ - lsb-release \ - && rm -rf /var/lib/apt/lists/* - -RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \ - && apt-get update \ - && apt-get install -y docker-ce-cli \ - && rm -rf /var/lib/apt/lists/* +FROM node:20-alpine AS base FROM base AS deps +RUN apk add --no-cache libc6-compat WORKDIR /app COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ @@ -42,26 +27,27 @@ WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 -RUN groupadd --system --gid 1001 nodejs -RUN useradd --system --uid 1001 nextjs +RUN apk add --no-cache su-exec docker-cli pciutils curl iputils util-linux ca-certificates + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs RUN mkdir -p /app/scripts /app/data /app/snippets && \ chown -R nextjs:nodejs /app/scripts /app/data /app/snippets -COPY --from=builder /app/public ./public - -COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next +RUN mkdir -p /app/.next/cache && \ + chown -R nextjs:nodejs /app/.next -COPY --from=builder --chown=nextjs:nodejs /app/app ./app +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static -COPY --from=builder /app/package.json ./package.json -COPY --from=builder /app/yarn.lock ./yarn.lock - -COPY --from=deps --chown=nextjs:nodejs /app/node_modules ./node_modules +COPY --from=builder /app/public ./public EXPOSE 3000 ENV PORT=3000 ENV HOSTNAME="0.0.0.0" -CMD ["yarn", "start"] +USER nextjs + +CMD ["node", "server.js"] diff --git a/README.md b/README.md index 8f35d21..619fa18 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

-## Table of Contents +## Quick links - [Features](#features) - [Quick Start](#quick-start) @@ -18,11 +18,16 @@ - [Managing Cron Jobs](#managing-cron-jobs) - [Job Execution Logging](#job-execution-logging) - [Managing Scripts](#managing-scripts) -- [Technologies Used](#technologies-used) -- [Contributing](#contributing) -- [License](#license) ---- + +
+ +| Desktop | Mobile | +|---------|--------| +| ![Dark Mode Desktop](screenshots/home-dark.png) | ![Dark Mode Mobile](screenshots/home-dark-mobile.png) | +| ![Light Mode Desktop](screenshots/home-light.png) | ![Light Mode Mobile](screenshots/home-light-mobile.png) | + +
## Features @@ -51,26 +56,13 @@

---- - -
- -## Before we start - -Hey there! 👋 Just a friendly heads-up: I'm a big believer in open source and love sharing my work with the community. Everything you find in my GitHub repos is and always will be 100% free. If someone tries to sell you a "premium" version of any of my projects while claiming to be me, please know that this is not legitimate. 🚫 - -If you find my projects helpful and want to fuel my late-night coding sessions with caffeine, I'd be super grateful for any support! ☕ -

- Buy me a coffee + Buy me a coffee

-
- - -
+--- @@ -93,7 +85,6 @@ services: - "40123:3000" environment: - NODE_ENV=production - - DOCKER=true - NEXT_PUBLIC_CLOCK_UPDATE_INTERVAL=30000 - AUTH_PASSWORD=very_strong_password - HOST_CRONTAB_USER=root @@ -335,16 +326,6 @@ I would like to thank the following members for raising issues and help test/deb - - -## License - -This project is licensed under the MIT License. - -## Support - -For issues and questions, please open an issue on the GitHub repository. - ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=fccview/cronmaster&type=Date)](https://www.star-history.com/#fccview/cronmaster&Date) diff --git a/app/_components/FeatureComponents/Cronjobs/CronJobList.tsx b/app/_components/FeatureComponents/Cronjobs/CronJobList.tsx index 638a635..b028003 100644 --- a/app/_components/FeatureComponents/Cronjobs/CronJobList.tsx +++ b/app/_components/FeatureComponents/Cronjobs/CronJobList.tsx @@ -9,16 +9,16 @@ import { import { Button } from "@/app/_components/GlobalComponents/UIElements/Button"; import { Switch } from "@/app/_components/GlobalComponents/UIElements/Switch"; import { - Clock, - Plus, + ClockIcon, + PlusIcon, Archive, - ChevronDown, - Code, - MessageSquare, - Settings, - Loader2, - Filter, -} from "lucide-react"; + CaretDownIcon, + CodeIcon, + ChatTextIcon, + GearIcon, + CircleNotchIcon, + FunnelIcon, +} from "@phosphor-icons/react"; import { CronJob } from "@/app/_utils/cronjob-utils"; import { Script } from "@/app/_utils/scripts-utils"; import { UserFilter } from "@/app/_components/FeatureComponents/User/UserFilter"; @@ -236,8 +236,8 @@ export const CronJobList = ({ cronJobs, scripts }: CronJobListProps) => {
-
- +
+
@@ -261,7 +261,7 @@ export const CronJobList = ({ cronJobs, scripts }: CronJobListProps) => { className="btn-outline" title={t("cronjobs.filters")} > - +
@@ -301,7 +301,7 @@ export const CronJobList = ({ cronJobs, scripts }: CronJobListProps) => { onNewTaskClick={() => setIsNewCronModalOpen(true)} /> ) : ( -
+
{loadedSettings ? ( filteredJobs.map((job) => minimalMode ? ( @@ -347,7 +347,7 @@ export const CronJobList = ({ cronJobs, scripts }: CronJobListProps) => { ) ) : (
- +
)}
diff --git a/app/_components/FeatureComponents/Cronjobs/Parts/CronJobEmptyState.tsx b/app/_components/FeatureComponents/Cronjobs/Parts/CronJobEmptyState.tsx index 5c9b9fc..e47a69c 100644 --- a/app/_components/FeatureComponents/Cronjobs/Parts/CronJobEmptyState.tsx +++ b/app/_components/FeatureComponents/Cronjobs/Parts/CronJobEmptyState.tsx @@ -1,7 +1,7 @@ "use client"; import { Button } from "@/app/_components/GlobalComponents/UIElements/Button"; -import { Clock, Plus } from "lucide-react"; +import { ClockIcon, PlusIcon } from "@phosphor-icons/react"; interface CronJobEmptyStateProps { selectedUser: string | null; @@ -15,7 +15,7 @@ export const CronJobEmptyState = ({ return (
- +

{selectedUser @@ -32,7 +32,7 @@ export const CronJobEmptyState = ({ className="btn-primary glow-primary" size="lg" > - + Create Your First Task

diff --git a/app/_components/FeatureComponents/Cronjobs/Parts/CronJobItem.tsx b/app/_components/FeatureComponents/Cronjobs/Parts/CronJobItem.tsx index 23cd238..4068a31 100644 --- a/app/_components/FeatureComponents/Cronjobs/Parts/CronJobItem.tsx +++ b/app/_components/FeatureComponents/Cronjobs/Parts/CronJobItem.tsx @@ -4,24 +4,24 @@ import { useState, useEffect } from "react"; import { Button } from "@/app/_components/GlobalComponents/UIElements/Button"; import { DropdownMenu } from "@/app/_components/GlobalComponents/UIElements/DropdownMenu"; import { - Trash2, - Edit, - Files, - User, - Play, - Pause, - Code, - Info, - FileOutput, - FileX, - FileText, - AlertCircle, - CheckCircle, - AlertTriangle, - Download, - Hash, - Check, -} from "lucide-react"; + TrashIcon, + PencilSimpleIcon, + FilesIcon, + UserIcon, + PlayIcon, + PauseIcon, + CodeIcon, + InfoIcon, + FileArrowDownIcon, + FileXIcon, + FileTextIcon, + WarningCircleIcon, + CheckCircleIcon, + WarningIcon, + DownloadIcon, + HashIcon, + CheckIcon, +} from "@phosphor-icons/react"; import { CronJob } from "@/app/_utils/cronjob-utils"; import { JobError } from "@/app/_utils/error-utils"; import { ErrorBadge } from "@/app/_components/GlobalComponents/Badges/ErrorBadge"; @@ -92,7 +92,7 @@ export const CronJobItem = ({ const dropdownMenuItems = [ { label: t("cronjobs.editCronJob"), - icon: , + icon: , onClick: () => onEdit(job), }, { @@ -100,45 +100,45 @@ export const CronJobItem = ({ ? t("cronjobs.disableLogging") : t("cronjobs.enableLogging"), icon: job.logsEnabled ? ( - + ) : ( - + ), onClick: () => onToggleLogging(job.id), }, ...(job.logsEnabled ? [ - { - label: t("cronjobs.viewLogs"), - icon: , - onClick: () => onViewLogs(job), - }, - ] + { + label: t("cronjobs.viewLogs"), + icon: , + onClick: () => onViewLogs(job), + }, + ] : []), { label: job.paused ? t("cronjobs.resumeCronJob") : t("cronjobs.pauseCronJob"), icon: job.paused ? ( - + ) : ( - + ), onClick: () => (job.paused ? onResume(job.id) : onPause(job.id)), }, { label: t("cronjobs.cloneCronJob"), - icon: , + icon: , onClick: () => onClone(job), }, { label: t("cronjobs.backupJob"), - icon: , + icon: , onClick: () => onBackup(job.id), }, { label: t("cronjobs.deleteCronJob"), - icon: , + icon: , onClick: () => onDelete(job), variant: "destructive" as const, disabled: deletingId === job.id, @@ -148,22 +148,21 @@ export const CronJobItem = ({ return (
{(scheduleDisplayMode === "cron" || scheduleDisplayMode === "both") && ( - - {job.schedule} - - )} + + {job.schedule} + + )} {scheduleDisplayMode === "human" && cronExplanation?.isValid && ( -
- +
+

{cronExplanation.humanReadable}

@@ -172,7 +171,7 @@ export const CronJobItem = ({
{commandCopied === job.id && ( - + )}
 {
@@ -181,7 +180,7 @@ export const CronJobItem = ({
                     setCommandCopied(job.id);
                     setTimeout(() => setCommandCopied(null), 3000);
                   }}
-                  className="w-full cursor-pointer overflow-x-auto text-sm font-medium text-foreground bg-muted/30 px-2 py-1 rounded border border-border/30 hide-scrollbar"
+                  className="w-full cursor-pointer overflow-x-auto text-sm font-medium terminal-font bg-background1 px-2 py-1 ascii-border hide-scrollbar"
                 >
                   {unwrapCommand(displayCommand)}
                 
@@ -191,8 +190,8 @@ export const CronJobItem = ({
{scheduleDisplayMode === "both" && cronExplanation?.isValid && ( -
- +
+

{cronExplanation.humanReadable}

@@ -201,7 +200,7 @@ export const CronJobItem = ({ {job.comment && (

{job.comment} @@ -210,13 +209,13 @@ export const CronJobItem = ({

-
- +
+ {job.user}
{ const success = await copyToClipboard(job.id); @@ -227,22 +226,22 @@ export const CronJobItem = ({ }} > {showCopyConfirmation ? ( - + ) : ( - + )} {job.id}
{job.paused && ( - - {t("cronjobs.paused")} + + {t("cronjobs.paused")} )} {job.logsEnabled && ( - - {t("cronjobs.logged")} + + {t("cronjobs.logged")} )} @@ -252,11 +251,11 @@ export const CronJobItem = ({ e.stopPropagation(); onViewLogs(job); }} - className="flex items-center gap-1 text-xs bg-red-500/10 text-red-600 dark:text-red-400 px-2 py-0.5 rounded border border-red-500/30 hover:bg-red-500/20 transition-colors cursor-pointer" + className="flex items-center gap-1 text-xs bg-background0 px-2 py-0.5 ascii-border hover:bg-background1 transition-colors cursor-pointer terminal-font" title="Latest execution failed - Click to view error log" > - - + + {t("cronjobs.failed", { exitCode: job.logError?.exitCode?.toString() ?? "", })} @@ -272,12 +271,12 @@ export const CronJobItem = ({ e.stopPropagation(); onViewLogs(job); }} - className="flex items-center gap-1 text-xs bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 px-2 py-0.5 rounded border border-yellow-500/30 hover:bg-yellow-500/20 transition-colors cursor-pointer" + className="flex items-center gap-1 text-xs bg-background0 px-2 py-0.5 ascii-border hover:bg-background1 transition-colors cursor-pointer terminal-font" title="Latest execution succeeded, but has historical failures - Click to view logs" > - - {t("cronjobs.healthy")} - + + {t("cronjobs.healthy")} + )} @@ -285,9 +284,9 @@ export const CronJobItem = ({ !job.logError?.hasError && !job.logError?.hasHistoricalFailures && job.logError?.latestExitCode === 0 && ( -
- - {t("cronjobs.healthy")} +
+ + {t("cronjobs.healthy")}
)} @@ -315,7 +314,7 @@ export const CronJobItem = ({ {runningJobId === job.id ? (
) : ( - + )} @@ -334,9 +333,9 @@ export const CronJobItem = ({ aria-label={t("cronjobs.pauseCronJob")} > {job.paused ? ( - + ) : ( - + )} @@ -363,9 +362,9 @@ export const CronJobItem = ({ } > {job.logsEnabled ? ( - + ) : ( - + )}
diff --git a/app/_components/FeatureComponents/Cronjobs/Parts/MinimalCronJobItem.tsx b/app/_components/FeatureComponents/Cronjobs/Parts/MinimalCronJobItem.tsx index 5ded090..5222e4a 100644 --- a/app/_components/FeatureComponents/Cronjobs/Parts/MinimalCronJobItem.tsx +++ b/app/_components/FeatureComponents/Cronjobs/Parts/MinimalCronJobItem.tsx @@ -4,19 +4,19 @@ import { useState, useEffect } from "react"; import { Button } from "@/app/_components/GlobalComponents/UIElements/Button"; import { DropdownMenu } from "@/app/_components/GlobalComponents/UIElements/DropdownMenu"; import { - Trash2, - Edit, - Files, - Play, - Pause, - Code, - Info, - Download, - Check, - FileX, - FileText, - FileOutput, -} from "lucide-react"; + TrashIcon, + PencilSimpleIcon, + FilesIcon, + PlayIcon, + PauseIcon, + CodeIcon, + InfoIcon, + DownloadIcon, + CheckIcon, + FileXIcon, + FileTextIcon, + FileArrowDownIcon, +} from "@phosphor-icons/react"; import { CronJob } from "@/app/_utils/cronjob-utils"; import { JobError } from "@/app/_utils/error-utils"; import { @@ -83,7 +83,7 @@ export const MinimalCronJobItem = ({ const dropdownMenuItems = [ { label: t("cronjobs.editCronJob"), - icon: , + icon: , onClick: () => onEdit(job), }, { @@ -91,9 +91,9 @@ export const MinimalCronJobItem = ({ ? t("cronjobs.disableLogging") : t("cronjobs.enableLogging"), icon: job.logsEnabled ? ( - + ) : ( - + ), onClick: () => onToggleLogging(job.id), }, @@ -101,7 +101,7 @@ export const MinimalCronJobItem = ({ ? [ { label: t("cronjobs.viewLogs"), - icon: , + icon: , onClick: () => onViewLogs(job), }, ] @@ -111,25 +111,25 @@ export const MinimalCronJobItem = ({ ? t("cronjobs.resumeCronJob") : t("cronjobs.pauseCronJob"), icon: job.paused ? ( - + ) : ( - + ), onClick: () => (job.paused ? onResume(job.id) : onPause(job.id)), }, { label: t("cronjobs.cloneCronJob"), - icon: , + icon: , onClick: () => onClone(job), }, { label: t("cronjobs.backupJob"), - icon: , + icon: , onClick: () => onBackup(job.id), }, { label: t("cronjobs.deleteCronJob"), - icon: , + icon: , onClick: () => onDelete(job), variant: "destructive" as const, disabled: deletingId === job.id, @@ -139,19 +139,19 @@ export const MinimalCronJobItem = ({ return (
{scheduleDisplayMode === "cron" && ( - + {job.schedule} )} {scheduleDisplayMode === "human" && cronExplanation?.isValid && ( -
- +
+ {cronExplanation.humanReadable} @@ -159,15 +159,15 @@ export const MinimalCronJobItem = ({ )} {scheduleDisplayMode === "both" && (
- + {job.schedule} {cronExplanation?.isValid && (
- +
)}
@@ -177,7 +177,7 @@ export const MinimalCronJobItem = ({
{commandCopied === job.id && ( - + )}
 {
@@ -186,7 +186,7 @@ export const MinimalCronJobItem = ({
                 setCommandCopied(job.id);
                 setTimeout(() => setCommandCopied(null), 3000);
               }}
-              className="flex-1 cursor-pointer overflow-hidden text-sm font-medium text-foreground bg-muted/30 px-2 py-1 rounded border border-border/30 truncate"
+              className="flex-1 cursor-pointer overflow-hidden text-sm font-medium terminal-font bg-background1 px-2 py-1 ascii-border truncate"
               title={unwrapCommand(job.command)}
             >
               {unwrapCommand(displayCommand)}
@@ -197,25 +197,25 @@ export const MinimalCronJobItem = ({
         
{job.logsEnabled && (
)} {job.paused && (
)} {!job.logError?.hasError && job.logsEnabled && (
)} {job.logsEnabled && job.logError?.hasError && (
{ e.stopPropagation(); @@ -225,31 +225,32 @@ export const MinimalCronJobItem = ({ )} {!job.logsEnabled && errors.length > 0 && (
onErrorClick(errors[0])} /> )}
-
+
diff --git a/app/_components/FeatureComponents/Games/SnakeGame.tsx b/app/_components/FeatureComponents/Games/SnakeGame.tsx new file mode 100644 index 0000000..f994e3b --- /dev/null +++ b/app/_components/FeatureComponents/Games/SnakeGame.tsx @@ -0,0 +1,431 @@ +"use client"; + +import { useEffect, useRef, useState, useCallback } from "react"; +import { useTranslations } from "next-intl"; +import { ArrowUpIcon, ArrowDownIcon, ArrowLeftIcon, ArrowRightIcon, ArrowClockwiseIcon, PlayIcon, PauseIcon } from "@phosphor-icons/react"; + +interface Position { + x: number; + y: number; +} + +type Direction = "UP" | "DOWN" | "LEFT" | "RIGHT"; + +const GRID_SIZE = 20; +const INITIAL_SNAKE: Position[] = [ + { x: 10, y: 10 }, + { x: 9, y: 10 }, + { x: 8, y: 10 }, +]; +const INITIAL_DIRECTION: Direction = "RIGHT"; +const GAME_SPEED = 150; + +export const SnakeGame = () => { + const t = useTranslations("notFound"); + const canvasRef = useRef(null); + const containerRef = useRef(null); + const [snake, setSnake] = useState(INITIAL_SNAKE); + const [direction, setDirection] = useState(INITIAL_DIRECTION); + const [food, setFood] = useState({ x: 15, y: 15 }); + const [gameOver, setGameOver] = useState(false); + const [gameStarted, setGameStarted] = useState(false); + const [score, setScore] = useState(0); + const [highScore, setHighScore] = useState(0); + const [isPaused, setIsPaused] = useState(false); + const [colors, setColors] = useState({ snake: "#00ff00", food: "#ff0000", grid: "#333333" }); + const [cellSize, setCellSize] = useState(20); + + const directionRef = useRef(INITIAL_DIRECTION); + const gameLoopRef = useRef(null); + + useEffect(() => { + const savedHighScore = localStorage.getItem("snakeHighScore"); + if (savedHighScore) { + setHighScore(parseInt(savedHighScore)); + } + + const updateColors = () => { + const theme = document.documentElement.getAttribute("data-webtui-theme"); + if (theme === "catppuccin-mocha") { + setColors({ + snake: "#9ca0b0", + food: "#f38ba8", + grid: "#313244", + }); + } else { + setColors({ + snake: "#313244", + food: "#d20f39", + grid: "#9ca0b0", + }); + } + }; + + updateColors(); + + const observer = new MutationObserver(updateColors); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["data-webtui-theme"], + }); + + const updateCellSize = () => { + if (containerRef.current) { + const containerWidth = containerRef.current.offsetWidth; + const maxCanvasSize = Math.min(containerWidth - 32, 400); + const newCellSize = Math.floor(maxCanvasSize / GRID_SIZE); + setCellSize(newCellSize); + } + }; + + updateCellSize(); + window.addEventListener("resize", updateCellSize); + + return () => { + observer.disconnect(); + window.removeEventListener("resize", updateCellSize); + }; + }, []); + + const generateFood = useCallback((): Position => { + let newFood: Position; + do { + newFood = { + x: Math.floor(Math.random() * GRID_SIZE), + y: Math.floor(Math.random() * GRID_SIZE), + }; + } while (snake.some((segment) => segment.x === newFood.x && segment.y === newFood.y)); + return newFood; + }, [snake]); + + const resetGame = useCallback(() => { + setSnake(INITIAL_SNAKE); + setDirection(INITIAL_DIRECTION); + directionRef.current = INITIAL_DIRECTION; + setFood(generateFood()); + setGameOver(false); + setGameStarted(true); + setScore(0); + setIsPaused(false); + }, [generateFood]); + + const draw = useCallback(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + const theme = document.documentElement.getAttribute("data-webtui-theme"); + const bgColor = theme === "catppuccin-mocha" ? "#1e1e2e" : "#eff1f5"; + + ctx.fillStyle = bgColor; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.strokeStyle = colors.grid; + ctx.lineWidth = 1; + for (let i = 0; i <= GRID_SIZE; i++) { + ctx.beginPath(); + ctx.moveTo(i * cellSize, 0); + ctx.lineTo(i * cellSize, GRID_SIZE * cellSize); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(0, i * cellSize); + ctx.lineTo(GRID_SIZE * cellSize, i * cellSize); + ctx.stroke(); + } + + snake.forEach((segment) => { + ctx.fillStyle = colors.snake; + ctx.fillRect( + segment.x * cellSize + 1, + segment.y * cellSize + 1, + cellSize - 2, + cellSize - 2 + ); + }); + + ctx.fillStyle = colors.food; + ctx.fillRect( + food.x * cellSize + 1, + food.y * cellSize + 1, + cellSize - 2, + cellSize - 2 + ); + }, [snake, food, colors, cellSize]); + + useEffect(() => { + draw(); + }, [draw]); + + const moveSnake = useCallback(() => { + if (gameOver || !gameStarted || isPaused) return; + + setSnake((prevSnake) => { + const head = prevSnake[0]; + const newHead: Position = { ...head }; + + switch (directionRef.current) { + case "UP": + newHead.y -= 1; + break; + case "DOWN": + newHead.y += 1; + break; + case "LEFT": + newHead.x -= 1; + break; + case "RIGHT": + newHead.x += 1; + break; + } + + if ( + newHead.x < 0 || + newHead.x >= GRID_SIZE || + newHead.y < 0 || + newHead.y >= GRID_SIZE || + prevSnake.some((segment) => segment.x === newHead.x && segment.y === newHead.y) + ) { + setGameOver(true); + setGameStarted(false); + return prevSnake; + } + + const newSnake = [newHead, ...prevSnake]; + + if (newHead.x === food.x && newHead.y === food.y) { + setScore((prev) => { + const newScore = prev + 10; + if (newScore > highScore) { + setHighScore(newScore); + localStorage.setItem("snakeHighScore", newScore.toString()); + } + return newScore; + }); + setFood(generateFood()); + } else { + newSnake.pop(); + } + + return newSnake; + }); + }, [gameOver, gameStarted, isPaused, food, highScore, generateFood]); + + useEffect(() => { + if (gameStarted && !gameOver && !isPaused) { + gameLoopRef.current = setInterval(moveSnake, GAME_SPEED); + } + + return () => { + if (gameLoopRef.current) { + clearInterval(gameLoopRef.current); + } + }; + }, [gameStarted, gameOver, isPaused, moveSnake]); + + useEffect(() => { + const handleKeyPress = (e: KeyboardEvent) => { + if (e.code === "Space") { + e.preventDefault(); + if (gameOver) { + resetGame(); + } else if (!gameStarted) { + setGameStarted(true); + } + return; + } + + if (e.code === "KeyP") { + e.preventDefault(); + if (gameStarted && !gameOver) { + setIsPaused((prev) => !prev); + } + return; + } + + if (!gameStarted || gameOver || isPaused) return; + + let newDirection: Direction | null = null; + + switch (e.key) { + case "ArrowUp": + if (directionRef.current !== "DOWN") { + newDirection = "UP"; + } + break; + case "ArrowDown": + if (directionRef.current !== "UP") { + newDirection = "DOWN"; + } + break; + case "ArrowLeft": + if (directionRef.current !== "RIGHT") { + newDirection = "LEFT"; + } + break; + case "ArrowRight": + if (directionRef.current !== "LEFT") { + newDirection = "RIGHT"; + } + break; + } + + if (newDirection) { + e.preventDefault(); + directionRef.current = newDirection; + setDirection(newDirection); + } + }; + + window.addEventListener("keydown", handleKeyPress); + return () => window.removeEventListener("keydown", handleKeyPress); + }, [gameOver, gameStarted, isPaused, resetGame]); + + const handleTouchMove = (dir: Direction) => { + if (!gameStarted) { + setGameStarted(true); + directionRef.current = dir; + setDirection(dir); + return; + } + if (gameOver || isPaused) return; + + let canMove = false; + + switch (dir) { + case "UP": + canMove = directionRef.current !== "DOWN"; + break; + case "DOWN": + canMove = directionRef.current !== "UP"; + break; + case "LEFT": + canMove = directionRef.current !== "RIGHT"; + break; + case "RIGHT": + canMove = directionRef.current !== "LEFT"; + break; + } + + if (canMove) { + directionRef.current = dir; + setDirection(dir); + } + }; + + const handleCanvasClick = () => { + if (gameOver) { + resetGame(); + } else if (!gameStarted) { + setGameStarted(true); + } + }; + + return ( +
+
+
+
+ {t("score")}: {score} +
+
+ {t("highScore")}: {highScore} +
+
+ +
+ + + {!gameStarted && !gameOver && ( +
+
+

{t("pressToStart")}

+

{t("pauseGame")}

+
+
+ )} + + {gameOver && ( +
+
+

+ {t("gameOver")} +

+

+ {t("score")}: {score} +

+

{t("pressToRestart")}

+
+
+ )} + + {isPaused && ( +
+
+

+ {t("paused")} +

+
+
+ )} +
+ +
+
+ +
+ + + +
+ +
+
+ +
+

{t("useArrowKeys")}

+

{t("tapToMove")}

+
+
+
+ ); +}; diff --git a/app/_components/FeatureComponents/Layout/Sidebar.tsx b/app/_components/FeatureComponents/Layout/Sidebar.tsx index 0102260..994e51c 100644 --- a/app/_components/FeatureComponents/Layout/Sidebar.tsx +++ b/app/_components/FeatureComponents/Layout/Sidebar.tsx @@ -2,15 +2,15 @@ import { cn } from "@/app/_utils/global-utils"; import { HTMLAttributes, forwardRef, useState, useEffect } from "react"; import React from "react"; import { - ChevronLeft, - ChevronRight, - Server, - Menu, - X, - Cpu, - HardDrive, - Wifi, -} from "lucide-react"; + CaretLeftIcon, + CaretRightIcon, + HardDrivesIcon, + ListIcon, + XIcon, + CpuIcon, + HardDriveIcon, + WifiHighIcon, +} from "@phosphor-icons/react"; import { useTranslations } from "next-intl"; export interface SidebarProps extends HTMLAttributes { @@ -54,18 +54,18 @@ export const Sidebar = forwardRef( <>
setIsMobileOpen(false)} @@ -74,7 +74,7 @@ export const Sidebar = forwardRef(
( > -
-
-
- -
- {(!isCollapsed || !isCollapsed) && ( -

- {t("sidebar.systemOverview")} -

- )} -
-
-
{isCollapsed ? ( @@ -131,22 +113,22 @@ export const Sidebar = forwardRef(
{quickStats ? ( <> -
- +
+ {quickStats.cpu}%
-
- +
+ {quickStats.memory}%
-
- +
+
{quickStats.network} @@ -163,9 +145,9 @@ export const Sidebar = forwardRef( return (
- +
); } diff --git a/app/_components/FeatureComponents/Layout/TabbedInterface.tsx b/app/_components/FeatureComponents/Layout/TabbedInterface.tsx index 06d006d..ae04087 100644 --- a/app/_components/FeatureComponents/Layout/TabbedInterface.tsx +++ b/app/_components/FeatureComponents/Layout/TabbedInterface.tsx @@ -5,7 +5,7 @@ import { CronJobList } from "@/app/_components/FeatureComponents/Cronjobs/CronJo import { ScriptsManager } from "@/app/_components/FeatureComponents/Scripts/ScriptsManager"; import { CronJob } from "@/app/_utils/cronjob-utils"; import { Script } from "@/app/_utils/scripts-utils"; -import { Clock, FileText } from "lucide-react"; +import { ClockIcon, FileTextIcon } from "@phosphor-icons/react"; import { useTranslations } from "next-intl"; interface TabbedInterfaceProps { @@ -24,33 +24,31 @@ export const TabbedInterface = ({ return (
-
-
+
+
diff --git a/app/_components/FeatureComponents/LoginForm/LoginForm.tsx b/app/_components/FeatureComponents/LoginForm/LoginForm.tsx index fb5fb6a..3583aa1 100644 --- a/app/_components/FeatureComponents/LoginForm/LoginForm.tsx +++ b/app/_components/FeatureComponents/LoginForm/LoginForm.tsx @@ -12,7 +12,7 @@ import { CardDescription, CardContent, } from "@/app/_components/GlobalComponents/Cards/Card"; -import { Lock, Eye, EyeOff, Shield, AlertTriangle, Loader2 } from "lucide-react"; +import { LockIcon, EyeIcon, EyeSlashIcon, ShieldIcon, WarningIcon, CircleNotchIcon } from "@phosphor-icons/react"; interface LoginFormProps { hasPassword?: boolean; @@ -88,7 +88,7 @@ export const LoginForm = ({
- +

{t("login.redirectingToOIDC")}

@@ -105,15 +105,15 @@ export const LoginForm = ({

- +
{t("login.welcomeTitle")} {hasPassword && hasOIDC ? t("login.signInWithPasswordOrSSO") : hasOIDC - ? t("login.signInWithSSO") - : t("login.enterPasswordToContinue")} + ? t("login.signInWithSSO") + : t("login.enterPasswordToContinue")} @@ -121,7 +121,7 @@ export const LoginForm = ({ {!hasPassword && !hasOIDC && (
- +
{t("login.authenticationNotConfigured")} @@ -152,9 +152,9 @@ export const LoginForm = ({ disabled={isLoading} > {showPassword ? ( - + ) : ( - + )}
@@ -175,7 +175,7 @@ export const LoginForm = ({
- + {t("login.orContinueWith")}
@@ -190,7 +190,7 @@ export const LoginForm = ({ onClick={handleOIDCLogin} disabled={isLoading} > - + {isLoading ? t("login.redirecting") : t("login.signInWithSSO")} )} @@ -203,7 +203,7 @@ export const LoginForm = ({
{version && ( -
+
Cr*nMaster {t("common.version", { version })}
diff --git a/app/_components/FeatureComponents/LoginForm/LogoutButton.tsx b/app/_components/FeatureComponents/LoginForm/LogoutButton.tsx index d3c05d4..3141841 100644 --- a/app/_components/FeatureComponents/LoginForm/LogoutButton.tsx +++ b/app/_components/FeatureComponents/LoginForm/LogoutButton.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { Button } from "@/app/_components/GlobalComponents/UIElements/Button"; -import { LogOut } from "lucide-react"; +import { SignOutIcon } from "@phosphor-icons/react"; export const LogoutButton = () => { const [isLoading, setIsLoading] = useState(false); @@ -35,7 +35,7 @@ export const LogoutButton = () => { disabled={isLoading} title="Logout" > - + Logout ); diff --git a/app/_components/FeatureComponents/Modals/CloneScriptModal.tsx b/app/_components/FeatureComponents/Modals/CloneScriptModal.tsx index befa1df..47e3b13 100644 --- a/app/_components/FeatureComponents/Modals/CloneScriptModal.tsx +++ b/app/_components/FeatureComponents/Modals/CloneScriptModal.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { Copy } from "lucide-react"; +import { CopyIcon } from "@phosphor-icons/react"; import { Button } from "@/app/_components/GlobalComponents/UIElements/Button"; import { Modal } from "@/app/_components/GlobalComponents/UIElements/Modal"; import { Input } from "@/app/_components/GlobalComponents/FormElements/Input"; @@ -89,7 +89,7 @@ export const CloneScriptModal = ({ ) : ( <> - + Clone Script )} diff --git a/app/_components/FeatureComponents/Modals/CloneTaskModal.tsx b/app/_components/FeatureComponents/Modals/CloneTaskModal.tsx index 5244c8d..9806027 100644 --- a/app/_components/FeatureComponents/Modals/CloneTaskModal.tsx +++ b/app/_components/FeatureComponents/Modals/CloneTaskModal.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { Copy } from "lucide-react"; +import { CopyIcon } from "@phosphor-icons/react"; import { Button } from "@/app/_components/GlobalComponents/UIElements/Button"; import { Modal } from "@/app/_components/GlobalComponents/UIElements/Modal"; import { Input } from "@/app/_components/GlobalComponents/FormElements/Input"; @@ -89,7 +89,7 @@ export const CloneTaskModal = ({ ) : ( <> - + Clone Cron Job )} diff --git a/app/_components/FeatureComponents/Modals/CreateScriptModal.tsx b/app/_components/FeatureComponents/Modals/CreateScriptModal.tsx index 74905c7..5cf8f1c 100644 --- a/app/_components/FeatureComponents/Modals/CreateScriptModal.tsx +++ b/app/_components/FeatureComponents/Modals/CreateScriptModal.tsx @@ -1,6 +1,6 @@ "use client"; -import { Plus } from "lucide-react"; +import { PlusIcon } from "@phosphor-icons/react"; import { ScriptModal } from "@/app/_components/FeatureComponents/Modals/ScriptModal"; interface CreateScriptModalProps { @@ -35,7 +35,7 @@ export const CreateScriptModal = ({ onSubmit={onSubmit} title="Create New Script" submitButtonText="Create Script" - submitButtonIcon={} + submitButtonIcon={} form={form} onFormChange={onFormChange} isDraft={isDraft} diff --git a/app/_components/FeatureComponents/Modals/CreateTaskModal.tsx b/app/_components/FeatureComponents/Modals/CreateTaskModal.tsx index a850a2f..c026225 100644 --- a/app/_components/FeatureComponents/Modals/CreateTaskModal.tsx +++ b/app/_components/FeatureComponents/Modals/CreateTaskModal.tsx @@ -4,10 +4,11 @@ import { useState, useEffect } from "react"; import { Modal } from "@/app/_components/GlobalComponents/UIElements/Modal"; import { Button } from "@/app/_components/GlobalComponents/UIElements/Button"; import { Input } from "@/app/_components/GlobalComponents/FormElements/Input"; +import { Switch } from "@/app/_components/GlobalComponents/UIElements/Switch"; import { CronExpressionHelper } from "@/app/_components/FeatureComponents/Scripts/CronExpressionHelper"; import { SelectScriptModal } from "@/app/_components/FeatureComponents/Modals/SelectScriptModal"; import { UserSwitcher } from "@/app/_components/FeatureComponents/User/UserSwitcher"; -import { Plus, Terminal, FileText, X, FileOutput } from "lucide-react"; +import { PlusIcon, TerminalIcon, FileTextIcon, XIcon, FileArrowDownIcon } from "@phosphor-icons/react"; import { getScriptContent } from "@/app/_server/actions/scripts"; import { getHostScriptPath } from "@/app/_server/actions/scripts"; import { useTranslations } from "next-intl"; @@ -100,7 +101,7 @@ export const CreateTaskModal = ({ onFormChange({ user })} + onUserChange={(user: string) => onFormChange({ user })} />
@@ -124,13 +125,13 @@ export const CreateTaskModal = ({
@@ -222,12 +223,12 @@ export const CreateTaskModal = ({ ? "/app/scripts/script_name.sh" : "/usr/bin/command" } - className="w-full h-24 p-2 border border-border rounded bg-background text-foreground font-mono text-sm resize-none focus:outline-none focus:ring-1 focus:ring-primary/20" + className="w-full h-24 p-2 border border-border rounded bg-background0 text-foreground font-mono text-sm resize-none focus:outline-none focus:ring-1 focus:ring-primary/20" required readOnly={!!form.selectedScriptId} />
- +
{form.selectedScriptId && ( @@ -249,29 +250,28 @@ export const CreateTaskModal = ({ value={form.comment} onChange={(e) => onFormChange({ comment: e.target.value })} placeholder={t("cronjobs.whatDoesThisTaskDo")} - className="bg-muted/30 border-border/50 focus:border-primary/50" + className="bg-muted/30 border-border focus:border-primary/50" />
-
-
- +
onFormChange({ logsEnabled: !form.logsEnabled })} + > + - onFormChange({ logsEnabled: e.target.checked }) + onCheckedChange={(checked) => + onFormChange({ logsEnabled: checked }) } - className="mt-1 h-4 w-4 rounded border-border text-primary focus:ring-primary/20 cursor-pointer" + className="mt-1" />
-
-
+ +
diff --git a/app/_components/FeatureComponents/Modals/DeleteScriptModal.tsx b/app/_components/FeatureComponents/Modals/DeleteScriptModal.tsx index 80a90a2..d3b83cf 100644 --- a/app/_components/FeatureComponents/Modals/DeleteScriptModal.tsx +++ b/app/_components/FeatureComponents/Modals/DeleteScriptModal.tsx @@ -2,7 +2,7 @@ import { Modal } from "@/app/_components/GlobalComponents/UIElements/Modal"; import { Button } from "@/app/_components/GlobalComponents/UIElements/Button"; -import { FileText, AlertCircle, Trash2 } from "lucide-react"; +import { FileTextIcon, WarningCircleIcon, TrashIcon } from "@phosphor-icons/react"; import { Script } from "@/app/_utils/scripts-utils"; interface DeleteScriptModalProps { @@ -25,10 +25,10 @@ export const DeleteScriptModal = ({ return (
-
+
- + {script.name} @@ -36,7 +36,7 @@ export const DeleteScriptModal = ({ {script.description && (
- +

{script.description}

@@ -44,8 +44,8 @@ export const DeleteScriptModal = ({ )}
- - + + {script.filename}
@@ -54,7 +54,7 @@ export const DeleteScriptModal = ({
- +

This action cannot be undone @@ -66,7 +66,7 @@ export const DeleteScriptModal = ({

-
+
diff --git a/app/_components/FeatureComponents/Modals/EditScriptModal.tsx b/app/_components/FeatureComponents/Modals/EditScriptModal.tsx index 2c24743..770d161 100644 --- a/app/_components/FeatureComponents/Modals/EditScriptModal.tsx +++ b/app/_components/FeatureComponents/Modals/EditScriptModal.tsx @@ -1,6 +1,6 @@ "use client"; -import { Edit } from "lucide-react"; +import { PencilSimpleIcon } from "@phosphor-icons/react"; import { Script } from "@/app/_utils/scripts-utils"; import { ScriptModal } from "@/app/_components/FeatureComponents/Modals/ScriptModal"; @@ -36,7 +36,7 @@ export const EditScriptModal = ({ onSubmit={onSubmit} title="Edit Script" submitButtonText="Update Script" - submitButtonIcon={} + submitButtonIcon={} form={form} onFormChange={onFormChange} additionalFormData={{ id: script.id }} diff --git a/app/_components/FeatureComponents/Modals/EditTaskModal.tsx b/app/_components/FeatureComponents/Modals/EditTaskModal.tsx index 928ebef..260c579 100644 --- a/app/_components/FeatureComponents/Modals/EditTaskModal.tsx +++ b/app/_components/FeatureComponents/Modals/EditTaskModal.tsx @@ -3,8 +3,9 @@ import { Modal } from "@/app/_components/GlobalComponents/UIElements/Modal"; import { Button } from "@/app/_components/GlobalComponents/UIElements/Button"; import { Input } from "@/app/_components/GlobalComponents/FormElements/Input"; +import { Switch } from "@/app/_components/GlobalComponents/UIElements/Switch"; import { CronExpressionHelper } from "@/app/_components/FeatureComponents/Scripts/CronExpressionHelper"; -import { Edit, Terminal, FileOutput } from "lucide-react"; +import { PencilSimpleIcon, TerminalIcon, FileArrowDownIcon } from "@phosphor-icons/react"; import { useTranslations } from "next-intl"; interface EditTaskModalProps { @@ -59,11 +60,11 @@ export const EditTaskModal = ({ value={form.command} onChange={(e) => onFormChange({ command: e.target.value })} placeholder="/usr/bin/command" - className="font-mono bg-muted/30 border-border/50 focus:border-primary/50" + className="font-mono bg-muted/30 border-border focus:border-primary/50" required />
- +
@@ -80,27 +81,26 @@ export const EditTaskModal = ({ value={form.comment} onChange={(e) => onFormChange({ comment: e.target.value })} placeholder={t("cronjobs.whatDoesThisTaskDo")} - className="bg-muted/30 border-border/50 focus:border-primary/50" + className="bg-muted/30 border-border focus:border-primary/50" />
-
-
- +
onFormChange({ logsEnabled: !form.logsEnabled })} + > + onFormChange({ logsEnabled: e.target.checked })} - className="mt-1 h-4 w-4 rounded border-border text-primary focus:ring-primary/20 cursor-pointer" + onCheckedChange={(checked) => onFormChange({ logsEnabled: checked })} + className="mt-1" />
-
-
+ +
diff --git a/app/_components/FeatureComponents/Modals/ErrorDetailsModal.tsx b/app/_components/FeatureComponents/Modals/ErrorDetailsModal.tsx index d6e34ca..dbcd84b 100644 --- a/app/_components/FeatureComponents/Modals/ErrorDetailsModal.tsx +++ b/app/_components/FeatureComponents/Modals/ErrorDetailsModal.tsx @@ -2,7 +2,7 @@ import { Modal } from "@/app/_components/GlobalComponents/UIElements/Modal"; import { Button } from "@/app/_components/GlobalComponents/UIElements/Button"; -import { AlertCircle, Copy, X } from "lucide-react"; +import { WarningCircleIcon, CopyIcon, XIcon } from "@phosphor-icons/react"; import { showToast } from "@/app/_components/GlobalComponents/UIElements/Toast"; interface ErrorDetails { @@ -54,7 +54,7 @@ Timestamp: ${error.timestamp}
- +

{error.title} @@ -69,7 +69,7 @@ Timestamp: ${error.timestamp}

Details

-
+
                 {error.details}
               
@@ -82,7 +82,7 @@ Timestamp: ${error.timestamp}

Command

-
+
{error.command} @@ -93,7 +93,7 @@ Timestamp: ${error.timestamp} {error.output && (

Output

-
+
                 {error.output}
               
@@ -106,7 +106,7 @@ Timestamp: ${error.timestamp}

Error Output

-
+
                 {error.stderr}
               
@@ -118,14 +118,14 @@ Timestamp: ${error.timestamp} Timestamp: {error.timestamp}
-
+
{isScheduleDropdownOpen && ( -
+
@@ -144,7 +141,7 @@ export const FiltersModal = ({
-
+
diff --git a/app/_components/FeatureComponents/Modals/LiveLogModal.tsx b/app/_components/FeatureComponents/Modals/LiveLogModal.tsx index ffe72a0..d84b0e3 100644 --- a/app/_components/FeatureComponents/Modals/LiveLogModal.tsx +++ b/app/_components/FeatureComponents/Modals/LiveLogModal.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect, useRef, useCallback } from "react"; -import { Loader2, CheckCircle2, XCircle, AlertTriangle, Minimize2, Maximize2 } from "lucide-react"; +import { CircleNotchIcon, CheckCircleIcon, XCircleIcon, WarningIcon, ArrowsInIcon, ArrowsOutIcon } from "@phosphor-icons/react"; import { Modal } from "@/app/_components/GlobalComponents/UIElements/Modal"; import { Button } from "@/app/_components/GlobalComponents/UIElements/Button"; import { useSSEContext } from "@/app/_contexts/SSEContext"; @@ -228,20 +228,20 @@ export const LiveLogModal = ({
{t("cronjobs.liveJobExecution")}{jobComment && `: ${jobComment}`} {status === "running" && ( - - + + {t("cronjobs.running")} )} {status === "completed" && ( - - + + {t("cronjobs.completed", { exitCode: exitCode ?? 0 })} )} {status === "failed" && ( - - + + {t("cronjobs.jobFailed", { exitCode: exitCode ?? 1 })} )} @@ -268,7 +268,7 @@ export const LiveLogModal = ({ id="maxLines" value={maxLines} onChange={(e) => setMaxLines(parseInt(e.target.value, 10))} - className="bg-background border border-border rounded px-2 py-1 text-sm" + className="bg-background0 border border-border rounded px-2 py-1 text-sm" > @@ -316,8 +316,8 @@ export const LiveLogModal = ({ )}
{truncated && !showFullLog && ( -
- +
+ {t("cronjobs.showingLastOf", { lineCount: lineCount.toLocaleString(), totalLines: totalLines.toLocaleString() @@ -327,8 +327,8 @@ export const LiveLogModal = ({
{showSizeWarning && ( -
- +
+

{t("cronjobs.largeLogFileDetected")} ({formatFileSize(fileSize)}) @@ -340,16 +340,16 @@ export const LiveLogModal = ({ variant="ghost" size="sm" onClick={toggleTailMode} - className="text-orange-500 hover:text-orange-400 hover:bg-orange-500/10 h-auto py-1 px-2 text-xs" + className="text-status-warning hover:text-status-warning hover:bg-background2 h-auto py-1 px-2 text-xs" title={tailMode ? t("cronjobs.showAllLines") : t("cronjobs.enableTailMode")} > - {tailMode ? : } + {tailMode ? : }

)} -
-
+        
+
             {logContent || t("cronjobs.waitingForJobToStart")}
             
diff --git a/app/_components/FeatureComponents/Modals/LogsModal.tsx b/app/_components/FeatureComponents/Modals/LogsModal.tsx index a462955..d7afa41 100644 --- a/app/_components/FeatureComponents/Modals/LogsModal.tsx +++ b/app/_components/FeatureComponents/Modals/LogsModal.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from "react"; import { Modal } from "@/app/_components/GlobalComponents/UIElements/Modal"; import { Button } from "@/app/_components/GlobalComponents/UIElements/Button"; -import { FileText, Trash2, Eye, X, RefreshCw, AlertCircle, CheckCircle } from "lucide-react"; +import { FileTextIcon, TrashIcon, EyeIcon, XIcon, ArrowsClockwiseIcon, WarningCircleIcon, CheckCircleIcon } from "@phosphor-icons/react"; import { useTranslations } from "next-intl"; import { getJobLogs, @@ -173,7 +173,7 @@ export const LogsModal = ({ className="btn-primary glow-primary" size="sm" > - @@ -182,10 +182,10 @@ export const LogsModal = ({ {logs.length > 0 && ( )} @@ -208,11 +208,11 @@ export const LogsModal = ({ logs.map((log) => (
handleViewLog(log.filename)} > @@ -220,11 +220,11 @@ export const LogsModal = ({
{log.hasError ? ( - + ) : log.exitCode === 0 ? ( - + ) : ( - + )} {formatTimestamp(log.timestamp)} @@ -236,9 +236,9 @@ export const LogsModal = ({

{log.exitCode !== undefined && ( Exit: {log.exitCode} @@ -251,10 +251,10 @@ export const LogsModal = ({ e.stopPropagation(); handleDeleteLog(log.filename); }} - className="btn-destructive glow-primary p-1 h-auto" + variant="destructive" size="sm" > - +
@@ -271,13 +271,13 @@ export const LogsModal = ({ {t("common.loading")}...
) : selectedLog ? ( -
+                
                   {logContent}
                 
) : (
- +

{t("cronjobs.selectLogToView")}

@@ -288,7 +288,7 @@ export const LogsModal = ({
diff --git a/app/_components/FeatureComponents/Modals/RestoreBackupModal.tsx b/app/_components/FeatureComponents/Modals/RestoreBackupModal.tsx index 2fb4795..98651a7 100644 --- a/app/_components/FeatureComponents/Modals/RestoreBackupModal.tsx +++ b/app/_components/FeatureComponents/Modals/RestoreBackupModal.tsx @@ -4,14 +4,14 @@ import { useState, useEffect } from "react"; import { Modal } from "@/app/_components/GlobalComponents/UIElements/Modal"; import { Button } from "@/app/_components/GlobalComponents/UIElements/Button"; import { - Upload, - Trash2, - Calendar, - User, - Download, - RefreshCw, - Check, -} from "lucide-react"; + UploadIcon, + TrashIcon, + CalendarIcon, + UserIcon, + DownloadIcon, + ArrowsClockwiseIcon, + CheckIcon, +} from "@phosphor-icons/react"; import { useTranslations } from "next-intl"; import { CronJob } from "@/app/_utils/cronjob-utils"; import { unwrapCommand } from "@/app/_utils/wrapper-utils-client"; @@ -86,14 +86,15 @@ export const RestoreBackupModal = ({ size="xl" >
-
+
{backups.length > 0 && ( )}
@@ -120,15 +123,80 @@ export const RestoreBackupModal = ({

{t("cronjobs.noBackupsFound")}

) : ( -
+
{backups.map((backup) => (
-
+
+
+ + {backup.job.schedule} + +
+ + +
+
+ +
+ {commandCopied === backup.filename && ( + + )} +
 {
+                        e.stopPropagation();
+                        copyToClipboard(unwrapCommand(backup.job.command));
+                        setCommandCopied(backup.filename);
+                        setTimeout(() => setCommandCopied(null), 3000);
+                      }}
+                      className="max-w-full overflow-x-auto flex-1 cursor-pointer text-sm font-medium terminal-font bg-background1 px-2 py-1 ascii-border break-all"
+                      title={unwrapCommand(backup.job.command)}
+                    >
+                      {unwrapCommand(backup.job.command)}
+                    
+
+ +
+
+ + {backup.job.user} +
+
+ + {formatDate(backup.backedUpAt)} +
+
+
+ +
- + {backup.job.schedule}
@@ -136,7 +204,7 @@ export const RestoreBackupModal = ({
{commandCopied === backup.filename && ( - + )}
 {
@@ -145,7 +213,7 @@ export const RestoreBackupModal = ({
                           setCommandCopied(backup.filename);
                           setTimeout(() => setCommandCopied(null), 3000);
                         }}
-                        className="flex-1 cursor-pointer overflow-hidden text-sm font-medium text-foreground bg-muted/30 px-2 py-1 rounded border border-border/30 truncate"
+                        className="flex-1 cursor-pointer overflow-hidden text-sm font-medium terminal-font bg-background1 px-2 py-1 ascii-border truncate"
                         title={unwrapCommand(backup.job.command)}
                       >
                         {unwrapCommand(backup.job.command)}
@@ -155,47 +223,47 @@ export const RestoreBackupModal = ({
 
                   
- + {backup.job.user}
- + {formatDate(backup.backedUpAt)}
-
+
{backup.job.comment && ( -

+

{backup.job.comment}

)} @@ -204,11 +272,11 @@ export const RestoreBackupModal = ({
)} -
-

+

+

{t("cronjobs.availableBackups")}: {backups.length}

-
diff --git a/app/_components/FeatureComponents/Modals/ScriptModal.tsx b/app/_components/FeatureComponents/Modals/ScriptModal.tsx index c09a4a0..145c295 100644 --- a/app/_components/FeatureComponents/Modals/ScriptModal.tsx +++ b/app/_components/FeatureComponents/Modals/ScriptModal.tsx @@ -6,7 +6,7 @@ import { Input } from "@/app/_components/GlobalComponents/FormElements/Input"; import { BashEditor } from "@/app/_components/FeatureComponents/Scripts/BashEditor"; import { BashSnippetHelper } from "@/app/_components/FeatureComponents/Scripts/BashSnippetHelper"; import { showToast } from "@/app/_components/GlobalComponents/UIElements/Toast"; -import { FileText, Code, Info, Trash2 } from "lucide-react"; +import { FileTextIcon, CodeIcon, InfoIcon, TrashIcon } from "@phosphor-icons/react"; import { useTranslations } from "next-intl"; interface ScriptModalProps { @@ -80,11 +80,11 @@ export const ScriptModal = ({ return ( -
+
-
-