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.
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.
- 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).
- Insert one device with
devSkipRepeated=2 and devLastNotification set to a real ~20-minutes-ago UTC timestamp, plus one pending event (evePendingAlertEmail=1).
- 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.
- 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
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.
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.
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), anydevSkipRepeatedvalue 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
devSkipRepeatedof 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) withTZ=Europe/Warsaw, separate from any production data.LOG_LEVEL='trace',TIMEZONE='Europe/Warsaw', containerTZ=Europe/Warsaw(see note below -app.conf'sTIMEZONEalone is not sufficient to reproduce this, the container's actual OS-levelTZmust also be set, since that's what SQLite'slocaltimemodifier actually consults).devSkipRepeated=2anddevLastNotificationset to a real ~20-minutes-ago UTC timestamp, plus one pending event (evePendingAlertEmail=1).process_scan(), called from the main loop on the ARPSCAN cron schedule) to run naturally, the same way it does in a real deployment.evePendingAlertEmailfor that event immediately after the scheduled cycle runs.Result:
evePendingAlertEmailstayed1(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 underdown_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'sTIMEZONEsetting only feedsconf.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. SettingTIMEZONEinapp.confalone will not reproduce this if the container's realTZis UTC; you need-e TZ=<zone>(and ideally a bind-mounted/etc/localtime) on the container itself.Relevant
app.confsettingsdocker-compose.yml
Debug or Trace enabled
Relevant
app.logsection[Process Scan] Skipping repeated notifications/[Skip Repeated Notifications] Skip Repeatedis the realskip_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. Fullapp.logfor 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:Exact live comparison numbers, from this instance's own clock (
SELECT strftime('%s','now'), strftime('%s','now','localtime')):With the bug's
localtimemodifier removed (UTC on both sides), the same inputs correctly evaluatewould_suppress=1for a genuinely-recent notification.Full
docker logsoutput for this run is attached here: docker_logs_full.txt.