Files
shopdb-flask/plugins/notifications/migrations/versions/0003_gracewindowminutes.py
cproudlock fef5e28952 Let an end time mean the card leaves the board
The shopfloor feed kept every ended notification up for a hardcoded 30 minutes,
flagged resolved. A card with an 8:00 end time was still on the board at 8:29,
which reads as an expiry that did not work - and in the carousel, grid and
banner sections it read that way with no visual sign at all, since only the
standard cards render the resolved state.

The tail is now notificationtypes.gracewindowminutes, set per type on the
Notification Types page and defaulting to 0, so an end time means what it says.
A type whose cards are worth acknowledging after they clear - an incident, say
- opts into a tail, and only that type's cards get one.

The feed widens its query to the largest configured tail and then holds each
row to its own type's window. That keeps one portable query rather than a
per-type interval expression in SQL, and with every type at 0 it collapses to
"still showing".

Also fixes resolved serializing as null rather than false for a card with no
end time, which the and-chain produced.
2026-08-07 09:22:34 -04:00

48 lines
1.4 KiB
Python

"""Add notificationtypes.gracewindowminutes (post-expiry tail on the board).
The shopfloor feed kept every ended notification on the board for a hardcoded
30 minutes, flagged resolved. A card with an 8:00 end time therefore sat there
until 8:30, and in the carousel/grid/banner sections it did so with no visual
sign it had ended at all. The tail is now per type and defaults to 0, so an end
time means what it says; a type that wants an acknowledged tail opts in.
Idempotent; downgrade drops the column.
Revision ID: notifications0003grace
Revises: notifications0002buidx
"""
from alembic import op
import sqlalchemy as sa
revision = 'notifications0003grace'
down_revision = 'notifications0002buidx'
branch_labels = None
depends_on = None
_TABLE = 'notificationtypes'
_COLUMN = 'gracewindowminutes'
def _column_names(insp, table):
return {c['name'] for c in insp.get_columns(table)}
def upgrade():
bind = op.get_bind()
insp = sa.inspect(bind)
if _TABLE not in insp.get_table_names():
return
if _COLUMN not in _column_names(insp, _TABLE):
op.add_column(_TABLE, sa.Column(_COLUMN, sa.Integer(), nullable=False,
server_default='0'))
def downgrade():
bind = op.get_bind()
insp = sa.inspect(bind)
if _TABLE not in insp.get_table_names():
return
if _COLUMN in _column_names(insp, _TABLE):
op.drop_column(_TABLE, _COLUMN)