diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml
new file mode 100644
index 0000000..bef6c5a
--- /dev/null
+++ b/.github/workflows/docker-build.yml
@@ -0,0 +1,52 @@
+name: Reusable Docker Build Logic
+
+on:
+ workflow_call:
+ inputs:
+ platform: { required: true, type: string }
+ suffix: { required: true, type: string }
+ runner: { required: true, type: string }
+ secrets:
+ token: { required: true }
+
+jobs:
+ build:
+ runs-on: ${{ inputs.runner }}
+ permissions: { contents: read, packages: write }
+ steps:
+ - { name: Checkout repository, uses: actions/checkout@v4 }
+
+ - name: "Prepare repository name in lowercase"
+ id: repo
+ run: echo "name=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
+
+ - { name: Set up QEMU, uses: docker/setup-qemu-action@v3 }
+ - { name: Set up Docker Buildx, uses: docker/setup-buildx-action@v3 }
+ - name: Log in to the Container registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.token }}
+
+ - name: Extract metadata
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: |
+ ghcr.io/${{ steps.repo.outputs.name }}
+ tags: |
+ type=ref,event=branch,suffix=${{ inputs.suffix }}
+ type=ref,event=tag,suffix=${{ inputs.suffix }}
+ type=raw,value=latest,suffix=${{ inputs.suffix }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ platforms: ${{ inputs.platform }}
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
\ No newline at end of file
diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
index 0d1d491..0d3b2d2 100644
--- a/.github/workflows/docker-publish.yml
+++ b/.github/workflows/docker-publish.yml
@@ -1,27 +1,69 @@
-name: Docker
+name: Build and Publish Multi-Platform Docker Image
on:
push:
- branches: ["main", "legacy", "feature/*"]
+ branches: ["main", "develop"]
tags: ["*"]
- pull_request:
- branches: ["main"]
-
-env:
- REGISTRY: ghcr.io
- IMAGE_NAME: ${{ github.repository }}
jobs:
- build:
+ build-amd64:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
-
steps:
- name: Checkout repository
uses: actions/checkout@v4
+ - name: Prepare repository name in lowercase
+ id: repo
+ run: echo "name=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to the Container registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: |
+ ghcr.io/${{ steps.repo.outputs.name }}
+ tags: |
+ type=ref,event=branch,suffix=-amd64
+ type=ref,event=tag,suffix=-amd64
+ type=raw,value=latest,suffix=-amd64,enable=${{ startsWith(github.ref, 'refs/tags/') }}
+
+ - name: Build and push AMD64 Docker image
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ platforms: linux/amd64
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ build-arm64:
+ runs-on: ubuntu-22.04-arm
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Prepare repository name in lowercase
+ id: repo
+ run: echo "name=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
+
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -31,27 +73,68 @@ jobs:
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
- registry: ${{ env.REGISTRY }}
+ registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- - name: Extract metadata (tags, labels) for Docker
+ - name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ images: |
+ ghcr.io/${{ steps.repo.outputs.name }}
tags: |
- type=ref,event=branch
- type=ref,event=pr
- type=semver,pattern={{version}}
- type=semver,pattern={{major}}.{{minor}}
- type=sha
+ type=ref,event=branch,suffix=-arm64
+ type=ref,event=tag,suffix=-arm64
+ type=raw,value=latest,suffix=-arm64,enable=${{ startsWith(github.ref, 'refs/tags/') }}
- - name: Build and push Docker image
+ - name: Build and push ARM64 Docker image
uses: docker/build-push-action@v5
with:
context: .
- platforms: linux/amd64,linux/arm64
+ platforms: linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ manifest:
+ needs: [build-amd64, build-arm64]
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Prepare repository name in lowercase
+ id: repo
+ run: echo "name=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
+
+ - name: Log in to the Container registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata for final manifest
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: |
+ ghcr.io/${{ steps.repo.outputs.name }}
+ tags: |
+ type=ref,event=branch
+ type=ref,event=tag
+ type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
+
+ - name: Create and push manifest list
+ run: |
+ echo "${{ steps.meta.outputs.tags }}" | while read -r tag; do
+ if [ -z "$tag" ]; then continue; fi
+ echo "Creating manifest for ${tag}"
+ docker buildx imagetools create --tag "${tag}" \
+ "${tag}-amd64" \
+ "${tag}-arm64"
+ done
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 94c7e7e..375e0d3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,4 +13,5 @@ node_modules
.cursorignore
.idea
tsconfig.tsbuildinfo
-docker-compose.test.yml
\ No newline at end of file
+docker-compose.test.yml
+/data
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index ca34af5..3846f42 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,6 +5,15 @@ RUN apt-get update && apt-get install -y \
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 base AS deps
diff --git a/README.md b/README.md
index c4d5099..8dcc625 100644
--- a/README.md
+++ b/README.md
@@ -2,12 +2,40 @@
-
+
+
+ {t("cronjobs.nOfNJObs", { + filtered: filteredJobs.length, + total: cronJobs.length, + })}{" "} + {selectedUser && + t("cronjobs.forUser", { user: selectedUser })} +
++ {selectedUser + ? `No scheduled tasks found for user ${selectedUser}. Try selecting a different user or create a new task.` + : "Create your first scheduled task to automate your system operations and boost productivity."} +
+ +
+ {job.schedule}
+
+ )}
+ {scheduleDisplayMode === "human" && cronExplanation?.isValid && (
+ + {cronExplanation.humanReadable} +
+ {
+ e.stopPropagation();
+ copyToClipboard(unwrapCommand(job.command));
+ 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"
+ >
+ {unwrapCommand(displayCommand)}
+
+ + {cronExplanation.humanReadable} +
++ {job.comment} +
+ )} +
+ ),
+ onClick: () => onToggleLogging(job.id),
+ },
+ ...(job.logsEnabled
+ ? [
+ {
+ label: t("cronjobs.viewLogs"),
+ icon: ,
+ onClick: () => onViewLogs(job),
+ },
+ ]
+ : []),
+ {
+ label: job.paused
+ ? t("cronjobs.resumeCronJob")
+ : t("cronjobs.pauseCronJob"),
+ icon: job.paused ? (
+
+ {job.schedule}
+
+ )}
+ {scheduleDisplayMode === "human" && cronExplanation?.isValid && (
+
+ {job.schedule}
+
+ {cronExplanation?.isValid && (
+ {
+ e.stopPropagation();
+ copyToClipboard(unwrapCommand(job.command));
+ 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"
+ title={unwrapCommand(job.command)}
+ >
+ {unwrapCommand(displayCommand)}
+
+