Show live values on the mockup, blink a screen to identify it, try an alert, copy and share layouts (0.2.74)

The tiles draw what Home Assistant reports (a new /api/states answers for the tiles on the page); the ···
menu gets Identify (the screen's own show_alert with flash), Copy layout from another screen, Export and
Import as JSON; the Alerts page opens with Try it (/api/alerts/test, one screen or all); the library filters
by room and hides what is placed, with lit icons for what is on; ⌘K searches screens, entities and actions;
the Update badge lists what's new from the changelog (copied into the image) and a running update shows a
progress bar with the ESPHome stage and the last log line.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-18 08:21:37 +02:00
parent ef8e94f6d1
commit 6148dfd211
27 changed files with 818 additions and 79 deletions
+48 -3
View File
@@ -1,11 +1,32 @@
<script setup lang="ts">
// Alerts: the cheatsheet for esphome.<node>_show_alert, built from the inventory.
import { computed, ref } from "vue";
import { computed, reactive, ref } from "vue";
import { versionAtLeast } from "../model/layout";
import { glyph } from "../model/topbar";
import { copyText, go, state } from "../store";
import { canAlert, copyText, go, sendTestAlert, state } from "../store";
const alerts = computed(() => state.inventory.alerts);
// Try it: the same seven fields an automation sends, to one screen or to all of them.
const tryForm = reactive({ screen: "all", title: "Someone is at the door", subtitle: "Door 3, back", icon: "doorbell", color: "orange", button_text: "Coming", timeout: 30, flash: true });
const trying = ref(false);
const tryResult = ref("");
const readyScreens = computed(() => state.inventory.screens.filter((s) => canAlert(s) && s.online));
async function tryAlert() {
trying.value = true;
tryResult.value = "";
try {
const { screen, ...data } = tryForm;
const result = await sendTestAlert(screen, data);
const where = screen === "all" ? `${result.sent} screen${result.sent === 1 ? "" : "s"}` : state.inventory.screens.find((s) => s.id === screen)?.name || "the screen";
tryResult.value = result.sent
? `Sent to ${where}.${result.skipped ? ` ${result.skipped} skipped (offline or old firmware).` : ""}${result.unusable?.length ? ` Left empty: ${result.unusable.join(", ")}.` : ""}`
: "No screen could show it: check that a screen is online with firmware " + (alerts.value?.min_firmware || "0.2.31") + " or newer.";
} catch (e: any) {
tryResult.value = e.message;
} finally {
trying.value = false;
}
}
const icons = computed(() => state.inventory.icons);
const exampleAction = ref("");
const iconQuery = ref("");
@@ -60,7 +81,7 @@ const doorbell = computed(() => alerts.value?.suggested_icons?.find((i: any) =>
const orange = computed(() => alerts.value?.colors?.find((c: any) => c.name === "orange"));
const types: Record<string, string> = { string: "text", int: "number", bool: "on / off" };
function jump(id: string) { document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" }); }
const sections = [["alerts-screens", "Your screens"], ["alerts-howto", "In an automation"], ["alerts-all", "All screens"], ["alerts-fields", "The fields"], ["alerts-icons", "Icons"], ["alerts-colors", "Colors"], ["alerts-behaviour", "Behavior"], ["alerts-events", "Events"], ["alerts-tips", "Tips"]];
const sections = [["alerts-try", "Try it"], ["alerts-screens", "Your screens"], ["alerts-howto", "In an automation"], ["alerts-all", "All screens"], ["alerts-fields", "The fields"], ["alerts-icons", "Icons"], ["alerts-colors", "Colors"], ["alerts-behaviour", "Behavior"], ["alerts-events", "Events"], ["alerts-tips", "Tips"]];
</script>
<template>
@@ -88,6 +109,30 @@ const sections = [["alerts-screens", "Your screens"], ["alerts-howto", "In an au
<button v-for="[id, label] in sections" :key="id" type="button" :data-jump="id" @click="jump(id)">{{ label }}</button>
</nav>
</div>
<section id="alerts-try" class="card">
<h2>Try it</h2>
<p>Send one alert now, with the same fields an automation sends. The screen shows the card and, with blinking on, flashes its backlight.</p>
<form class="try-grid" @submit.prevent="tryAlert">
<div class="field"><label class="f-label" for="try-screen">Screen</label>
<select id="try-screen" v-model="tryForm.screen">
<option value="all">All screens ({{ readyScreens.length }} ready)</option>
<option v-for="screen in state.inventory.screens" :key="screen.id" :value="screen.id" :disabled="!canAlert(screen) || !screen.online">{{ screen.name }}{{ canAlert(screen) && screen.online ? "" : " · not ready" }}</option>
</select></div>
<div class="field"><label class="f-label" for="try-title">Title</label><input id="try-title" v-model="tryForm.title" maxlength="64" /></div>
<div class="field"><label class="f-label" for="try-subtitle">Subtitle</label><input id="try-subtitle" v-model="tryForm.subtitle" maxlength="240" /></div>
<div class="field"><label class="f-label" for="try-icon">Icon</label><input id="try-icon" v-model="tryForm.icon" list="try-icons" placeholder="doorbell" />
<datalist id="try-icons"><option v-for="icon in alerts.suggested_icons" :key="icon.name" :value="icon.name">{{ icon.label || icon.name }}</option></datalist></div>
<div class="field"><label class="f-label" for="try-color">Color</label>
<select id="try-color" v-model="tryForm.color"><option value="">White (default)</option><option v-for="colour in alerts.colors" :key="colour.name" :value="colour.name">{{ colour.label }}</option></select></div>
<div class="field"><label class="f-label" for="try-button">Button text</label><input id="try-button" v-model="tryForm.button_text" maxlength="16" /></div>
<div class="field"><label class="f-label" for="try-timeout">Timeout (seconds, 0 waits for the button)</label><input id="try-timeout" v-model.number="tryForm.timeout" type="number" min="0" max="86400" /></div>
<label class="check field"><input type="checkbox" id="try-flash" v-model="tryForm.flash" /><span>Blink the backlight<small>Four times when the alert arrives.</small></span></label>
<div class="actions field wide">
<button type="submit" class="btn primary" id="try-send" :disabled="trying || !readyScreens.length">{{ trying ? "Sending…" : "Send alert" }}</button>
<span class="status-line" id="try-result" role="status">{{ tryResult }}</span>
</div>
</form>
</section>
<section id="alerts-screens" class="card">
<h2>Your screens</h2>
<p>Every screen has its own action, with the device name in it. From firmware <code id="alerts-min-firmware">{{ alerts.min_firmware }}</code>; update an older screen with its Update button under Screens.</p>