Bound a panel credential to its own widgets, and let one screen be re-paired

Three things a paired wall panel needed.

The scope check now walks the panel's widgets instead of allowing the
`/messages/` prefix wholesale: a screen may publish what its own controls and
querying charts point at, read the history of what its tiles draw, and nothing
else — the catalogue of every message in the installation included. The same
walk that already bounds its socket, so both surfaces agree.

Pending pairing codes moved out of the per-process dictionary into Redis, keyed
per code with the code's own TTL and indexed in a zset so the fifty-code cap
means the same thing to every worker. Without a Redis there is one process by
definition, and the dictionary stays.

And a per-panel nonce in the token, bumped by `POST /panels/{id}/unpair`: that
refuses the screen hanging there without touching the panel, its dashboards or
their arrangement. A save cannot write the nonce back, so a stale client cannot
undo a revocation. Only for a credential this installation signed — one the
portal minted carries no nonce and is revoked at the hub.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tULRZJUkZsw7rMJ3h4xvu
This commit is contained in:
2026-08-22 11:57:37 +02:00
co-authored by Claude Opus 5
parent dbeb49f5d3
commit 5369ccb68e
7 changed files with 504 additions and 67 deletions
+7 -1
View File
@@ -174,7 +174,7 @@ def decode_worker_token(token: str) -> dict[str, Any]:
def create_panel_token(
panel: str, user_id: uuid.UUID | str, expires_delta: timedelta
panel: str, user_id: uuid.UUID | str, expires_delta: timedelta, nonce: int = 0
) -> str:
"""The credential a paired wall panel holds.
@@ -185,6 +185,11 @@ def create_panel_token(
``fluksio.api.deps`` lets it reach only that panel's dashboards and the message
endpoints its widgets need.
``pnc`` is the panel's nonce at the moment of pairing, and the filter
refuses a credential naming any other. Bumping the panel's nonce is
therefore how one screen is re-paired without deleting the panel out from
under its dashboards.
Long-lived on purpose: a wall tablet is set up once and left running, and
it has no keyboard to log in again with.
"""
@@ -193,6 +198,7 @@ def create_panel_token(
"sub": str(user_id),
"aud": PANEL_AUDIENCE,
"panel": panel,
"pnc": nonce,
"iat": now,
"exp": now + expires_delta,
}