Skip to content

Commit 858a3d9

Browse files
committed
feat(homecore-ui): Dashboard page + seed script — UI is no longer empty
Before: `<hc-app-shell>` was a layout-only component with an empty `<slot>` (the auditor flagged it as "scaffold + no dashboard page"); operators saw the appbar + nav + footer but nothing in `<main>`. After: three small additions wire the existing components to real backend data. frontend/src/pages/Dashboard.ts (~110 LOC) — new Lit `<hc-dashboard>` - Reads bearer from localStorage / ?token= / <meta name=> / falls back to "dev-token" (matches the DEV-token mode the backend reports when HOMECORE_TOKENS is unset) - Calls client.getConfig() + client.getStates() on mount - Renders a `.meta` line (location · version · entity count) plus a responsive grid of `<hc-state-card>` from the live state list - Polls /api/states every 5 s for live refresh - Surface a structured error block if the backend is unreachable so operators see WHAT broke rather than a blank page frontend/src/main.ts (+9 LOC) — appends `<hc-dashboard>` into the `<hc-app-shell>` slot on DOMContentLoaded scripts/homecore-seed.sh (+95 LOC, executable) — POSTs 10 representative entities to the HA-compat `/api/states/<id>` endpoint so a fresh `homecore-server` boot has demo content. Live numbers from RuView's sensing-server when RUVIEW_URL is reachable (sensor.living_room_presence / bedroom_breathing_rate / bedroom_heart_rate); plausible defaults otherwise. Empirical (after `bash scripts/homecore-seed.sh` against a fresh homecore-server on :8123, browser at http://localhost:5173): .meta: "Home | HOMECORE v0.1.0-alpha.0 | 10 entities" grid : 10 <hc-state-card> elements rendered, e.g. binary_sensor.front_door off updated 12:17:34 switch.coffee_maker off updated 12:17:34 sensor.living_room_motion_score 0.0 updated 12:17:33 … curl : GET /api/config → 200 GET /api/states → 200 (returns array of 10) The dashboard now provides real value-vs-empty-page proof that the frontend ↔ HOMECORE-API chain is wired end-to-end. Co-Authored-By: claude-flow <ruv@ruv.net>
1 parent f891329 commit 858a3d9

3 files changed

Lines changed: 236 additions & 0 deletions

File tree

frontend/src/main.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,13 @@ import './styles/base.css';
99
// Register custom elements
1010
import './components/AppShell.js';
1111
import './components/StateCard.js';
12+
import './pages/Dashboard.js';
13+
14+
// Mount the Dashboard inside the AppShell's slot so the empty `<main>`
15+
// layout actually shows something on first paint.
16+
window.addEventListener('DOMContentLoaded', () => {
17+
const shell = document.querySelector('hc-app-shell');
18+
if (shell && !shell.querySelector('hc-dashboard')) {
19+
shell.appendChild(document.createElement('hc-dashboard'));
20+
}
21+
});

frontend/src/pages/Dashboard.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/**
2+
* Dashboard page — fetches HOMECORE state + config from the backend and
3+
* populates the `<hc-app-shell>` slot with a grid of `<hc-state-card>`.
4+
*
5+
* Auth: reads bearer from `localStorage["homecore.token"]`, the
6+
* `?token=` query string, or `HOMECORE_TOKEN` `<meta>` tag — in that
7+
* order. Falls back to the literal "dev-token" in DEV-mode backends
8+
* (any non-empty bearer is accepted when HOMECORE_TOKENS is unset).
9+
*/
10+
11+
import { LitElement, html, css } from 'lit';
12+
import { customElement, state } from 'lit/decorators.js';
13+
14+
import { HomecoreClient } from '../api/client.js';
15+
import type { ApiConfig, StateView } from '../api/types.js';
16+
17+
function resolveToken(): string {
18+
if (typeof localStorage !== 'undefined') {
19+
const stored = localStorage.getItem('homecore.token');
20+
if (stored) return stored;
21+
}
22+
const url = new URL(window.location.href);
23+
const qs = url.searchParams.get('token');
24+
if (qs) return qs;
25+
const meta = document.querySelector<HTMLMetaElement>('meta[name="homecore-token"]');
26+
if (meta?.content) return meta.content;
27+
return 'dev-token';
28+
}
29+
30+
@customElement('hc-dashboard')
31+
export class Dashboard extends LitElement {
32+
static styles = css`
33+
:host {
34+
display: block;
35+
padding: 24px;
36+
color: var(--hc-fg, #e6e9ec);
37+
font-family: var(--hc-font-sans, 'Outfit', system-ui, sans-serif);
38+
}
39+
.meta {
40+
display: flex;
41+
gap: 16px;
42+
flex-wrap: wrap;
43+
color: var(--hc-fg-dim, #8a93a0);
44+
font-size: 14px;
45+
margin-bottom: 16px;
46+
}
47+
.meta strong { color: var(--hc-fg, #e6e9ec); }
48+
.grid {
49+
display: grid;
50+
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
51+
gap: 16px;
52+
}
53+
.empty,
54+
.err {
55+
padding: 24px;
56+
border: 1px dashed var(--hc-border, #2a323e);
57+
border-radius: 8px;
58+
text-align: center;
59+
color: var(--hc-fg-dim, #8a93a0);
60+
}
61+
.err {
62+
border-color: #b35a5a;
63+
color: #f0c0c0;
64+
text-align: left;
65+
font-family: var(--hc-font-mono, 'JetBrains Mono', monospace);
66+
font-size: 13px;
67+
white-space: pre-wrap;
68+
}
69+
`;
70+
71+
@state() private states: StateView[] = [];
72+
@state() private config: ApiConfig | null = null;
73+
@state() private error: string | null = null;
74+
@state() private loading = true;
75+
76+
private client = new HomecoreClient({ token: resolveToken() });
77+
private pollTimer: number | undefined;
78+
79+
connectedCallback(): void {
80+
super.connectedCallback();
81+
void this.refresh();
82+
this.pollTimer = window.setInterval(() => void this.refresh(), 5000);
83+
}
84+
85+
disconnectedCallback(): void {
86+
if (this.pollTimer !== undefined) window.clearInterval(this.pollTimer);
87+
super.disconnectedCallback();
88+
}
89+
90+
private async refresh(): Promise<void> {
91+
try {
92+
const [cfg, states] = await Promise.all([
93+
this.client.getConfig(),
94+
this.client.getStates(),
95+
]);
96+
this.config = cfg;
97+
this.states = states;
98+
this.error = null;
99+
} catch (e) {
100+
this.error = e instanceof Error ? e.message : String(e);
101+
} finally {
102+
this.loading = false;
103+
}
104+
}
105+
106+
render() {
107+
if (this.error) {
108+
return html`<div class="err">backend unreachable — ${this.error}\n\n
109+
hint: make sure homecore-server is running on :8123 and that
110+
the token in localStorage["homecore.token"] is accepted.
111+
</div>`;
112+
}
113+
if (this.loading) {
114+
return html`<div class="empty">loading HOMECORE state…</div>`;
115+
}
116+
const v = this.config?.version ?? '?';
117+
const loc = this.config?.location_name ?? 'Home';
118+
return html`
119+
<div class="meta">
120+
<span><strong>${loc}</strong></span>
121+
<span>HOMECORE v<strong>${v}</strong></span>
122+
<span><strong>${this.states.length}</strong> entities</span>
123+
</div>
124+
${this.states.length === 0
125+
? html`<div class="empty">
126+
No entities registered yet. Run
127+
<code>bash scripts/homecore-seed.sh</code> to populate
128+
~10 demo entities, or connect a plugin / integration.
129+
</div>`
130+
: html`<div class="grid">
131+
${this.states.map(
132+
(s) => html`<hc-state-card .state=${s}></hc-state-card>`
133+
)}
134+
</div>`}
135+
`;
136+
}
137+
}
138+
139+
declare global {
140+
interface HTMLElementTagNameMap {
141+
'hc-dashboard': Dashboard;
142+
}
143+
}

scripts/homecore-seed.sh

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/usr/bin/env bash
2+
#
3+
# homecore-seed.sh — populate the empty HOMECORE state machine with a
4+
# representative cross-section of entities so the web UI renders
5+
# useful content right after `homecore-server` boots.
6+
#
7+
# When homecore-server starts with no plugins loaded and no
8+
# integrations enabled, its state machine is empty by design — the
9+
# web UI shows "No entities registered yet". This script POSTs ~10
10+
# real-looking entities via the HA-compat REST surface.
11+
#
12+
# Where the numbers come from:
13+
# - sensor.living_room_presence / _motion / bedroom_breathing_rate /
14+
# bedroom_heart_rate are pulled live from the RuView sensing-server
15+
# (RUVIEW_URL/api/v1/vitals/12/latest) when reachable.
16+
# - Other entities use plausible literals.
17+
#
18+
# Usage:
19+
# bash scripts/homecore-seed.sh
20+
# HOMECORE_URL=http://localhost:8123 HOMECORE_TOKEN=dev-token bash scripts/homecore-seed.sh
21+
# RUVIEW_URL=http://ruv-mac-mini:3000 bash scripts/homecore-seed.sh # live numbers
22+
#
23+
# Idempotent: re-running just updates the values.
24+
25+
set -euo pipefail
26+
27+
URL="${HOMECORE_URL:-http://127.0.0.1:8123}"
28+
TOKEN="${HOMECORE_TOKEN:-dev-token}"
29+
RUVIEW_URL="${RUVIEW_URL:-http://localhost:3000}"
30+
31+
post() {
32+
local entity_id="$1"; shift
33+
local body="$1"; shift
34+
curl -fsS -X POST "$URL/api/states/$entity_id" \
35+
-H "Authorization: Bearer $TOKEN" \
36+
-H "Content-Type: application/json" \
37+
-d "$body" >/dev/null && echo " set $entity_id"
38+
}
39+
40+
# Pull a live snapshot from the RuView sensing-server (optional).
41+
ruview_snapshot="{}"
42+
if curl -fsS --max-time 2 "$RUVIEW_URL/api/v1/vitals/12/latest" -o /tmp/ruview-vitals.json 2>/dev/null; then
43+
ruview_snapshot=$(cat /tmp/ruview-vitals.json)
44+
echo "Pulled live RuView snapshot from $RUVIEW_URL"
45+
else
46+
echo "RuView snapshot unreachable — using defaults (set RUVIEW_URL to your sensing-server to pull live values)"
47+
fi
48+
49+
get_num() {
50+
local key="$1" default="$2"
51+
echo "$ruview_snapshot" | python3 -c "
52+
import sys, json
53+
try:
54+
d = json.loads(sys.stdin.read())
55+
v = d.get('$key')
56+
print(v if v is not None else '$default')
57+
except Exception:
58+
print('$default')
59+
" 2>/dev/null || echo "$default"
60+
}
61+
62+
presence=$(get_num presence false)
63+
breathing=$(get_num breathing_rate_bpm 14.5)
64+
heart_rate=$(get_num heartrate_bpm 68.0)
65+
motion=$(get_num motion 0.0)
66+
67+
echo
68+
echo "Seeding HOMECORE at $URL ..."
69+
70+
post sensor.living_room_presence "{\"state\": \"$presence\", \"attributes\": {\"friendly_name\": \"Living Room Presence\", \"device_class\": \"occupancy\", \"source\": \"RuView ESP32-C6 BFLD\"}}"
71+
post sensor.living_room_motion_score "{\"state\": \"$motion\", \"attributes\": {\"friendly_name\": \"Living Room Motion Score\", \"unit_of_measurement\": \"score\", \"icon\": \"mdi:motion-sensor\"}}"
72+
post sensor.bedroom_breathing_rate "{\"state\": \"$breathing\", \"attributes\": {\"friendly_name\": \"Bedroom Breathing Rate\", \"unit_of_measurement\": \"BPM\", \"device_class\": \"frequency\", \"source\": \"Seeed MR60BHA2 mmWave\"}}"
73+
post sensor.bedroom_heart_rate "{\"state\": \"$heart_rate\", \"attributes\": {\"friendly_name\": \"Bedroom Heart Rate\", \"unit_of_measurement\": \"BPM\", \"device_class\": \"frequency\", \"source\": \"Seeed MR60BHA2 mmWave\"}}"
74+
post light.kitchen_ceiling '{"state": "on", "attributes": {"friendly_name": "Kitchen Ceiling", "brightness": 230, "color_temp_kelvin": 4000, "supported_color_modes": ["color_temp"]}}'
75+
post light.living_room_lamp '{"state": "off", "attributes": {"friendly_name": "Living Room Lamp", "brightness": 0, "supported_color_modes": ["brightness"]}}'
76+
post switch.coffee_maker '{"state": "off", "attributes": {"friendly_name": "Coffee Maker", "device_class": "outlet"}}'
77+
post binary_sensor.front_door '{"state": "off", "attributes": {"friendly_name": "Front Door", "device_class": "door"}}'
78+
post climate.thermostat '{"state": "heat", "attributes": {"friendly_name": "Thermostat", "current_temperature": 21.5, "temperature": 22.0, "hvac_modes": ["off", "heat", "cool", "auto"], "supported_features": 387}}'
79+
post sensor.air_quality_index '{"state": "42", "attributes": {"friendly_name": "Air Quality Index", "unit_of_measurement": "AQI", "device_class": "aqi"}}'
80+
81+
echo
82+
echo "Done. The HOMECORE web UI at http://localhost:5173 should now"
83+
echo "show 10 entities. The Dashboard auto-refreshes every 5 s."

0 commit comments

Comments
 (0)