4 Commits

Author SHA1 Message Date
fccview
64a75d265f next 16 update, security fixes and pwa update 2026-02-11 19:22:29 +00:00
fccview
f2fb381964 create tarball for next release and fix few minor issues, also add download logs 2026-02-11 18:48:51 +00:00
fccview
0ed4942a30 make image full width in readme 2026-01-01 15:54:54 +00:00
fccview
aeab383116 fix heading image in readme 2026-01-01 15:52:06 +00:00
32 changed files with 1373 additions and 2218 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: fccview

View File

@@ -1,4 +1,4 @@
name: Reusable Docker Build Logic name: Builder
on: on:
workflow_call: workflow_call:

View File

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

View File

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

View File

@@ -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" />

View File

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

View File

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

View File

@@ -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();

View File

@@ -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?",

View 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?",

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

View File

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

View File

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

View File

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

View File

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

View 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
View 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
View 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
View File

@@ -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.

View File

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

View File

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

View File

@@ -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 (

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/logo-pwa.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -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"

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

@@ -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"

3094
yarn.lock
View File

File diff suppressed because it is too large Load Diff