mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-01-18 03:28:19 +01:00
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:
@@ -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 ExtLink from "./ExtLink";
|
||||
import { SelectMenuBasic } from "./SelectMenuBasic";
|
||||
|
||||
export interface JigglerConfig {
|
||||
inactivity_limit_seconds: number;
|
||||
jitter_percentage: number;
|
||||
schedule_cron_tab: string;
|
||||
timezone?: string;
|
||||
}
|
||||
|
||||
export function JigglerSetting({
|
||||
onSave,
|
||||
defaultJigglerState,
|
||||
}: {
|
||||
onSave: (jigglerConfig: JigglerConfig) => void;
|
||||
defaultJigglerState?: JigglerConfig;
|
||||
}) {
|
||||
const [jigglerConfigState, setJigglerConfigState] = useState<JigglerConfig>({
|
||||
inactivity_limit_seconds: 20,
|
||||
jitter_percentage: 0,
|
||||
schedule_cron_tab: "*/20 * * * * *",
|
||||
});
|
||||
const [jigglerConfigState, setJigglerConfigState] = useState<JigglerConfig>(
|
||||
defaultJigglerState || {
|
||||
inactivity_limit_seconds: 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 (
|
||||
<div className="space-y-2">
|
||||
<div className="grid max-w-sm grid-cols-1 items-end gap-y-2">
|
||||
<div className="space-y-4">
|
||||
<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
|
||||
required
|
||||
size="SM"
|
||||
label="Cron Schedule"
|
||||
description={
|
||||
<span>
|
||||
Generate with{" "}
|
||||
<ExtLink className="text-blue-700 underline" href="https://crontab.guru/">
|
||||
crontab.guru
|
||||
</ExtLink>
|
||||
</span>
|
||||
}
|
||||
description="Cron expression for scheduling"
|
||||
placeholder="*/20 * * * * *"
|
||||
defaultValue={jigglerConfigState.schedule_cron_tab}
|
||||
value={jigglerConfigState.schedule_cron_tab}
|
||||
onChange={e =>
|
||||
setJigglerConfigState({
|
||||
...jigglerConfigState,
|
||||
@@ -50,7 +115,7 @@ export function JigglerSetting({
|
||||
<InputFieldWithLabel
|
||||
size="SM"
|
||||
label="Inactivity Limit Seconds"
|
||||
description="Seconds of inactivity before triggering a jiggle again"
|
||||
description="Inactivity time before jiggle"
|
||||
value={jigglerConfigState.inactivity_limit_seconds}
|
||||
type="number"
|
||||
min="1"
|
||||
@@ -70,7 +135,7 @@ export function JigglerSetting({
|
||||
description="To avoid recognizable patterns"
|
||||
placeholder="25"
|
||||
TrailingElm={<span className="px-2 text-xs text-slate-500">%</span>}
|
||||
defaultValue={jigglerConfigState.jitter_percentage}
|
||||
value={jigglerConfigState.jitter_percentage}
|
||||
type="number"
|
||||
min="0"
|
||||
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 className="mt-6 flex gap-x-2">
|
||||
<div className="flex gap-x-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface JigglerConfig {
|
||||
inactivity_limit_seconds: number;
|
||||
jitter_percentage: number;
|
||||
schedule_cron_tab: string;
|
||||
timezone?: string;
|
||||
}
|
||||
|
||||
const jigglerOptions = [
|
||||
@@ -32,6 +33,8 @@ const jigglerOptions = [
|
||||
inactivity_limit_seconds: 30,
|
||||
jitter_percentage: 25,
|
||||
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,
|
||||
jitter_percentage: 25,
|
||||
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,
|
||||
jitter_percentage: 25,
|
||||
schedule_cron_tab: "0 */5 * * * *",
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
// We don't care about the timezone for this preset
|
||||
// timezone: "UTC",
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
@@ -77,6 +75,9 @@ export default function SettingsMouseRoute() {
|
||||
|
||||
const [selectedJigglerOption, setSelectedJigglerOption] =
|
||||
useState<JigglerValues | null>(null);
|
||||
const [currentJigglerConfig, setCurrentJigglerConfig] = useState<JigglerConfig | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const scrollThrottlingOptions = [
|
||||
{ value: "0", label: "Off" },
|
||||
@@ -99,6 +100,8 @@ export default function SettingsMouseRoute() {
|
||||
send("getJigglerConfig", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
const result = resp.result as JigglerConfig;
|
||||
setCurrentJigglerConfig(result);
|
||||
|
||||
const value = jigglerOptions.find(
|
||||
o =>
|
||||
o?.config?.inactivity_limit_seconds === result.inactivity_limit_seconds &&
|
||||
@@ -128,9 +131,20 @@ export default function SettingsMouseRoute() {
|
||||
|
||||
send("setJigglerConfig", { jigglerConfig }, async resp => {
|
||||
if ("error" in resp) {
|
||||
return notifications.error(
|
||||
`Failed to set jiggler config: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
const errorMsg = 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`);
|
||||
@@ -202,10 +216,7 @@ export default function SettingsMouseRoute() {
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title="Jiggler"
|
||||
description="Simulate movement of a computer mouse. Prevents sleep mode, standby mode or the screensaver from activating"
|
||||
>
|
||||
<SettingsItem title="Jiggler" description="Simulate movement of a computer mouse">
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
@@ -222,13 +233,15 @@ export default function SettingsMouseRoute() {
|
||||
e.target.value as (typeof jigglerOptions)[number]["value"],
|
||||
);
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
{selectedJigglerOption === "custom" && (
|
||||
<SettingsNestedSection>
|
||||
<JigglerSetting onSave={saveJigglerConfig} />
|
||||
<JigglerSetting
|
||||
onSave={saveJigglerConfig}
|
||||
defaultJigglerState={currentJigglerConfig || undefined}
|
||||
/>
|
||||
</SettingsNestedSection>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
|
||||
Reference in New Issue
Block a user