diff --git a/client/package.json b/client/package.json index d5d30fa8e..f9991e088 100644 --- a/client/package.json +++ b/client/package.json @@ -46,6 +46,7 @@ "canvas-confetti": "^1.9.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cronstrue": "^3.14.0", "dompurify": "^3.4.3", "driver.js": "^1.4.0", "echarts": "^6.0.0", diff --git a/client/src/features/library/utils/__tests__/cron.spec.ts b/client/src/features/library/utils/__tests__/cron.spec.ts new file mode 100644 index 000000000..2e0e919c9 --- /dev/null +++ b/client/src/features/library/utils/__tests__/cron.spec.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from 'vitest' + +import { parseCronToHuman } from '../cron' + +describe('parseCronToHuman', () => { + it('returns null for null input', () => { + expect(parseCronToHuman(null)).toBeNull() + }) + + it('returns null for undefined input', () => { + expect(parseCronToHuman(undefined)).toBeNull() + }) + + it('returns null for empty string', () => { + expect(parseCronToHuman('')).toBeNull() + }) + + it('parses a daily cron to a human-readable string', () => { + const result = parseCronToHuman('0 2 * * *') + expect(result).not.toBeNull() + expect(result!.toLowerCase()).toContain('02:00 am') + }) + + it('parses an every-5-minutes cron', () => { + const result = parseCronToHuman('*/5 * * * *') + expect(result).not.toBeNull() + expect(result!.toLowerCase()).toContain('every 5 minutes') + }) + + it('parses a weekly cron', () => { + const result = parseCronToHuman('0 0 * * 1') + expect(result).not.toBeNull() + expect(result!.toLowerCase()).toContain('monday') + }) + + it('falls back to the raw cron string on invalid input', () => { + const garbage = 'not-a-cron-at-all' + expect(parseCronToHuman(garbage)).toBe(garbage) + }) + + it('parses a monthly cron', () => { + const result = parseCronToHuman('0 0 1 * *') + expect(result).not.toBeNull() + expect(result!.toLowerCase()).toContain('day 1') + }) + + it('parses an hourly cron', () => { + const result = parseCronToHuman('0 * * * *') + expect(result).not.toBeNull() + expect(result!.toLowerCase()).toContain('every hour') + }) +}) diff --git a/client/src/features/library/utils/cron.ts b/client/src/features/library/utils/cron.ts new file mode 100644 index 000000000..cda3a7ac6 --- /dev/null +++ b/client/src/features/library/utils/cron.ts @@ -0,0 +1,10 @@ +import cronstrue from 'cronstrue' + +export function parseCronToHuman(cron: string | null | undefined): string | null { + if (!cron) return null + try { + return cronstrue.toString(cron, { use24HourTimeFormat: false, verbose: false }) + } catch { + return cron + } +} diff --git a/client/src/features/settings/LibrariesSettings.vue b/client/src/features/settings/LibrariesSettings.vue index 308c71880..eee40f73d 100644 --- a/client/src/features/settings/LibrariesSettings.vue +++ b/client/src/features/settings/LibrariesSettings.vue @@ -1,7 +1,22 @@