- 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)
106 lines
4.4 KiB
HTML
106 lines
4.4 KiB
HTML
{% 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 %}
|