Files
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

106 lines
4.4 KiB
HTML
Raw Permalink 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 %}Layouts Location Management{% endblock %}
{% block content %}
<div class="d-flex align-items-center justify-content-between mb-4">
<h2 class="fw-bold mb-0"><i class="bi bi-map me-2 text-info"></i>Layouts</h2>
{% if current_user.is_admin() %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#newLayoutModal">
<i class="bi bi-plus-circle me-1"></i> New Layout
</button>
{% endif %}
</div>
{% if layouts %}
<div class="row g-3">
{% for layout in layouts %}
<div class="col-sm-6 col-xl-4">
<div class="card border-0 rounded-4 h-100">
<!-- thumbnail or placeholder -->
<div class="rounded-top-4 overflow-hidden d-flex align-items-center justify-content-center bg-dark"
style="height:180px">
{% if layout.thumbnail_b64 %}
<img src="{{ layout.thumbnail_b64 }}" alt="thumbnail"
style="max-width:100%;max-height:100%;object-fit:contain" />
{% else %}
<i class="bi bi-map display-1 text-secondary opacity-25"></i>
{% endif %}
</div>
<div class="card-body">
<h5 class="fw-semibold mb-1">{{ layout.name }}</h5>
{% if layout.description %}
<p class="text-secondary small mb-0">{{ layout.description }}</p>
{% endif %}
</div>
<div class="card-footer bg-transparent d-flex justify-content-between align-items-center">
<span class="text-secondary small">
{% if layout.updated_at %}
Updated {{ layout.updated_at.strftime('%Y-%m-%d %H:%M') }}
{% else %}
Created {{ layout.created_at.strftime('%Y-%m-%d') }}
{% endif %}
</span>
<div class="d-flex gap-1">
<a href="{{ url_for('layouts.builder', layout_id=layout.id) }}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil-square me-1"></i>Edit
</a>
{% if current_user.is_admin() %}
<form method="POST" action="{{ url_for('layouts.delete_layout', layout_id=layout.id) }}"
class="d-inline" onsubmit="return confirm('Delete layout \'{{ layout.name }}\'?')">
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
</form>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5 text-secondary">
<i class="bi bi-map display-2 opacity-25"></i>
<p class="mt-3 fs-5">No layouts yet.</p>
{% if current_user.is_admin() %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#newLayoutModal">
<i class="bi bi-plus-circle me-1"></i> Create your first layout
</button>
{% endif %}
</div>
{% endif %}
<!-- ── New layout modal ───────────────────────────────────────────────────── -->
{% if current_user.is_admin() %}
<div class="modal fade" id="newLayoutModal" tabindex="-1">
<div class="modal-dialog">
<form method="POST" action="{{ url_for('layouts.create_layout') }}">
<div class="modal-content">
<div class="modal-header border-0">
<h5 class="modal-title"><i class="bi bi-map me-2 text-info"></i>New Layout</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="name" required maxlength="128"
placeholder="e.g. Ground Floor, Garden" autofocus />
</div>
<div class="mb-3">
<label class="form-label text-secondary">Description <span class="text-secondary small">(optional)</span></label>
<input type="text" class="form-control" name="description" maxlength="256"
placeholder="Short description" />
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">
<i class="bi bi-arrow-right-circle me-1"></i>Create & Open Builder
</button>
</div>
</div>
</form>
</div>
</div>
{% endif %}
{% endblock %}