Compare commits
11 Commits
feature/re
...
2.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f098ded0c4 | ||
|
|
3ac9a5ca30 | ||
|
|
c708c013f3 | ||
|
|
fb6531d00d | ||
|
|
46e0838792 | ||
|
|
72f1c0a66d | ||
|
|
1adad49020 | ||
|
|
b2dc0a3cb3 | ||
|
|
4beb7053f7 | ||
|
|
d6b6aff44e | ||
|
|
0ab3358e28 |
44
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
|
||||
|
||||
RUN mkdir -p /app/.next/cache && \
|
||||
chown -R nextjs:nodejs /app/.next
|
||||
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
|
||||
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/app ./app
|
||||
|
||||
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
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
CMD ["yarn", "start"]
|
||||
USER nextjs
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
30
README.md
@@ -2,7 +2,7 @@
|
||||
<img src="public/heading.png" width="400px">
|
||||
</p>
|
||||
|
||||
## Table of Contents
|
||||
## Quick links
|
||||
|
||||
- [Features](#features)
|
||||
- [Quick Start](#quick-start)
|
||||
@@ -18,9 +18,16 @@
|
||||
- [Managing Cron Jobs](#managing-cron-jobs)
|
||||
- [Job Execution Logging](#job-execution-logging)
|
||||
- [Managing Scripts](#managing-scripts)
|
||||
- [Technologies Used](#technologies-used)
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
| Desktop | Mobile |
|
||||
|---------|--------|
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
</div>
|
||||
|
||||
## Features
|
||||
|
||||
@@ -49,26 +56,13 @@
|
||||
<br />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
<br />
|
||||
|
||||
## 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! ☕
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.buymeacoffee.com/fccview">
|
||||
<img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy me a coffee" width="150">
|
||||
<img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy me a coffee" width="120">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<img width="500px" src="screenshots/home.png">
|
||||
<img width="500px" src="screenshots/live-running.png" />
|
||||
</div>
|
||||
---
|
||||
|
||||
<a id="quick-start"></a>
|
||||
|
||||
|
||||
@@ -236,7 +236,7 @@ export const CronJobList = ({ cronJobs, scripts }: CronJobListProps) => {
|
||||
<CardHeader>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-primary/10 rounded-lg">
|
||||
<div className="p-2 bg-primary/10 ascii-border">
|
||||
<ClockIcon className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -301,7 +301,7 @@ export const CronJobList = ({ cronJobs, scripts }: CronJobListProps) => {
|
||||
onNewTaskClick={() => setIsNewCronModalOpen(true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-4 max-h-[55vh] min-h-[55vh] overflow-y-auto tui-scrollbar pr-2">
|
||||
<div className="space-y-4 max-h-[55vh] min-h-[55vh] overflow-y-auto tui-scrollbar pr-1">
|
||||
{loadedSettings ? (
|
||||
filteredJobs.map((job) =>
|
||||
minimalMode ? (
|
||||
|
||||
@@ -148,7 +148,7 @@ export const CronJobItem = ({
|
||||
return (
|
||||
<div
|
||||
key={job.id}
|
||||
className={`tui-card p-4 terminal-font transition-colors ${isDropdownOpen ? "relative z-10" : ""
|
||||
className={`border border-border lg:tui-card p-4 terminal-font transition-colors ${isDropdownOpen ? "relative z-10" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-start gap-4">
|
||||
|
||||
@@ -139,7 +139,7 @@ export const MinimalCronJobItem = ({
|
||||
return (
|
||||
<div
|
||||
key={job.id}
|
||||
className={`tui-card p-3 terminal-font transition-colors ${isDropdownOpen ? "relative z-10" : ""
|
||||
className={`border border-border lg:tui-card p-3 terminal-font transition-colors ${isDropdownOpen ? "relative z-10" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -238,7 +238,7 @@ export const MinimalCronJobItem = ({
|
||||
size="sm"
|
||||
onClick={() => onRun(job.id)}
|
||||
disabled={runningJobId === job.id || job.paused}
|
||||
className="btn-outline h-8 px-3"
|
||||
className="btn-outline h-8 px-3 hidden md:flex"
|
||||
title={t("cronjobs.runCronManually")}
|
||||
aria-label={t("cronjobs.runCronManually")}
|
||||
>
|
||||
@@ -259,7 +259,7 @@ export const MinimalCronJobItem = ({
|
||||
onPause(job.id);
|
||||
}
|
||||
}}
|
||||
className="btn-outline h-8 px-3"
|
||||
className="btn-outline h-8 px-3 hidden md:flex"
|
||||
title={t("cronjobs.pauseCronJob")}
|
||||
aria-label={t("cronjobs.pauseCronJob")}
|
||||
>
|
||||
@@ -280,7 +280,7 @@ export const MinimalCronJobItem = ({
|
||||
onToggleLogging(job.id);
|
||||
}
|
||||
}}
|
||||
className="btn-outline h-8 px-3"
|
||||
className="btn-outline h-8 px-3 hidden md:flex"
|
||||
title={
|
||||
job.logsEnabled
|
||||
? t("cronjobs.viewLogs")
|
||||
|
||||
@@ -101,29 +101,11 @@ export const Sidebar = forwardRef<HTMLDivElement, SidebarProps>(
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="p-4 ascii-border !border-t-0 border-l-0 !border-r-0 bg-background0">
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-3",
|
||||
isCollapsed && "lg:justify-center"
|
||||
)}
|
||||
>
|
||||
<div className="p-2 bg-background0 ascii-border flex-shrink-0">
|
||||
<HardDrivesIcon className="h-4 w-4" />
|
||||
</div>
|
||||
{(!isCollapsed || !isCollapsed) && (
|
||||
<h2 className="text-sm font-semibold truncate terminal-font">
|
||||
{t("sidebar.systemOverview")}
|
||||
</h2>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-y-auto tui-scrollbar",
|
||||
isCollapsed ? "lg:p-2" : "p-4",
|
||||
"h-full lg:h-[calc(100vh-88px-80px)]"
|
||||
"h-full lg:h-[calc(100vh-88px)]"
|
||||
)}
|
||||
>
|
||||
{isCollapsed ? (
|
||||
|
||||
@@ -28,7 +28,7 @@ export const TabbedInterface = ({
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setActiveTab("cronjobs")}
|
||||
className={`flex items-center gap-2 px-4 py-2 text-sm font-medium flex-1 justify-center terminal-font ${activeTab === "cronjobs"
|
||||
className={`flex items-center gap-2 px-4 py-2 border border-transparent text-sm font-medium flex-1 justify-center terminal-font ${activeTab === "cronjobs"
|
||||
? "bg-background0 ascii-border"
|
||||
: "hover:ascii-border"
|
||||
}`}
|
||||
@@ -41,7 +41,7 @@ export const TabbedInterface = ({
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("scripts")}
|
||||
className={`flex items-center gap-2 px-4 py-2 text-sm font-medium flex-1 justify-center terminal-font ${activeTab === "scripts"
|
||||
className={`flex items-center gap-2 px-4 py-2 border border-transparent text-sm font-medium flex-1 justify-center terminal-font ${activeTab === "scripts"
|
||||
? "bg-background0 ascii-border"
|
||||
: "hover:ascii-border"
|
||||
}`}
|
||||
|
||||
@@ -94,14 +94,14 @@ export const FiltersModal = ({
|
||||
</Button>
|
||||
|
||||
{isScheduleDropdownOpen && (
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-background0 border border-border rounded-md shadow-lg z-50 min-w-[140px]">
|
||||
<div className="absolute top-full left-0 p-1 right-0 mt-1 bg-background0 border border-border rounded-md shadow-lg z-50 min-w-[140px]">
|
||||
<button
|
||||
onClick={() => {
|
||||
setLocalScheduleMode("cron");
|
||||
setIsScheduleDropdownOpen(false);
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors flex items-center gap-2 ${localScheduleMode === "cron"
|
||||
? "bg-accent text-accent-foreground"
|
||||
className={`w-full text-left px-3 border py-2 text-sm hover:border-border border-transparent transition-colors flex items-center gap-2 ${localScheduleMode === "cron"
|
||||
? "border-border"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
@@ -113,8 +113,8 @@ export const FiltersModal = ({
|
||||
setLocalScheduleMode("human");
|
||||
setIsScheduleDropdownOpen(false);
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors flex items-center gap-2 ${localScheduleMode === "human"
|
||||
? "bg-accent text-accent-foreground"
|
||||
className={`w-full text-left px-3 py-2 border text-sm hover:border-border border-transparent transition-colors flex items-center gap-2 ${localScheduleMode === "human"
|
||||
? "border-border"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
@@ -126,8 +126,8 @@ export const FiltersModal = ({
|
||||
setLocalScheduleMode("both");
|
||||
setIsScheduleDropdownOpen(false);
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors flex items-center gap-2 ${localScheduleMode === "both"
|
||||
? "bg-accent text-accent-foreground"
|
||||
className={`w-full text-left px-3 py-2 border text-sm hover:border-border border-transparent transition-colors flex items-center gap-2 ${localScheduleMode === "both"
|
||||
? "border-border"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -348,8 +348,8 @@ export const LiveLogModal = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-black/90 dark:bg-black/60 p-4 max-h-[60vh] overflow-auto terminal-font ascii-border">
|
||||
<pre className="text-xs font-mono text-status-success whitespace-pre-wrap break-words">
|
||||
<div className="bg-background0 p-4 max-h-[60vh] overflow-auto terminal-font ascii-border">
|
||||
<pre className="text-xs text-status-success whitespace-pre-wrap break-words">
|
||||
{logContent || t("cronjobs.waitingForJobToStart")}
|
||||
<div ref={logEndRef} />
|
||||
</pre>
|
||||
|
||||
@@ -86,14 +86,15 @@ export const RestoreBackupModal = ({
|
||||
size="xl"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onBackupAll}
|
||||
className="btn-outline flex-1"
|
||||
>
|
||||
<DownloadIcon className="h-4 w-4 mr-2" />
|
||||
{t("cronjobs.backupAll")}
|
||||
<DownloadIcon className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">{t("cronjobs.backupAll")}</span>
|
||||
<span className="sm:hidden">Backup</span>
|
||||
</Button>
|
||||
{backups.length > 0 && (
|
||||
<Button
|
||||
@@ -101,17 +102,19 @@ export const RestoreBackupModal = ({
|
||||
onClick={handleRestoreAll}
|
||||
className="btn-primary flex-1"
|
||||
>
|
||||
<UploadIcon className="h-4 w-4 mr-2" />
|
||||
{t("cronjobs.restoreAll")}
|
||||
<UploadIcon className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">{t("cronjobs.restoreAll")}</span>
|
||||
<span className="sm:hidden">Restore</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onRefresh}
|
||||
className="btn-outline"
|
||||
className="btn-outline sm:w-auto"
|
||||
title={t("common.refresh")}
|
||||
>
|
||||
<ArrowsClockwiseIcon className="h-4 w-4" />
|
||||
<span className="sm:hidden ml-2">Refresh</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -126,7 +129,72 @@ export const RestoreBackupModal = ({
|
||||
key={backup.filename}
|
||||
className="tui-card p-3 terminal-font"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col gap-3 lg:hidden">
|
||||
<div className="flex items-center justify-between">
|
||||
<code className="text-xs bg-background0 text-status-warning px-1.5 py-0.5 terminal-font ascii-border">
|
||||
{backup.job.schedule}
|
||||
</code>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
onRestore(backup.filename);
|
||||
onClose();
|
||||
}}
|
||||
className="btn-outline h-8 px-3"
|
||||
title={t("cronjobs.restoreThisBackup")}
|
||||
>
|
||||
<UploadIcon className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(backup.filename)}
|
||||
disabled={deletingFilename === backup.filename}
|
||||
className="h-8 px-3"
|
||||
title={t("cronjobs.deleteBackup")}
|
||||
>
|
||||
{deletingFilename === backup.filename ? (
|
||||
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
) : (
|
||||
<TrashIcon className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{commandCopied === backup.filename && (
|
||||
<CheckIcon className="h-3 w-3 text-status-success flex-shrink-0" />
|
||||
)}
|
||||
<pre
|
||||
onClick={(e) => {
|
||||
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)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<UserIcon className="h-3 w-3" />
|
||||
<span>{backup.job.user}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<CalendarIcon className="h-3 w-3" />
|
||||
<span>{formatDate(backup.backedUpAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:flex items-center gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
<code className="text-xs bg-background0 text-status-warning px-1.5 py-0.5 terminal-font ascii-border">
|
||||
{backup.job.schedule}
|
||||
@@ -204,11 +272,11 @@ export const RestoreBackupModal = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between gap-2 pt-4 border-t border-border">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between gap-2 pt-4 border-t border-border">
|
||||
<p className="text-sm text-muted-foreground text-center sm:text-left">
|
||||
{t("cronjobs.availableBackups")}: {backups.length}
|
||||
</p>
|
||||
<Button variant="outline" onClick={onClose} className="btn-outline">
|
||||
<Button variant="outline" onClick={onClose} className="btn-outline w-full sm:w-auto">
|
||||
{t("common.close")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,6 @@ export const PWAInstallPrompt = (): JSX.Element | null => {
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const onBeforeInstallPrompt = (e: Event) => {
|
||||
e.preventDefault();
|
||||
setDeferred(e as BeforeInstallPromptEvent);
|
||||
};
|
||||
const onAppInstalled = () => {
|
||||
@@ -49,10 +48,10 @@ export const PWAInstallPrompt = (): JSX.Element | null => {
|
||||
|
||||
return (
|
||||
<button
|
||||
className="px-3 py-1 rounded-md border border-border bg-background/80 hover:bg-background/60"
|
||||
className="px-3 py-2 ascii-border bg-background0 hover:bg-background1 transition-colors terminal-font text-sm"
|
||||
onClick={onInstall}
|
||||
>
|
||||
Install App
|
||||
Install
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -180,8 +180,8 @@ export const BashSnippetHelper = ({
|
||||
{snippet.title}
|
||||
</h4>
|
||||
{snippet.source === "user" && (
|
||||
<span className="inline-block px-1.5 py-0.5 text-xs bg-green-100 text-green-700 rounded border border-green-200">
|
||||
UserIcon
|
||||
<span className="inline-block px-1.5 py-0.5 text-xs text-status-success border border-border">
|
||||
User
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SunIcon, MoonIcon } from '@phosphor-icons/react';
|
||||
|
||||
export const ThemeToggle = () => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
@@ -18,9 +19,15 @@ export const ThemeToggle = () => {
|
||||
return (
|
||||
<button
|
||||
onClick={() => setTheme(isDark ? 'light' : 'dark')}
|
||||
className="px-3 py-2 ascii-border terminal-font text-sm bg-background0"
|
||||
className="p-2 ascii-border bg-background0 hover:bg-background1 transition-colors"
|
||||
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
>
|
||||
{isDark ? 'LIGHT' : 'DARK'}
|
||||
{isDark ? (
|
||||
<SunIcon size={20} weight="regular" className="text-foreground" />
|
||||
) : (
|
||||
<MoonIcon size={20} weight="regular" className="text-foreground" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -79,13 +79,13 @@ export const UserFilter = ({
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-background0 border border-border rounded-md shadow-lg z-50 max-h-48 overflow-y-auto tui-scrollbar">
|
||||
<div className="absolute top-full left-0 right-0 p-1 mt-1 bg-background0 border border-border rounded-md shadow-lg z-50 max-h-48 overflow-y-auto tui-scrollbar">
|
||||
<button
|
||||
onClick={() => {
|
||||
onUserChange(null);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors ${!selectedUser ? "bg-accent text-accent-foreground" : ""
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:border-border transition-colors ${!selectedUser ? "border border-border" : "border border-transparent"
|
||||
}`}
|
||||
>
|
||||
{t("common.allUsers")}
|
||||
@@ -97,7 +97,7 @@ export const UserFilter = ({
|
||||
onUserChange(user);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors ${selectedUser === user ? "bg-accent text-accent-foreground" : ""
|
||||
className={`w-full text-left px-3 py-2 text-sm border border-transparent hover:border-border transition-colors ${selectedUser === user ? "border border-border" : "border border-transparent"
|
||||
}`}
|
||||
>
|
||||
{user}
|
||||
|
||||
@@ -80,7 +80,7 @@ export const UserSwitcher = ({
|
||||
onUserChange(user);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors ${selectedUser === user ? "bg-accent text-accent-foreground" : ""
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors ${selectedUser === user ? "border border-border" : "border border-transparent"
|
||||
}`}
|
||||
>
|
||||
{user}
|
||||
|
||||
@@ -7,10 +7,10 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className = '', variant = 'default', size = 'default', children, ...props }, ref) => {
|
||||
const baseClasses = 'terminal-font ascii-border px-4 py-2 cursor-pointer inline-flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed';
|
||||
const baseClasses = 'terminal-font border border-border px-4 py-2 cursor-pointer inline-flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed';
|
||||
const variantClasses = {
|
||||
default: 'bg-background1 hover:bg-background2',
|
||||
destructive: 'bg-status-error text-white hover:bg-status-error',
|
||||
destructive: 'text-status-error hover:bg-status-error hover:text-white',
|
||||
outline: 'bg-background0 hover:bg-background1',
|
||||
secondary: 'bg-background2 hover:bg-background1',
|
||||
ghost: 'border-0 bg-transparent hover:bg-background1',
|
||||
|
||||
@@ -101,17 +101,17 @@ export const DropdownMenu = ({
|
||||
className={`absolute right-0 w-56 ascii-border bg-background0 shadow-lg z-[9999] overflow-hidden terminal-font ${positionAbove ? "bottom-full mb-2" : "top-full mt-2"
|
||||
}`}
|
||||
>
|
||||
<div className="py-1">
|
||||
<div className="p-1">
|
||||
{items.map((item, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleItemClick(item)}
|
||||
disabled={item.disabled}
|
||||
className={`w-full flex items-center gap-3 px-4 py-2 text-sm transition-colors ${item.disabled
|
||||
className={`w-full flex items-center border border-transparent gap-3 px-4 py-2 text-sm transition-colors ${item.disabled
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: item.variant === "destructive"
|
||||
? "text-status-error hover:bg-background1"
|
||||
: "hover:bg-background1"
|
||||
? "text-status-error hover:border hover:border-border"
|
||||
: "hover:border-border"
|
||||
}`}
|
||||
>
|
||||
{item.icon && (
|
||||
|
||||
@@ -52,7 +52,7 @@ export const Modal = ({
|
||||
return (
|
||||
<dialog
|
||||
ref={dialogRef}
|
||||
className={`ascii-border terminal-font bg-background0 ${sizeClasses[size]} max-w-[95vw] ${className}`}
|
||||
className={`ascii-border terminal-font bg-background0 mobile-modal ${sizeClasses[size]} max-w-[95vw] ${className}`}
|
||||
onClick={(e) => {
|
||||
if (e.target === dialogRef.current && !preventCloseOnClickOutside) {
|
||||
onClose();
|
||||
|
||||
@@ -30,6 +30,45 @@
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'IBM Plex Mono', monospace !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: 'Azeret Mono Variable', monospace !important;
|
||||
}
|
||||
|
||||
p,
|
||||
span,
|
||||
a,
|
||||
label,
|
||||
input,
|
||||
textarea,
|
||||
button,
|
||||
select,
|
||||
option {
|
||||
font-family: 'IBM Plex Mono', monospace !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
pre {
|
||||
scrollbar-width: none !important;
|
||||
-ms-overflow-style: none !important;
|
||||
}
|
||||
|
||||
pre::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
@layer utilities {
|
||||
.ascii-border {
|
||||
border: 1px solid var(--box-border-color, var(--foreground2));
|
||||
@@ -37,12 +76,18 @@
|
||||
}
|
||||
|
||||
.border-border {
|
||||
border-color: var(--box-border-color, var(--foreground2))!important;
|
||||
border-color: var(--box-border-color, var(--foreground2)) !important;
|
||||
}
|
||||
|
||||
.tui-scrollbar {
|
||||
scrollbar-width: auto !important;
|
||||
padding-right: 15px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.tui-scrollbar {
|
||||
padding-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.tui-scrollbar::-webkit-scrollbar {
|
||||
@@ -182,7 +227,6 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Sidebar layout adjustment */
|
||||
.no-sidebar main {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
@@ -205,3 +249,15 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.mobile-modal {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
top: auto;
|
||||
width: 100% !important;
|
||||
max-height: 90vh;
|
||||
max-width: 100%;
|
||||
margin: inherit;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,10 @@ import "@/app/globals.css";
|
||||
import { ThemeProvider } from "@/app/_providers/ThemeProvider";
|
||||
import { ServiceWorkerRegister } from "@/app/_components/FeatureComponents/PWA/ServiceWorkerRegister";
|
||||
import { loadTranslationMessages } from "@/app/_server/actions/translations";
|
||||
import '@fontsource/ibm-plex-mono/400.css';
|
||||
import '@fontsource/ibm-plex-mono/500.css';
|
||||
import '@fontsource/ibm-plex-mono/600.css';
|
||||
import '@fontsource-variable/azeret-mono';
|
||||
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
|
||||
@@ -27,7 +31,7 @@ export const metadata: Metadata = {
|
||||
telephone: false,
|
||||
},
|
||||
icons: {
|
||||
icon: "/logo.png",
|
||||
icon: "/favicon.png",
|
||||
shortcut: "/logo.png",
|
||||
apple: "/logo.png",
|
||||
},
|
||||
|
||||
@@ -78,7 +78,7 @@ export default async function Home() {
|
||||
<Logo size={48} showGlow={true} />
|
||||
<div>
|
||||
<h1 className="text-xl sm:text-2xl lg:text-3xl font-bold terminal-font uppercase">
|
||||
Cr*nMaster
|
||||
Cr<span className="text-status-error">*</span>nMaster
|
||||
</h1>
|
||||
<p className="text-xs terminal-font flex items-center gap-2">
|
||||
{t("common.version").replace("{version}", version)}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
CronMaster supports internationalization (i18n) with both **unofficial custom translations** and **official translations** that can be contributed to the project.
|
||||
|
||||
## Table of Contents
|
||||
## Quick links
|
||||
|
||||
- [Custom User Translations (Unofficial)](#custom-user-translations-unofficial)
|
||||
- [Official Translations via Pull Request](#official-translations-via-pull-request)
|
||||
|
||||
@@ -1,49 +1,55 @@
|
||||
const withNextIntl = require('next-intl/plugin')('./app/i18n.ts');
|
||||
|
||||
const withPWA = require('next-pwa')({
|
||||
dest: 'public',
|
||||
register: true,
|
||||
skipWaiting: true,
|
||||
disable: process.env.NODE_ENV === 'development',
|
||||
buildExcludes: [/middleware-manifest\.json$/]
|
||||
dest: 'public',
|
||||
register: true,
|
||||
skipWaiting: true,
|
||||
disable: process.env.NODE_ENV === 'development',
|
||||
buildExcludes: [/middleware-manifest\.json$/]
|
||||
})
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const withNextIntl = require('next-intl/plugin')('./app/i18n.ts')
|
||||
|
||||
const nextConfig = {
|
||||
webpack: (config, { dev, isServer }) => {
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
'osx-temperature-sensor': false,
|
||||
};
|
||||
output: 'standalone',
|
||||
experimental: {
|
||||
serverComponentsExternalPackages: [],
|
||||
webpackBuildWorker: true
|
||||
},
|
||||
swcMinify: true,
|
||||
images: {
|
||||
unoptimized: true
|
||||
},
|
||||
webpack: (config, { dev, isServer }) => {
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
'osx-temperature-sensor': false,
|
||||
};
|
||||
|
||||
if (dev && !isServer) {
|
||||
config.watchOptions = {
|
||||
...config.watchOptions,
|
||||
ignored: /node_modules/,
|
||||
};
|
||||
}
|
||||
if (dev && !isServer) {
|
||||
config.watchOptions = {
|
||||
...config.watchOptions,
|
||||
ignored: /node_modules/,
|
||||
};
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/manifest.json',
|
||||
headers: [
|
||||
{ key: 'Content-Type', value: 'application/manifest+json' },
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '/sw.js',
|
||||
headers: [
|
||||
{ key: 'Service-Worker-Allowed', value: '/' },
|
||||
{ key: 'Cache-Control', value: 'no-cache' },
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
return config;
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/manifest.json',
|
||||
headers: [
|
||||
{ key: 'Content-Type', value: 'application/manifest+json' },
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '/sw.js',
|
||||
headers: [
|
||||
{ key: 'Service-Worker-Allowed', value: '/' },
|
||||
{ key: 'Cache-Control', value: 'no-cache' },
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = withNextIntl({
|
||||
...withPWA(nextConfig)
|
||||
});
|
||||
module.exports = withPWA(withNextIntl(nextConfig))
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.38.1",
|
||||
"@fontsource-variable/azeret-mono": "^5.2.11",
|
||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
@@ -42,7 +44,6 @@
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"systeminformation": "^5.27.11",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
|
||||
BIN
public/favicon.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 305 KiB |
BIN
screenshots/home-dark-mobile.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
screenshots/home-dark.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
screenshots/home-light-mobile.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
screenshots/home-light.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 340 KiB |
|
Before Width: | Height: | Size: 355 KiB |
BIN
screenshots/logs-view.png
Normal file
|
After Width: | Height: | Size: 638 KiB |
|
Before Width: | Height: | Size: 364 KiB |
|
Before Width: | Height: | Size: 292 KiB |
15
yarn.lock
@@ -954,6 +954,16 @@
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2"
|
||||
integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==
|
||||
|
||||
"@fontsource-variable/azeret-mono@^5.2.11":
|
||||
version "5.2.11"
|
||||
resolved "https://registry.yarnpkg.com/@fontsource-variable/azeret-mono/-/azeret-mono-5.2.11.tgz#b3292c862522a509f0929ebd22ab709c27ffe931"
|
||||
integrity sha512-C2lkE7Pg0yg46ze3+AaJkolyldWBxluGiRG4pB5vHSdQRApOdQME6WbdsfYyy6wFDn/zPNx+1mwRJPnMRnovpA==
|
||||
|
||||
"@fontsource/ibm-plex-mono@^5.2.7":
|
||||
version "5.2.7"
|
||||
resolved "https://registry.yarnpkg.com/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.2.7.tgz#ef5b6f052115fdf6666208a5f8a0f13fcd7ba1fd"
|
||||
integrity sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w==
|
||||
|
||||
"@formatjs/ecma402-abstract@2.3.6":
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz#d6ca9d3579054fe1e1a0a0b5e872e0d64922e4e1"
|
||||
@@ -4301,11 +4311,6 @@ react-dom@^18:
|
||||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.2"
|
||||
|
||||
react-icons@^5.5.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.5.0.tgz#8aa25d3543ff84231685d3331164c00299cdfaf2"
|
||||
integrity sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==
|
||||
|
||||
react-is@^16.13.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
|
||||