diff --git a/frontend/src/components/StateCard.ts b/frontend/src/components/StateCard.ts
index 90fdfff9..e076ad2a 100644
--- a/frontend/src/components/StateCard.ts
+++ b/frontend/src/components/StateCard.ts
@@ -38,8 +38,27 @@ export class StateCard extends LitElement {
border-color: hsl(185 80% 50% / 0.4);
}
- .card { cursor: pointer; }
+ .card { cursor: pointer; position: relative; }
.card:focus-visible { outline: 2px solid var(--hc-primary, #19d4e5); outline-offset: 2px; }
+ button.delete {
+ position: absolute;
+ top: 0.5rem; right: 0.5rem;
+ width: 24px; height: 24px;
+ border: none;
+ border-radius: 4px;
+ background: transparent;
+ color: var(--hc-text-muted, #7b899d);
+ cursor: pointer;
+ font-size: 16px;
+ line-height: 1;
+ padding: 0;
+ opacity: 0;
+ transition: opacity 150ms, background 150ms, color 150ms;
+ }
+ .card:hover button.delete,
+ .card:focus-within button.delete { opacity: 1; }
+ button.delete:hover { background: hsl(0 50% 30%); color: hsl(0 80% 88%); }
+ button.delete:focus-visible { opacity: 1; outline: 2px solid hsl(0 60% 55%); }
.header {
display: flex;
@@ -121,6 +140,11 @@ export class StateCard extends LitElement {
@click=${this._onClick}
@keydown=${(e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this._onClick(); } }}
aria-label="Edit ${entity_id}">
+ { e.stopPropagation(); }}
+ aria-label="Delete ${entity_id}"
+ title="Delete ${entity_id}">×
`
: html` this._openEdit(e as CustomEvent)}>
+ @hc-state-card-click=${(e: Event) => this._openEdit(e as CustomEvent)}
+ @hc-state-card-delete=${(e: Event) => this._openDeleteConfirm(e as CustomEvent)}>
${this.states.map(
(s) => html` `
)}
`}
+ (this.deletingState = null)}>
+
+ Permanently remove
+ ${this.deletingState?.entity_id ?? ''}
+ from the state machine?
+
+
+ This is immediate. To restore, re-create the entity via "+ Add entity".
+
+
+ (this.deletingState = null)}>Cancel
+ Delete
+
+
{ this.modalOpen = false; this.editingState = null; }}>
diff --git a/v2/crates/homecore-api/src/app.rs b/v2/crates/homecore-api/src/app.rs
index 37a48167..35b7b969 100644
--- a/v2/crates/homecore-api/src/app.rs
+++ b/v2/crates/homecore-api/src/app.rs
@@ -28,7 +28,12 @@ pub fn router(state: SharedState) -> Router {
.route("/api/", get(rest::api_root))
.route("/api/config", get(rest::get_config))
.route("/api/states", get(rest::get_states))
- .route("/api/states/:entity_id", get(rest::get_state).post(rest::set_state))
+ .route(
+ "/api/states/:entity_id",
+ get(rest::get_state)
+ .post(rest::set_state)
+ .delete(rest::delete_state),
+ )
.route("/api/services", get(rest::get_services))
.route("/api/services/:domain/:service", post(rest::call_service))
.route("/api/websocket", get(ws::websocket_handler))
diff --git a/v2/crates/homecore-api/src/rest.rs b/v2/crates/homecore-api/src/rest.rs
index 9ca6dc28..b2965fd3 100644
--- a/v2/crates/homecore-api/src/rest.rs
+++ b/v2/crates/homecore-api/src/rest.rs
@@ -92,6 +92,21 @@ pub struct SetStateRequest {
pub attributes: serde_json::Value,
}
+/// DELETE /api/states/:entity_id — remove an entity from the state
+/// machine. Idempotent: returns 204 whether or not the entity existed,
+/// matching HA's removal semantics. 4xx only for malformed entity_id or
+/// auth failure.
+pub async fn delete_state(
+ headers: HeaderMap,
+ State(s): State,
+ Path(entity_id): Path,
+) -> ApiResult {
+ let _ = BearerAuth::from_headers(&headers, s.tokens()).await?;
+ let id = EntityId::parse(entity_id).map_err(|e| ApiError::BadRequest(e.to_string()))?;
+ s.homecore().states().remove(&id);
+ Ok(StatusCode::NO_CONTENT)
+}
+
pub async fn set_state(
headers: HeaderMap,
State(s): State,