Skip to content

Skip Repeated Notifications (devSkipRepeated) is silently inert in most timezones due to a UTC/localtime mismatch in the cooldown comparison #1737

Description

@mormegil6

What installation are you running?

Production (netalertx) 📦

Is there an existing issue for this?

The issue occurs in the following browsers. Select at least 2.

  • Firefox
  • Chrome
  • Edge
  • Safari (unsupported) - PRs welcome
  • N/A - This is an issue with the backend

Current Behavior

The per-device "Skip Repeated Notifications" setting (devSkipRepeated, entered as a number of hours) is supposed to suppress repeat notifications for a device within N hours of the last one. In any timezone ahead of UTC, it does much less than configured - and for small values, nothing at all. In Europe/Warsaw during DST (UTC+2), any devSkipRepeated value of 2 hours or less provides zero suppression whatsoever; larger values are silently undercut by 2 hours (e.g. a 3-hour setting only actually suppresses for about 1 real hour).

Real-world impact I hit: a device set to a 2-hour cooldown kept generating repeat "Down"/"Disconnected" notifications every few minutes, as if the setting weren't applied at all - because, functionally, it wasn't.

Expected Behavior

A devSkipRepeated of N hours should suppress repeat notifications for a device for N real hours after the last one, regardless of the server's local timezone or daylight saving state.

Steps To Reproduce

Reproduced live, end-to-end, in an isolated Docker container (ghcr.io/jokob-sk/netalertx:latest, same image as production) with TZ=Europe/Warsaw, separate from any production data.

  1. Fresh NetAlertX instance, LOG_LEVEL='trace', TIMEZONE='Europe/Warsaw', container TZ=Europe/Warsaw (see note below - app.conf's TIMEZONE alone is not sufficient to reproduce this, the container's actual OS-level TZ must also be set, since that's what SQLite's localtime modifier actually consults).
  2. Insert one device with devSkipRepeated=2 and devLastNotification set to a real ~20-minutes-ago UTC timestamp, plus one pending event (evePendingAlertEmail=1).
  3. Do not call any suppression logic manually - just wait for the app's own normal scheduler (process_scan(), called from the main loop on the ARPSCAN cron schedule) to run naturally, the same way it does in a real deployment.
  4. Check evePendingAlertEmail for that event immediately after the scheduled cycle runs.

Result: evePendingAlertEmail stayed 1 (not suppressed), even though only ~23 minutes had passed against a 2-hour configured cooldown. 19 seconds later, the real notification pipeline picked up the still-pending flag and built/queued an actual duplicate notification containing this device under down_devices - i.e. this isn't just an internal flag glitch, it produces a real, user-visible repeat notification.

Important gotcha for anyone else reproducing this: app.conf's TIMEZONE setting only feeds conf.tz = pytz.timezone(...) (server/initialise.py), used for display formatting elsewhere in the app - it is not what SQLite's 'localtime' modifier consults in the query below. That reads the container's actual OS/process timezone. Setting TIMEZONE in app.conf alone will not reproduce this if the container's real TZ is UTC; you need -e TZ=<zone> (and ideally a bind-mounted /etc/localtime) on the container itself.

Relevant app.conf settings

TIMEZONE='Europe/Warsaw'
LOG_LEVEL='trace'

docker-compose.yml

services:
  netalertx:
    image: ghcr.io/jokob-sk/netalertx:latest
    container_name: netalertx-repro-skew
    cap_add:
      - CHOWN
      - NET_ADMIN
      - NET_BIND_SERVICE
      - NET_RAW
      - SETGID
      - SETUID
    ports:
      - "11031:1031"
      - "10212:20212"
    environment:
      TZ: Europe/Warsaw
      PORT: 1031
      GRAPHQL_PORT: 20212
      LISTEN_ADDR: 0.0.0.0
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ./data:/data

(Isolated repro - bridge networking instead of production's `network_mode: host`, distinct ports so it doesn't collide with a real instance on the same host. `cap_add` required for the entrypoint to start at all under bridge networking - unrelated to this bug, just a real prerequisite. `-e TZ=Europe/Warsaw` plus the `/etc/localtime` bind are the load-bearing part for reproducing *this specific* bug, per the gotcha above.)

Debug or Trace enabled

  • I have read and followed the steps in the wiki link above and provided the required debug logs and the log section covers the time when the issue occurs.

Relevant app.log section

00:10:57 [Process Scan] Skipping repeated notifications
00:10:57 [Skip Repeated Notifications] Skip Repeated
00:10:57 [SSE] Broadcasted event: unread_notifications_count_update
...
00:11:15 [Notification] Check if something to report
00:11:15 [Notification] Included sections:  ['new_devices', 'down_devices', 'events']
...
00:11:15 [Notification] final_json: {..., "down_devices": [{"devName": "SkewTestDevice", "eveMac": "AA:BB:CC:DD:EE:01", "devVendor": null, "eveIp": "172.17.0.99", "eveDateTime": "2026-08-12T23:47:41+02:00", "eveEventType": "Device Down", ...}], "down_devices_meta": {"title": "🔴 Down devices", ...}, ...}
00:11:16 [Send API] Updating notification_* files in  /tmp/api/
00:11:16 [Notification] Notifications changes:  1

[Process Scan] Skipping repeated notifications / [Skip Repeated Notifications] Skip Repeated is the real skip_repeated_notifications() call firing on its normal schedule (t=155s after the test device was inserted, no manual trigger). 19 seconds later the notification pipeline genuinely built a payload containing the "suppressed" device, because the flag was never actually cleared. Full app.log for this run is attached here: app.log.

Docker Logs

Before/after state of the test event, captured directly from the repro instance's own app.db:

BEFORE (immediately after insert, before any scan cycle):
  eveMac=AA:BB:CC:DD:EE:01  eveEventType=Device Down  evePendingAlertEmail=1

AFTER (immediately following the real scheduled scan cycle that ran skip_repeated_notifications()):
  eveMac=AA:BB:CC:DD:EE:01  eveEventType=Device Down  evePendingAlertEmail=1   <- UNCHANGED

Exact live comparison numbers, from this instance's own clock (SELECT strftime('%s','now'), strftime('%s','now','localtime')):

utc_epoch_now=1786572718  local_epoch_now=1786579918  diff_seconds=7200   (exactly Europe/Warsaw's +2h DST offset)

devMac             devSkipRepeated  last_notif_epoch_utc  lhs_min(last_notif+skip)  rhs_min(now_local)  would_suppress
AA:BB:CC:DD:EE:01  2                1786572676             29776211                 29776331             0  (false - not suppressed)

With the bug's localtime modifier removed (UTC on both sides), the same inputs correctly evaluate would_suppress=1 for a genuinely-recent notification.

Full docker logs output for this run is attached here: docker_logs_full.txt.

Metadata

Metadata

Labels

bug 🐛Something isn't workingnext release/in dev image 🚀This is coming in the next release or was already released if the issue is Closed.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions