displays: single display type with IP-driven role (dashboard/lobby/kiosk)

One 'display' image resolves what it shows from its own IP, like the existing
visitor-location BU mapping. Extend DashboardDefault with displayrole
(dashboard|lobby|partskiosk; migration 7d28, businessunitid now nullable since
only the dashboard role needs one) + a role->path map. New unauthenticated
GET /api/dashboarddefaults/display-role returns {role, path, businessunitid}
for the caller IP. Settings UI gains a Display selector, showing the business
unit only for the dashboard role.
This commit is contained in:
cproudlock
2026-07-21 09:49:14 -04:00
parent b05fa33278
commit 60e2947fc7
5 changed files with 218 additions and 21 deletions

View File

@@ -0,0 +1,63 @@
"""Add dashboarddefaults.displayrole; make businessunitid nullable
One "display" PC image resolves its role (dashboard / lobby / partskiosk) from
its IP. Existing rows are dashboard mappings, so displayrole defaults to
'dashboard'. Lobby / kiosk roles need no business unit, so businessunitid
becomes nullable.
Idempotent guards on column presence / nullability.
Revision ID: 7d28_dashboarddefault_displayrole
Revises: 7d27_roles_color
Create Date: 2026-07-21
"""
from alembic import op
import sqlalchemy as sa
revision = '7d28_dashboarddefault_displayrole'
down_revision = '7d27_roles_color'
branch_labels = None
depends_on = None
def _cols(insp):
return {c['name']: c for c in insp.get_columns('dashboarddefaults')}
def upgrade():
bind = op.get_bind()
insp = sa.inspect(bind)
if 'dashboarddefaults' not in insp.get_table_names():
return
cols = _cols(insp)
if 'displayrole' not in cols:
op.add_column('dashboarddefaults', sa.Column(
'displayrole', sa.String(length=20),
nullable=False, server_default='dashboard'))
if 'businessunitid' in cols and not cols['businessunitid']['nullable']:
op.alter_column('dashboarddefaults', 'businessunitid',
existing_type=sa.Integer(), nullable=True)
def downgrade():
bind = op.get_bind()
insp = sa.inspect(bind)
if 'dashboarddefaults' not in insp.get_table_names():
return
cols = _cols(insp)
# Only restore NOT NULL if no rows would violate it.
if 'businessunitid' in cols and cols['businessunitid']['nullable']:
nulls = bind.execute(sa.text(
'SELECT COUNT(*) FROM dashboarddefaults '
'WHERE businessunitid IS NULL')).scalar()
if not nulls:
op.alter_column('dashboarddefaults', 'businessunitid',
existing_type=sa.Integer(), nullable=False)
if 'displayrole' in cols:
op.drop_column('dashboarddefaults', 'displayrole')