mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
feat: add next_run_at column to sync config tables and update job schedules to cron expressions
This commit is contained in:
@@ -21,7 +21,8 @@
|
||||
"@std/assert": "jsr:@std/assert@^1.0.0",
|
||||
"marked": "npm:marked@^15.0.6",
|
||||
"simple-icons": "npm:simple-icons@^15.17.0",
|
||||
"highlight.js": "npm:highlight.js@^11.11.1"
|
||||
"highlight.js": "npm:highlight.js@^11.11.1",
|
||||
"croner": "npm:croner@^8.1.2"
|
||||
},
|
||||
"tasks": {
|
||||
"dev": "DENO_ENV=development PORT=6969 HOST=0.0.0.0 APP_BASE_PATH=./dist/dev PARSER_HOST=localhost PARSER_PORT=5000 deno run -A npm:vite dev",
|
||||
|
||||
15
deno.lock
generated
15
deno.lock
generated
@@ -26,10 +26,13 @@
|
||||
"npm:@tailwindcss/forms@~0.5.10": "0.5.10_tailwindcss@4.1.16",
|
||||
"npm:@tailwindcss/vite@^4.1.13": "4.1.16_vite@7.1.12__@types+node@22.19.0__picomatch@4.0.3_@types+node@22.19.0",
|
||||
"npm:@types/node@22": "22.19.0",
|
||||
"npm:croner@^8.1.2": "8.1.2",
|
||||
"npm:eslint-config-prettier@^10.1.8": "10.1.8_eslint@9.39.1",
|
||||
"npm:eslint-plugin-svelte@^3.12.4": "3.13.0_eslint@9.39.1_svelte@5.43.3__acorn@8.15.0_postcss@8.5.6",
|
||||
"npm:eslint@^9.36.0": "9.39.1",
|
||||
"npm:globals@^16.4.0": "16.5.0",
|
||||
"npm:highlight.js@^11.11.1": "11.11.1",
|
||||
"npm:kysely@0.27.6": "0.27.6",
|
||||
"npm:kysely@~0.27.2": "0.27.6",
|
||||
"npm:lucide-svelte@0.546": "0.546.0_svelte@5.43.3__acorn@8.15.0",
|
||||
"npm:marked@^15.0.6": "15.0.12",
|
||||
@@ -65,7 +68,7 @@
|
||||
"@soapbox/kysely-deno-sqlite@2.2.0": {
|
||||
"integrity": "668ec94600bc4b4d7bd618dd7ca65d4ef30ee61c46ffcb379b6f45203c08517a",
|
||||
"dependencies": [
|
||||
"npm:kysely"
|
||||
"npm:kysely@~0.27.2"
|
||||
]
|
||||
},
|
||||
"@std/assert@0.217.0": {
|
||||
@@ -1032,6 +1035,9 @@
|
||||
"cookie@0.6.0": {
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="
|
||||
},
|
||||
"croner@8.1.2": {
|
||||
"integrity": "sha512-ypfPFcAXHuAZRCzo3vJL6ltENzniTjwe/qsLleH1V2/7SRDjgvRQyrLmumFTLmjFax4IuSxfGXEn79fozXcJog=="
|
||||
},
|
||||
"cross-spawn@7.0.6": {
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dependencies": [
|
||||
@@ -1347,6 +1353,9 @@
|
||||
"has-flag@4.0.0": {
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
|
||||
},
|
||||
"highlight.js@11.11.1": {
|
||||
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="
|
||||
},
|
||||
"ignore@5.3.2": {
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="
|
||||
},
|
||||
@@ -1419,9 +1428,6 @@
|
||||
"kysely@0.27.6": {
|
||||
"integrity": "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ=="
|
||||
},
|
||||
"kysely@0.28.8": {
|
||||
"integrity": "sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA=="
|
||||
},
|
||||
"levn@0.4.1": {
|
||||
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
|
||||
"dependencies": [
|
||||
@@ -1947,6 +1953,7 @@
|
||||
"dependencies": [
|
||||
"jsr:@soapbox/kysely-deno-sqlite@^2.2.0",
|
||||
"jsr:@std/assert@1",
|
||||
"npm:croner@^8.1.2",
|
||||
"npm:highlight.js@^11.11.1",
|
||||
"npm:marked@^15.0.6",
|
||||
"npm:simple-icons@^15.17.0"
|
||||
|
||||
@@ -23,6 +23,7 @@ import { migration as migration018 } from './migrations/018_create_app_info.ts';
|
||||
import { migration as migration019 } from './migrations/019_default_log_level_debug.ts';
|
||||
import { migration as migration020 } from './migrations/020_create_tmdb_settings.ts';
|
||||
import { migration as migration021 } from './migrations/021_create_parsed_release_cache.ts';
|
||||
import { migration as migration022 } from './migrations/022_add_next_run_at.ts';
|
||||
|
||||
export interface Migration {
|
||||
version: number;
|
||||
@@ -258,7 +259,8 @@ export function loadMigrations(): Migration[] {
|
||||
migration018,
|
||||
migration019,
|
||||
migration020,
|
||||
migration021
|
||||
migration021,
|
||||
migration022
|
||||
];
|
||||
|
||||
// Sort by version number
|
||||
|
||||
25
src/lib/server/db/migrations/022_add_next_run_at.ts
Normal file
25
src/lib/server/db/migrations/022_add_next_run_at.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { Migration } from '../migrations.ts';
|
||||
|
||||
/**
|
||||
* Migration 022: Add next_run_at to sync config tables
|
||||
*
|
||||
* Stores when each scheduled config should next trigger.
|
||||
* Enables simple timestamp comparison instead of cron parsing on every evaluation.
|
||||
*/
|
||||
|
||||
export const migration: Migration = {
|
||||
version: 22,
|
||||
name: 'Add next_run_at to sync configs',
|
||||
|
||||
up: `
|
||||
ALTER TABLE arr_sync_quality_profiles_config ADD COLUMN next_run_at TEXT;
|
||||
ALTER TABLE arr_sync_delay_profiles_config ADD COLUMN next_run_at TEXT;
|
||||
ALTER TABLE arr_sync_media_management ADD COLUMN next_run_at TEXT;
|
||||
`,
|
||||
|
||||
down: `
|
||||
ALTER TABLE arr_sync_quality_profiles_config DROP COLUMN next_run_at;
|
||||
ALTER TABLE arr_sync_delay_profiles_config DROP COLUMN next_run_at;
|
||||
ALTER TABLE arr_sync_media_management DROP COLUMN next_run_at;
|
||||
`
|
||||
};
|
||||
@@ -11,6 +11,7 @@ export interface ProfileSelection {
|
||||
export interface SyncConfig {
|
||||
trigger: SyncTrigger;
|
||||
cron: string | null;
|
||||
nextRunAt?: string | null;
|
||||
}
|
||||
|
||||
export interface QualityProfilesSyncData {
|
||||
@@ -29,6 +30,7 @@ export interface MediaManagementSyncData {
|
||||
mediaSettingsDatabaseId: number | null;
|
||||
trigger: SyncTrigger;
|
||||
cron: string | null;
|
||||
nextRunAt?: string | null;
|
||||
}
|
||||
|
||||
// Row types
|
||||
@@ -99,14 +101,16 @@ export const arrSyncQueries = {
|
||||
|
||||
// Upsert config
|
||||
db.execute(
|
||||
`INSERT INTO arr_sync_quality_profiles_config (instance_id, trigger, cron)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(instance_id) DO UPDATE SET trigger = ?, cron = ?`,
|
||||
`INSERT INTO arr_sync_quality_profiles_config (instance_id, trigger, cron, next_run_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(instance_id) DO UPDATE SET trigger = ?, cron = ?, next_run_at = ?`,
|
||||
instanceId,
|
||||
config.trigger,
|
||||
config.cron,
|
||||
config.nextRunAt ?? null,
|
||||
config.trigger,
|
||||
config.cron
|
||||
config.cron,
|
||||
config.nextRunAt ?? null
|
||||
);
|
||||
},
|
||||
|
||||
@@ -155,14 +159,16 @@ export const arrSyncQueries = {
|
||||
|
||||
// Upsert config
|
||||
db.execute(
|
||||
`INSERT INTO arr_sync_delay_profiles_config (instance_id, trigger, cron)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(instance_id) DO UPDATE SET trigger = ?, cron = ?`,
|
||||
`INSERT INTO arr_sync_delay_profiles_config (instance_id, trigger, cron, next_run_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(instance_id) DO UPDATE SET trigger = ?, cron = ?, next_run_at = ?`,
|
||||
instanceId,
|
||||
config.trigger,
|
||||
config.cron,
|
||||
config.nextRunAt ?? null,
|
||||
config.trigger,
|
||||
config.cron
|
||||
config.cron,
|
||||
config.nextRunAt ?? null
|
||||
);
|
||||
},
|
||||
|
||||
@@ -186,25 +192,28 @@ export const arrSyncQueries = {
|
||||
saveMediaManagementSync(instanceId: number, data: MediaManagementSyncData): void {
|
||||
db.execute(
|
||||
`INSERT INTO arr_sync_media_management
|
||||
(instance_id, naming_database_id, quality_definitions_database_id, media_settings_database_id, trigger, cron)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
(instance_id, naming_database_id, quality_definitions_database_id, media_settings_database_id, trigger, cron, next_run_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(instance_id) DO UPDATE SET
|
||||
naming_database_id = ?,
|
||||
quality_definitions_database_id = ?,
|
||||
media_settings_database_id = ?,
|
||||
trigger = ?,
|
||||
cron = ?`,
|
||||
cron = ?,
|
||||
next_run_at = ?`,
|
||||
instanceId,
|
||||
data.namingDatabaseId,
|
||||
data.qualityDefinitionsDatabaseId,
|
||||
data.mediaSettingsDatabaseId,
|
||||
data.trigger,
|
||||
data.cron,
|
||||
data.nextRunAt ?? null,
|
||||
data.namingDatabaseId,
|
||||
data.qualityDefinitionsDatabaseId,
|
||||
data.mediaSettingsDatabaseId,
|
||||
data.trigger,
|
||||
data.cron
|
||||
data.cron,
|
||||
data.nextRunAt ?? null
|
||||
);
|
||||
},
|
||||
|
||||
@@ -339,5 +348,63 @@ export const arrSyncQueries = {
|
||||
delayProfiles: dp.map((r) => r.instance_id),
|
||||
mediaManagement: mm.map((r) => r.instance_id)
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all scheduled configs that haven't been marked for sync yet
|
||||
*/
|
||||
getScheduledConfigs(): {
|
||||
qualityProfiles: { instanceId: number; cron: string | null; nextRunAt: string | null }[];
|
||||
delayProfiles: { instanceId: number; cron: string | null; nextRunAt: string | null }[];
|
||||
mediaManagement: { instanceId: number; cron: string | null; nextRunAt: string | null }[];
|
||||
} {
|
||||
const qp = db.query<{ instance_id: number; cron: string | null; next_run_at: string | null }>(
|
||||
"SELECT instance_id, cron, next_run_at FROM arr_sync_quality_profiles_config WHERE trigger = 'schedule' AND should_sync = 0"
|
||||
);
|
||||
const dp = db.query<{ instance_id: number; cron: string | null; next_run_at: string | null }>(
|
||||
"SELECT instance_id, cron, next_run_at FROM arr_sync_delay_profiles_config WHERE trigger = 'schedule' AND should_sync = 0"
|
||||
);
|
||||
const mm = db.query<{ instance_id: number; cron: string | null; next_run_at: string | null }>(
|
||||
"SELECT instance_id, cron, next_run_at FROM arr_sync_media_management WHERE trigger = 'schedule' AND should_sync = 0"
|
||||
);
|
||||
|
||||
return {
|
||||
qualityProfiles: qp.map((r) => ({ instanceId: r.instance_id, cron: r.cron, nextRunAt: r.next_run_at })),
|
||||
delayProfiles: dp.map((r) => ({ instanceId: r.instance_id, cron: r.cron, nextRunAt: r.next_run_at })),
|
||||
mediaManagement: mm.map((r) => ({ instanceId: r.instance_id, cron: r.cron, nextRunAt: r.next_run_at }))
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Update next_run_at for a quality profiles config
|
||||
*/
|
||||
setQualityProfilesNextRunAt(instanceId: number, nextRunAt: string | null): void {
|
||||
db.execute(
|
||||
'UPDATE arr_sync_quality_profiles_config SET next_run_at = ? WHERE instance_id = ?',
|
||||
nextRunAt,
|
||||
instanceId
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update next_run_at for a delay profiles config
|
||||
*/
|
||||
setDelayProfilesNextRunAt(instanceId: number, nextRunAt: string | null): void {
|
||||
db.execute(
|
||||
'UPDATE arr_sync_delay_profiles_config SET next_run_at = ? WHERE instance_id = ?',
|
||||
nextRunAt,
|
||||
instanceId
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update next_run_at for a media management config
|
||||
*/
|
||||
setMediaManagementNextRunAt(instanceId: number, nextRunAt: string | null): void {
|
||||
db.execute(
|
||||
'UPDATE arr_sync_media_management SET next_run_at = ? WHERE instance_id = ?',
|
||||
nextRunAt,
|
||||
instanceId
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { JobDefinition, JobResult } from '../types.ts';
|
||||
export const cleanupBackupsJob: JobDefinition = {
|
||||
name: 'cleanup_backups',
|
||||
description: 'Delete backups according to retention policy',
|
||||
schedule: 'daily',
|
||||
schedule: '0 0 * * *', // Daily at midnight
|
||||
|
||||
handler: async (): Promise<JobResult> => {
|
||||
try {
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { JobDefinition, JobResult } from '../types.ts';
|
||||
export const cleanupLogsJob: JobDefinition = {
|
||||
name: 'cleanup_logs',
|
||||
description: 'Delete log files according to retention policy',
|
||||
schedule: 'daily',
|
||||
schedule: '0 0 * * *', // Daily at midnight
|
||||
|
||||
handler: async (): Promise<JobResult> => {
|
||||
try {
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { JobDefinition, JobResult } from '../types.ts';
|
||||
export const createBackupJob: JobDefinition = {
|
||||
name: 'create_backup',
|
||||
description: 'Create compressed backup of data directory',
|
||||
schedule: 'daily',
|
||||
schedule: '0 0 * * *', // Daily at midnight
|
||||
|
||||
handler: async (): Promise<JobResult> => {
|
||||
try {
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { JobDefinition, JobResult } from '../types.ts';
|
||||
export const syncDatabasesJob: JobDefinition = {
|
||||
name: 'sync_databases',
|
||||
description: 'Auto-sync PCD databases with remote repositories',
|
||||
schedule: '*/5 minutes',
|
||||
schedule: '*/5 * * * *', // Every 5 minutes
|
||||
|
||||
handler: async (): Promise<JobResult> => {
|
||||
try {
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { JobDefinition, JobResult } from '../types.ts';
|
||||
export const upgradeManagerJob: JobDefinition = {
|
||||
name: 'upgrade_manager',
|
||||
description: 'Process library upgrades for arr instances',
|
||||
schedule: '*/30 minutes',
|
||||
schedule: '*/30 * * * *', // Every 30 minutes
|
||||
|
||||
handler: async (): Promise<JobResult> => {
|
||||
try {
|
||||
|
||||
@@ -4,36 +4,38 @@ import { logger } from '$logger/logger.ts';
|
||||
import { notify } from '$notifications/builder.ts';
|
||||
import { NotificationTypes } from '$notifications/types.ts';
|
||||
import type { Job, JobResult } from './types.ts';
|
||||
import { Cron } from 'croner';
|
||||
|
||||
/**
|
||||
* Parse schedule string and calculate next run time
|
||||
* @param schedule Schedule string (e.g., "daily", "hourly", "every 5 minutes")
|
||||
* Supports cron expressions and legacy formats
|
||||
* @param schedule Schedule string (cron expression or legacy format)
|
||||
* @returns ISO timestamp for next run
|
||||
*/
|
||||
function calculateNextRun(schedule: string): string {
|
||||
const now = new Date();
|
||||
|
||||
// Parse schedule
|
||||
// Handle legacy formats for backwards compatibility
|
||||
if (schedule === 'daily') {
|
||||
// Run at midnight next day
|
||||
const next = new Date(now);
|
||||
next.setDate(next.getDate() + 1);
|
||||
next.setHours(0, 0, 0, 0);
|
||||
return next.toISOString();
|
||||
} else if (schedule === 'hourly') {
|
||||
// Run at start of next hour
|
||||
const next = new Date(now);
|
||||
next.setHours(next.getHours() + 1, 0, 0, 0);
|
||||
return next.toISOString();
|
||||
} else if (schedule.startsWith('*/')) {
|
||||
// Parse "*/N minutes" format
|
||||
const match = schedule.match(/^\*\/(\d+)\s+minutes?$/);
|
||||
if (match) {
|
||||
const minutes = parseInt(match[1], 10);
|
||||
const next = new Date(now);
|
||||
next.setMinutes(next.getMinutes() + minutes, 0, 0);
|
||||
return next.toISOString();
|
||||
}
|
||||
|
||||
// Try to parse as cron expression
|
||||
try {
|
||||
const cron = new Cron(schedule);
|
||||
const nextRun = cron.nextRun();
|
||||
if (nextRun) {
|
||||
return nextRun.toISOString();
|
||||
}
|
||||
} catch {
|
||||
// Invalid cron expression, fall through to default
|
||||
}
|
||||
|
||||
// Default: run in 1 hour if we can't parse
|
||||
|
||||
19
src/lib/server/sync/cron.ts
Normal file
19
src/lib/server/sync/cron.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Cron utility functions for sync scheduling
|
||||
*/
|
||||
|
||||
import { Cron } from 'croner';
|
||||
|
||||
/**
|
||||
* Calculate the next run time from a cron expression
|
||||
*/
|
||||
export function calculateNextRun(cronExpr: string | null): string | null {
|
||||
if (!cronExpr) return null;
|
||||
try {
|
||||
const cron = new Cron(cronExpr);
|
||||
const nextRun = cron.nextRun();
|
||||
return nextRun?.toISOString() ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -89,31 +89,39 @@
|
||||
name="schedule"
|
||||
bind:value={schedule}
|
||||
required
|
||||
placeholder="e.g., daily, hourly, */5 minutes"
|
||||
class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 text-neutral-900 placeholder-neutral-400 focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-50 dark:placeholder-neutral-500 dark:focus:border-neutral-500 dark:focus:ring-neutral-500"
|
||||
placeholder="* * * * *"
|
||||
class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 font-mono text-neutral-900 placeholder-neutral-400 focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-50 dark:placeholder-neutral-500 dark:focus:border-neutral-500 dark:focus:ring-neutral-500"
|
||||
/>
|
||||
<div class="mt-2 space-y-1">
|
||||
<p class="text-sm font-medium text-neutral-700 dark:text-neutral-300">Examples:</p>
|
||||
<ul
|
||||
class="list-inside list-disc space-y-1 text-sm text-neutral-600 dark:text-neutral-400"
|
||||
>
|
||||
<li>
|
||||
<code class="rounded bg-neutral-100 px-1 py-0.5 dark:bg-neutral-800">daily</code> - Runs
|
||||
once per day
|
||||
</li>
|
||||
<li>
|
||||
<code class="rounded bg-neutral-100 px-1 py-0.5 dark:bg-neutral-800">hourly</code> - Runs
|
||||
every hour
|
||||
</li>
|
||||
<li>
|
||||
<code class="rounded bg-neutral-100 px-1 py-0.5 dark:bg-neutral-800">*/5 minutes</code
|
||||
> - Runs every 5 minutes
|
||||
</li>
|
||||
<li>
|
||||
<code class="rounded bg-neutral-100 px-1 py-0.5 dark:bg-neutral-800">weekly</code> - Runs
|
||||
once per week
|
||||
</li>
|
||||
</ul>
|
||||
<div class="mt-2 space-y-2">
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
Cron expression: <code class="rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-xs dark:bg-neutral-800">minute hour day month weekday</code>
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-xs dark:bg-neutral-800">* * * * *</code>
|
||||
<span>Every minute</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-xs dark:bg-neutral-800">*/5 * * * *</code>
|
||||
<span>Every 5 minutes</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-xs dark:bg-neutral-800">0 * * * *</code>
|
||||
<span>Every hour</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-xs dark:bg-neutral-800">0 0 * * *</code>
|
||||
<span>Daily at midnight</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-xs dark:bg-neutral-800">0 6 * * 1</code>
|
||||
<span>Mondays at 6am</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-xs dark:bg-neutral-800">0 0 1 * *</code>
|
||||
<span>First of month</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user