feat(homecore-server): seed 13 default services across 6 domains on boot

Operators (and the new web UI) saw "No services registered" on every
vanilla boot because nothing in the boot sequence called
`ServiceRegistry::register()`. The Assist pipeline registers intent
handlers — a different surface — but `/api/services` stayed empty
until a plugin or integration loaded.

Adds `seed_default_services()` after `HomeCore::new()`. Each handler
is a `FnHandler` that echoes the call back as a JSON acknowledgement
so the service registry is exercise-able from day one. Integrations
override these by re-registering the same `ServiceName` with a real
handler later.

Seeded set:

  homeassistant: restart, stop, reload_core_config
  light:         turn_on, turn_off, toggle
  switch:        turn_on, turn_off, toggle
  scene:         apply
  automation:    trigger
  homecore:      ping, snapshot_state   (HOMECORE-native)

Boot log now reports:

  Service registry seeded with 13 default service(s)

GET /api/services now returns 6 domains with 13 services total.
The HOMECORE web UI's Services page shows them under proper
domain headings.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv 2026-05-26 14:07:52 -04:00
parent 4253c0e4fc
commit 75f984e515
1 changed files with 56 additions and 1 deletions

View File

@ -25,7 +25,8 @@ use anyhow::Result;
use clap::Parser; use clap::Parser;
use tracing::{info, warn}; use tracing::{info, warn};
use homecore::HomeCore; use homecore::{HomeCore, ServiceCall, ServiceError, ServiceName};
use homecore::service::FnHandler;
use homecore_api::{router, LongLivedTokenStore, SharedState}; use homecore_api::{router, LongLivedTokenStore, SharedState};
use homecore_assist::pipeline::default_pipeline; use homecore_assist::pipeline::default_pipeline;
use homecore_assist::RegexIntentRecognizer; use homecore_assist::RegexIntentRecognizer;
@ -66,6 +67,13 @@ async fn main() -> Result<()> {
let hc = HomeCore::new(); let hc = HomeCore::new();
info!("HomeCore state machine + event bus + service registry online"); info!("HomeCore state machine + event bus + service registry online");
// Seed a representative set of built-in services so the web UI
// and HA-wire-compat clients see a populated /api/services on
// first boot. These are no-op handlers (they just echo back the
// call as JSON for observability) — integrations override them
// by registering the same ServiceName later.
seed_default_services(&hc).await;
// ── 2. Recorder (optional) ────────────────────────────────────── // ── 2. Recorder (optional) ──────────────────────────────────────
if !cli.no_recorder { if !cli.no_recorder {
match Recorder::open(&cli.db).await { match Recorder::open(&cli.db).await {
@ -154,3 +162,50 @@ fn init_tracing() {
) )
.init(); .init();
} }
/// Register a representative set of built-in services so `/api/services`
/// is non-empty on first boot. Each handler simply echoes the call back
/// as a JSON acknowledgement — integrations override these by
/// re-registering the same `ServiceName` with a real handler later.
///
/// The set covers the HA wire-compat "starter pack" (homeassistant /
/// light / switch / scene / automation domains) plus a `homecore.*`
/// domain so operators can see HOMECORE-native services distinguished
/// from the HA-compat ones.
async fn seed_default_services(hc: &HomeCore) {
let echo = || FnHandler(|call: ServiceCall| async move {
Ok(serde_json::json!({
"called": format!("{}.{}", call.name.domain, call.name.service),
"service_data": call.data,
"acknowledged": true,
}))
});
let svcs = [
// Conventional HA wire-compat services
("homeassistant", "restart"),
("homeassistant", "stop"),
("homeassistant", "reload_core_config"),
("light", "turn_on"),
("light", "turn_off"),
("light", "toggle"),
("switch", "turn_on"),
("switch", "turn_off"),
("switch", "toggle"),
("scene", "apply"),
("automation", "trigger"),
// HOMECORE-native services
("homecore", "ping"),
("homecore", "snapshot_state"),
];
for (domain, service) in svcs {
hc.services()
.register(ServiceName::new(domain, service), echo())
.await;
}
let count = hc.services().registered_services().await.len();
let _ = ServiceError::NotRegistered { domain: String::new(), service: String::new() };
info!("Service registry seeded with {} default service(s)", count);
}