Files
location_managemet/app/templates/layouts/builder.html
ske087 90cbf4e1f0 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)
2026-02-27 13:34:44 +02:00

334 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Builder {{ layout.name }}{% endblock %}
{% block content %}
<style>
/* Make builder fill the full content area with no extra padding */
#page-content { padding: 0 !important; overflow: hidden; }
/* ── Builder shell ───────────────────────────────────────────────────────── */
#lb-wrap {
display: flex;
height: calc(100vh);
background: #0d1117;
font-size: .85rem;
}
/* ── Left sidebar ────────────────────────────────────────────────────────── */
#lb-sidebar {
width: 230px;
min-width: 230px;
background: #161b22;
border-right: 1px solid #30363d;
display: flex;
flex-direction: column;
overflow: hidden;
}
#lb-sidebar-header {
padding: .75rem 1rem;
font-weight: 600;
border-bottom: 1px solid #30363d;
display: flex;
align-items: center;
gap: .5rem;
white-space: nowrap;
overflow: hidden;
}
#lb-sidebar-body {
flex: 1;
overflow-y: auto;
padding: .5rem;
}
/* ── Tool palette ────────────────────────────────────────────────────────── */
.lb-tool-btn {
display: flex;
align-items: center;
gap: .5rem;
width: 100%;
padding: .45rem .7rem;
border: 1px solid transparent;
border-radius: .4rem;
background: transparent;
color: #8b949e;
cursor: pointer;
text-align: left;
transition: background .15s, color .15s, border-color .15s;
margin-bottom: 2px;
font-size: .82rem;
}
.lb-tool-btn:hover { background: #1f2937; color: #e6edf3; }
.lb-tool-btn.active {
background: rgba(79,140,205,.15);
border-color: #4f8ccd;
color: #4f8ccd;
}
.lb-section-label {
font-size: .7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .06em;
color: #484f58;
padding: .4rem .3rem .2rem;
margin-top: .25rem;
}
/* ── Device chips (draggable) ────────────────────────────────────────────── */
.lb-board-group { margin-bottom: .5rem; }
.lb-board-name {
font-size: .72rem;
font-weight: 600;
color: #58a6ff;
padding: .2rem .3rem;
text-transform: uppercase;
letter-spacing: .05em;
display: flex;
align-items: center;
gap: .3rem;
}
.lb-device-chip {
display: flex;
align-items: center;
gap: .45rem;
padding: .35rem .6rem;
margin-bottom: 3px;
border-radius: .4rem;
border: 1px solid #30363d;
background: #1c2129;
color: #c9d1d9;
cursor: grab;
user-select: none;
font-size: .8rem;
transition: border-color .15s, background .15s;
}
.lb-device-chip:hover { border-color: #58a6ff; background: #1f2937; }
.lb-device-chip:active { cursor: grabbing; }
.lb-device-chip .chip-dot {
width: 8px; height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
/* ── Canvas area ─────────────────────────────────────────────────────────── */
#lb-canvas-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ── Toolbar ─────────────────────────────────────────────────────────────── */
#lb-toolbar {
background: #161b22;
border-bottom: 1px solid #30363d;
padding: .4rem .75rem;
display: flex;
align-items: center;
gap: .5rem;
flex-wrap: wrap;
}
#lb-toolbar .tb-sep { width: 1px; height: 24px; background: #30363d; margin: 0 .25rem; }
#lb-toolbar .tb-label { font-size: .72rem; color: #484f58; font-weight: 600; text-transform: uppercase; }
.tb-btn {
padding: .25rem .55rem;
border-radius: .35rem;
border: 1px solid #30363d;
background: #1c2129;
color: #8b949e;
cursor: pointer;
font-size: .8rem;
display: flex;
align-items: center;
gap: .3rem;
transition: background .15s, color .15s, border-color .15s;
white-space: nowrap;
}
.tb-btn:hover { background: #21262d; color: #e6edf3; }
.tb-btn.active { background: rgba(79,140,205,.18); border-color: #4f8ccd; color: #4f8ccd; }
.tb-btn.tb-mode { font-weight: 600; }
#lb-save-status {
font-size: .75rem;
color: #484f58;
margin-left: auto;
white-space: nowrap;
}
#lb-save-status.saving { color: #f0883e; }
#lb-save-status.saved { color: #3fb950; }
/* ── Konva stage container ───────────────────────────────────────────────── */
#lb-stage { flex: 1; overflow: hidden; position: relative; }
#konva-container { width: 100%; height: 100%; }
#konva-container.drag-over { outline: 2px dashed #4f8ccd; outline-offset: -2px; }
/* Ghost that follows cursor during drag-over canvas */
#lb-drag-ghost {
position: fixed;
pointer-events: none;
background: #1f2937;
border: 1px solid #58a6ff;
border-radius: .4rem;
padding: .3rem .6rem;
font-size: .8rem;
color: #58a6ff;
display: none;
z-index: 9999;
}
</style>
<div id="lb-wrap">
<!-- ══ Sidebar ══════════════════════════════════════════════════════════════ -->
<div id="lb-sidebar">
<div id="lb-sidebar-header">
<i class="bi bi-map text-info"></i>
<span class="text-truncate" title="{{ layout.name }}">{{ layout.name }}</span>
</div>
<div id="lb-sidebar-body">
<!-- ── Structure tools (shown in structure mode) ──────────────────── -->
<div id="lb-structure-palette">
<div class="lb-section-label">Draw tool</div>
<button class="lb-tool-btn active" data-tool="select">
<i class="bi bi-cursor"></i> Select / Move
</button>
<button class="lb-tool-btn" data-tool="pan">
<i class="bi bi-arrows-move"></i> Pan
</button>
<div class="lb-section-label" style="margin-top:.5rem">Structure shapes</div>
<button class="lb-tool-btn" data-tool="wall" style="border-left:3px solid #6e7681">
<i class="bi bi-square-fill" style="color:#6e7681"></i> Wall
<span class="ms-auto text-secondary" style="font-size:.7rem">drag</span>
</button>
<button class="lb-tool-btn" data-tool="room" style="border-left:3px solid #4f8ccd">
<i class="bi bi-square" style="color:#4f8ccd"></i> Room
<span class="ms-auto text-secondary" style="font-size:.7rem">drag</span>
</button>
<button class="lb-tool-btn" data-tool="door" style="border-left:3px solid #f0883e">
<i class="bi bi-slash-circle" style="color:#f0883e"></i> Door
<span class="ms-auto text-secondary" style="font-size:.7rem">drag</span>
</button>
<button class="lb-tool-btn" data-tool="window" style="border-left:3px solid #79c0ff">
<i class="bi bi-grip-horizontal" style="color:#79c0ff"></i> Window
<span class="ms-auto text-secondary" style="font-size:.7rem">drag</span>
</button>
<button class="lb-tool-btn" data-tool="fence" style="border-left:3px solid #8b5cf6">
<i class="bi bi-border-all" style="color:#8b5cf6"></i> Fence
<span class="ms-auto text-secondary" style="font-size:.7rem">drag</span>
</button>
<button class="lb-tool-btn" data-tool="text" style="border-left:3px solid #f0883e">
<i class="bi bi-fonts" style="color:#f0883e"></i> Text label
<span class="ms-auto text-secondary" style="font-size:.7rem">click</span>
</button>
<div class="lb-section-label" style="margin-top:.5rem">Canvas</div>
<button class="lb-tool-btn" id="btn-clear-structure" onclick="LB.clearStructure()">
<i class="bi bi-eraser text-danger"></i> Clear structure
</button>
</div>
<!-- ── Device palette (shown in devices mode) ─────────────────────── -->
<div id="lb-devices-palette" style="display:none">
<!-- Selected device panel (visible when a device is clicked) -->
<div id="lb-device-selected" style="display:none">
<div class="lb-section-label">Selected</div>
<div id="lb-sel-card" class="p-2 rounded mb-2" style="background:#1c2129;border:1px solid #4f8ccd">
<div id="lb-sel-name" style="color:#c9d1d9;font-weight:600;font-size:.84rem"></div>
<div id="lb-sel-board" class="text-secondary" style="font-size:.72rem"></div>
<div id="lb-sel-type" style="font-size:.7rem;color:#f0883e;margin-top:.15rem"></div>
</div>
<div class="text-secondary mb-2" style="font-size:.75rem;padding:.1rem .3rem">
<i class="bi bi-arrows-move me-1"></i>Drag to reposition
</div>
<button class="lb-tool-btn" onclick="LB.removeSelectedDevice()">
<i class="bi bi-trash text-danger"></i> Remove from canvas
</button>
<button class="lb-tool-btn" onclick="LB.deselectDevice()">
<i class="bi bi-x-circle" style="color:#8b949e"></i> Deselect
</button>
<hr style="border-color:#30363d;margin:.4rem 0">
</div>
<div class="lb-section-label" id="lb-drag-hint">Drag onto canvas</div>
<div id="lb-device-list">
<!-- populated by JS from LB_CONFIG.boards -->
</div>
</div>
<!-- ── View mode info (shown in view mode) ─────────────────────────── -->
<div id="lb-view-info" style="display:none; padding:.5rem; color:#8b949e; font-size:.8rem; line-height:1.5">
<i class="bi bi-info-circle me-1"></i>
<strong>Live view</strong><br>
Click relay / switch devices to toggle them.<br>
State updates in real-time via WebSocket.
</div>
</div><!-- sidebar-body -->
</div><!-- sidebar -->
<!-- ══ Canvas area ══════════════════════════════════════════════════════════ -->
<div id="lb-canvas-area">
<!-- Toolbar -->
<div id="lb-toolbar">
<span class="tb-label">Mode</span>
<button class="tb-btn tb-mode active" data-mode="structure">
<i class="bi bi-pencil-square"></i> Structure
</button>
<button class="tb-btn tb-mode" data-mode="devices">
<i class="bi bi-grid-3x3-gap"></i> Devices
</button>
<button class="tb-btn tb-mode" data-mode="view">
<i class="bi bi-eye"></i> Live View
</button>
<div class="tb-sep"></div>
<span class="tb-label">Zoom</span>
<button class="tb-btn" onclick="LB.zoom(1.2)"><i class="bi bi-zoom-in"></i></button>
<button class="tb-btn" onclick="LB.zoom(1/1.2)"><i class="bi bi-zoom-out"></i></button>
<button class="tb-btn" onclick="LB.resetView()"><i class="bi bi-fullscreen"></i></button>
<div class="tb-sep"></div>
<button class="tb-btn" onclick="LB.clearDevices()" id="btn-clear-devices" title="Remove all devices from canvas">
<i class="bi bi-trash text-warning"></i> Clear devices
</button>
<div class="tb-sep"></div>
<button class="tb-btn" onclick="LB.save()" id="btn-save">
<i class="bi bi-floppy"></i> Save
</button>
<span id="lb-save-status">Unsaved</span>
</div>
<!-- Konva container -->
<div id="lb-stage">
<div id="konva-container"></div>
</div>
</div><!-- canvas-area -->
</div><!-- lb-wrap -->
<!-- Drag ghost tooltip -->
<div id="lb-drag-ghost"></div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='vendor/konva/konva.min.js') }}"></script>
<script>
// ── Server-injected config ────────────────────────────────────────────────────
window.LB_CONFIG = {
layoutId: {{ layout.id }},
saveUrl: "{{ url_for('layouts.save_layout', layout_id=layout.id) }}",
canvasJson: {{ (layout.canvas_json or 'null') | safe }},
boards: {{ boards_data | tojson | safe }},
toggleBase: "/boards/{boardId}/relay/{relayNum}/toggle",
};
</script>
<script src="{{ url_for('static', filename='js/layout_builder.js') }}"></script>
{% endblock %}