diff --git a/README.md b/README.md
index 9e47046..9275f0b 100755
--- a/README.md
+++ b/README.md
@@ -28,8 +28,6 @@ With a modern architecture, Fredy provides a **clean Web UI**, removes
duplicates across platforms, and stores results so you never see the
same listing twice.
-
-
------------------------------------------------------------------------
## ✨ Key Features
diff --git a/lib/api/routes/versionRouter.js b/lib/api/routes/versionRouter.js
index 1db98a3..4a685e6 100644
--- a/lib/api/routes/versionRouter.js
+++ b/lib/api/routes/versionRouter.js
@@ -1,6 +1,7 @@
import restana from 'restana';
import fetch from 'node-fetch';
import { getPackageVersion } from '../../utils.js';
+import semver from 'semver';
const service = restana();
const versionRouter = service.newRouter();
@@ -15,7 +16,7 @@ async function getCurrentVersionFromGithub() {
const raw = await fetch('https://api.github.com/repos/orangecoding/fredy/releases/latest');
const data = await raw.json();
const localFredyVersion = await getPackageVersion();
- if (localFredyVersion === data.tag_name) {
+ if (data.tag_name == null || semver.gte(localFredyVersion, data.tag_name)) {
return null;
}
return {
diff --git a/lib/utils.js b/lib/utils.js
index 7f9c3bd..e440796 100755
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -109,11 +109,22 @@ function timeStringToMs(timeString, now) {
}
/**
- * Check whether current time is within configured working hours, or no hours are set.
- * If working hours are missing or incomplete, returns true.
- * @param {{workingHours?: {from?: string, to?: string}}} config
- * @param {number} now - Epoch ms
- * @returns {boolean}
+ * Determine whether the given timestamp is within the configured working hours, or return true when the window is not set.
+ * - If workingHours is missing or either 'from' or 'to' is empty/null, returns true.
+ * - Supports windows that cross midnight (e.g., from '23:00' to '06:00').
+ *
+ * Time parsing is based on the local timezone of the running process.
+ *
+ * @param {{workingHours?: {from?: string|null, to?: string|null}}} config - Configuration object containing working hours in 'HH:mm' format.
+ * @param {number} now - Epoch milliseconds to evaluate.
+ * @returns {boolean} True when execution is allowed at 'now'.
+ * @example
+ * // Same-day window
+ * duringWorkingHoursOrNotSet({ workingHours: { from: '08:00', to: '17:00' } }, someTime);
+ * @example
+ * // Window crossing midnight
+ * // For { from: '05:00', to: '00:30' } → 23:00 => true, 01:00 => false, 06:00 => true
+ * duringWorkingHoursOrNotSet({ workingHours: { from: '05:00', to: '00:30' } }, Date.now());
*/
function duringWorkingHoursOrNotSet(config, now) {
const { workingHours } = config;
@@ -122,7 +133,20 @@ function duringWorkingHoursOrNotSet(config, now) {
}
const toDate = timeStringToMs(workingHours.to, now);
const fromDate = timeStringToMs(workingHours.from, now);
- return fromDate <= now && toDate >= now;
+
+ // If parsing fails (e.g., malformed time), be lenient and allow.
+ if (isNaN(toDate) || isNaN(fromDate)) {
+ return true;
+ }
+
+ if (toDate >= fromDate) {
+ // Same-day window (e.g., 08:00 - 17:00)
+ return now >= fromDate && now <= toDate;
+ }
+
+ // Window crosses midnight (e.g., 05:00 -> 00:30 next day)
+ // Accept if we are after 'from' today OR before 'to' today (which represents next day's cutoff).
+ return now >= fromDate || now <= toDate;
}
/**
@@ -244,13 +268,13 @@ function sleep(ms) {
}
/**
- * returns a random into between start and end
- * @param a start int
- * @param b max int
- * @returns {*}
+ * Return a random integer between min and max (inclusive).
+ * @param {number} min - Minimum integer value.
+ * @param {number} max - Maximum integer value.
+ * @returns {number} A random integer N where min <= N <= max.
*/
-function randomBetween(a, b) {
- return Math.floor(Math.random() * (b - a + 1)) + a;
+function randomBetween(min, max) {
+ return Math.floor(Math.random() * (max - min + 1)) + min;
}
// Call refreshConfig() from the application entrypoint during startup to populate config.
diff --git a/package.json b/package.json
index f1e94f9..f6749d7 100755
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "fredy",
- "version": "12.2.0",
+ "version": "12.2.1",
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
"scripts": {
"prepare": "husky",
@@ -85,6 +85,7 @@
"react-router": "7.9.2",
"react-router-dom": "7.9.2",
"restana": "5.1.0",
+ "semver": "^7.7.2",
"serve-static": "2.2.0",
"slack": "11.0.2",
"vite": "7.1.7",
@@ -104,7 +105,7 @@
"history": "5.3.0",
"husky": "9.1.7",
"less": "4.4.1",
- "lint-staged": "16.2.0",
+ "lint-staged": "16.2.1",
"mocha": "11.7.2",
"nodemon": "^3.1.10",
"prettier": "3.6.2"
diff --git a/test/provider/utils.test.js b/test/provider/utils.test.js
index 8dc9fb3..15fb16c 100644
--- a/test/provider/utils.test.js
+++ b/test/provider/utils.test.js
@@ -8,6 +8,7 @@ const fakeWorkingHoursConfig = (from, to) => ({
from,
},
});
+
describe('utils', () => {
describe('#isOneOf()', () => {
it('should be false', () => {
@@ -33,5 +34,19 @@ describe('utils', () => {
it('should be true if only from is set', () => {
expect(duringWorkingHoursOrNotSet(fakeWorkingHoursConfig('12:00', null), 1622026740000)).to.be.true;
});
+ it('should handle working hours that cross midnight (e.g., 05:00 → 00:30)', () => {
+ const cfg = fakeWorkingHoursConfig('05:00', '00:30');
+ const mkTs = (h, m = 0) => {
+ const d = new Date();
+ d.setHours(h);
+ d.setMinutes(m);
+ d.setSeconds(0);
+ d.setMilliseconds(0);
+ return d.getTime();
+ };
+ expect(duringWorkingHoursOrNotSet(cfg, mkTs(23, 0))).to.be.true; // 23:00 => within window
+ expect(duringWorkingHoursOrNotSet(cfg, mkTs(1, 0))).to.be.false; // 01:00 => outside window
+ expect(duringWorkingHoursOrNotSet(cfg, mkTs(6, 0))).to.be.true; // 06:00 => within window
+ });
});
});
diff --git a/ui/src/components/menu/Menu.jsx b/ui/src/components/menu/Menu.jsx
index f49f9a8..45683ae 100644
--- a/ui/src/components/menu/Menu.jsx
+++ b/ui/src/components/menu/Menu.jsx
@@ -53,7 +53,7 @@ const TopMenu = function TopMenu({ isAdmin }) {
tab={
A new version of Fredy is available. Update now to take advantage of the latest features and bug fixes.
{stripFullChangelog(versionUpdate.body)}
+