Merge pull request #98 from 9technologygroup/dev

Fixed Crontab timing Expression
This commit is contained in:
9 Technology Group LTD
2025-09-30 09:38:37 +01:00
committed by GitHub
5 changed files with 145 additions and 43 deletions

View File

@@ -84,7 +84,7 @@ apt install curl -y
#### Script #### Script
```bash ```bash
curl -fsSL -o setup.sh https://raw.githubusercontent.com/9technologygroup/patchmon.net/main/setup.sh && chmod +x setup.sh && bash setup.sh curl -fsSL -o setup.sh https://raw.githubusercontent.com/9technologygroup/patchmon.net/refs/heads/main/setup.sh && chmod +x setup.sh && bash setup.sh
``` ```
#### Minimum specs for building : ##### #### Minimum specs for building : #####

View File

@@ -1093,16 +1093,33 @@ update_crontab() {
local response=$(curl -ks -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/settings/update-interval") local response=$(curl -ks -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/settings/update-interval")
if [[ $? -eq 0 ]]; then if [[ $? -eq 0 ]]; then
local update_interval=$(echo "$response" | grep -o '"updateInterval":[0-9]*' | cut -d':' -f2) local update_interval=$(echo "$response" | grep -o '"updateInterval":[0-9]*' | cut -d':' -f2)
# Fallback if not found
if [[ -z "$update_interval" ]]; then
update_interval=60
fi
# Normalize interval: 5-59 valid, otherwise snap to hour presets
if [[ $update_interval -lt 5 ]]; then
update_interval=5
elif [[ $update_interval -gt 1440 ]]; then
update_interval=1440
fi
if [[ -n "$update_interval" ]]; then if [[ -n "$update_interval" ]]; then
# Generate the expected crontab entry # Generate the expected crontab entry
local expected_crontab="" local expected_crontab=""
if [[ $update_interval -eq 60 ]]; then if [[ $update_interval -lt 60 ]]; then
# Hourly updates starting at current minute # Every N minutes (5-59)
local current_minute=$(date +%M)
expected_crontab="$current_minute * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1"
else
# Custom interval updates
expected_crontab="*/$update_interval * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1" expected_crontab="*/$update_interval * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1"
else
# Hour-based schedules
if [[ $update_interval -eq 60 ]]; then
# Hourly updates starting at current minute to spread load
local current_minute=$(date +%M)
expected_crontab="$current_minute * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1"
else
# For 120, 180, 360, 720, 1440 -> every H hours at minute 0
local hours=$((update_interval / 60))
expected_crontab="0 */$hours * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1"
fi
fi fi
# Get current crontab (without patchmon entries) # Get current crontab (without patchmon entries)

View File

@@ -206,23 +206,35 @@ fi
setup_crontab() { setup_crontab() {
local update_interval="$1" local update_interval="$1"
local patchmon_pattern="/usr/local/bin/patchmon-agent.sh update" local patchmon_pattern="/usr/local/bin/patchmon-agent.sh update"
# Normalize interval: min 5, max 1440
if [[ -z "$update_interval" ]]; then update_interval=60; fi
if [[ "$update_interval" -lt 5 ]]; then update_interval=5; fi
if [[ "$update_interval" -gt 1440 ]]; then update_interval=1440; fi
# Get current crontab, remove any existing patchmon entries # Get current crontab, remove any existing patchmon entries
local current_cron=$(crontab -l 2>/dev/null | grep -v "$patchmon_pattern" || true) local current_cron=$(crontab -l 2>/dev/null | grep -v "$patchmon_pattern" || true)
# Determine new cron entry # Determine new cron entry
local new_entry local new_entry
if [[ "$update_interval" -eq 60 ]]; then if [[ "$update_interval" -lt 60 ]]; then
# Hourly updates - use a random minute to spread load # Every N minutes (5-59)
local current_minute=$(date +%M)
new_entry="$current_minute * * * * $patchmon_pattern >/dev/null 2>&1"
info "📋 Configuring hourly updates at minute $current_minute"
else
# Custom interval updates
new_entry="*/$update_interval * * * * $patchmon_pattern >/dev/null 2>&1" new_entry="*/$update_interval * * * * $patchmon_pattern >/dev/null 2>&1"
info "📋 Configuring updates every $update_interval minutes" info "📋 Configuring updates every $update_interval minutes"
else
if [[ "$update_interval" -eq 60 ]]; then
# Hourly updates - use current minute to spread load
local current_minute=$(date +%M)
new_entry="$current_minute * * * * $patchmon_pattern >/dev/null 2>&1"
info "📋 Configuring hourly updates at minute $current_minute"
else
# For 120, 180, 360, 720, 1440 -> every H hours at minute 0
local hours=$((update_interval / 60))
new_entry="0 */$hours * * * $patchmon_pattern >/dev/null 2>&1"
info "📋 Configuring updates every $hours hour(s)"
fi
fi fi
# Combine existing cron (without patchmon entries) + new entry # Combine existing cron (without patchmon entries) + new entry
{ {
if [[ -n "$current_cron" ]]; then if [[ -n "$current_cron" ]]; then
@@ -230,7 +242,7 @@ setup_crontab() {
fi fi
echo "$new_entry" echo "$new_entry"
} | crontab - } | crontab -
success "✅ Crontab configured successfully (duplicates removed)" success "✅ Crontab configured successfully (duplicates removed)"
} }

View File

@@ -105,6 +105,45 @@ async function triggerCrontabUpdates() {
} }
} }
// Helpers
function normalizeUpdateInterval(minutes) {
let m = parseInt(minutes, 10);
if (Number.isNaN(m)) return 60;
if (m < 5) m = 5;
if (m > 1440) m = 1440;
if (m < 60) {
// Clamp to 5-59, step 5
const snapped = Math.round(m / 5) * 5;
return Math.min(59, Math.max(5, snapped));
}
// Allowed hour-based presets
const allowed = [60, 120, 180, 360, 720, 1440];
let nearest = allowed[0];
let bestDiff = Math.abs(m - nearest);
for (const a of allowed) {
const d = Math.abs(m - a);
if (d < bestDiff) {
bestDiff = d;
nearest = a;
}
}
return nearest;
}
function buildCronExpression(minutes) {
const m = normalizeUpdateInterval(minutes);
if (m < 60) {
return `*/${m} * * * *`;
}
if (m === 60) {
// Hourly at current minute is chosen by agent; default 0 here
return `0 * * * *`;
}
const hours = Math.floor(m / 60);
// Every N hours at minute 0
return `0 */${hours} * * *`;
}
// Get current settings // Get current settings
router.get("/", authenticateToken, requireManageSettings, async (_req, res) => { router.get("/", authenticateToken, requireManageSettings, async (_req, res) => {
try { try {
@@ -191,11 +230,13 @@ router.put(
const oldUpdateInterval = currentSettings.update_interval; const oldUpdateInterval = currentSettings.update_interval;
// Update settings using the service // Update settings using the service
const normalizedInterval = normalizeUpdateInterval(updateInterval || 60);
const updatedSettings = await updateSettings(currentSettings.id, { const updatedSettings = await updateSettings(currentSettings.id, {
server_protocol: serverProtocol, server_protocol: serverProtocol,
server_host: serverHost, server_host: serverHost,
server_port: serverPort, server_port: serverPort,
update_interval: updateInterval || 60, update_interval: normalizedInterval,
auto_update: autoUpdate || false, auto_update: autoUpdate || false,
signup_enabled: signupEnabled || false, signup_enabled: signupEnabled || false,
default_user_role: default_user_role:
@@ -211,9 +252,9 @@ router.put(
console.log("Settings updated successfully:", updatedSettings); console.log("Settings updated successfully:", updatedSettings);
// If update interval changed, trigger crontab updates on all hosts with auto-update enabled // If update interval changed, trigger crontab updates on all hosts with auto-update enabled
if (oldUpdateInterval !== (updateInterval || 60)) { if (oldUpdateInterval !== normalizedInterval) {
console.log( console.log(
`Update interval changed from ${oldUpdateInterval} to ${updateInterval || 60} minutes. Triggering crontab updates...`, `Update interval changed from ${oldUpdateInterval} to ${normalizedInterval} minutes. Triggering crontab updates...`,
); );
await triggerCrontabUpdates(); await triggerCrontabUpdates();
} }
@@ -262,9 +303,10 @@ router.get("/update-interval", async (req, res) => {
} }
const settings = await getSettings(); const settings = await getSettings();
const interval = normalizeUpdateInterval(settings.update_interval || 60);
res.json({ res.json({
updateInterval: settings.update_interval, updateInterval: interval,
cronExpression: `*/${settings.update_interval} * * * *`, // Generate cron expression cronExpression: buildCronExpression(interval),
}); });
} catch (error) { } catch (error) {
console.error("Update interval fetch error:", error); console.error("Update interval fetch error:", error);

View File

@@ -272,9 +272,37 @@ const Settings = () => {
} }
}; };
// Normalize update interval to safe presets
const normalizeInterval = (minutes) => {
let m = parseInt(minutes, 10);
if (Number.isNaN(m)) return 60;
if (m < 5) m = 5;
if (m > 1440) m = 1440;
// If less than 60 minutes, keep within 5-59 and step of 5
if (m < 60) {
return Math.min(59, Math.max(5, Math.round(m / 5) * 5));
}
// 60 or more: only allow exact hour multiples (60, 120, 180, 360, 720, 1440)
const allowed = [60, 120, 180, 360, 720, 1440];
// Snap to nearest allowed value
let nearest = allowed[0];
let bestDiff = Math.abs(m - nearest);
for (const a of allowed) {
const d = Math.abs(m - a);
if (d < bestDiff) {
bestDiff = d;
nearest = a;
}
}
return nearest;
};
const handleInputChange = (field, value) => { const handleInputChange = (field, value) => {
setFormData((prev) => { setFormData((prev) => {
const newData = { ...prev, [field]: value }; const newData = {
...prev,
[field]: field === "updateInterval" ? normalizeInterval(value) : value,
};
return newData; return newData;
}); });
setIsDirty(true); setIsDirty(true);
@@ -563,21 +591,23 @@ const Settings = () => {
{/* Quick presets */} {/* Quick presets */}
<div className="mt-3 flex flex-wrap items-center gap-2"> <div className="mt-3 flex flex-wrap items-center gap-2">
{[15, 30, 60, 120, 360, 720, 1440].map((m) => ( {[5, 10, 15, 30, 45, 60, 120, 180, 360, 720, 1440].map(
<button (m) => (
key={m} <button
type="button" key={m}
onClick={() => handleInputChange("updateInterval", m)} type="button"
className={`px-3 py-1.5 rounded-full text-xs font-medium border ${ onClick={() => handleInputChange("updateInterval", m)}
formData.updateInterval === m className={`px-3 py-1.5 rounded-full text-xs font-medium border ${
? "bg-primary-600 text-white border-primary-600" formData.updateInterval === m
: "bg-white dark:bg-secondary-700 text-secondary-700 dark:text-secondary-200 border-secondary-300 dark:border-secondary-600 hover:bg-secondary-50 dark:hover:bg-secondary-600" ? "bg-primary-600 text-white border-primary-600"
}`} : "bg-white dark:bg-secondary-700 text-secondary-700 dark:text-secondary-200 border-secondary-300 dark:border-secondary-600 hover:bg-secondary-50 dark:hover:bg-secondary-600"
aria-label={`Set ${m} minutes`} }`}
> aria-label={`Set ${m} minutes`}
{m % 60 === 0 ? `${m / 60}h` : `${m}m`} >
</button> {m % 60 === 0 ? `${m / 60}h` : `${m}m`}
))} </button>
),
)}
</div> </div>
{/* Range slider */} {/* Range slider */}
@@ -588,12 +618,13 @@ const Settings = () => {
max="1440" max="1440"
step="5" step="5"
value={formData.updateInterval} value={formData.updateInterval}
onChange={(e) => onChange={(e) => {
const raw = parseInt(e.target.value, 10);
handleInputChange( handleInputChange(
"updateInterval", "updateInterval",
parseInt(e.target.value, 10), normalizeInterval(raw),
) );
} }}
className="w-full accent-primary-600" className="w-full accent-primary-600"
aria-label="Update interval slider" aria-label="Update interval slider"
/> />