feat: improve cron syntax validation and force 6 digits with seconds

This commit is contained in:
Maxi Quoß
2025-02-07 14:04:44 +01:00
parent bba23e1f7a
commit cee1669a0e
25 changed files with 2063 additions and 821 deletions

View File

@@ -11,8 +11,8 @@ import (
var (
PingRunning = false
WakeShutdownRunning = false
CronPing = cron.New()
CronWakeShutdown = cron.New()
CronPing = cron.New(cron.WithSeconds())
CronWakeShutdown = cron.New(cron.WithSeconds())
)
func SetPingJobs(app *pocketbase.PocketBase) {
@@ -113,15 +113,18 @@ func SetWakeShutdownJobs(app *pocketbase.PocketBase) {
if wake_cron_enabled && wake_cron != "" {
_, err := CronWakeShutdown.AddFunc(wake_cron, func() {
isOnline := networking.PingDevice(dev)
if isOnline {
return
}
d, err := app.FindRecordById("devices", dev.Id)
if err != nil {
logger.Error.Println(err)
return
}
if d.GetString("status") == "pending" {
return
}
isOnline := networking.PingDevice(dev)
if isOnline {
return
}
d.Set("status", "pending")
if err := app.Save(d); err != nil {
logger.Error.Println("Failed to save record:", err)
@@ -144,15 +147,18 @@ func SetWakeShutdownJobs(app *pocketbase.PocketBase) {
if shutdown_cron_enabled && shutdown_cron != "" {
_, err := CronWakeShutdown.AddFunc(shutdown_cron, func() {
isOnline := networking.PingDevice(dev)
if !isOnline {
return
}
d, err := app.FindRecordById("devices", dev.Id)
if err != nil {
logger.Error.Println(err)
return
}
if d.GetString("status") == "pending" {
return
}
isOnline := networking.PingDevice(dev)
if !isOnline {
return
}
status := d.GetString("status")
if status != "online" {
return
@@ -187,8 +193,7 @@ func StartWakeShutdown() {
func StopWakeShutdown() {
if WakeShutdownRunning {
logger.Info.Println("Stopping wake/shutdown cronjob")
ctx := CronWakeShutdown.Stop()
<-ctx.Done()
CronWakeShutdown.Stop()
}
WakeShutdownRunning = false
}
@@ -201,8 +206,7 @@ func StartPing() {
func StopPing() {
if PingRunning {
logger.Info.Println("Stopping wake/shutdown cronjob")
ctx := CronPing.Stop()
<-ctx.Done()
CronPing.Stop()
}
PingRunning = false
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/plugins/migratecmd"
"github.com/robfig/cron/v3"
"github.com/seriousm4x/upsnap/cronjobs"
"github.com/seriousm4x/upsnap/logger"
_ "github.com/seriousm4x/upsnap/migrations"
@@ -212,7 +213,8 @@ func importSettings(app *pocketbase.PocketBase) error {
// 1st: env var
// 2nd: database entry
// 3rd: default values
interval := "@every 3s"
defaultInterval := "*/3 * * * * *"
interval := defaultInterval
if settingsPrivate.GetString("interval") != "" {
interval = settingsPrivate.GetString("interval")
}
@@ -220,6 +222,13 @@ func importSettings(app *pocketbase.PocketBase) error {
interval = os.Getenv("UPSNAP_INTERVAL")
}
// validate interval before saving
p := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
if _, err := p.Parse(interval); err != nil {
interval = defaultInterval
logger.Warning.Println(errors.New("Ping interval is not valid."))
}
settingsPrivate.Set("interval", interval)
if scanRange := os.Getenv("UPSNAP_SCAN_RANGE"); scanRange != "" {
settingsPrivate.Set("scan_range", scanRange)

2466
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,10 @@
<script lang="ts">
import { goto } from '$app/navigation';
import LL, { locale } from '$lib/i18n/i18n-svelte';
import { parseCron } from '$lib/helpers/cron';
import LL from '$lib/i18n/i18n-svelte';
import { dateFnsLocale } from '$lib/stores/locale';
import { backendUrl, permission, pocketbase } from '$lib/stores/pocketbase';
import type { Device } from '$lib/types/device';
import { type Device } from '$lib/types/device';
import {
faBed,
faCircleArrowDown,
@@ -11,20 +13,7 @@
faPen,
faRotateLeft
} from '@fortawesome/free-solid-svg-icons';
import cronParser from 'cron-parser';
import type { Locale } from 'date-fns';
import { formatDistance, formatRelative, parseISO } from 'date-fns';
import { de } from 'date-fns/locale/de';
import { enUS } from 'date-fns/locale/en-US';
import { es } from 'date-fns/locale/es';
import { fr } from 'date-fns/locale/fr';
import { it } from 'date-fns/locale/it';
import { ja } from 'date-fns/locale/ja';
import { nl } from 'date-fns/locale/nl';
import { pl } from 'date-fns/locale/pl';
import { pt } from 'date-fns/locale/pt';
import { zhCN } from 'date-fns/locale/zh-CN';
import { zhTW } from 'date-fns/locale/zh-TW';
import { formatDistance, parseISO } from 'date-fns';
import Fa from 'svelte-fa';
import toast from 'svelte-french-toast';
import { scale } from 'svelte/transition';
@@ -71,62 +60,6 @@
}, 1000);
}
// dynamically import locales
// it's ugly but the only working solution i found in ~ 3 hours
var dateFnsLocale: Locale;
$: if ($locale !== undefined) {
(async () => {
switch ($locale) {
case 'de':
case 'de-DE':
dateFnsLocale = de;
break;
case 'en':
case 'en-US':
dateFnsLocale = enUS;
break;
case 'fr':
case 'fr-FR':
dateFnsLocale = fr;
break;
case 'it':
case 'it-IT':
dateFnsLocale = it;
break;
case 'ja':
case 'ja-JP':
dateFnsLocale = ja;
break;
case 'es':
case 'es-ES':
dateFnsLocale = es;
break;
case 'nl':
case 'nl-NL':
dateFnsLocale = nl;
break;
case 'pl':
case 'pl-PL':
dateFnsLocale = pl;
break;
case 'pt':
case 'pt-PT':
dateFnsLocale = pt;
break;
case 'zh':
case 'zh-CN':
dateFnsLocale = zhCN;
break;
case 'zh-TW':
dateFnsLocale = zhTW;
break;
default:
dateFnsLocale = enUS;
break;
}
})();
}
function sleep() {
fetch(`${backendUrl}api/upsnap/sleep/${device.id}`, {
headers: {
@@ -147,13 +80,6 @@
});
}
function getNextCronRelativeTime(expression: string, now: number) {
const cron = cronParser.parseExpression(expression);
return formatRelative(cron.next().toISOString(), now, {
locale: dateFnsLocale
});
}
function askRebootConfirmation() {
if (device.shutdown_confirm) {
modalReboot.showModal();
@@ -183,17 +109,14 @@
{#if device.wake_cron_enabled}
<div class="tooltip" data-tip={$LL.device.card_tooltip_wake_cron()}>
<span class="badge badge-success gap-1 p-3"
><Fa icon={faCircleArrowUp} />{getNextCronRelativeTime(device.wake_cron, now)}</span
><Fa icon={faCircleArrowUp} />{parseCron(device.wake_cron, now)}</span
>
</div>
{/if}
{#if device.shutdown_cron_enabled}
<div class="tooltip" data-tip={$LL.device.card_tooltip_shutdown_cron()}>
<span class="badge badge-error gap-1 p-3"
><Fa icon={faCircleArrowDown} />{getNextCronRelativeTime(
device.shutdown_cron,
now
)}</span
><Fa icon={faCircleArrowDown} />{parseCron(device.shutdown_cron, now)}</span
>
</div>
{/if}
@@ -209,13 +132,11 @@
class="tooltip"
data-tip="{$LL.device.card_tooltip_last_status_change()}: {device.updated}"
>
{#if dateFnsLocale !== undefined}
{formatDistance(parseISO(device.updated), now, {
includeSeconds: true,
addSuffix: true,
locale: dateFnsLocale
})}
{/if}
{formatDistance(parseISO(device.updated), now, {
includeSeconds: true,
addSuffix: true,
locale: $dateFnsLocale
})}
</span>
{#if moreButtons.filter((btn) => btn.requires).length > 0}
<div class="ms-auto flex flex-row flex-wrap gap-1">

View File

@@ -1,10 +1,12 @@
<script lang="ts">
import { goto } from '$app/navigation';
import DeviceFormPort from '$lib/components/DeviceFormPort.svelte';
import { cronRegex, parseCron } from '$lib/helpers/cron';
import LL from '$lib/i18n/i18n-svelte';
import { permission, pocketbase } from '$lib/stores/pocketbase';
import type { Device, Group, Port } from '$lib/types/device';
import { faSave, faTrash, faX } from '@fortawesome/free-solid-svg-icons';
import cronParser from 'cron-parser';
import { onMount } from 'svelte';
import Fa from 'svelte-fa';
import toast from 'svelte-french-toast';
@@ -20,6 +22,24 @@
});
async function save() {
// validate crons
if (
device.wake_cron_enabled &&
(!cronRegex.test(device.wake_cron) ||
Object.keys(cronParser.parseString(device.wake_cron).errors).length > 0)
) {
toast.error($LL.settings.invalid_cron());
throw new Error('wake_cron not valid');
}
if (
device.shutdown_cron_enabled &&
(!cronRegex.test(device.shutdown_cron) ||
Object.keys(cronParser.parseString(device.shutdown_cron).errors).length > 0)
) {
toast.error($LL.settings.invalid_cron());
throw new Error('shutdown_cron not valid');
}
// create/update all ports
let portIds: string[] = [];
await Promise.all(
@@ -183,7 +203,7 @@
<h2 class="card-title">{$LL.device.general()}</h2>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
<fieldset class="fieldset">
<label class="floating-label">
<label class="floating-label mt-2">
<span>{$LL.device.general_name()} <span class="text-error">*</span></span>
<input
type="text"
@@ -195,7 +215,7 @@
</label>
</fieldset>
<fieldset class="fieldset">
<label class="floating-label">
<label class="floating-label mt-2">
<span>{$LL.device.general_ip()} <span class="text-error">*</span></span>
<input
type="text"
@@ -207,7 +227,7 @@
</label>
</fieldset>
<fieldset class="fieldset">
<label class="floating-label">
<label class="floating-label mt-2">
<span>{$LL.device.general_mac()} <span class="text-error">*</span></span>
<input
type="text"
@@ -219,7 +239,7 @@
</label>
</fieldset>
<fieldset class="fieldset">
<label class="floating-label">
<label class="floating-label mt-2">
<span>{$LL.device.general_netmask()} <span class="text-error">*</span></span>
<input
type="text"
@@ -231,7 +251,7 @@
</label>
</fieldset>
<fieldset class="fieldset">
<label class="floating-label">
<label class="floating-label mt-2">
<span>{$LL.device.general_description()}</span>
<input
type="text"
@@ -248,7 +268,7 @@
<div class="card bg-base-200 mt-6 w-full shadow-sm">
<div class="card-body">
<h2 class="card-title">{$LL.device.ports()}</h2>
<p class="my-2">{$LL.device.ports_desc()}</p>
<p>{$LL.device.ports_desc()}</p>
<div class="w-full">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
@@ -267,12 +287,12 @@
<div class="card bg-base-200 mt-6 w-full shadow-sm">
<div class="card-body">
<h2 class="card-title">{$LL.device.link()}</h2>
<p class="my-2">
<p>
{$LL.device.link_desc()}
</p>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<fieldset class="fieldset">
<label class="floating-label">
<label class="floating-label mt-2">
<span>{$LL.device.link()}</span>
<input
type="url"
@@ -283,7 +303,7 @@
</label>
</fieldset>
<fieldset class="fieldset">
<label class="floating-label">
<label class="floating-label mt-2">
<span>{$LL.device.link_open()}</span>
<select class="select" bind:value={device.link_open}>
<option value="">{$LL.device.link_open_no()}</option>
@@ -298,12 +318,12 @@
<div class="card bg-base-200 mt-6 w-full shadow-sm">
<div class="card-body">
<h2 class="card-title">{$LL.device.ping()}</h2>
<p class="my-2">
<p>
<!-- eslint-disable svelte/no-at-html-tags -->
{@html $LL.device.ping_desc()}
</p>
<fieldset class="fieldset">
<label class="floating-label">
<label class="floating-label mt-2">
<span>{$LL.device.ping_cmd()}</span>
<input
type="text"
@@ -318,13 +338,13 @@
<div class="card bg-base-200 mt-6 w-full shadow-sm">
<div class="card-body">
<h2 class="card-title">{$LL.device.wake()}</h2>
<p class="my-2">
<p>
{$LL.device.wake_desc()}
<!-- eslint-disable svelte/no-at-html-tags -->
{@html $LL.settings.ping_interval_desc2()}
</p>
<fieldset class="fieldset">
<label class="floating-label">
<label class="floating-label mt-2">
<span>{$LL.device.wake_cmd()}</span>
<input
type="text"
@@ -345,7 +365,7 @@
/>
{$LL.device.wake_cron_enable()}
</label>
<label class="floating-label mt-4">
<label class="floating-label mt-2">
<span
>{$LL.device.wake_cron()}
{#if device.wake_cron_enabled}
@@ -360,6 +380,9 @@
disabled={!device.wake_cron_enabled}
required={device.wake_cron_enabled}
/>
{#if device.wake_cron_enabled}
<p class="fieldset-label">{parseCron(device.wake_cron, Date.now())}</p>
{/if}
</label>
</fieldset>
<fieldset class="fieldset bg-base-100 border-base-300 rounded-box w-64 border p-4">
@@ -380,7 +403,7 @@
<div class="card bg-base-200 mt-6 w-full shadow-sm">
<div class="card-body">
<h2 class="card-title">Sleep-On-LAN</h2>
<p class="mt-2">
<p>
<!-- eslint-disable svelte/no-at-html-tags -->
{@html $LL.device.sol_desc1()}
</p>
@@ -402,7 +425,7 @@
/>
{$LL.device.sol_enable()}
</label>
<label class="floating-label mt-4">
<label class="floating-label mt-2">
<span
>{$LL.device.sol_port()}
{#if device.sol_enabled}
@@ -428,7 +451,7 @@
<input type="checkbox" bind:checked={device.sol_auth} class="toggle toggle-success" />
{$LL.device.sol_authorization()}
</label>
<label class="floating-label mt-4">
<label class="floating-label mt-2">
<span
>{$LL.device.sol_user()}
{#if device.sol_auth}
@@ -444,7 +467,7 @@
required={device.sol_auth}
/>
</label>
<label class="floating-label">
<label class="floating-label mt-2">
<span
>{$LL.device.sol_password()}
{#if device.sol_auth}
@@ -469,12 +492,12 @@
<div class="card bg-base-200 mt-6 w-full shadow-sm">
<div class="card-body">
<h2 class="card-title">{$LL.device.shutdown()}</h2>
<p class="my-2">
<p>
<!-- eslint-disable svelte/no-at-html-tags -->
{@html $LL.device.shutdown_desc()}
</p>
<fieldset class="fieldset">
<label class="floating-label">
<label class="floating-label mt-2">
<span>{$LL.device.shutdown_cmd()}</span>
<input
type="text"
@@ -484,7 +507,7 @@
/>
</label>
</fieldset>
<p class="my-2 font-bold">{$LL.device.shutdown_examples()}</p>
<p class="font-bold">{$LL.device.shutdown_examples()}</p>
<div class="mockup-code max-w-fit min-w-0 text-sm">
<pre data-prefix="#" class="italic"><code>{$LL.device.shutdown_examples_windows()}</code
></pre>
@@ -499,7 +522,7 @@
>sshpass -p password ssh -o "StrictHostKeyChecking=no" user@192.168.1.13 "sudo poweroff"</code
></pre>
</div>
<p class="mt-4">
<p>
{$LL.device.shutdown_cron_desc()}
</p>
<div class="flex flex-row flex-wrap gap-4">
@@ -513,7 +536,7 @@
/>
{$LL.device.shutdown_cron_enable()}
</label>
<label class="floating-label mt-4">
<label class="floating-label mt-2">
<span
>{$LL.device.shutdown_cron()}
{#if device.shutdown_cron_enabled}
@@ -523,11 +546,14 @@
<input
type="text"
placeholder={$LL.device.shutdown_cron()}
class="input"
class="input validator"
bind:value={device.shutdown_cron}
disabled={!device.shutdown_cron_enabled}
required={device.shutdown_cron_enabled}
/>
{#if device.shutdown_cron_enabled}
<p class="fieldset-label">{parseCron(device.shutdown_cron, Date.now())}</p>
{/if}
</label>
</fieldset>
<fieldset class="fieldset bg-base-100 border-base-300 rounded-box w-64 border p-4">
@@ -548,12 +574,12 @@
<div class="card bg-base-200 mt-6 w-full shadow-sm">
<div class="card-body">
<h2 class="card-title">{$LL.device.password()}</h2>
<p class="my-2">
<p>
<!-- eslint-disable svelte/no-at-html-tags -->
{@html $LL.device.password_desc()}
</p>
<fieldset class="fieldset">
<label class="floating-label">
<label class="floating-label mt-2">
<span>{$LL.device.password()}</span>
<input
type="text"
@@ -569,7 +595,7 @@
<div class="card bg-base-200 mt-6 w-full shadow-sm">
<div class="card-body">
<h2 class="card-title">{$LL.device.groups()}</h2>
<p class="my-2">
<p>
{$LL.device.groups_desc()}
</p>
<div class="flex flex-row flex-wrap gap-2">

View File

@@ -0,0 +1,28 @@
import LL from '$lib/i18n/i18n-svelte';
import { dateFnsLocale } from '$lib/stores/locale';
import cronParser from 'cron-parser';
import { formatDistanceStrict } from 'date-fns';
import { get } from 'svelte/store';
export const cronRegex = new RegExp(/((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){6})/);
export function parseCron(expression: string, now: number) {
try {
if (
expression === '' ||
!cronRegex.test(expression) ||
Object.keys(cronParser.parseString(expression).errors).length > 0
) {
throw new Error('cron-parser failed to parse string');
}
const cron = cronParser.parseExpression(expression, {});
return formatDistanceStrict(cron.next().toISOString(), new Date(now), {
locale: get(dateFnsLocale),
addSuffix: true
});
} catch {
return get(LL).settings.invalid_cron();
}
}

View File

@@ -141,7 +141,7 @@ const de = {
ping_interval_desc1:
'Legt den Intervall fest, in dem Geräte gepingt werden. Leer lassen um den Standardwert von <span class="badge">@every 3s</span> zu verwenden.',
ping_interval_desc2:
'Lerne mehr über die korrekte Cron Syntax auf <a class="link" href="https://de.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> oder sieh dir die <a class="link" href="https://pkg.go.dev/github.com/robfig/cron/v3" target="_blank">Paketdokumentation</a> an.',
'Lerne mehr über die korrekte Cron Syntax auf <a class="link" href="https://de.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> oder sieh dir die <a class="link" href="https://github.com/harrisiirak/cron-parser" target="_blank">Paketdokumentation</a> an.',
lazy_ping_title: 'Lazy Ping',
lazy_ping_desc:
'Wenn Lazy Ping aktiviert ist, pingt UpSnap die Geräte nur an, wenn ein aktiver Benutzer die Website besucht. Wenn es ausgeschaltet ist, pingt UpSnap die Geräte immer an.',
@@ -150,7 +150,8 @@ const de = {
website_title_desc: 'Setzt den Seiten- und den Tabtitel.',
icon_title: 'Icon',
icon_desc: 'Eigenes Favicon nutzen. Unterstützte Dateitypen sind:',
upsnap_version: 'UpSnap Version'
upsnap_version: 'UpSnap Version',
invalid_cron: 'Ungültige cron Syntax'
},
users: {
page_title: 'Benutzer',

View File

@@ -138,7 +138,7 @@ const en = {
ping_interval_desc1:
'Sets the interval in which the devices are pinged. Leave blank to use default value of <span class="badge">@every 3s</span>.',
ping_interval_desc2:
'Learn more about the correct syntax for cron on <a class="link" href="https://en.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> or refer to the <a class="link" href="https://pkg.go.dev/github.com/robfig/cron/v3" target="_blank">package documentation</a>.',
'Learn more about the correct syntax for cron on <a class="link" href="https://en.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> or refer to the <a class="link" href="https://github.com/harrisiirak/cron-parser" target="_blank">package documentation</a>.',
lazy_ping_title: 'Lazy ping',
lazy_ping_desc:
"When lazy ping is turned on, UpSnap will only ping devices if there is an active user visiting the website. If it's turned off, UpSnap will always ping devices.",
@@ -147,7 +147,8 @@ const en = {
website_title_desc: 'Sets the title of the website and in the browser tab.',
icon_title: 'Icon',
icon_desc: 'Set a custom favicon. Supported file types are:',
upsnap_version: 'UpSnap version'
upsnap_version: 'UpSnap version',
invalid_cron: 'Invalid cron syntax'
},
users: {
page_title: 'Users',

View File

@@ -140,7 +140,7 @@ const es = {
ping_interval_desc1:
'Establece el intervalo en el que se realiza el ping. Déjelo en blanco para usar el valor predeterminado de <span class="badge">@every 3s</span>.',
ping_interval_desc2:
'Puedes ver la sintaxis correcta para cron en <a class="link" href="https://en.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> o en <a class="link" href="https://pkg.go.dev/github.com/robfig/cron/v3" target="_blank">documentación cron</a>.',
'Puedes ver la sintaxis correcta para cron en <a class="link" href="https://en.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> o en <a class="link" href="https://github.com/harrisiirak/cron-parser" target="_blank">documentación cron</a>.',
lazy_ping_title: 'Lazy ping',
lazy_ping_desc:
'Cuando el ping diferido está activado, UpSnap solo hará ping a los dispositivos si hay un usuario activo visitando el sitio web. Si está desactivado, UpSnap siempre hará ping a los dispositivos.',
@@ -149,7 +149,8 @@ const es = {
website_title_desc: 'Establece el título del sitio web y en la pestaña del navegador.',
icon_title: 'Icon',
icon_desc: 'Establece un favicon personalizado. Los tipos de archivos admitidos son:',
upsnap_version: 'Version UpSnap'
upsnap_version: 'Version UpSnap',
invalid_cron: 'Sintaxis cron no válida'
},
users: {
page_title: 'Usuarios',

View File

@@ -142,7 +142,7 @@ const fr = {
ping_interval_desc1:
'Définit l\'intervalle dans lequel les appareils reçoivent une requête ping. Laissez vide pour utiliser la valeur par défaut de <span class="badge">@every 3s</span>.',
ping_interval_desc2:
'Pour écrire une syntaxe correcte pour l\'utilitaire cron, consultez <a class="link" href="https://fr.wikipedia.org/wiki/Cron" target="_blank">Wikipédia</a> ou référez-vous à la <a class="link" href="https://pkg.go.dev/github.com/robfig/cron/v3" target="_blank">documentation du paquet</a>.',
'Pour écrire une syntaxe correcte pour l\'utilitaire cron, consultez <a class="link" href="https://fr.wikipedia.org/wiki/Cron" target="_blank">Wikipédia</a> ou référez-vous à la <a class="link" href="https://github.com/harrisiirak/cron-parser" target="_blank">documentation du paquet</a>.',
lazy_ping_title: 'Ping fainéant',
lazy_ping_desc:
"Lorsque le mode ping fainéant est activé, UpSnap ne va effectuer des requêtes de ping sur les appareils que lorsqu'un utilisateur actif visite le site. Si vous désactivez cette fonction, UpSnap exécutera toujours ces requêtes.",
@@ -152,7 +152,8 @@ const fr = {
"Définit le titre du site ainsi que le texte affiché dans l'onglet du navigateur.",
icon_title: 'Icône',
icon_desc: 'Définit un favicon personnalisé. Les types de fichier compatibles sont :',
upsnap_version: 'Version de UpSnap '
upsnap_version: 'Version de UpSnap ',
invalid_cron: 'Syntaxe cron invalide'
},
users: {
page_title: 'Utilisateurs',

View File

@@ -1,4 +1,5 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
import { initI18nSvelte } from 'typesafe-i18n/svelte';
import type { Formatters, Locales, TranslationFunctions, Translations } from './i18n-types';

View File

@@ -521,7 +521,7 @@ type RootTranslation = {
*/
ping_interval_desc1: string;
/**
* Learn more about the correct syntax for cron on <a class="link" href="https://en.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> or refer to the <a class="link" href="https://pkg.go.dev/github.com/robfig/cron/v3" target="_blank">package documentation</a>.
* Learn more about the correct syntax for cron on <a class="link" href="https://en.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> or refer to the <a class="link" href="https://github.com/harrisiirak/cron-parser" target="_blank">package documentation</a>.
*/
ping_interval_desc2: string;
/**
@@ -556,6 +556,10 @@ type RootTranslation = {
* UpSnap version
*/
upsnap_version: string;
/**
* Invalid cron syntax
*/
invalid_cron: string;
};
users: {
/**
@@ -1316,7 +1320,7 @@ export type TranslationFunctions = {
*/
ping_interval_desc1: () => LocalizedString;
/**
* Learn more about the correct syntax for cron on <a class="link" href="https://en.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> or refer to the <a class="link" href="https://pkg.go.dev/github.com/robfig/cron/v3" target="_blank">package documentation</a>.
* Learn more about the correct syntax for cron on <a class="link" href="https://en.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> or refer to the <a class="link" href="https://github.com/harrisiirak/cron-parser" target="_blank">package documentation</a>.
*/
ping_interval_desc2: () => LocalizedString;
/**
@@ -1351,6 +1355,10 @@ export type TranslationFunctions = {
* UpSnap version
*/
upsnap_version: () => LocalizedString;
/**
* Invalid cron syntax
*/
invalid_cron: () => LocalizedString;
};
users: {
/**

View File

@@ -1,4 +1,5 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
import { initFormatters } from './formatters';
import type { Locales, Translations } from './i18n-types';

View File

@@ -1,4 +1,5 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
import { initFormatters } from './formatters';
import type { Locales, Translations } from './i18n-types';

View File

@@ -1,4 +1,5 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
import {
i18n as initI18n,

View File

@@ -141,7 +141,7 @@ const it = {
ping_interval_desc1:
'Imposta l\'intervallo nel quale i dispositivi vengono testati con ping. Lasciare in bianco per lasciare il valore predefinito <span class="badge">@every 3s</span>.',
ping_interval_desc2:
'Per ulteriori dettagli circa la sintassi corretta di cron, fare riferimento a <a class="link" href="https://it.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> oppure a <a class="link" href="https://pkg.go.dev/github.com/robfig/cron/v3" target="_blank">questa documentazione</a> (in inglese).',
'Per ulteriori dettagli circa la sintassi corretta di cron, fare riferimento a <a class="link" href="https://it.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> oppure a <a class="link" href="https://github.com/harrisiirak/cron-parser" target="_blank">questa documentazione</a> (in inglese).',
lazy_ping_title: 'Ping pigro',
lazy_ping_desc:
"Quando il ping pigro e' abilitato, UpSnap effettuera' i ping verso di dispositivi solamente quando un utente effettivamente visita il sito. Se e' spento, allora UpSnap continuera' con il ping sempre.",
@@ -150,7 +150,8 @@ const it = {
website_title_desc: 'Imposta il nome del sito.',
icon_title: 'Icona',
icon_desc: 'Imposta una icona personalizzata. I tipi di file supportati sono:',
upsnap_version: 'Versione UpSnap'
upsnap_version: 'Versione UpSnap',
invalid_cron: 'Sintassi cron non valida'
},
users: {
page_title: 'Utenti',

View File

@@ -139,7 +139,7 @@ const ja = {
ping_interval_desc1:
'デバイスが ping される間隔を設定します。空白の場合、デフォルト値 <span class="badge">@every 3s</span> が使用されます。',
ping_interval_desc2:
'cron の正しい構文については、<a class="link" href="https://en.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> または <a class="link" href="https://pkg.go.dev/github.com/robfig/cron/v3" target="_blank">パッケージドキュメント</a> を参照してください。',
'cron の正しい構文については、<a class="link" href="https://en.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> または <a class="link" href="https://github.com/harrisiirak/cron-parser" target="_blank">パッケージドキュメント</a> を参照してください。',
lazy_ping_title: 'Lazy ping',
lazy_ping_desc:
'Lazy ping をオンにすると、UpSnap はアクティブなユーザーがウェブサイトを訪れている場合にのみデバイスを ping します。オフの場合、UpSnap は常にデバイスを ping します。',
@@ -148,7 +148,8 @@ const ja = {
website_title_desc: 'ウェブサイトおよびブラウザタブのタイトルを設定します。',
icon_title: 'アイコン',
icon_desc: 'カスタムファビコンを設定します。サポートされているファイルタイプ:',
upsnap_version: 'UpSnap バージョン'
upsnap_version: 'UpSnap バージョン',
invalid_cron: '無効なcron構文'
},
users: {
page_title: 'ユーザー',

View File

@@ -141,7 +141,7 @@ const nl = {
ping_interval_desc1:
'Stelt de interval in waarin de apparaten worden gepingt. Laat leeg om de standaardwaarde van <span class="badge">@every 3s</span> te gebruiken.',
ping_interval_desc2:
'Leer meer over de juiste syntax voor cron op <a class="link" href="https://en.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> of raadpleeg de <a class="link" href="https://pkg.go.dev/github.com/robfig/cron/v3" target="_blank">pakketdocumentatie</a>.',
'Leer meer over de juiste syntax voor cron op <a class="link" href="https://en.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> of raadpleeg de <a class="link" href="https://github.com/harrisiirak/cron-parser" target="_blank">pakketdocumentatie</a>.',
lazy_ping_title: 'Lazy ping',
lazy_ping_desc:
'Wanneer lazy ping is ingeschakeld, pingt UpSnap alleen apparaten als er een actieve gebruiker de website bezoekt. Als het is uitgeschakeld, pingt UpSnap altijd apparaten.',
@@ -150,7 +150,8 @@ const nl = {
website_title_desc: 'Stelt de titel van de website en in het browsertabblad in.',
icon_title: 'Icoon',
icon_desc: 'Stel een aangepast favicon in. Ondersteunde bestandstypen zijn:',
upsnap_version: 'UpSnap versie'
upsnap_version: 'UpSnap versie',
invalid_cron: 'Ongeldige cron syntaxis'
},
users: {
page_title: 'Gebruikers',

View File

@@ -139,7 +139,7 @@ const pl = {
ping_interval_desc1:
'Ustawia interwał, w którym urządzenia są pingowane. Pozostaw puste aby użyć domyślnej wartości <span class="badge">@every 3s</span>.',
ping_interval_desc2:
'Dowiedz się więcej na temat składni cron na <a class="link" href="https://pl.wikipedia.org/wiki/Cron" target="_blank">Wikipedii</a> lub w <a class="link" href="https://pkg.go.dev/github.com/robfig/cron/v3" target="_blank">dokumentacji pakietu cron</a>.',
'Dowiedz się więcej na temat składni cron na <a class="link" href="https://pl.wikipedia.org/wiki/Cron" target="_blank">Wikipedii</a> lub w <a class="link" href="https://github.com/harrisiirak/cron-parser" target="_blank">dokumentacji pakietu cron</a>.',
lazy_ping_title: 'Leniwe pingowanie',
lazy_ping_desc:
'Gdy leniwe pingowanie jest włączone, UpSnap będzie pingować urządzenia tylko wtedy, gdy użytkownicy odwiedzają stronę. Gdy ta opcja jest wyłączona, UpSnap będzie pingować urządzenia nieustannie.',
@@ -148,7 +148,8 @@ const pl = {
website_title_desc: 'Ustawia tytuł strony internetowej i na karcie przeglądarki.',
icon_title: 'Ikona',
icon_desc: 'Ustaw niestandardowy favicon. Obsługiwane są typy plików:',
upsnap_version: 'Wersja UpSnap'
upsnap_version: 'Wersja UpSnap',
invalid_cron: 'Nieprawidłowa składnia cron'
},
users: {
page_title: 'Użytkownicy',

View File

@@ -140,7 +140,7 @@ const pt = {
ping_interval_desc1:
'Define o intervalo em que o ping é executado. Deixe em branco para usar o valor por defeito de <span class="badge">@every 3s</span>.',
ping_interval_desc2:
'Poderá ver a sintaxe correta para cron em <a class="link" href="https://en.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> ou em <a class="link" href="https://pkg.go.dev/github.com/robfig/cron/v3" target="_blank">documentação cron</a>.',
'Poderá ver a sintaxe correta para cron em <a class="link" href="https://en.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> ou em <a class="link" href="https://github.com/harrisiirak/cron-parser" target="_blank">documentação cron</a>.',
lazy_ping_title: 'Lazy ping',
lazy_ping_desc:
'Quando o lazy ping está activo, o UpSnap irá apenas executar o ping se existir um utilizador ativo no website. Se desligado, o UpSnap irá executar sempre o ping.',
@@ -149,7 +149,8 @@ const pt = {
website_title_desc: 'Define o título do website.',
icon_title: 'Ícone',
icon_desc: 'Define um favicon personalizado. Tipos de ficheiros suportados são:',
upsnap_version: 'Versão UpSnap'
upsnap_version: 'Versão UpSnap',
invalid_cron: 'Sintaxe inválida do cron'
},
users: {
page_title: 'Utilizadores',

View File

@@ -135,7 +135,7 @@ const zh_TW = {
ping_interval_title: 'Ping 間隔',
ping_interval_desc1: '設定 ping 的間隔。留空使用預設值 <span class="badge">@every 3s</span>.',
ping_interval_desc2:
'有關 cron 的更多資訊,請見<a class="link" href="https://zh.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> 或參閱 <a class="link" href="https://pkg.go.dev/github.com/robfig/cron/v3" target="_blank">cron 文檔</a>.',
'有關 cron 的更多資訊,請見<a class="link" href="https://zh.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> 或參閱 <a class="link" href="https://github.com/harrisiirak/cron-parser" target="_blank">cron 文檔</a>.',
lazy_ping_title: 'Lazy ping',
lazy_ping_desc: '當開關打開時, UpSnap 只會在有訪問者訪問網站時才對設備進行 ping 測試',
lazy_ping_enable: '啟用',
@@ -143,7 +143,8 @@ const zh_TW = {
website_title_desc: '設置網站的標題,同時顯示在瀏覽器的標籤中。',
icon_title: '圖標',
icon_desc: '設定一個自訂義圖標。支援的檔案類型:',
upsnap_version: 'UpSnap 版本'
upsnap_version: 'UpSnap 版本',
invalid_cron: '「無效的 Cron 語法」'
},
users: {
page_title: '使用者',

View File

@@ -133,7 +133,7 @@ const zh = {
ping_interval_title: 'Ping 间隔',
ping_interval_desc1: '设置 ping 的间隔。为空时使用默认值 <span class="badge">@every 3s</span>.',
ping_interval_desc2:
'有关 cron 的更多信息, 请访问<a class="link" href="https://zh.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> 或参阅 <a class="link" href="https://pkg.go.dev/github.com/robfig/cron/v3" target="_blank">cron 文档</a>.',
'有关 cron 的更多信息, 请访问<a class="link" href="https://zh.wikipedia.org/wiki/Cron" target="_blank">Wikipedia</a> 或参阅 <a class="link" href="https://github.com/harrisiirak/cron-parser" target="_blank">cron 文档</a>.',
lazy_ping_title: 'Lazy ping',
lazy_ping_desc: '当开关打开时, UpSnap 只会在用户访问网站期间进行 ping 操作.',
lazy_ping_enable: '启用',
@@ -141,7 +141,8 @@ const zh = {
website_title_desc: '设置在浏览器选项卡和网站中显示标题',
icon_title: '图标',
icon_desc: '设置一个自定义图标. 支持的文件类型:',
upsnap_version: 'UpSnap 版本'
upsnap_version: 'UpSnap 版本',
invalid_cron: '无效的 cron 语法'
},
users: {
page_title: '用户',

View File

@@ -0,0 +1,69 @@
import { locale } from '$lib/i18n/i18n-svelte';
import type { Locale } from 'date-fns';
import { de } from 'date-fns/locale/de';
import { enUS } from 'date-fns/locale/en-US';
import { es } from 'date-fns/locale/es';
import { fr } from 'date-fns/locale/fr';
import { it } from 'date-fns/locale/it';
import { ja } from 'date-fns/locale/ja';
import { nl } from 'date-fns/locale/nl';
import { pl } from 'date-fns/locale/pl';
import { pt } from 'date-fns/locale/pt';
import { zhCN } from 'date-fns/locale/zh-CN';
import { zhTW } from 'date-fns/locale/zh-TW';
import { writable, type Writable } from 'svelte/store';
// dynamically import locales
// it's ugly but the only working solution i found in ~ 3 hours
export const dateFnsLocale: Writable<Locale> = writable(enUS);
locale.subscribe((l) => {
switch (l) {
case 'de':
case 'de-DE':
dateFnsLocale.set(de);
break;
case 'en':
case 'en-US':
dateFnsLocale.set(enUS);
break;
case 'fr':
case 'fr-FR':
dateFnsLocale.set(fr);
break;
case 'it':
case 'it-IT':
dateFnsLocale.set(it);
break;
case 'ja':
case 'ja-JP':
dateFnsLocale.set(ja);
break;
case 'es':
case 'es-ES':
dateFnsLocale.set(es);
break;
case 'nl':
case 'nl-NL':
dateFnsLocale.set(nl);
break;
case 'pl':
case 'pl-PL':
dateFnsLocale.set(pl);
break;
case 'pt':
case 'pt-PT':
dateFnsLocale.set(pt);
break;
case 'zh':
case 'zh-CN':
dateFnsLocale.set(zhCN);
break;
case 'zh-TW':
dateFnsLocale.set(zhTW);
break;
default:
dateFnsLocale.set(enUS);
break;
}
});

View File

@@ -8,7 +8,7 @@ export type SettingsPublic = RecordModel & {
};
export type SettingsPrivate = RecordModel & {
interval: number;
interval: string;
lazy_ping: boolean;
scan_range: string;
};

View File

@@ -3,11 +3,13 @@
import { page } from '$app/state';
import { PUBLIC_VERSION } from '$env/static/public';
import PageLoading from '$lib/components/PageLoading.svelte';
import { cronRegex, parseCron } from '$lib/helpers/cron';
import LL from '$lib/i18n/i18n-svelte';
import { backendUrl, pocketbase } from '$lib/stores/pocketbase';
import { settingsPriv, settingsPub } from '$lib/stores/settings';
import type { SettingsPrivate, SettingsPublic } from '$lib/types/settings';
import { faSave } from '@fortawesome/free-solid-svg-icons';
import cronParser from 'cron-parser';
import { onMount } from 'svelte';
import Fa from 'svelte-fa';
import toast from 'svelte-french-toast';
@@ -17,6 +19,15 @@
let faviconPreview: HTMLImageElement;
let faviconInputElement: HTMLInputElement;
let now = Date.now();
let interval: number;
$: {
clearInterval(interval);
interval = setInterval(() => {
now = Date.now();
}, 1000);
}
onMount(() => {
if (!$pocketbase.authStore.isSuperuser) {
toast($LL.toasts.no_permission({ url: page.url.pathname }), {
@@ -58,6 +69,15 @@
)
return;
if (
settingsPrivClone.interval === '' ||
!cronRegex.test(settingsPrivClone.interval) ||
Object.keys(cronParser.parseString(settingsPrivClone.interval).errors).length > 0
) {
toast.error($LL.settings.invalid_cron());
throw new Error('ping_interval not valid');
}
if (faviconInputElement.files !== null && faviconInputElement.files?.length > 0) {
let form = new FormData();
form.append('favicon', faviconInputElement.files[0]);
@@ -114,6 +134,8 @@
bind:value={settingsPrivClone.interval}
/>
</div>
<p class="fieldset-label">{parseCron(settingsPrivClone.interval, now)}</p>
<h2 class="card-title mt-2">{$LL.settings.lazy_ping_title()}</h2>
<p class="mt-2">
{$LL.settings.lazy_ping_desc()}