Add Layouts module with Konva.js builder; smart offline polling; UI improvements

- Move board cards from dashboard to top of boards list page
- Fix Werkzeug duplicate polling (WERKZEUG_RUN_MAIN guard)
- Smart offline polling: fast loop for online boards, slow recheck for offline
- Add manual ping endpoint POST /api/boards/<id>/ping
- Add spin animation CSS for ping button

Layouts module (new):
- app/models/layout.py: Layout model (canvas_json, thumbnail_b64)
- app/routes/layouts.py: 5 routes (list, create, builder, save, delete)
- app/templates/layouts/: list and builder templates
- app/static/js/layout_builder.js: full Konva.js builder engine
- app/static/vendor/konva/: vendored Konva.js 9
- Structure mode: wall, room, door, window, fence, text shapes
- Devices mode: drag relay/input/Sonoff channels onto canvas
- Live view mode: click relays/Sonoff to toggle, socket.io state updates
- Device selection: click to select, remove individual device, Delete key
- Fix door/Arc size persistence across save/reload (outerRadius, scaleX/Y)
- Fix Sonoff devices missing from palette (add makeSonoffChip function)
This commit is contained in:
ske087
2026-02-27 13:34:44 +02:00
parent 30806560a6
commit 90cbf4e1f0
15 changed files with 2006 additions and 177 deletions

View File

@@ -84,14 +84,14 @@ def poll_board(app, board_id: int) -> None:
"is_online": board.is_online,
"relay_states": board.relay_states,
"input_states": board.input_states,
"last_seen": board.last_seen.isoformat() if board.last_seen else None,
})
def poll_all_boards(app) -> None:
"""Poll every registered board in parallel."""
with app.app_context():
board_ids = [r[0] for r in db.session.query(Board.id).all()]
def _poll_boards_by_ids(app, board_ids: list) -> None:
"""Spawn one thread per board_id and poll them in parallel."""
if not board_ids:
return
threads = [
threading.Thread(target=poll_board, args=(app, bid), daemon=True)
for bid in board_ids
@@ -99,7 +99,30 @@ def poll_all_boards(app) -> None:
for t in threads:
t.start()
for t in threads:
t.join(timeout=4)
t.join(timeout=6)
def poll_online_boards(app) -> None:
"""Poll only boards currently marked online (fast background loop)."""
with app.app_context():
board_ids = [
r[0] for r in db.session.query(Board.id).filter_by(is_online=True).all()
]
_poll_boards_by_ids(app, board_ids)
def recheck_offline_boards(app) -> None:
"""Single-pass connectivity check for boards marked offline.
Called infrequently (default every 60 s) so we don't flood the network
with timeout requests for devices that are simply powered off.
Also triggered immediately when the user clicks 'Check Status'.
"""
with app.app_context():
board_ids = [
r[0] for r in db.session.query(Board.id).filter_by(is_online=False).all()
]
_poll_boards_by_ids(app, board_ids)
# ── webhook registration ──────────────────────────────────────────────────────