diff --git a/plugins/notifications/api/routes.py b/plugins/notifications/api/routes.py
index da8d187..844652e 100644
--- a/plugins/notifications/api/routes.py
+++ b/plugins/notifications/api/routes.py
@@ -140,6 +140,10 @@ def _apply_expiry_fields(t, data):
_DISPLAY_STYLES = ('standard', 'carousel', 'grid', 'banner')
+# Ceiling on the per-type post-expiry tail. A day is already far longer than
+# "recently ended"; anything more is an end time that should have been later.
+_MAX_GRACE_MINUTES = 1440
+
def _apply_display_fields(t, data):
"""Set shopfloor display-behavior columns on a NotificationType from request
@@ -153,6 +157,17 @@ def _apply_display_fields(t, data):
if ds not in _DISPLAY_STYLES:
return "displaystyle must be one of: %s" % ", ".join(_DISPLAY_STYLES)
t.displaystyle = ds
+ if 'gracewindowminutes' in data:
+ raw = data.get('gracewindowminutes')
+ raw = 0 if raw in (None, '') else raw
+ try:
+ minutes = int(raw)
+ except (TypeError, ValueError):
+ return "gracewindowminutes must be a whole number of minutes"
+ if minutes < 0 or minutes > _MAX_GRACE_MINUTES:
+ return ("gracewindowminutes must be between 0 and %d"
+ % _MAX_GRACE_MINUTES)
+ t.gracewindowminutes = minutes
return None
@@ -163,10 +178,11 @@ def _config_version():
reach pages that are already open."""
types = NotificationType.query.order_by(NotificationType.notificationtypeid).all()
parts = [
- "%s|%s|%s|%d|%d|%s|%s|%s|%d" % (
+ "%s|%s|%s|%d|%d|%s|%s|%s|%d|%d" % (
t.notificationtypeid, t.typecolor, t.displaystyle,
int(bool(t.splitperemployee)), int(bool(t.showemployeephoto)),
t.expirymode, t.expirydays, t.expiryhour, int(bool(t.isactive)),
+ int(t.gracewindowminutes or 0),
)
for t in types
]
@@ -698,8 +714,15 @@ def get_shopfloor_notifications():
# All units: only show notifications with NULL businessunitid
base_query = base_query.filter(Notification.businessunitid.is_(None))
- # Current notifications (active now or ended within 30 minutes)
- thirty_min_ago = now - timedelta(minutes=30)
+ # Current notifications: showing now, plus anything whose type asks to keep
+ # ended cards up for a while (gracewindowminutes, 0 by default - an end time
+ # means the card leaves the board then). The window is per type, so the SQL
+ # only widens to the largest configured tail and each row is then held to
+ # its own; that keeps this one portable query instead of a per-type interval
+ # expression, and with every type at 0 it collapses to "still showing".
+ widest_grace = db.session.query(
+ db.func.max(NotificationType.gracewindowminutes)).scalar() or 0
+ widest_grace_start = now - timedelta(minutes=int(widest_grace))
current_query = base_query.filter(
db.or_(
# Active and currently showing
@@ -708,16 +731,23 @@ def get_shopfloor_notifications():
db.or_(Notification.starttime.is_(None), Notification.starttime <= now),
db.or_(Notification.endtime.is_(None), Notification.endtime >= now)
),
- # Recently ended (within 30 min) - show as resolved
+ # Ended, but possibly inside its type's tail - narrowed below
db.and_(
Notification.endtime.isnot(None),
- Notification.endtime >= thirty_min_ago,
+ Notification.endtime >= widest_grace_start,
Notification.endtime < now
)
)
).order_by(Notification.notificationid.desc())
- current_notifications = current_query.all()
+ def _within_grace(n):
+ """True unless the row ended outside its own type's tail."""
+ if n.endtime is None or n.endtime >= now:
+ return True
+ grace = (n.notificationtype.gracewindowminutes or 0) if n.notificationtype else 0
+ return n.endtime >= now - timedelta(minutes=int(grace))
+
+ current_notifications = [n for n in current_query.all() if _within_grace(n)]
# Upcoming notifications (starts within next 5 days)
five_days = now + timedelta(days=5)
@@ -731,7 +761,9 @@ def get_shopfloor_notifications():
def notification_to_shopfloor(n, employee_override=None):
"""Convert notification to shopfloor format."""
- is_resolved = n.endtime and n.endtime < now
+ # bool, not the datetime-or-None the and-chain yields: a card with no
+ # end time was serializing resolved as null.
+ is_resolved = bool(n.endtime and n.endtime < now)
ntype = n.notificationtype
show_photo = bool(ntype and ntype.showemployeephoto)
diff --git a/plugins/notifications/frontend/views/NotificationTypesList.vue b/plugins/notifications/frontend/views/NotificationTypesList.vue
index 6642158..1303047 100644
--- a/plugins/notifications/frontend/views/NotificationTypesList.vue
+++ b/plugins/notifications/frontend/views/NotificationTypesList.vue
@@ -87,6 +87,18 @@
+
+