Add button for home screen add instructions

This commit is contained in:
Ettore
2026-05-22 23:50:05 +02:00
parent eeea8dfad8
commit 6f90bd2891
6 changed files with 69 additions and 33 deletions

View File

@@ -6,7 +6,8 @@ A web-based gate access management and control system. Authorized users can remo
## Features ## Features
- **Keypass authentication** — users authenticate with an access code; each keypass can have a per-gate allowlist and an optional expiration date - **Keypass authentication** — users authenticate with an access code; each keypass can have a per-gate allowlist, an optional expiration date, and an optional time/day-of-week schedule
- **Keypass schedules** — restrict a keypass to specific days of the week and/or a time window (e.g. MondayFriday 08:0018:00, server local time)
- **Remote gate control** — integrates with [AVConnect](https://www.avconnect.it) and [Shelly Cloud](https://shelly.cloud) to trigger gate macros/relays - **Remote gate control** — integrates with [AVConnect](https://www.avconnect.it) and [Shelly Cloud](https://shelly.cloud) to trigger gate macros/relays
- **Gate icons** — each gate can be assigned any UTF-8 character or emoji as its icon, displayed on the user app button and keypass selector - **Gate icons** — each gate can be assigned any UTF-8 character or emoji as its icon, displayed on the user app button and keypass selector
- **Role-based admin panel** — two roles (`admin`, `manager`) with different permission levels - **Role-based admin panel** — two roles (`admin`, `manager`) with different permission levels
@@ -93,7 +94,7 @@ data/
|---|---|---| |---|---|---|
| GET | `/api/admin/keypasses` | List all keypasses | | GET | `/api/admin/keypasses` | List all keypasses |
| POST | `/api/admin/keypasses` | Create a keypass | | POST | `/api/admin/keypasses` | Create a keypass |
| PATCH | `/api/admin/keypasses/{kp_id}` | Update a keypass | | PATCH | `/api/admin/keypasses/{kp_id}` | Update a keypass (description, expiry, gates, schedule) |
| DELETE | `/api/admin/keypasses/{kp_id}` | Revoke a keypass | | DELETE | `/api/admin/keypasses/{kp_id}` | Revoke a keypass |
| GET | `/api/admin/keypasses/{kp_id}/qr` | Download QR code PNG for a keypass | | GET | `/api/admin/keypasses/{kp_id}/qr` | Download QR code PNG for a keypass |
@@ -240,6 +241,23 @@ The Shelly server URI and auth key are stored encrypted. Configure them under **
**Mock mode** — when enabled via the admin dashboard, gate open requests always succeed without contacting any external API. Useful for testing. **Mock mode** — when enabled via the admin dashboard, gate open requests always succeed without contacting any external API. Useful for testing.
## Keypass Schedules
In addition to an expiry date and a per-gate allowlist, each keypass can be restricted to a configurable time window:
- **Days of the week** — select any combination of MonSun. If no day is checked the restriction applies on any day.
- **Time window** — a `From` / `To` time in 24-hour format (server local time). Both values must be provided together.
When a keypass with a schedule is used outside its allowed window the API returns **HTTP 403** (`Keypass is not valid today` / `Keypass is not valid at this time`) and the gate will not open. The restriction is enforced in `require_keypass` in `src/core/dependencies.py`.
The schedule is stored as a JSON object in the `schedule` column of the `keypasses` table:
```json
{ "days": [0, 1, 2, 3, 4], "time_start": "08:00", "time_end": "18:00" }
```
`days` uses Python's `weekday()` convention: 0 = Monday, 6 = Sunday. Any absent key is treated as unrestricted (e.g. omitting `days` means any day is allowed).
## Keypass QR Codes ## Keypass QR Codes
Each active keypass has a **QR** button in the admin panel. Clicking it generates a PNG QR code that encodes the URL: Each active keypass has a **QR** button in the admin panel. Clicking it generates a PNG QR code that encodes the URL:

View File

@@ -103,8 +103,11 @@ async def _security_headers(request: Request, call_next) -> Response:
response.headers["X-Frame-Options"] = "DENY" response.headers["X-Frame-Options"] = "DENY"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Content-Security-Policy"] = ( response.headers["Content-Security-Policy"] = (
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" "default-src 'self';"
" img-src 'self' data: blob: https://*.tile.openstreetmap.org;" " script-src 'self' https://cdn.jsdelivr.net;"
" style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com;"
" font-src 'self' https://fonts.gstatic.com;"
" img-src 'self' data: blob: https://*.tile.openstreetmap.org https://cdn.jsdelivr.net;"
" connect-src 'self' https://*.tile.openstreetmap.org" " connect-src 'self' https://*.tile.openstreetmap.org"
) )
return response return response

View File

@@ -342,33 +342,43 @@ if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js").catch(() => {}); navigator.serviceWorker.register("/sw.js").catch(() => {});
} }
// ── PWA install banner ──────────────────────────────────────────────────────── // ── Add to Home Screen ────────────────────────────────────────────────────────
const _isInstalled = window.matchMedia("(display-mode: standalone)").matches || !!navigator.standalone;
const _installAppBtn = document.getElementById("install-app-btn");
const _installBanner = document.getElementById("install-banner");
const INSTALL_DISMISSED_KEY = "lg_install_dismissed"; const INSTALL_DISMISSED_KEY = "lg_install_dismissed";
let _deferredInstallPrompt = null;
window.addEventListener("beforeinstallprompt", (e) => { if (_isInstalled) {
e.preventDefault(); // Already installed — hide both
if (sessionStorage.getItem(INSTALL_DISMISSED_KEY)) return; _installAppBtn.style.display = "none";
_deferredInstallPrompt = e; } else if (typeof window.AddToHomeScreen === "function") {
document.getElementById("install-banner").classList.remove("hidden"); const _addToHomeScreenInstance = window.AddToHomeScreen({
}); appName: "Lagomare Gates",
appIconUrl: "/static/images/mobile_icon.png",
assetUrl: "https://cdn.jsdelivr.net/npm/pwa-add-to-homescreen@4/dist/assets/img/",
maxModalDisplayCount: -1,
displayOptions: { showMobile: true, showDesktop: true },
allowClose: true,
showArrow: true,
});
document.getElementById("install-btn").addEventListener("click", async () => { // Header button — always triggers the library UI
const banner = document.getElementById("install-banner"); _installAppBtn.addEventListener("click", () => {
banner.classList.add("hidden"); _addToHomeScreenInstance.show("en");
if (!_deferredInstallPrompt) return; });
_deferredInstallPrompt.prompt();
await _deferredInstallPrompt.userChoice;
_deferredInstallPrompt = null;
});
document.getElementById("install-dismiss").addEventListener("click", () => { // Auto-show banner once per session
document.getElementById("install-banner").classList.add("hidden"); if (!sessionStorage.getItem(INSTALL_DISMISSED_KEY)) {
_installBanner.classList.remove("hidden");
}
document.getElementById("install-btn").addEventListener("click", () => {
_installBanner.classList.add("hidden");
_addToHomeScreenInstance.show("en");
});
document.getElementById("install-dismiss").addEventListener("click", () => {
_installBanner.classList.add("hidden");
sessionStorage.setItem(INSTALL_DISMISSED_KEY, "1"); sessionStorage.setItem(INSTALL_DISMISSED_KEY, "1");
_deferredInstallPrompt = null; });
}); }
window.addEventListener("appinstalled", () => {
document.getElementById("install-banner").classList.add("hidden");
_deferredInstallPrompt = null;
});

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M0 0h24v24H0V0z" fill="none"/><path d="M18 1.01L8 1c-1.1 0-2 .9-2 2v3h2V5h10v14H8v-1H6v3c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM10 15h2V8H5v2h3.59L3 15.59 4.41 17 10 11.41z"/></svg>

After

Width:  |  Height:  |  Size: 329 B

View File

@@ -12,6 +12,7 @@
<link rel="apple-touch-icon" href="/static/images/mobile_icon.png" /> <link rel="apple-touch-icon" href="/static/images/mobile_icon.png" />
<link rel="stylesheet" href="/static/style.css" /> <link rel="stylesheet" href="/static/style.css" />
<link rel="stylesheet" href="/static/leaflet.css" /> <link rel="stylesheet" href="/static/leaflet.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pwa-add-to-homescreen@4/dist/add-to-homescreen.min.css" />
<style> <style>
/* ── Login view ──────────────────────────────────────────────────────── */ /* ── Login view ──────────────────────────────────────────────────────── */
@@ -148,6 +149,9 @@
<div class="app-header h2">Lagomare Gates</div> <div class="app-header h2">Lagomare Gates</div>
</div> </div>
<div style="display:flex;align-items:center;gap:.5rem"> <div style="display:flex;align-items:center;gap:.5rem">
<button id="install-app-btn" class="btn btn-primary" style="padding:.4rem .6rem;line-height:0" aria-label="Add to Home Screen" title="Add to Home Screen">
<img src="/static/images/add_to_home_screen.svg" alt="" style="width:22px;height:22px;display:block;filter:invert(1)" />
</button>
<button id="map-btn" class="btn btn-ghost hidden" style="padding:.4rem .6rem;line-height:0" aria-label="Show map"> <button id="map-btn" class="btn btn-ghost hidden" style="padding:.4rem .6rem;line-height:0" aria-label="Show map">
<img src="/static/images/map.svg" alt="Map" style="width:22px;height:22px;filter:invert(1)" /> <img src="/static/images/map.svg" alt="Map" style="width:22px;height:22px;filter:invert(1)" />
</button> </button>
@@ -184,7 +188,7 @@
</div> </div>
</div> </div>
<!-- ── PWA install banner ──────────────────────────────────────────────── --> <!-- ── PWA install banner ─────────────────────────────────────────────────────── -->
<div id="install-banner" class="install-banner hidden" role="banner" aria-label="Install app"> <div id="install-banner" class="install-banner hidden" role="banner" aria-label="Install app">
<div class="install-banner-body"> <div class="install-banner-body">
<img src="/static/images/logo.svg" alt="" class="install-banner-icon" /> <img src="/static/images/logo.svg" alt="" class="install-banner-icon" />
@@ -200,6 +204,7 @@
</div> </div>
<script src="/static/leaflet.js"></script> <script src="/static/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pwa-add-to-homescreen@4/dist/add-to-homescreen.min.js"></script>
<script src="/static/app.js"></script> <script src="/static/app.js"></script>
</body> </body>
</html> </html>

View File

@@ -188,8 +188,7 @@ tr:last-child td { border-bottom: none; }
/* ── Error text ────────────────────────────────────────────────────────────── */ /* ── Error text ────────────────────────────────────────────────────────────── */
.error-msg { color: var(--red); font-size: .85rem; margin-top: .5rem; } .error-msg { color: var(--red); font-size: .85rem; margin-top: .5rem; }
/* ── PWA install banner ──────────────────────────────────────────────── */
/* ── PWA install banner ────────────────────────────────────────────────────── */
.install-banner { .install-banner {
position: fixed; position: fixed;
bottom: 1rem; bottom: 1rem;