Skip to content

Conversation

@jokob-sk
Copy link
Collaborator

@jokob-sk jokob-sk commented Feb 11, 2026

✅ Step 1: Refactored datetime_utils.py with timeNowUTC(as_string=True)
✅ Step 2: Updated 25+ server modules
✅ Step 3: Updated 40+ plugin files
✅ Step 4: Created VERSION-based UTC migration logic
✅ Step 5: Updated 4 existing test files
✅ Step 6: Created 25 comprehensive new tests

Summary by CodeRabbit

  • New Features

    • Added event management endpoints for creating, viewing, and removing events.
  • Bug Fixes

    • Consistent timezone handling: timestamps are normalized and displayed correctly in the user's configured timezone.
  • Documentation

    • Clarified and emphasized UTC-as-database-storage guidance with a critical note.
  • Chores

    • Standardized timestamp generation across the app and added a one-time migration to convert stored timestamps to UTC on startup.
  • Tests

    • Added unit tests covering timestamp migration and UTC time utilities.

@jokob-sk
Copy link
Collaborator Author

@coderabbitai review

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request performs a comprehensive refactoring of timestamp handling across the NetAlertX codebase, centralizing all timestamp operations to use UTC and introducing a single source of truth function timeNowUTC(). The PR addresses timezone inconsistencies by migrating from multiple datetime functions (timeNowTZ, timeNowDB, timeNow) to a unified approach.

Changes:

  • Refactored datetime_utils.py with new timeNowUTC(as_string=True) function as the single source of truth for current time
  • Updated 25+ server modules and 40+ plugin files to use the new timestamp function
  • Created VERSION-based database migration logic to convert existing local timestamps to UTC
  • Added comprehensive test coverage with 25 new tests across two test files
  • Updated frontend JavaScript to properly handle UTC timestamps from the database

Reviewed changes

Copilot reviewed 44 out of 44 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
server/utils/datetime_utils.py Core refactoring - replaced timeNowTZ/timeNowDB/timeNow with timeNowUTC(), updated format functions to assume UTC DB storage
server/db/db_upgrade.py Added UTC migration logic with VERSION detection and timezone offset calculations
server/database.py Integrated migration call into database initialization
test/server/test_datetime_utils.py New comprehensive unit tests for timeNowUTC() function
test/db/test_timestamp_migration.py New tests for migration logic covering version detection and edge cases
test/api_endpoints/*.py Updated test imports from timeNowDB/timeNowTZ to timeNowUTC
server/main.py, api.py, app_state.py, initialise.py, logger.py, scheduler.py Updated all timestamp calls to use timeNowUTC()
server/scan/.py, models/.py, messaging/.py, api_server/.py Replaced deprecated functions with timeNowUTC()
front/plugins/*.py Updated all 40+ plugin files to use timeNowUTC()
front/js/common.js Enhanced timestamp parsing to explicitly mark DB timestamps as UTC before conversion
.github/workflows/run-all-tests.yml Added test/db/ to backend test suite
.github/skills/code-standards/SKILL.md Updated documentation with UTC timestamp conventions

Comment on lines 50 to 54
def get_timezone_offset():
now = datetime.datetime.now(conf.tz)
offset_hours = now.utcoffset().total_seconds() / 3600
now = timeNowUTC(as_string=False).replace(tzinfo=conf.tz) if conf.tz else timeNowUTC(as_string=False)
offset_hours = now.utcoffset().total_seconds() / 3600 if now.utcoffset() else 0
offset_formatted = "{:+03d}:{:02d}".format(int(offset_hours), int((offset_hours % 1) * 60))
return offset_formatted
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic in get_timezone_offset() appears incorrect. The function replaces the tzinfo of a UTC datetime with conf.tz, which doesn't perform a timezone conversion - it just changes the timezone label without adjusting the actual time value. This will give an incorrect offset calculation.

The correct approach should be to convert the UTC time to the target timezone using astimezone(), then calculate the offset. For example:

  • Create UTC datetime: timeNowUTC(as_string=False)
  • Convert to target timezone: utc_time.astimezone(conf.tz)
  • Get offset from the converted datetime

This will properly account for DST and give the correct offset between UTC and the target timezone.

Copilot uses AI. Check for mistakes.
Comment on lines +186 to +192
ensure_Settings(self.sql)

# Parameters tables setup
ensure_Parameters(self.sql)

# One-time UTC timestamp migration (must run after Parameters table exists)
migrate_timestamps_to_utc(self.sql)
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical ordering issue: The migration function migrate_timestamps_to_utc() is called AFTER ensure_Settings(), which drops and recreates the Settings table (line 383: "DROP TABLE IF EXISTS Settings"). This means the VERSION value that the migration logic depends on to determine if migration is needed will always be empty/missing, causing the migration to be skipped even for upgrades from old versions.

The migration should be called BEFORE ensure_Settings(), or ensure_Settings() should preserve the VERSION value before dropping the table. Otherwise, all upgrades from pre-26.2.6 versions will skip the timestamp migration, leaving timestamps in local time format instead of UTC.

Copilot uses AI. Check for mistakes.
Comment on lines 44 to 47
def test_timeNowUTC_datetime_has_UTC_timezone(self):
"""Test that datetime object has UTC timezone"""
result = timeNowUTC(as_string=False)
assert result.tzinfo is datetime.UTC or result.tzinfo is not None
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion assert result.tzinfo is datetime.UTC or result.tzinfo is not None is too weak. According to the function implementation, it should always return a datetime with datetime.UTC timezone, not just "any timezone". The assertion should be assert result.tzinfo is datetime.UTC to properly verify the UTC timezone requirement.

Copilot uses AI. Check for mistakes.
Comment on lines 463 to 467
let isoStr = str.trim();
if (!hasOffset) {
// Ensure proper ISO format before appending Z
// Replace space with 'T' if needed: "2026-02-11 11:37:02" → "2026-02-11T11:37:02Z"
isoStr = isoStr.replace(' ', 'T') + 'Z';
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string replacement .replace(' ', 'T') only replaces the first occurrence of a space. While this should work for well-formed timestamps like "2026-02-11 11:37:02", if there are any leading spaces or malformed inputs with multiple spaces, this could create invalid ISO strings.

Consider using a more robust approach that specifically targets the space between date and time, or trim the string first and then replace. For example: isoStr.trim().replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})$/, '$1T$2') + 'Z' would be more precise.

Copilot uses AI. Check for mistakes.
"""
# --- Setup: create two sessions for the test MAC ---
now = timeNowTZ()
now = datetime.now()
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent datetime usage in test. This line uses datetime.now() directly instead of timeNowUTC(as_string=False), which contradicts the PR's goal of making timeNowUTC() the single source of truth for current time. This could lead to timezone-related test failures if the test runs in a different timezone than UTC.

Should be changed to: now = timeNowUTC(as_string=False)

Copilot uses AI. Check for mistakes.
@@ -140,7 +140,7 @@ def test_delete_events_dynamic_days(client, api_token, test_mac):
# Count pre-existing events younger than 30 days for test_mac
# These will remain after delete operation
from datetime import datetime
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant import statement. The datetime module is already imported at the top of the file (line 5). This local import on line 142 is unnecessary and can cause confusion about which datetime is being used.

Copilot uses AI. Check for mistakes.
eve_PendingAlertEmail, eve_PairEventRowid
) VALUES (?,?,?,?,?,?,?)
""", (mac, ip, datetime.now(), eventType, info,
""", (mac, ip, timeNowUTC(as_string=False), eventType, info,
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type mismatch in database insertion. The code passes timeNowUTC(as_string=False) which returns a datetime object, but the database column expects a TEXT/string timestamp. According to the PR's pattern, database writes should use timeNowUTC() (or explicitly timeNowUTC(as_string=True)) to get the formatted string.

This should be: timeNowUTC() instead of timeNowUTC(as_string=False)

While SQLite can handle datetime objects and convert them, this breaks the consistency of the refactoring which aims to always use string timestamps for DB storage.

Copilot uses AI. Check for mistakes.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 11, 2026

📝 Walkthrough

Walkthrough

Replaces legacy time helpers with a single UTC utility timeNowUTC(), updates call sites across frontend, plugins, server, and tests, and adds a conditional one-time DB migration to convert stored timestamps to UTC. New tests validate the UTC utility and migration behavior.

Changes

Cohort / File(s) Summary
Core Time Utility
server/utils/datetime_utils.py
Removes timeNowTZ, timeNow, timeNowDB; adds timeNowUTC(as_string=True) and updates parsing/formatting utilities to treat DB timestamps as UTC and convert to user TZ.
DB Migration
server/db/db_upgrade.py, server/database.py
Adds is_timestamps_in_utc() and migrate_timestamps_to_utc() and invokes migration during DB init (version-gated, sample-based detection and per-column timezone offset updates).
Server Core & API
server/__main__.py, server/api.py, server/logger.py, server/scheduler.py, server/initialise.py, server/app_state.py
Replaces legacy time calls with timeNowUTC() / timeNowUTC(as_string=False) for loop start, API timestamps, logging, scheduler, initialization, and app-state checks.
Server Models & Endpoints
server/models/.../*.py, server/api_server/sessions_endpoint.py, server/api_server/sync_endpoint.py
Switches models to UTC timestamps; event_instance gains public methods (create/get/delete events, getEventsTotals); session/sync endpoints updated to use timeNowUTC.
Scan & Device Handling
server/scan/...
Updates device last-seen, session and event timestamps, and plugin-check timestamps to timeNowUTC() for all scan/session flows.
Plugins & Frontend
front/plugins/..., front/plugins/_publisher_*/..., front/plugins/csv_backup/script.py, front/js/common.js
Replaces timeNowDB()/datetime.now() with timeNowUTC() across publisher and collector plugins; common.js normalizes tz-less timestamps by appending Z and consistently converts UTC inputs to user timezone.
Plugin Helpers & Messaging
front/plugins/plugin_helper.py, server/plugin.py, server/messaging/in_app.py
Standardizes plugin lifecycle and in-app messaging timestamps to use timeNowUTC().
CI / Docs
.github/skills/code-standards/SKILL.md, .github/workflows/run-all-tests.yml
Documents UTC storage and timeNowUTC() as source of truth; expands workflow inputs/path logic to include db/ tests and renames scan input to run_scan.
Tests
test/db/test_timestamp_migration.py, test/server/test_datetime_utils.py, test/api_endpoints/test_*.py
Adds migration and datetime utility tests; updates endpoint tests to use timeNowUTC() and validates migration/version gating and detection logic.

Sequence Diagram

sequenceDiagram
    participant App as Application
    participant DB as Database
    participant Upgrader as DB Upgrade (migrate_timestamps_to_utc)
    participant Config as Config/Timezone

    App->>DB: Initialize DB (ensure tables)
    DB->>Upgrader: migrate_timestamps_to_utc(sql)
    Upgrader->>DB: Read Settings.VERSION
    alt VERSION fresh/unknown or >= UTC-aware
        Upgrader->>DB: Skip migration (return success)
    else Old version detected
        Upgrader->>DB: Sample timestamps from Devices
        Upgrader->>Config: Read configured timezone (conf.tz)
        Upgrader->>Upgrader: Compute local→UTC offset
        loop For each table.column needing migration
            Upgrader->>DB: Update non-NULL timestamps by adding offset
            Upgrader->>Upgrader: Log progress/errors
        end
        Upgrader->>DB: Return success/failure
    end
    DB->>App: DB init complete (migration done or skipped)
Loading

Possibly related PRs

Poem

🐰 I hopped through code at break of dawn,

Swapped scattered clocks for UTC's calm song.
Timestamps aligned, no more local rue,
One true time to trust — from me to you. 🕰️✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'timestamp cleanup' is vague and generic, using non-descriptive terminology that doesn't convey the specific nature or scope of the changes made in this comprehensive refactoring. Consider a more specific title that reflects the main objective, such as 'Refactor timestamp utilities to use UTC as single source of truth' or 'Implement UTC-based timestamps with migration logic'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore_timestamps

No actionable comments were generated in the recent review. 🎉


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
server/utils/datetime_utils.py (1)

82-87: ⚠️ Potential issue | 🟠 Major

fromtimestamp() uses local timezone — inconsistent with UTC-first approach.

datetime.datetime.fromtimestamp(inputTimeStamp) interprets the epoch in the system's local timezone and returns a naive datetime in local time. In a UTC-first codebase, this is a latent mismatch: callers that later assume the result is UTC (as the rest of the module does for naive datetimes) will get wrong values on non-UTC servers.

Suggested fix
     if isinstance(inputTimeStamp, (int, float)):
         try:
-            return datetime.datetime.fromtimestamp(inputTimeStamp)
+            return datetime.datetime.fromtimestamp(inputTimeStamp, tz=datetime.UTC)
         except (OSError, OverflowError, ValueError):
             return None
front/plugins/freebox/freebox.py (1)

170-170: ⚠️ Potential issue | 🟠 Major

datetime.fromtimestamp() returns local time, not UTC — inconsistent with the PR's UTC mandate.

datetime.fromtimestamp(ts) interprets the Unix timestamp in the system's local timezone. For UTC-only timestamps, use the tz parameter:

🐛 Proposed fix
+import datetime as dt
...
-                        watched4=datetime.fromtimestamp(ip.get("last_time_reachable", 0)).strftime(DATETIME_PATTERN),
+                        watched4=datetime.fromtimestamp(ip.get("last_time_reachable", 0), tz=dt.timezone.utc).strftime(DATETIME_PATTERN),

Or, since the datetime module is already imported at line 7 as from datetime import datetime, you can use:

-                        watched4=datetime.fromtimestamp(ip.get("last_time_reachable", 0)).strftime(DATETIME_PATTERN),
+                        watched4=datetime.fromtimestamp(ip.get("last_time_reachable", 0), tz=__import__('datetime').timezone.utc).strftime(DATETIME_PATTERN),

A cleaner approach — adjust the import at line 7:

-from datetime import datetime
+import datetime

Then at line 170:

-                        watched4=datetime.fromtimestamp(ip.get("last_time_reachable", 0)).strftime(DATETIME_PATTERN),
+                        watched4=datetime.datetime.fromtimestamp(ip.get("last_time_reachable", 0), tz=datetime.UTC).strftime(DATETIME_PATTERN),
server/models/notification_instance.py (1)

286-296: ⚠️ Potential issue | 🟠 Major

Remove tz_offset parameter from Device Down alert time comparisons.

datetime('now') in SQLite returns UTC. Since eve_DateTime is stored in UTC (via timeNowUTC()), applying tz_offset incorrectly shifts the comparison threshold. The offset was compensating for local-time storage that no longer applies after the UTC migration.

This issue exists in two locations:

  • server/models/notification_instance.py line 293
  • server/messaging/reporting.py line 130
Suggested fix for notification_instance.py
-                AND eve_DateTime < datetime('now', ?, ?)
+                AND eve_DateTime < datetime('now', ?)
                 """,
-            (f"-{minutes} minutes", tz_offset),
+            (f"-{minutes} minutes",),
front/plugins/_publisher_mqtt/mqtt.py (1)

622-637: ⚠️ Potential issue | 🟠 Major

Bug: prepTimeStamp incorrectly treats naive UTC timestamps as local time.

The database stores all timestamps in UTC format (as documented in timeNowUTC()). When prepTimeStamp receives a naive datetime string from the database (lines 507-508 call with device["devLastConnection"] and device["devFirstConnection"]), line 629 incorrectly applies conf.tz.localize(), which treats it as if it were in the user's configured timezone rather than UTC. This adds an incorrect timezone offset.

For example, a UTC timestamp 2024-01-01 12:00:00 becomes EST -05:00 instead of UTC +00:00. The fallback path on line 634 correctly returns a UTC-aware datetime, exposing the inconsistency.

Replace line 629 with parsed_datetime = parsed_datetime.replace(tzinfo=datetime.timezone.utc) and add import datetime as dt_module at the top of the file, or use datetime.timezone.utc directly if importing the module.

Proposed fix
+import datetime as dt_module
+
 def prepTimeStamp(datetime_str):
     try:
         parsed_datetime = datetime.fromisoformat(datetime_str)
 
         if parsed_datetime.tzinfo is None:
-            parsed_datetime = conf.tz.localize(parsed_datetime)
+            parsed_datetime = parsed_datetime.replace(tzinfo=dt_module.timezone.utc)
 
     except ValueError:
         mylog('verbose', [f"[{pluginName}]  Timestamp conversion failed of string '{datetime_str}'"])
         parsed_datetime = timeNowUTC(as_string=False)
 
     return parsed_datetime.isoformat()
🤖 Fix all issues with AI agents
In @.github/skills/code-standards/SKILL.md:
- Around line 50-55: The Markdown lines beginning with `#` (the explanatory
comments about UTC/timeNowUTC()) are being rendered as headings; update the
SKILL.md content so those lines are not interpreted as H1 headers — either
remove the leading `#` characters or place the explanatory lines inside a fenced
code block or blockquote; ensure the guidance still references the canonical
function name timeNowUTC() and the rule that all database timestamps must be
stored in UTC so readers can find the SINGLE SOURCE OF TRUTH for current time.

In `@front/js/common.js`:
- Around line 455-468: The current logic unconditionally appends 'Z' to any
timestamp lacking an offset (using hasOffset and isoStr), which breaks non-ISO
inputs like RFC2822; change the behavior so you only append 'Z' when the input
is ISO-shaped: first test str against an ISO pattern (e.g.,
/^\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?)?$/) and only then perform
the space→'T' replacement and append 'Z' to isoStr; leave other formats (e.g.,
RFC2822) untouched so they are parsed as-is by new Date().

In `@server/database.py`:
- Around line 191-192: The call to migrate_timestamps_to_utc in initDB currently
ignores its boolean return; update initDB to check
migrate_timestamps_to_utc(self.sql) and handle a False result by preventing the
subsequent commit (e.g., rollback the transaction, log an error via the existing
logger, and propagate/raise an exception or return an error status from initDB)
so the DB is not left in a partially-migrated state; reference the
migrate_timestamps_to_utc function and the initDB commit path to locate where to
insert the check and the rollback/abort logic.

In `@server/db/db_upgrade.py`:
- Around line 646-650: The DST handling computes offset_hours from the current
timezone offset (using tz, now_local, and offset_hours) which misapplies DST to
historical timestamps; add a log message in the same block where offset_hours is
computed (the tz/now_local/offset_hours code in db_upgrade.py) that clearly
documents this limitation and warns that historical timestamps may be off by up
to ±1 hour for DST-observing zones (mention conf.tz or tz name), so
callers/operators are aware of the caveat.
🧹 Nitpick comments (10)
server/utils/datetime_utils.py (2)

188-190: Use !r conversion flag per Ruff RUF010.

Suggested fix
-            return f"invalid:{repr(date_str)}"
+            return f"invalid:{date_str!r}"

Also applies to the similar pattern on line 207:

-        return f"invalid:{repr(date_str)} e: {e}"
+        return f"invalid:{date_str!r} e: {e}"

192-204: format_date mirrors format_date_iso — consistent UTC conversion pattern.

The handling of conf.tz as string vs. tzinfo (lines 198-201) duplicates the pattern from format_date_iso (line 133). Consider extracting a small helper like get_target_tz() to reduce duplication, but this is not blocking.

front/plugins/internet_ip/script.py (1)

77-77: Redundant str() wrapper — timeNowUTC() already returns a string.

timeNowUTC() with the default as_string=True returns a str, making the str() call unnecessary.

Suggested fix
-    append_line_to_file(logPath + '/IP_changes.log', '[' + str(timeNowUTC()) + ']\t' + new_internet_IP + '\n')
+    append_line_to_file(logPath + '/IP_changes.log', '[' + timeNowUTC() + ']\t' + new_internet_IP + '\n')
server/api_server/sync_endpoint.py (1)

25-25: Timestamps are passed explicitly, but write_notification already defaults to timeNowUTC().

Since write_notification uses timeNowUTC() as its default when timestamp is None, these explicit passes are redundant. Not a bug, but you could simplify by omitting the timestamp argument entirely.

Also applies to: 31-31, 71-71, 76-76

server/db/db_upgrade.py (1)

517-568: current_offset_seconds is computed but never used for detection logic.

Lines 524–540 compute current_offset_seconds from the configured timezone, but the actual decision (Lines 556–568) relies solely on whether timezone markers (+, Z) exist in the sampled strings, or defaults to False. The offset variable is only logged on Line 566. If it's intentionally informational-only, that's fine, but if the intent was to use it for smarter heuristics, it's incomplete.

test/db/test_timestamp_migration.py (1)

122-139: Consider verifying actual timestamp content after migration.

test_migrate_old_version_triggers_migration only asserts result is True but doesn't verify that the timestamp values in the Devices table were actually modified. Adding a check that reads back the migrated timestamps and compares them to the expected shifted values would strengthen confidence in the migration logic.

server/scan/session_events.py (1)

174-229: Pre-existing: startTime is interpolated via f-strings into SQL.

startTime from timeNowUTC() is safe (controlled format), but the insert_events function uses f-string interpolation for all its SQL inserts (Lines 177, 190, 207, 221) while insertOnlineHistory on Line 272 correctly uses parameterized queries. Consider migrating these to parameterized queries for consistency and defense-in-depth.

Not introduced by this PR, so deferring is fine.

server/logger.py (1)

127-127: Log timestamps are now UTC with no timezone indicator.

The file_print function now emits HH:MM:SS in UTC. Since there's no "Z" or "UTC" suffix, operators may interpret these as local time. Consider appending a "Z" suffix or a brief indicator so log readers know the time is UTC.

💡 Suggested tweak
-    result = timeNowUTC(as_string=False).strftime("%H:%M:%S") + " "
+    result = timeNowUTC(as_string=False).strftime("%H:%M:%S") + "Z "
server/models/event_instance.py (2)

113-121: Dead branch: both if/else paths are identical.

ensure_datetime already handles str, datetime, and None inputs, so the isinstance check on line 118 is redundant — both branches execute the same call.

♻️ Simplify
     def createEvent(self, mac: str, ip: str, event_type: str = "Device Down", additional_info: str = "", pending_alert: int = 1, event_time: datetime | None = None):
-        if isinstance(event_time, str):
-            start_time = ensure_datetime(event_time)
-        else:
-            start_time = ensure_datetime(event_time)
+        start_time = ensure_datetime(event_time)

102-109: Two deletion methods with different time sources.

delete_older_than (line 102) computes the cutoff in Python via timeNowUTC, while deleteEventsOlderThan (line 160) delegates to SQLite's date('now', ...). Both happen to use UTC (SQLite's date('now') is UTC), so results are consistent — but having two methods for the same operation with different implementations is a maintenance risk.

Consider consolidating into one, or having deleteEventsOlderThan delegate to delete_older_than.

Also applies to: 160-172

Comment on lines +455 to +468
const hasOffset = /Z$/i.test(str.trim()) ||
/GMT[+-]\d{2,4}/.test(str) ||
/[+-]\d{2}:?\d{2}$/.test(str.trim()) ||
/\([^)]+\)$/.test(str.trim());

// ⚠️ CRITICAL: All DB timestamps are stored in UTC without timezone markers.
// If no offset is present, we must explicitly mark it as UTC by appending 'Z'
// so JavaScript doesn't interpret it as local browser time.
let isoStr = str.trim();
if (!hasOffset) {
// Ensure proper ISO format before appending Z
// Replace space with 'T' if needed: "2026-02-11 11:37:02" → "2026-02-11T11:37:02Z"
isoStr = isoStr.trim().replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})$/, '$1T$2') + 'Z';
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Appending Z to non-ISO strings (e.g., RFC2822) may break parsing.

When case 5 (RFC2822, line 425) matches a string like "25 Aug 2025 13:45:22" without an offset, hasOffset is false and Z gets appended, producing "25 Aug 2025 13:45:22Z". This is not a valid format for new Date() in all browsers and will likely yield NaN/"Failed conversion".

Consider limiting the Z-append to ISO-shaped strings only:

Proposed fix
     let isoStr = str.trim();
     if (!hasOffset) {
-      // Ensure proper ISO format before appending Z
-      // Replace space with 'T' if needed: "2026-02-11 11:37:02" → "2026-02-11T11:37:02Z"
-      isoStr = isoStr.trim().replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})$/, '$1T$2') + 'Z';
+      // Only force UTC for ISO-shaped strings (DB timestamps)
+      if (/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?$/.test(isoStr)) {
+        isoStr = isoStr.replace(' ', 'T') + 'Z';
+      }
     }
🤖 Prompt for AI Agents
In `@front/js/common.js` around lines 455 - 468, The current logic unconditionally
appends 'Z' to any timestamp lacking an offset (using hasOffset and isoStr),
which breaks non-ISO inputs like RFC2822; change the behavior so you only append
'Z' when the input is ISO-shaped: first test str against an ISO pattern (e.g.,
/^\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?)?$/) and only then perform
the space→'T' replacement and append 'Z' to isoStr; leave other formats (e.g.,
RFC2822) untouched so they are parsed as-is by new Date().

Comment on lines +191 to +192
# One-time UTC timestamp migration (must run after Parameters table exists)
migrate_timestamps_to_utc(self.sql)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Return value of migrate_timestamps_to_utc is silently discarded.

Per the function's contract, it returns False on top-level failure (while internally catching per-table errors). If it returns False, initDB will proceed to commit, potentially leaving the database with inconsistent (mixed local/UTC) timestamps. Consider checking the return value:

💡 Suggested fix
             # One-time UTC timestamp migration (must run after Parameters table exists)
-            migrate_timestamps_to_utc(self.sql)
+            if not migrate_timestamps_to_utc(self.sql):
+                mylog("minimal", ["[Database] - WARNING: UTC timestamp migration failed"])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# One-time UTC timestamp migration (must run after Parameters table exists)
migrate_timestamps_to_utc(self.sql)
# One-time UTC timestamp migration (must run after Parameters table exists)
if not migrate_timestamps_to_utc(self.sql):
mylog("minimal", ["[Database] - WARNING: UTC timestamp migration failed"])
🤖 Prompt for AI Agents
In `@server/database.py` around lines 191 - 192, The call to
migrate_timestamps_to_utc in initDB currently ignores its boolean return; update
initDB to check migrate_timestamps_to_utc(self.sql) and handle a False result by
preventing the subsequent commit (e.g., rollback the transaction, log an error
via the existing logger, and propagate/raise an exception or return an error
status from initDB) so the DB is not left in a partially-migrated state;
reference the migrate_timestamps_to_utc function and the initDB commit path to
locate where to insert the check and the rollback/abort logic.

Comment on lines +646 to +650
if tz:
now_local = dt.datetime.now(tz)
offset_hours = (now_local.utcoffset().total_seconds()) / 3600
else:
offset_hours = 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

DST offset applied uniformly to all historical timestamps.

offset_hours is computed from the current UTC offset of conf.tz. For timezones with DST (e.g., CET alternating between UTC+1 and UTC+2), timestamps stored during a different DST period will be shifted by the wrong amount. This is inherently hard to fix perfectly, but worth documenting as a known limitation — affected timestamps could be off by up to 1 hour.

Consider adding a log line noting this caveat.

🤖 Prompt for AI Agents
In `@server/db/db_upgrade.py` around lines 646 - 650, The DST handling computes
offset_hours from the current timezone offset (using tz, now_local, and
offset_hours) which misapplies DST to historical timestamps; add a log message
in the same block where offset_hours is computed (the tz/now_local/offset_hours
code in db_upgrade.py) that clearly documents this limitation and warns that
historical timestamps may be off by up to ±1 hour for DST-observing zones
(mention conf.tz or tz name), so callers/operators are aware of the caveat.

@jokob-sk jokob-sk merged commit 7af850c into main Feb 11, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant