mirror of
https://github.com/fccview/cronmaster.git
synced 2026-02-19 07:15:47 -05:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64a75d265f | ||
|
|
f2fb381964 | ||
|
|
0ed4942a30 | ||
|
|
aeab383116 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
github: fccview
|
||||||
2
.github/workflows/docker-build.yml
vendored
2
.github/workflows/docker-build.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Reusable Docker Build Logic
|
name: Builder
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
|||||||
2
.github/workflows/docker-publish.yml
vendored
2
.github/workflows/docker-publish.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Build and Publish Multi-Platform Docker Image
|
name: Build and Publish
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|||||||
48
.github/workflows/pr-checks.yml
vendored
Normal file
48
.github/workflows/pr-checks.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
name: PR Checks
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: ["**"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-branch:
|
||||||
|
name: Validate Target Branch
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: YOU SHALL NOT PASS
|
||||||
|
if: github.base_ref == 'main'
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.head_ref }}" != "develop" ]; then
|
||||||
|
echo "ERROR: Pull requests to 'main' are not allowed."
|
||||||
|
echo "Current source branch: ${{ github.head_ref }}"
|
||||||
|
echo ""
|
||||||
|
echo "Please create a PR to 'develop' first, this will become a release candidate when merged into 'main' by a maintainer"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Valid PR: develop → main"
|
||||||
|
|
||||||
|
- name: PR info
|
||||||
|
run: |
|
||||||
|
echo "PR validation passed"
|
||||||
|
echo "Source: ${{ github.head_ref }}"
|
||||||
|
echo "Target: ${{ github.base_ref }}"
|
||||||
|
|
||||||
|
typing:
|
||||||
|
name: Type and install checks
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: validate-branch
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup the best engine ever
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
cache: "yarn"
|
||||||
|
|
||||||
|
- name: Install all dependencies
|
||||||
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: This will totally fail if you only use AI
|
||||||
|
run: yarn tsc --noEmit
|
||||||
60
.github/workflows/prebuild-release.yml
vendored
Normal file
60
.github/workflows/prebuild-release.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: Build and Release Prebuilt Tarball
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ["*"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-prebuild:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup the best engine ever
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Bake cronmaster
|
||||||
|
env:
|
||||||
|
NODE_ENV: production
|
||||||
|
NEXT_TELEMETRY_DISABLED: 1
|
||||||
|
run: yarn build
|
||||||
|
|
||||||
|
- name: Get version from tag
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
|
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Structure the prebuild stuff
|
||||||
|
run: |
|
||||||
|
mkdir -p prebuild-release/cronmaster/.next
|
||||||
|
cp -r .next/standalone/. prebuild-release/cronmaster/
|
||||||
|
cp -r .next/static prebuild-release/cronmaster/.next/static
|
||||||
|
if [ -f .next/BUILD_ID ]; then
|
||||||
|
cp .next/BUILD_ID prebuild-release/cronmaster/.next/BUILD_ID
|
||||||
|
fi
|
||||||
|
cp -r public prebuild-release/cronmaster/public
|
||||||
|
cp -r howto prebuild-release/cronmaster/howto
|
||||||
|
|
||||||
|
- name: Create tarball - tarball is a funny name
|
||||||
|
run: |
|
||||||
|
cd prebuild-release
|
||||||
|
tar -czf cronmaster_${{ steps.version.outputs.version }}_prebuild.tar.gz cronmaster
|
||||||
|
sha256sum cronmaster_${{ steps.version.outputs.version }}_prebuild.tar.gz > cronmaster_${{ steps.version.outputs.version }}_prebuild.tar.gz.sha256
|
||||||
|
|
||||||
|
- name: Attach to Release - pray it works
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
prebuild-release/cronmaster_*_prebuild.tar.gz
|
||||||
|
prebuild-release/cronmaster_*_prebuild.tar.gz.sha256
|
||||||
|
tag_name: ${{ steps.version.outputs.version }}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="public/heading.png" width="400px">
|
<img src="public/heading.png">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Quick links
|
## Quick links
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export const Sidebar = forwardRef<HTMLDivElement, SidebarProps>(
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||||
className="absolute -right-3 top-[21.5vh] w-6 h-6 bg-background0 ascii-border items-center justify-center transition-colors z-40 hidden lg:flex"
|
className="sidebar-shrinker absolute -right-3 top-[21.5vh] w-6 h-6 bg-background0 ascii-border items-center justify-center transition-colors z-40 hidden lg:flex"
|
||||||
>
|
>
|
||||||
{isCollapsed ? (
|
{isCollapsed ? (
|
||||||
<CaretRightIcon className="h-3 w-3" />
|
<CaretRightIcon className="h-3 w-3" />
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Modal } from "@/app/_components/GlobalComponents/UIElements/Modal";
|
import { Modal } from "@/app/_components/GlobalComponents/UIElements/Modal";
|
||||||
import { Button } from "@/app/_components/GlobalComponents/UIElements/Button";
|
import { Button } from "@/app/_components/GlobalComponents/UIElements/Button";
|
||||||
import { FileTextIcon, TrashIcon, EyeIcon, XIcon, ArrowsClockwiseIcon, WarningCircleIcon, CheckCircleIcon } from "@phosphor-icons/react";
|
import { FileTextIcon, TrashIcon, EyeIcon, XIcon, ArrowsClockwiseIcon, WarningCircleIcon, CheckCircleIcon, DownloadIcon } from "@phosphor-icons/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { zipSync, strToU8 } from "fflate";
|
||||||
import {
|
import {
|
||||||
getJobLogs,
|
getJobLogs,
|
||||||
getLogContent,
|
getLogContent,
|
||||||
@@ -44,6 +45,7 @@ export const LogsModal = ({
|
|||||||
const [logContent, setLogContent] = useState<string>("");
|
const [logContent, setLogContent] = useState<string>("");
|
||||||
const [isLoadingLogs, setIsLoadingLogs] = useState(false);
|
const [isLoadingLogs, setIsLoadingLogs] = useState(false);
|
||||||
const [isLoadingContent, setIsLoadingContent] = useState(false);
|
const [isLoadingContent, setIsLoadingContent] = useState(false);
|
||||||
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
const [stats, setStats] = useState<{
|
const [stats, setStats] = useState<{
|
||||||
count: number;
|
count: number;
|
||||||
totalSize: number;
|
totalSize: number;
|
||||||
@@ -133,6 +135,28 @@ export const LogsModal = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDownloadLogs = async () => {
|
||||||
|
if (logs.length === 0) return;
|
||||||
|
setIsDownloading(true);
|
||||||
|
try {
|
||||||
|
const files: Record<string, Uint8Array> = {};
|
||||||
|
for (const log of logs) {
|
||||||
|
const content = await getLogContent(jobId, log.filename);
|
||||||
|
files[log.filename] = strToU8(content);
|
||||||
|
}
|
||||||
|
const zipped = zipSync(files);
|
||||||
|
const blob = new Blob([zipped as unknown as ArrayBuffer], { type: "application/zip" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${jobComment || jobId}_logs.zip`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} finally {
|
||||||
|
setIsDownloading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const formatFileSize = (bytes: number): string => {
|
const formatFileSize = (bytes: number): string => {
|
||||||
if (bytes < 1024) return `${bytes} B`;
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
|
||||||
@@ -157,16 +181,29 @@ export const LogsModal = ({
|
|||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} title={t("cronjobs.viewLogs")} size="xl">
|
<Modal isOpen={isOpen} onClose={onClose} title={t("cronjobs.viewLogs")} size="xl">
|
||||||
<div className="flex flex-col h-[600px]">
|
<div className="flex flex-col h-[600px]">
|
||||||
<div className="flex items-center justify-between mb-4 pb-4 border-b border-border">
|
<div className="block sm:flex items-center justify-between mb-4 pb-4 border-b border-border">
|
||||||
<div>
|
<div className="min-w-0 mb-4 sm:mb-0">
|
||||||
<h3 className="font-semibold text-lg">{jobComment || jobId}</h3>
|
<h3 className="font-semibold text-lg truncate">{jobComment || jobId}</h3>
|
||||||
{stats && (
|
{stats && (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{stats.count} {t("cronjobs.logs")} • {stats.totalSizeMB} MB
|
{stats.count} {t("cronjobs.logs")} • {stats.totalSizeMB} MB
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2 flex-shrink-0">
|
||||||
|
<Button
|
||||||
|
onClick={handleDownloadLogs}
|
||||||
|
disabled={logs.length === 0 || isDownloading}
|
||||||
|
className="btn-primary glow-primary"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{isDownloading ? (
|
||||||
|
<ArrowsClockwiseIcon className="w-4 h-4 sm:mr-2 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<DownloadIcon className="w-4 h-4 sm:mr-2" />
|
||||||
|
)}
|
||||||
|
<span className="hidden sm:inline">{t("cronjobs.downloadLog")}</span>
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={loadLogs}
|
onClick={loadLogs}
|
||||||
disabled={isLoadingLogs}
|
disabled={isLoadingLogs}
|
||||||
@@ -174,10 +211,10 @@ export const LogsModal = ({
|
|||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<ArrowsClockwiseIcon
|
<ArrowsClockwiseIcon
|
||||||
className={`w-4 h-4 mr-2 ${isLoadingLogs ? "animate-spin" : ""
|
className={`w-4 h-4 sm:mr-2 ${isLoadingLogs ? "animate-spin" : ""
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{t("common.refresh")}
|
<span className="hidden sm:inline">{t("common.refresh")}</span>
|
||||||
</Button>
|
</Button>
|
||||||
{logs.length > 0 && (
|
{logs.length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
@@ -185,15 +222,15 @@ export const LogsModal = ({
|
|||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<TrashIcon className="w-4 h-4 mr-2" />
|
<TrashIcon className="w-4 h-4 sm:mr-2" />
|
||||||
{t("cronjobs.deleteAll")}
|
<span className="hidden sm:inline">{t("cronjobs.deleteAll")}</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 flex gap-4 overflow-hidden">
|
<div className="flex-1 flex flex-col sm:flex-row gap-4 overflow-hidden">
|
||||||
<div className="w-1/3 flex flex-col border-r border-border pr-4 overflow-hidden">
|
<div className="sm:w-1/3 flex flex-col sm:border-r border-b sm:border-b-0 border-border sm:pr-4 pb-4 sm:pb-0 overflow-hidden max-h-[40%] sm:max-h-none">
|
||||||
<h4 className="font-semibold mb-2">{t("cronjobs.logFiles")}</h4>
|
<h4 className="font-semibold mb-2">{t("cronjobs.logFiles")}</h4>
|
||||||
<div className="flex-1 overflow-y-auto space-y-2">
|
<div className="flex-1 overflow-y-auto space-y-2">
|
||||||
{isLoadingLogs ? (
|
{isLoadingLogs ? (
|
||||||
@@ -288,8 +325,8 @@ export const LogsModal = ({
|
|||||||
|
|
||||||
<div className="mt-4 pt-4 border-t border-border flex justify-end">
|
<div className="mt-4 pt-4 border-t border-border flex justify-end">
|
||||||
<Button onClick={onClose} className="btn-primary glow-primary">
|
<Button onClick={onClose} className="btn-primary glow-primary">
|
||||||
<XIcon className="w-4 h-4 mr-2" />
|
<XIcon className="w-4 h-4 sm:mr-2" />
|
||||||
{t("common.close")}
|
<span className="hidden sm:inline">{t("common.close")}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState, type JSX } from "react";
|
||||||
|
|
||||||
type BeforeInstallPromptEvent = Event & {
|
type BeforeInstallPromptEvent = Event & {
|
||||||
prompt: () => Promise<void>;
|
prompt: () => Promise<void>;
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ export const ServiceWorkerRegister = (): null => {
|
|||||||
r.scope.endsWith("/")
|
r.scope.endsWith("/")
|
||||||
);
|
);
|
||||||
if (alreadyRegistered) return;
|
if (alreadyRegistered) return;
|
||||||
await navigator.serviceWorker.register("/sw.js", { scope: "/" });
|
await navigator.serviceWorker.register("/serwist/sw.js", {
|
||||||
|
scope: "/",
|
||||||
|
updateViaCache: "none",
|
||||||
|
});
|
||||||
} catch (_err) {}
|
} catch (_err) {}
|
||||||
};
|
};
|
||||||
register();
|
register();
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
"logs": "logs",
|
"logs": "logs",
|
||||||
"logFiles": "Log Files",
|
"logFiles": "Log Files",
|
||||||
"logContent": "Log Content",
|
"logContent": "Log Content",
|
||||||
|
"downloadLog": "Download",
|
||||||
"selectLogToView": "Select a log file to view its content",
|
"selectLogToView": "Select a log file to view its content",
|
||||||
"noLogsFound": "No logs found for this job",
|
"noLogsFound": "No logs found for this job",
|
||||||
"confirmDeleteLog": "Are you sure you want to delete this log file?",
|
"confirmDeleteLog": "Are you sure you want to delete this log file?",
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
"logs": "log",
|
"logs": "log",
|
||||||
"logFiles": "File",
|
"logFiles": "File",
|
||||||
"logContent": "Contenuto Log",
|
"logContent": "Contenuto Log",
|
||||||
|
"downloadLog": "Scarica",
|
||||||
"selectLogToView": "Seleziona un file per visualizzarne il contenuto",
|
"selectLogToView": "Seleziona un file per visualizzarne il contenuto",
|
||||||
"noLogsFound": "Nessun log trovato per questa operazione",
|
"noLogsFound": "Nessun log trovato per questa operazione",
|
||||||
"confirmDeleteLog": "Sei sicuro di voler eliminare questo file?",
|
"confirmDeleteLog": "Sei sicuro di voler eliminare questo file?",
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ import { executeJob } from "@/app/_server/actions/cronjobs";
|
|||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(request: NextRequest, props: { params: Promise<{ id: string }> }) {
|
||||||
request: NextRequest,
|
const params = await props.params;
|
||||||
{ params }: { params: { id: string } }
|
|
||||||
) {
|
|
||||||
const authError = await requireAuth(request);
|
const authError = await requireAuth(request);
|
||||||
if (authError) return authError;
|
if (authError) return authError;
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,8 @@ import {
|
|||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(request: NextRequest, props: { params: Promise<{ id: string }> }) {
|
||||||
request: NextRequest,
|
const params = await props.params;
|
||||||
{ params }: { params: { id: string } }
|
|
||||||
) {
|
|
||||||
const authError = await requireAuth(request);
|
const authError = await requireAuth(request);
|
||||||
if (authError) return authError;
|
if (authError) return authError;
|
||||||
|
|
||||||
@@ -40,10 +38,8 @@ export async function GET(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function PATCH(
|
export async function PATCH(request: NextRequest, props: { params: Promise<{ id: string }> }) {
|
||||||
request: NextRequest,
|
const params = await props.params;
|
||||||
{ params }: { params: { id: string } }
|
|
||||||
) {
|
|
||||||
const authError = await requireAuth(request);
|
const authError = await requireAuth(request);
|
||||||
if (authError) return authError;
|
if (authError) return authError;
|
||||||
|
|
||||||
@@ -79,10 +75,8 @@ export async function PATCH(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function DELETE(
|
export async function DELETE(request: NextRequest, props: { params: Promise<{ id: string }> }) {
|
||||||
request: NextRequest,
|
const params = await props.params;
|
||||||
{ params }: { params: { id: string } }
|
|
||||||
) {
|
|
||||||
const authError = await requireAuth(request);
|
const authError = await requireAuth(request);
|
||||||
if (authError) return authError;
|
if (authError) return authError;
|
||||||
|
|
||||||
|
|||||||
@@ -260,4 +260,20 @@ pre::-webkit-scrollbar {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin: inherit;
|
margin: inherit;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-shrinker {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-shrinker:before {
|
||||||
|
content: '';
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 6px solid var(--box-border-color, var(--foreground2));
|
||||||
|
border-right: 12px solid transparent;
|
||||||
|
position: absolute;
|
||||||
|
right: -1px;
|
||||||
|
bottom: -6px;
|
||||||
|
z-index: -1;
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ export const metadata: Metadata = {
|
|||||||
icons: {
|
icons: {
|
||||||
icon: "/favicon.png",
|
icon: "/favicon.png",
|
||||||
shortcut: "/logo.png",
|
shortcut: "/logo.png",
|
||||||
apple: "/logo.png",
|
apple: "/logo-pwa.png",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,9 @@ export default async function Home() {
|
|||||||
Cr<span className="text-status-error">*</span>nMaster
|
Cr<span className="text-status-error">*</span>nMaster
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs terminal-font flex items-center gap-2">
|
<p className="text-xs terminal-font flex items-center gap-2">
|
||||||
{t("common.version").replace("{version}", version)}
|
<a href={`https://github.com/fccview/cronmaster/releases/tag/${version}`} target="_blank" rel="noopener noreferrer">
|
||||||
|
{t("common.version").replace("{version}", version)}
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
7
app/serwist/[path]/route.ts
Normal file
7
app/serwist/[path]/route.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createSerwistRoute } from "@serwist/turbopack";
|
||||||
|
|
||||||
|
const { GET } = createSerwistRoute({
|
||||||
|
swSrc: "app/sw.ts",
|
||||||
|
});
|
||||||
|
|
||||||
|
export { GET };
|
||||||
89
app/sw.ts
Normal file
89
app/sw.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import type { PrecacheEntry, SerwistGlobalConfig } from "serwist";
|
||||||
|
import {
|
||||||
|
Serwist,
|
||||||
|
NetworkFirst,
|
||||||
|
CacheableResponsePlugin,
|
||||||
|
ExpirationPlugin,
|
||||||
|
StaleWhileRevalidate,
|
||||||
|
CacheFirst,
|
||||||
|
} from "serwist";
|
||||||
|
import { cleanupOutdatedCaches } from "serwist/internal";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface WorkerGlobalScope extends SerwistGlobalConfig {
|
||||||
|
__SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const self: WorkerGlobalScope;
|
||||||
|
|
||||||
|
const pageStrategy = new NetworkFirst({
|
||||||
|
cacheName: "pages",
|
||||||
|
matchOptions: { ignoreVary: true },
|
||||||
|
networkTimeoutSeconds: 5,
|
||||||
|
plugins: [
|
||||||
|
new CacheableResponsePlugin({ statuses: [200] }),
|
||||||
|
new ExpirationPlugin({
|
||||||
|
maxEntries: 128,
|
||||||
|
maxAgeSeconds: 30 * 24 * 60 * 60,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
cleanupOutdatedCaches();
|
||||||
|
|
||||||
|
const serwist = new Serwist({
|
||||||
|
precacheEntries: self.__SW_MANIFEST,
|
||||||
|
skipWaiting: true,
|
||||||
|
clientsClaim: true,
|
||||||
|
navigationPreload: false,
|
||||||
|
runtimeCaching: [
|
||||||
|
{
|
||||||
|
matcher: ({ request }) => request.mode === "navigate",
|
||||||
|
handler: pageStrategy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: ({ request, sameOrigin }) =>
|
||||||
|
request.headers.get("RSC") === "1" && sameOrigin,
|
||||||
|
handler: pageStrategy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: /\/_next\/static.+\.js$/i,
|
||||||
|
handler: new CacheFirst({
|
||||||
|
cacheName: "static-js",
|
||||||
|
plugins: [
|
||||||
|
new ExpirationPlugin({
|
||||||
|
maxEntries: 64,
|
||||||
|
maxAgeSeconds: 30 * 24 * 60 * 60,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: /\.(?:css|woff|woff2|ttf|eot|otf)$/i,
|
||||||
|
handler: new StaleWhileRevalidate({
|
||||||
|
cacheName: "static-assets",
|
||||||
|
plugins: [
|
||||||
|
new ExpirationPlugin({
|
||||||
|
maxEntries: 64,
|
||||||
|
maxAgeSeconds: 30 * 24 * 60 * 60,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: /\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,
|
||||||
|
handler: new StaleWhileRevalidate({
|
||||||
|
cacheName: "images",
|
||||||
|
plugins: [
|
||||||
|
new ExpirationPlugin({
|
||||||
|
maxEntries: 128,
|
||||||
|
maxAgeSeconds: 30 * 24 * 60 * 60,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
serwist.addEventListeners();
|
||||||
18
eslint.config.mjs
Normal file
18
eslint.config.mjs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
|
||||||
|
import nextTypescript from "eslint-config-next/typescript";
|
||||||
|
|
||||||
|
const eslintConfig = [
|
||||||
|
...nextCoreWebVitals,
|
||||||
|
...nextTypescript,
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
"node_modules/**",
|
||||||
|
".next/**",
|
||||||
|
"out/**",
|
||||||
|
"build/**",
|
||||||
|
"next-env.d.ts",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
3
next-env.d.ts
vendored
3
next-env.d.ts
vendored
@@ -1,5 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
const withPWA = require('next-pwa')({
|
|
||||||
dest: 'public',
|
|
||||||
register: true,
|
|
||||||
skipWaiting: true,
|
|
||||||
disable: process.env.NODE_ENV === 'development',
|
|
||||||
buildExcludes: [/middleware-manifest\.json$/]
|
|
||||||
})
|
|
||||||
|
|
||||||
const withNextIntl = require('next-intl/plugin')('./app/i18n.ts')
|
|
||||||
|
|
||||||
const nextConfig = {
|
|
||||||
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/,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = withPWA(withNextIntl(nextConfig))
|
|
||||||
39
next.config.mjs
Normal file
39
next.config.mjs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { withSerwist } from "@serwist/turbopack";
|
||||||
|
import createNextIntlPlugin from "next-intl/plugin";
|
||||||
|
|
||||||
|
const withNextIntl = createNextIntlPlugin("./app/i18n.ts");
|
||||||
|
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
output: "standalone",
|
||||||
|
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/,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
async headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/manifest.json",
|
||||||
|
headers: [
|
||||||
|
{ key: "Content-Type", value: "application/manifest+json" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withSerwist(withNextIntl(nextConfig));
|
||||||
32
package.json
32
package.json
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "cronjob-manager",
|
"name": "cronjob-manager",
|
||||||
"version": "2.0.0",
|
"version": "2.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.18.6",
|
"@codemirror/autocomplete": "^6.18.6",
|
||||||
@@ -23,8 +23,8 @@
|
|||||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||||
"@phosphor-icons/react": "^2.1.10",
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "19.2.14",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "19.2.3",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"@webtui/css": "^0.1.5",
|
"@webtui/css": "^0.1.5",
|
||||||
"@webtui/theme-catppuccin": "^0.0.3",
|
"@webtui/theme-catppuccin": "^0.0.3",
|
||||||
@@ -34,27 +34,37 @@
|
|||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
"cron-parser": "^5.3.0",
|
"cron-parser": "^5.3.0",
|
||||||
"cronstrue": "^3.2.0",
|
"cronstrue": "^3.2.0",
|
||||||
|
"fflate": "^0.8.2",
|
||||||
"jose": "^6.1.1",
|
"jose": "^6.1.1",
|
||||||
"minimatch": "^10.0.3",
|
"minimatch": "^10.0.3",
|
||||||
"next": "14.2.35",
|
"next": "16.1.6",
|
||||||
"next-intl": "^4.4.0",
|
"next-intl": "^4.4.0",
|
||||||
"next-pwa": "^5.6.0",
|
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"proper-lockfile": "^4.1.2",
|
"proper-lockfile": "^4.1.2",
|
||||||
"react": "^18",
|
"react": "19.2.4",
|
||||||
"react-dom": "^18",
|
"react-dom": "19.2.4",
|
||||||
"react-syntax-highlighter": "^15.6.1",
|
"react-syntax-highlighter": "^15.6.1",
|
||||||
"systeminformation": "^5.27.11",
|
"serwist": "^9.5.5",
|
||||||
|
"systeminformation": "^5.27.14",
|
||||||
"tailwind-merge": "^2.0.0",
|
"tailwind-merge": "^2.0.0",
|
||||||
"tailwindcss": "^3.3.0",
|
"tailwindcss": "^3.3.0",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@serwist/turbopack": "^9.5.5",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/minimatch": "^6.0.0",
|
"@types/minimatch": "^6.0.0",
|
||||||
"eslint": "^8",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "14.0.4",
|
"eslint-config-next": "16.1.6",
|
||||||
"postcss-import": "^16.1.1"
|
"postcss-import": "^16.1.1"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"@isaacs/brace-expansion": "^5.0.1",
|
||||||
|
"@types/react": "19.2.14",
|
||||||
|
"@types/react-dom": "19.2.3",
|
||||||
|
"lodash": "^4.17.23",
|
||||||
|
"prismjs": "^1.30.0",
|
||||||
|
"systeminformation": "^5.27.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import type { NextRequest } from "next/server";
|
import type { NextRequest } from "next/server";
|
||||||
|
|
||||||
export const middleware = async (request: NextRequest) => {
|
export const proxy = async (request: NextRequest) => {
|
||||||
const { pathname } = request.nextUrl;
|
const { pathname } = request.nextUrl;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
BIN
public/logo-pwa.png
Normal file
BIN
public/logo-pwa.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -9,13 +9,13 @@
|
|||||||
"orientation": "portrait-primary",
|
"orientation": "portrait-primary",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/logo.png",
|
"src": "/logo-pwa.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/logo.png",
|
"src": "/logo-pwa.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any maskable"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -3,7 +3,8 @@
|
|||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
"es6"
|
"es6",
|
||||||
|
"webworker"
|
||||||
],
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "react-jsx",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
@@ -26,13 +27,15 @@
|
|||||||
"@/*": [
|
"@/*": [
|
||||||
"./*"
|
"./*"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"target": "ES2017"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
".next/types/**/*.ts"
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules"
|
"node_modules"
|
||||||
|
|||||||
Reference in New Issue
Block a user