feat: improve custom jiggler settings and add timezone support (#742)

* feat: add timezone support to jiggler and fix custom settings persistence

- Add timezone field to JigglerConfig with comprehensive IANA timezone list
- Fix custom settings not loading current values
- Remove business hours preset, add as examples in custom settings
- Improve error handling for invalid cron expressions

* fix: format jiggler.go with gofmt

* fix: add embedded timezone data and validation

- Import time/tzdata to embed timezone database in binary
- Add timezone validation in runJigglerCronTab() to gracefully fallback to UTC
- Add timezone to debug logging in rpcSetJigglerConfig
- Fixes 'unknown time zone' errors when system lacks timezone data

* refactor: add timezone field comments from jiggler options

* chore: move tzdata to backend

* refactor: fix JigglerSetting linting

- Adjusted useEffect dependency to include send function for better data fetching
- Modified layout classes for improved responsiveness and consistency
- Cleaned up code formatting for better readability

---------

Co-authored-by: Siyuan Miao <i@xswan.net>
This commit is contained in:
Adam Shiervani 2025-08-19 16:50:42 +02:00 committed by Alex P
parent 199cca83ed
commit 0651faeceb
8 changed files with 831 additions and 42 deletions

View File

@ -34,7 +34,7 @@ OPTIM_CFLAGS := -O3 -mcpu=cortex-a7 -mfpu=neon -mfloat-abi=hard -ftree-vectorize
PROMETHEUS_TAG := github.com/prometheus/common/version PROMETHEUS_TAG := github.com/prometheus/common/version
KVM_PKG_NAME := github.com/jetkvm/kvm KVM_PKG_NAME := github.com/jetkvm/kvm
GO_BUILD_ARGS := -tags netgo GO_BUILD_ARGS := -tags netgo -tags timetzdata
GO_RELEASE_BUILD_ARGS := -trimpath $(GO_BUILD_ARGS) GO_RELEASE_BUILD_ARGS := -trimpath $(GO_BUILD_ARGS)
GO_LDFLAGS := \ GO_LDFLAGS := \
-s -w \ -s -w \

View File

@ -123,6 +123,7 @@ var defaultConfig = &Config{
InactivityLimitSeconds: 60, InactivityLimitSeconds: 60,
JitterPercentage: 25, JitterPercentage: 25,
ScheduleCronTab: "0 * * * * *", ScheduleCronTab: "0 * * * * *",
Timezone: "UTC",
}, },
TLSMode: "", TLSMode: "",
UsbConfig: &usbgadget.Config{ UsbConfig: &usbgadget.Config{

72
internal/tzdata/gen.go Normal file
View File

@ -0,0 +1,72 @@
//go:build ignore
package main
import (
"archive/zip"
"bytes"
"flag"
"fmt"
"os"
"text/template"
)
var tmpl = `// Code generated by "go run gen.go". DO NOT EDIT.
//go:generate env ZONEINFO=$GOROOT/lib/time/zoneinfo.zip go run gen.go -output tzdata.go
package tzdata
var TimeZones = []string{
{{- range . }}
"{{.}}",
{{- end }}
}
`
var filename = flag.String("output", "tzdata.go", "output file name")
func main() {
flag.Parse()
path := os.Getenv("ZONEINFO")
if path == "" {
fmt.Println("ZONEINFO is not set")
os.Exit(1)
}
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("ZONEINFO %s does not exist\n", path)
os.Exit(1)
}
zipfile, err := zip.OpenReader(path)
if err != nil {
fmt.Printf("Error opening ZONEINFO %s: %v\n", path, err)
os.Exit(1)
}
defer zipfile.Close()
timezones := []string{}
for _, file := range zipfile.File {
timezones = append(timezones, file.Name)
}
var buf bytes.Buffer
tmpl, err := template.New("tzdata").Parse(tmpl)
if err != nil {
fmt.Printf("Error parsing template: %v\n", err)
os.Exit(1)
}
err = tmpl.Execute(&buf, timezones)
if err != nil {
fmt.Printf("Error executing template: %v\n", err)
os.Exit(1)
}
err = os.WriteFile(*filename, buf.Bytes(), 0644)
if err != nil {
fmt.Printf("Error writing file %s: %v\n", *filename, err)
os.Exit(1)
}
}

602
internal/tzdata/tzdata.go Normal file
View File

@ -0,0 +1,602 @@
// Code generated by "go run gen.go". DO NOT EDIT.
//go:generate env ZONEINFO=$GOROOT/lib/time/zoneinfo.zip go run gen.go -output tzdata.go
package tzdata
var TimeZones = []string{
"Africa/Abidjan",
"Africa/Accra",
"Africa/Addis_Ababa",
"Africa/Algiers",
"Africa/Asmara",
"Africa/Asmera",
"Africa/Bamako",
"Africa/Bangui",
"Africa/Banjul",
"Africa/Bissau",
"Africa/Blantyre",
"Africa/Brazzaville",
"Africa/Bujumbura",
"Africa/Cairo",
"Africa/Casablanca",
"Africa/Ceuta",
"Africa/Conakry",
"Africa/Dakar",
"Africa/Dar_es_Salaam",
"Africa/Djibouti",
"Africa/Douala",
"Africa/El_Aaiun",
"Africa/Freetown",
"Africa/Gaborone",
"Africa/Harare",
"Africa/Johannesburg",
"Africa/Juba",
"Africa/Kampala",
"Africa/Khartoum",
"Africa/Kigali",
"Africa/Kinshasa",
"Africa/Lagos",
"Africa/Libreville",
"Africa/Lome",
"Africa/Luanda",
"Africa/Lubumbashi",
"Africa/Lusaka",
"Africa/Malabo",
"Africa/Maputo",
"Africa/Maseru",
"Africa/Mbabane",
"Africa/Mogadishu",
"Africa/Monrovia",
"Africa/Nairobi",
"Africa/Ndjamena",
"Africa/Niamey",
"Africa/Nouakchott",
"Africa/Ouagadougou",
"Africa/Porto-Novo",
"Africa/Sao_Tome",
"Africa/Timbuktu",
"Africa/Tripoli",
"Africa/Tunis",
"Africa/Windhoek",
"America/Adak",
"America/Anchorage",
"America/Anguilla",
"America/Antigua",
"America/Araguaina",
"America/Argentina/Buenos_Aires",
"America/Argentina/Catamarca",
"America/Argentina/ComodRivadavia",
"America/Argentina/Cordoba",
"America/Argentina/Jujuy",
"America/Argentina/La_Rioja",
"America/Argentina/Mendoza",
"America/Argentina/Rio_Gallegos",
"America/Argentina/Salta",
"America/Argentina/San_Juan",
"America/Argentina/San_Luis",
"America/Argentina/Tucuman",
"America/Argentina/Ushuaia",
"America/Aruba",
"America/Asuncion",
"America/Atikokan",
"America/Atka",
"America/Bahia",
"America/Bahia_Banderas",
"America/Barbados",
"America/Belem",
"America/Belize",
"America/Blanc-Sablon",
"America/Boa_Vista",
"America/Bogota",
"America/Boise",
"America/Buenos_Aires",
"America/Cambridge_Bay",
"America/Campo_Grande",
"America/Cancun",
"America/Caracas",
"America/Catamarca",
"America/Cayenne",
"America/Cayman",
"America/Chicago",
"America/Chihuahua",
"America/Ciudad_Juarez",
"America/Coral_Harbour",
"America/Cordoba",
"America/Costa_Rica",
"America/Creston",
"America/Cuiaba",
"America/Curacao",
"America/Danmarkshavn",
"America/Dawson",
"America/Dawson_Creek",
"America/Denver",
"America/Detroit",
"America/Dominica",
"America/Edmonton",
"America/Eirunepe",
"America/El_Salvador",
"America/Ensenada",
"America/Fort_Nelson",
"America/Fort_Wayne",
"America/Fortaleza",
"America/Glace_Bay",
"America/Godthab",
"America/Goose_Bay",
"America/Grand_Turk",
"America/Grenada",
"America/Guadeloupe",
"America/Guatemala",
"America/Guayaquil",
"America/Guyana",
"America/Halifax",
"America/Havana",
"America/Hermosillo",
"America/Indiana/Indianapolis",
"America/Indiana/Knox",
"America/Indiana/Marengo",
"America/Indiana/Petersburg",
"America/Indiana/Tell_City",
"America/Indiana/Vevay",
"America/Indiana/Vincennes",
"America/Indiana/Winamac",
"America/Indianapolis",
"America/Inuvik",
"America/Iqaluit",
"America/Jamaica",
"America/Jujuy",
"America/Juneau",
"America/Kentucky/Louisville",
"America/Kentucky/Monticello",
"America/Knox_IN",
"America/Kralendijk",
"America/La_Paz",
"America/Lima",
"America/Los_Angeles",
"America/Louisville",
"America/Lower_Princes",
"America/Maceio",
"America/Managua",
"America/Manaus",
"America/Marigot",
"America/Martinique",
"America/Matamoros",
"America/Mazatlan",
"America/Mendoza",
"America/Menominee",
"America/Merida",
"America/Metlakatla",
"America/Mexico_City",
"America/Miquelon",
"America/Moncton",
"America/Monterrey",
"America/Montevideo",
"America/Montreal",
"America/Montserrat",
"America/Nassau",
"America/New_York",
"America/Nipigon",
"America/Nome",
"America/Noronha",
"America/North_Dakota/Beulah",
"America/North_Dakota/Center",
"America/North_Dakota/New_Salem",
"America/Nuuk",
"America/Ojinaga",
"America/Panama",
"America/Pangnirtung",
"America/Paramaribo",
"America/Phoenix",
"America/Port-au-Prince",
"America/Port_of_Spain",
"America/Porto_Acre",
"America/Porto_Velho",
"America/Puerto_Rico",
"America/Punta_Arenas",
"America/Rainy_River",
"America/Rankin_Inlet",
"America/Recife",
"America/Regina",
"America/Resolute",
"America/Rio_Branco",
"America/Rosario",
"America/Santa_Isabel",
"America/Santarem",
"America/Santiago",
"America/Santo_Domingo",
"America/Sao_Paulo",
"America/Scoresbysund",
"America/Shiprock",
"America/Sitka",
"America/St_Barthelemy",
"America/St_Johns",
"America/St_Kitts",
"America/St_Lucia",
"America/St_Thomas",
"America/St_Vincent",
"America/Swift_Current",
"America/Tegucigalpa",
"America/Thule",
"America/Thunder_Bay",
"America/Tijuana",
"America/Toronto",
"America/Tortola",
"America/Vancouver",
"America/Virgin",
"America/Whitehorse",
"America/Winnipeg",
"America/Yakutat",
"America/Yellowknife",
"Antarctica/Casey",
"Antarctica/Davis",
"Antarctica/DumontDUrville",
"Antarctica/Macquarie",
"Antarctica/Mawson",
"Antarctica/McMurdo",
"Antarctica/Palmer",
"Antarctica/Rothera",
"Antarctica/South_Pole",
"Antarctica/Syowa",
"Antarctica/Troll",
"Antarctica/Vostok",
"Arctic/Longyearbyen",
"Asia/Aden",
"Asia/Almaty",
"Asia/Amman",
"Asia/Anadyr",
"Asia/Aqtau",
"Asia/Aqtobe",
"Asia/Ashgabat",
"Asia/Ashkhabad",
"Asia/Atyrau",
"Asia/Baghdad",
"Asia/Bahrain",
"Asia/Baku",
"Asia/Bangkok",
"Asia/Barnaul",
"Asia/Beirut",
"Asia/Bishkek",
"Asia/Brunei",
"Asia/Calcutta",
"Asia/Chita",
"Asia/Choibalsan",
"Asia/Chongqing",
"Asia/Chungking",
"Asia/Colombo",
"Asia/Dacca",
"Asia/Damascus",
"Asia/Dhaka",
"Asia/Dili",
"Asia/Dubai",
"Asia/Dushanbe",
"Asia/Famagusta",
"Asia/Gaza",
"Asia/Harbin",
"Asia/Hebron",
"Asia/Ho_Chi_Minh",
"Asia/Hong_Kong",
"Asia/Hovd",
"Asia/Irkutsk",
"Asia/Istanbul",
"Asia/Jakarta",
"Asia/Jayapura",
"Asia/Jerusalem",
"Asia/Kabul",
"Asia/Kamchatka",
"Asia/Karachi",
"Asia/Kashgar",
"Asia/Kathmandu",
"Asia/Katmandu",
"Asia/Khandyga",
"Asia/Kolkata",
"Asia/Krasnoyarsk",
"Asia/Kuala_Lumpur",
"Asia/Kuching",
"Asia/Kuwait",
"Asia/Macao",
"Asia/Macau",
"Asia/Magadan",
"Asia/Makassar",
"Asia/Manila",
"Asia/Muscat",
"Asia/Nicosia",
"Asia/Novokuznetsk",
"Asia/Novosibirsk",
"Asia/Omsk",
"Asia/Oral",
"Asia/Phnom_Penh",
"Asia/Pontianak",
"Asia/Pyongyang",
"Asia/Qatar",
"Asia/Qostanay",
"Asia/Qyzylorda",
"Asia/Rangoon",
"Asia/Riyadh",
"Asia/Saigon",
"Asia/Sakhalin",
"Asia/Samarkand",
"Asia/Seoul",
"Asia/Shanghai",
"Asia/Singapore",
"Asia/Srednekolymsk",
"Asia/Taipei",
"Asia/Tashkent",
"Asia/Tbilisi",
"Asia/Tehran",
"Asia/Tel_Aviv",
"Asia/Thimbu",
"Asia/Thimphu",
"Asia/Tokyo",
"Asia/Tomsk",
"Asia/Ujung_Pandang",
"Asia/Ulaanbaatar",
"Asia/Ulan_Bator",
"Asia/Urumqi",
"Asia/Ust-Nera",
"Asia/Vientiane",
"Asia/Vladivostok",
"Asia/Yakutsk",
"Asia/Yangon",
"Asia/Yekaterinburg",
"Asia/Yerevan",
"Atlantic/Azores",
"Atlantic/Bermuda",
"Atlantic/Canary",
"Atlantic/Cape_Verde",
"Atlantic/Faeroe",
"Atlantic/Faroe",
"Atlantic/Jan_Mayen",
"Atlantic/Madeira",
"Atlantic/Reykjavik",
"Atlantic/South_Georgia",
"Atlantic/St_Helena",
"Atlantic/Stanley",
"Australia/ACT",
"Australia/Adelaide",
"Australia/Brisbane",
"Australia/Broken_Hill",
"Australia/Canberra",
"Australia/Currie",
"Australia/Darwin",
"Australia/Eucla",
"Australia/Hobart",
"Australia/LHI",
"Australia/Lindeman",
"Australia/Lord_Howe",
"Australia/Melbourne",
"Australia/NSW",
"Australia/North",
"Australia/Perth",
"Australia/Queensland",
"Australia/South",
"Australia/Sydney",
"Australia/Tasmania",
"Australia/Victoria",
"Australia/West",
"Australia/Yancowinna",
"Brazil/Acre",
"Brazil/DeNoronha",
"Brazil/East",
"Brazil/West",
"CET",
"CST6CDT",
"Canada/Atlantic",
"Canada/Central",
"Canada/Eastern",
"Canada/Mountain",
"Canada/Newfoundland",
"Canada/Pacific",
"Canada/Saskatchewan",
"Canada/Yukon",
"Chile/Continental",
"Chile/EasterIsland",
"Cuba",
"EET",
"EST",
"EST5EDT",
"Egypt",
"Eire",
"Etc/GMT",
"Etc/GMT+0",
"Etc/GMT+1",
"Etc/GMT+10",
"Etc/GMT+11",
"Etc/GMT+12",
"Etc/GMT+2",
"Etc/GMT+3",
"Etc/GMT+4",
"Etc/GMT+5",
"Etc/GMT+6",
"Etc/GMT+7",
"Etc/GMT+8",
"Etc/GMT+9",
"Etc/GMT-0",
"Etc/GMT-1",
"Etc/GMT-10",
"Etc/GMT-11",
"Etc/GMT-12",
"Etc/GMT-13",
"Etc/GMT-14",
"Etc/GMT-2",
"Etc/GMT-3",
"Etc/GMT-4",
"Etc/GMT-5",
"Etc/GMT-6",
"Etc/GMT-7",
"Etc/GMT-8",
"Etc/GMT-9",
"Etc/GMT0",
"Etc/Greenwich",
"Etc/UCT",
"Etc/UTC",
"Etc/Universal",
"Etc/Zulu",
"Europe/Amsterdam",
"Europe/Andorra",
"Europe/Astrakhan",
"Europe/Athens",
"Europe/Belfast",
"Europe/Belgrade",
"Europe/Berlin",
"Europe/Bratislava",
"Europe/Brussels",
"Europe/Bucharest",
"Europe/Budapest",
"Europe/Busingen",
"Europe/Chisinau",
"Europe/Copenhagen",
"Europe/Dublin",
"Europe/Gibraltar",
"Europe/Guernsey",
"Europe/Helsinki",
"Europe/Isle_of_Man",
"Europe/Istanbul",
"Europe/Jersey",
"Europe/Kaliningrad",
"Europe/Kiev",
"Europe/Kirov",
"Europe/Kyiv",
"Europe/Lisbon",
"Europe/Ljubljana",
"Europe/London",
"Europe/Luxembourg",
"Europe/Madrid",
"Europe/Malta",
"Europe/Mariehamn",
"Europe/Minsk",
"Europe/Monaco",
"Europe/Moscow",
"Europe/Nicosia",
"Europe/Oslo",
"Europe/Paris",
"Europe/Podgorica",
"Europe/Prague",
"Europe/Riga",
"Europe/Rome",
"Europe/Samara",
"Europe/San_Marino",
"Europe/Sarajevo",
"Europe/Saratov",
"Europe/Simferopol",
"Europe/Skopje",
"Europe/Sofia",
"Europe/Stockholm",
"Europe/Tallinn",
"Europe/Tirane",
"Europe/Tiraspol",
"Europe/Ulyanovsk",
"Europe/Uzhgorod",
"Europe/Vaduz",
"Europe/Vatican",
"Europe/Vienna",
"Europe/Vilnius",
"Europe/Volgograd",
"Europe/Warsaw",
"Europe/Zagreb",
"Europe/Zaporozhye",
"Europe/Zurich",
"Factory",
"GB",
"GB-Eire",
"GMT",
"GMT+0",
"GMT-0",
"GMT0",
"Greenwich",
"HST",
"Hongkong",
"Iceland",
"Indian/Antananarivo",
"Indian/Chagos",
"Indian/Christmas",
"Indian/Cocos",
"Indian/Comoro",
"Indian/Kerguelen",
"Indian/Mahe",
"Indian/Maldives",
"Indian/Mauritius",
"Indian/Mayotte",
"Indian/Reunion",
"Iran",
"Israel",
"Jamaica",
"Japan",
"Kwajalein",
"Libya",
"MET",
"MST",
"MST7MDT",
"Mexico/BajaNorte",
"Mexico/BajaSur",
"Mexico/General",
"NZ",
"NZ-CHAT",
"Navajo",
"PRC",
"PST8PDT",
"Pacific/Apia",
"Pacific/Auckland",
"Pacific/Bougainville",
"Pacific/Chatham",
"Pacific/Chuuk",
"Pacific/Easter",
"Pacific/Efate",
"Pacific/Enderbury",
"Pacific/Fakaofo",
"Pacific/Fiji",
"Pacific/Funafuti",
"Pacific/Galapagos",
"Pacific/Gambier",
"Pacific/Guadalcanal",
"Pacific/Guam",
"Pacific/Honolulu",
"Pacific/Johnston",
"Pacific/Kanton",
"Pacific/Kiritimati",
"Pacific/Kosrae",
"Pacific/Kwajalein",
"Pacific/Majuro",
"Pacific/Marquesas",
"Pacific/Midway",
"Pacific/Nauru",
"Pacific/Niue",
"Pacific/Norfolk",
"Pacific/Noumea",
"Pacific/Pago_Pago",
"Pacific/Palau",
"Pacific/Pitcairn",
"Pacific/Pohnpei",
"Pacific/Ponape",
"Pacific/Port_Moresby",
"Pacific/Rarotonga",
"Pacific/Saipan",
"Pacific/Samoa",
"Pacific/Tahiti",
"Pacific/Tarawa",
"Pacific/Tongatapu",
"Pacific/Truk",
"Pacific/Wake",
"Pacific/Wallis",
"Pacific/Yap",
"Poland",
"Portugal",
"ROC",
"ROK",
"Singapore",
"Turkey",
"UCT",
"US/Alaska",
"US/Aleutian",
"US/Arizona",
"US/Central",
"US/East-Indiana",
"US/Eastern",
"US/Hawaii",
"US/Indiana-Starke",
"US/Michigan",
"US/Mountain",
"US/Pacific",
"US/Samoa",
"UTC",
"Universal",
"W-SU",
"WET",
"Zulu",
}

View File

@ -4,14 +4,17 @@ import (
"fmt" "fmt"
"math/rand" "math/rand"
"time" "time"
_ "time/tzdata"
"github.com/go-co-op/gocron/v2" "github.com/go-co-op/gocron/v2"
"github.com/jetkvm/kvm/internal/tzdata"
) )
type JigglerConfig struct { type JigglerConfig struct {
InactivityLimitSeconds int `json:"inactivity_limit_seconds"` InactivityLimitSeconds int `json:"inactivity_limit_seconds"`
JitterPercentage int `json:"jitter_percentage"` JitterPercentage int `json:"jitter_percentage"`
ScheduleCronTab string `json:"schedule_cron_tab"` ScheduleCronTab string `json:"schedule_cron_tab"`
Timezone string `json:"timezone,omitempty"`
} }
var jigglerEnabled = false var jigglerEnabled = false
@ -21,16 +24,21 @@ var scheduler gocron.Scheduler = nil
func rpcSetJigglerState(enabled bool) { func rpcSetJigglerState(enabled bool) {
jigglerEnabled = enabled jigglerEnabled = enabled
} }
func rpcGetJigglerState() bool { func rpcGetJigglerState() bool {
return jigglerEnabled return jigglerEnabled
} }
func rpcGetTimezones() []string {
return tzdata.TimeZones
}
func rpcGetJigglerConfig() (JigglerConfig, error) { func rpcGetJigglerConfig() (JigglerConfig, error) {
return *config.JigglerConfig, nil return *config.JigglerConfig, nil
} }
func rpcSetJigglerConfig(jigglerConfig JigglerConfig) error { func rpcSetJigglerConfig(jigglerConfig JigglerConfig) error {
logger.Info().Msgf("jigglerConfig: %v, %v, %v", jigglerConfig.InactivityLimitSeconds, jigglerConfig.JitterPercentage, jigglerConfig.ScheduleCronTab) logger.Info().Msgf("jigglerConfig: %v, %v, %v, %v", jigglerConfig.InactivityLimitSeconds, jigglerConfig.JitterPercentage, jigglerConfig.ScheduleCronTab, jigglerConfig.Timezone)
config.JigglerConfig = &jigglerConfig config.JigglerConfig = &jigglerConfig
err := removeExistingCrobJobs(scheduler) err := removeExistingCrobJobs(scheduler)
if err != nil { if err != nil {
@ -68,6 +76,18 @@ func initJiggler() {
func runJigglerCronTab() error { func runJigglerCronTab() error {
cronTab := config.JigglerConfig.ScheduleCronTab cronTab := config.JigglerConfig.ScheduleCronTab
// Apply timezone if specified and valid
if config.JigglerConfig.Timezone != "" && config.JigglerConfig.Timezone != "UTC" {
// Validate timezone before applying
if _, err := time.LoadLocation(config.JigglerConfig.Timezone); err != nil {
logger.Warn().Msgf("Invalid timezone '%s', falling back to UTC: %v", config.JigglerConfig.Timezone, err)
// Don't add TZ prefix, let it run in UTC
} else {
cronTab = fmt.Sprintf("TZ=%s %s", config.JigglerConfig.Timezone, cronTab)
}
}
s, err := gocron.NewScheduler() s, err := gocron.NewScheduler()
if err != nil { if err != nil {
return err return err

View File

@ -1093,6 +1093,7 @@ var rpcHandlers = map[string]RPCHandler{
"getJigglerState": {Func: rpcGetJigglerState}, "getJigglerState": {Func: rpcGetJigglerState},
"setJigglerConfig": {Func: rpcSetJigglerConfig, Params: []string{"jigglerConfig"}}, "setJigglerConfig": {Func: rpcSetJigglerConfig, Params: []string{"jigglerConfig"}},
"getJigglerConfig": {Func: rpcGetJigglerConfig}, "getJigglerConfig": {Func: rpcGetJigglerConfig},
"getTimezones": {Func: rpcGetTimezones},
"sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}}, "sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}},
"getStreamQualityFactor": {Func: rpcGetStreamQualityFactor}, "getStreamQualityFactor": {Func: rpcGetStreamQualityFactor},
"setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}}, "setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}},

View File

@ -1,44 +1,109 @@
import { useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { LuExternalLink } from "react-icons/lu";
import { Button } from "@components/Button"; import { Button, LinkButton } from "@components/Button";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { InputFieldWithLabel } from "./InputField"; import { InputFieldWithLabel } from "./InputField";
import ExtLink from "./ExtLink"; import { SelectMenuBasic } from "./SelectMenuBasic";
export interface JigglerConfig { export interface JigglerConfig {
inactivity_limit_seconds: number; inactivity_limit_seconds: number;
jitter_percentage: number; jitter_percentage: number;
schedule_cron_tab: string; schedule_cron_tab: string;
timezone?: string;
} }
export function JigglerSetting({ export function JigglerSetting({
onSave, onSave,
defaultJigglerState,
}: { }: {
onSave: (jigglerConfig: JigglerConfig) => void; onSave: (jigglerConfig: JigglerConfig) => void;
defaultJigglerState?: JigglerConfig;
}) { }) {
const [jigglerConfigState, setJigglerConfigState] = useState<JigglerConfig>({ const [jigglerConfigState, setJigglerConfigState] = useState<JigglerConfig>(
inactivity_limit_seconds: 20, defaultJigglerState || {
jitter_percentage: 0, inactivity_limit_seconds: 20,
schedule_cron_tab: "*/20 * * * * *", jitter_percentage: 0,
}); schedule_cron_tab: "*/20 * * * * *",
timezone: "UTC",
},
);
const [send] = useJsonRpc();
const [timezones, setTimezones] = useState<string[]>([]);
useEffect(() => {
send("getTimezones", {}, resp => {
if ("error" in resp) return;
setTimezones(resp.result as string[]);
});
}, [send]);
const timezoneOptions = useMemo(
() =>
timezones.map((timezone: string) => ({
value: timezone,
label: timezone,
})),
[timezones],
);
const exampleConfigs = [
{
name: "Business Hours 9-17",
config: {
inactivity_limit_seconds: 60,
jitter_percentage: 25,
schedule_cron_tab: "0 * 9-17 * * 1-5",
timezone: jigglerConfigState.timezone || "UTC",
},
},
{
name: "Business Hours 8-17",
config: {
inactivity_limit_seconds: 60,
jitter_percentage: 25,
schedule_cron_tab: "0 * 8-17 * * 1-5",
timezone: jigglerConfigState.timezone || "UTC",
},
},
];
return ( return (
<div className="space-y-2"> <div className="space-y-4">
<div className="grid max-w-sm grid-cols-1 items-end gap-y-2"> <div className="space-y-2">
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
Examples
</h4>
<div className="flex flex-wrap gap-2">
{exampleConfigs.map((example, index) => (
<Button
key={index}
size="XS"
theme="light"
text={example.name}
onClick={() => setJigglerConfigState(example.config)}
/>
))}
<LinkButton
to="https://crontab.guru/examples.html"
size="XS"
theme="light"
text="More examples"
LeadingIcon={LuExternalLink}
/>
</div>
</div>
<div className="grid grid-cols-1 items-end gap-4 md:grid-cols-2">
<InputFieldWithLabel <InputFieldWithLabel
required required
size="SM" size="SM"
label="Cron Schedule" label="Cron Schedule"
description={ description="Cron expression for scheduling"
<span>
Generate with{" "}
<ExtLink className="text-blue-700 underline" href="https://crontab.guru/">
crontab.guru
</ExtLink>
</span>
}
placeholder="*/20 * * * * *" placeholder="*/20 * * * * *"
defaultValue={jigglerConfigState.schedule_cron_tab} value={jigglerConfigState.schedule_cron_tab}
onChange={e => onChange={e =>
setJigglerConfigState({ setJigglerConfigState({
...jigglerConfigState, ...jigglerConfigState,
@ -50,7 +115,7 @@ export function JigglerSetting({
<InputFieldWithLabel <InputFieldWithLabel
size="SM" size="SM"
label="Inactivity Limit Seconds" label="Inactivity Limit Seconds"
description="Seconds of inactivity before triggering a jiggle again" description="Inactivity time before jiggle"
value={jigglerConfigState.inactivity_limit_seconds} value={jigglerConfigState.inactivity_limit_seconds}
type="number" type="number"
min="1" min="1"
@ -70,7 +135,7 @@ export function JigglerSetting({
description="To avoid recognizable patterns" description="To avoid recognizable patterns"
placeholder="25" placeholder="25"
TrailingElm={<span className="px-2 text-xs text-slate-500">%</span>} TrailingElm={<span className="px-2 text-xs text-slate-500">%</span>}
defaultValue={jigglerConfigState.jitter_percentage} value={jigglerConfigState.jitter_percentage}
type="number" type="number"
min="0" min="0"
max="100" max="100"
@ -81,9 +146,24 @@ export function JigglerSetting({
}) })
} }
/> />
<SelectMenuBasic
size="SM"
label="Timezone"
description="Timezone for cron schedule"
value={jigglerConfigState.timezone || "UTC"}
disabled={timezones.length === 0}
onChange={e =>
setJigglerConfigState({
...jigglerConfigState,
timezone: e.target.value,
})
}
options={timezoneOptions}
/>
</div> </div>
<div className="mt-6 flex gap-x-2"> <div className="flex gap-x-2">
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"

View File

@ -21,6 +21,7 @@ export interface JigglerConfig {
inactivity_limit_seconds: number; inactivity_limit_seconds: number;
jitter_percentage: number; jitter_percentage: number;
schedule_cron_tab: string; schedule_cron_tab: string;
timezone?: string;
} }
const jigglerOptions = [ const jigglerOptions = [
@ -32,6 +33,8 @@ const jigglerOptions = [
inactivity_limit_seconds: 30, inactivity_limit_seconds: 30,
jitter_percentage: 25, jitter_percentage: 25,
schedule_cron_tab: "*/30 * * * * *", schedule_cron_tab: "*/30 * * * * *",
// We don't care about the timezone for this preset
// timezone: "UTC",
}, },
}, },
{ {
@ -41,6 +44,8 @@ const jigglerOptions = [
inactivity_limit_seconds: 60, inactivity_limit_seconds: 60,
jitter_percentage: 25, jitter_percentage: 25,
schedule_cron_tab: "0 * * * * *", schedule_cron_tab: "0 * * * * *",
// We don't care about the timezone for this preset
// timezone: "UTC",
}, },
}, },
{ {
@ -50,15 +55,8 @@ const jigglerOptions = [
inactivity_limit_seconds: 300, inactivity_limit_seconds: 300,
jitter_percentage: 25, jitter_percentage: 25,
schedule_cron_tab: "0 */5 * * * *", schedule_cron_tab: "0 */5 * * * *",
}, // We don't care about the timezone for this preset
}, // timezone: "UTC",
{
value: "business_hours",
label: "Business Hours - 1m - (Mon-Fri 9-17)",
config: {
inactivity_limit_seconds: 60,
jitter_percentage: 25,
schedule_cron_tab: "0 * 9-17 * * 1-5",
}, },
}, },
] as const; ] as const;
@ -77,6 +75,9 @@ export default function SettingsMouseRoute() {
const [selectedJigglerOption, setSelectedJigglerOption] = const [selectedJigglerOption, setSelectedJigglerOption] =
useState<JigglerValues | null>(null); useState<JigglerValues | null>(null);
const [currentJigglerConfig, setCurrentJigglerConfig] = useState<JigglerConfig | null>(
null,
);
const scrollThrottlingOptions = [ const scrollThrottlingOptions = [
{ value: "0", label: "Off" }, { value: "0", label: "Off" },
@ -99,6 +100,8 @@ export default function SettingsMouseRoute() {
send("getJigglerConfig", {}, resp => { send("getJigglerConfig", {}, resp => {
if ("error" in resp) return; if ("error" in resp) return;
const result = resp.result as JigglerConfig; const result = resp.result as JigglerConfig;
setCurrentJigglerConfig(result);
const value = jigglerOptions.find( const value = jigglerOptions.find(
o => o =>
o?.config?.inactivity_limit_seconds === result.inactivity_limit_seconds && o?.config?.inactivity_limit_seconds === result.inactivity_limit_seconds &&
@ -128,9 +131,20 @@ export default function SettingsMouseRoute() {
send("setJigglerConfig", { jigglerConfig }, async resp => { send("setJigglerConfig", { jigglerConfig }, async resp => {
if ("error" in resp) { if ("error" in resp) {
return notifications.error( const errorMsg = resp.error.data || "Unknown error";
`Failed to set jiggler config: ${resp.error.data || "Unknown error"}`,
); // Check for cron syntax errors and provide user-friendly message
if (
errorMsg.includes("invalid syntax") ||
errorMsg.includes("parse failure") ||
errorMsg.includes("invalid cron")
) {
return notifications.error(
"Invalid cron expression. Please check your schedule format (e.g., '0 * * * * *' for every minute).",
);
}
return notifications.error(`Failed to set jiggler config: ${errorMsg}`);
} }
notifications.success(`Jiggler Config successfully updated`); notifications.success(`Jiggler Config successfully updated`);
@ -202,10 +216,7 @@ export default function SettingsMouseRoute() {
/> />
</SettingsItem> </SettingsItem>
<SettingsItem <SettingsItem title="Jiggler" description="Simulate movement of a computer mouse">
title="Jiggler"
description="Simulate movement of a computer mouse. Prevents sleep mode, standby mode or the screensaver from activating"
>
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
label="" label=""
@ -222,13 +233,15 @@ export default function SettingsMouseRoute() {
e.target.value as (typeof jigglerOptions)[number]["value"], e.target.value as (typeof jigglerOptions)[number]["value"],
); );
}} }}
fullWidth
/> />
</SettingsItem> </SettingsItem>
{selectedJigglerOption === "custom" && ( {selectedJigglerOption === "custom" && (
<SettingsNestedSection> <SettingsNestedSection>
<JigglerSetting onSave={saveJigglerConfig} /> <JigglerSetting
onSave={saveJigglerConfig}
defaultJigglerState={currentJigglerConfig || undefined}
/>
</SettingsNestedSection> </SettingsNestedSection>
)} )}
<div className="space-y-4"> <div className="space-y-4">