No description
  • TypeScript 48.8%
  • JavaScript 46.5%
  • HTML 3.7%
  • CSS 1%
Find a file
Chris Harrington 57201ac675
All checks were successful
ci / check (push) Successful in 11s
Floating person nameplates, tracking and restyled map markers
Replace the locate buttons and toast with a row of clickable nameplates
floating over the map. Click flies to a person, double click follows them,
and clicking a followed person's nameplate or panning the map stops
following. Markers are now just the avatar, people show their zone, and
the Leaflet zoom controls are removed. Adds DOM test setup to the map
workspace.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6WV37dDVNs4Zhq7RQB4ku
2026-10-04 19:29:51 -06:00
.devcontainer Import ha-map card as the starting point for the map dashboard 2026-09-29 20:17:49 -06:00
.forgejo/workflows [HOMEASSIST-7] Forgejo Actions CI for format, lint, typecheck, test and build (#7) 2026-10-03 22:11:03 -06:00
dashboards/map Floating person nameplates, tracking and restyled map markers 2026-10-04 19:29:51 -06:00
packages/ui [HOMEASSIST-6] Shared build and deploy pipeline to ha/www with panel registration (#6) 2026-10-03 21:50:59 -06:00
scripts [HOMEASSIST-8] Document repo layout, dev loop, and add-a-dashboard guide (#8) 2026-10-03 22:23:21 -06:00
.env.example Move to Maptiler for tiles. 2026-10-03 23:32:57 -06:00
.gitignore [HOMEASSIST-6] Shared build and deploy pipeline to ha/www with panel registration (#6) 2026-10-03 21:50:59 -06:00
.prettierrc Import ha-map card as the starting point for the map dashboard 2026-09-29 20:17:49 -06:00
bun.lock Floating person nameplates, tracking and restyled map markers 2026-10-04 19:29:51 -06:00
CLAUDE.md [HOMEASSIST-8] Document repo layout, dev loop, and add-a-dashboard guide (#8) 2026-10-03 22:23:21 -06:00
docker-compose.yml Add dev map container for hosting out of HA. 2026-10-03 23:21:35 -06:00
Dockerfile.dev [HOMEASSIST-1] Restructure ha-map into a bun workspace monorepo (#1) 2026-09-30 06:31:28 -06:00
Dockerfile.map Add dev map container for hosting out of HA. 2026-10-03 23:21:35 -06:00
eslint.config.js [HOMEASSIST-6] Shared build and deploy pipeline to ha/www with panel registration (#6) 2026-10-03 21:50:59 -06:00
package.json [HOMEASSIST-1] Restructure ha-map into a bun workspace monorepo (#1) 2026-09-30 06:31:28 -06:00
README.md Floating person nameplates, tracking and restyled map markers 2026-10-04 19:29:51 -06:00
tsconfig.base.json [HOMEASSIST-1] Restructure ha-map into a bun workspace monorepo (#1) 2026-09-30 06:31:28 -06:00
tsconfig.json [HOMEASSIST-1] Restructure ha-map into a bun workspace monorepo (#1) 2026-09-30 06:31:28 -06:00

HA dashboards

A bun workspace monorepo of dashboards for Home Assistant, each delivered as a panel_custom sidebar panel. Conventions for contributors (and Claude) live in CLAUDE.md.

Layout

.
├── packages/ui/          shared code: HA types, definePanel, hooks, Tailwind theme, Vite helpers, dev harness
├── dashboards/map/       one workspace per dashboard
├── scripts/              run.ts (root script dispatcher) and deploy.ts (hashed copy into HA's www)
├── .forgejo/             CI workflows
├── .devcontainer/        VS Code dev container config
├── Dockerfile.dev        dev container image; copies every workspace manifest
├── docker-compose.yml    dev container; one anonymous node_modules volume per workspace
├── .env.example          copy to .env: dev container user/UID/GID, Forgejo token, HA_WWW_DIR, VITE_PORT,
│                         optional live-HA URL and token
└── .prettierrc, eslint.config.js, tsconfig.base.json   shared tooling config

Tests are colocated with the code (*.test.ts(x)); packages/ui also keeps shared test setup in packages/ui/test/ and scripts/ has run.test.ts and deploy.test.ts. packages/ui/README.md covers theme tokens, definePanel and the dev harness internals.

Dev loop

There are two ways to check a dashboard.

Dev harness (fast, no HA needed). bun run dev <dashboard> starts Vite with HMR and mounts the panel in the shared harness. With VITE_HA_URL and VITE_HA_TOKEN set in the repo-root .env it connects to a real Home Assistant; leave them blank and it uses the dashboard's src/mock.ts fixtures. Append ?mock to the URL to force mock mode. The dev server uses port 5173 with strictPort, so only one dashboard can run at a time (the dev container publishes it on VITE_PORT).

docker compose up -d map runs the same harness in a container (Dockerfile.map) with the repo bind-mounted for HMR; open http://localhost:${MAP_PORT:-5176}. It reads VITE_HA_URL / VITE_HA_TOKEN from .env, so blank means mock mode.

Inside HA. bun run build <dashboard> --watch keeps dist-panel/ current while you edit. The watch loop does not deploy, so run bun run deploy <dashboard> (or copy the file) whenever you want HA to pick the change up; see Registering the panel for the follow-up steps.

Tests

bun run test runs bun test in every workspace (bun run test map for one); unscoped, it also runs bun test scripts. A workspace that needs DOM tests must set up its own preload; see packages/ui/README.md.

Adding a dashboard

This walks through adding a dashboard called foo, with the panel element ha-foo-panel. Replace foo, Foo and the sidebar details with your own. Dashboard names are lowercase letters, digits and hyphens. Copy from dashboards/map when in doubt.

1. Create the workspace files

Create dashboards/foo/ with these files.

package.json (add whatever extra dependencies the dashboard needs, as dashboards/map does for Leaflet):

{
    "name": "@ha-dashboards/foo",
    "version": "0.1.0",
    "private": true,
    "type": "module",
    "scripts": {
        "dev": "vite --configLoader runner",
        "build": "vite build --configLoader runner --config vite.panel.config.ts",
        "build:harness": "vite build --configLoader runner",
        "preview": "vite preview --configLoader runner",
        "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.node.json",
        "test": "bun test --pass-with-no-tests"
    },
    "dependencies": {
        "@ha-dashboards/ui": "workspace:*",
        "react": "^19.0.0",
        "react-dom": "^19.0.0"
    },
    "devDependencies": {
        "@types/bun": "^1.4.2",
        "@types/react": "^19.0.0",
        "@types/react-dom": "^19.0.0",
        "tailwindcss": "^4.1.0",
        "vite": "^6.1.0"
    }
}

tsconfig.json:

{
    "extends": "../../tsconfig.base.json",
    "compilerOptions": { "types": ["vite/client", "bun"] },
    "include": ["src"]
}

tsconfig.node.json (both Vite configs must be listed here):

{
    "extends": "../../tsconfig.base.json",
    "compilerOptions": {
        "target": "ES2023",
        "lib": ["ES2023"],
        "types": ["node"]
    },
    "include": ["vite.config.ts", "vite.panel.config.ts"]
}

index.html (the dev harness page):

<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>HA Foo — Dev Preview</title>
    </head>
    <body>
        <div id="root"></div>
        <script type="module" src="/src/main.tsx"></script>
    </body>
</html>

vite.config.ts (dev server only; not what gets deployed):

import { defineConfig } from 'vite';
import { dashboardPlugins } from '@ha-dashboards/ui/vite';

export default defineConfig(({ command }) => ({
    plugins: dashboardPlugins(),
    // Read VITE_HA_URL / VITE_HA_TOKEN from the repo-root .env, for the dev server only.
    envDir: command === 'build' ? undefined : '../..',
    server: { host: true, port: 5173, strictPort: true },
}));

vite.panel.config.ts (the deployable build; it always outputs to dist-panel/, which deploy reads):

import { defineConfig } from 'vite';
import { panelBuildConfig } from '@ha-dashboards/ui/vite';
import { fileURLToPath } from 'node:url';
import { PANEL_NAME } from './src/panel';

export default defineConfig(
    panelBuildConfig({
        entry: fileURLToPath(new URL('./src/element.tsx', import.meta.url)),
        panelName: PANEL_NAME,
    }),
);

The build is a Vite library-mode build to a single ES module (dist-panel/ha-foo-panel.js) with React bundled in, because the HA page has no shared React runtime to resolve against.

src/panel.ts (the one place the element name lives):

/** The custom element name: registered by element.tsx, used as the output file name and in `panel_custom`. */
export const PANEL_NAME = 'ha-foo-panel';

src/FooPanel.tsx (the panel component; read entities with useHass()):

import { useHass } from '@ha-dashboards/ui/panel';

export default function FooPanel() {
    const hass = useHass();
    return <div className='p-4 text-fg'>Entities: {Object.keys(hass.states).length}</div>;
}

src/element.tsx (registers the custom element; side effects only, so fast refresh works):

import { definePanel } from '@ha-dashboards/ui/panel';
import FooPanel from './FooPanel';
import { PANEL_NAME } from './panel';
import styles from './index.css?inline';

definePanel(PANEL_NAME, FooPanel, { styles });

src/main.tsx (the dev harness entry):

import { mountHarness } from '@ha-dashboards/ui/dev';
import FooPanel from './FooPanel';
import { mock } from './mock';
import './index.css';

mountHarness({ Panel: FooPanel, mock, env: import.meta.env });

src/mock.ts (fixtures used when no live HA is configured):

import { makeEntity, type MockHassOptions } from '@ha-dashboards/ui/dev';

export const mock: MockHassOptions = {
    entities: [makeEntity('sensor.example', '42')],
};

src/index.css (Tailwind plus the shared theme; add third-party CSS imports here):

@import 'tailwindcss';
@import '@ha-dashboards/ui/theme.css';
@source not './**/*.test.{ts,tsx}';

2. Register the workspace with the dev container

In Dockerfile.dev, add a COPY line next to the other workspace manifests, so bun install can resolve the workspace:

COPY --chown=${DEV_UID}:${DEV_GID} dashboards/foo/package.json ./dashboards/foo/

In docker-compose.yml, add an anonymous node_modules volume next to the existing ones under dev.volumes:

- /app/dashboards/foo/node_modules

3. Install and commit the lockfile

Run bun install at the repo root and commit the updated bun.lock. CI installs with --frozen-lockfile, so a stale lockfile fails the build.

4. Add a test

Add at least one colocated test, for example dashboards/foo/src/mock.test.ts asserting the fixtures. Prefer testing pure logic. If the dashboard needs DOM tests, add a bunfig.toml preload ([test] preload = ["../../packages/ui/test/dom.ts", "../../packages/ui/test/setup.ts"]), add ../../packages/ui/test/jest-dom.d.ts to the tsconfig.json include, and add @testing-library/react and @testing-library/dom to devDependencies; see packages/ui/README.md.

5. Run it, deploy it, register it

  1. bun run dev foo and open the printed URL (mock data unless .env has a live HA); from the host, use http://localhost:$VITE_PORT.
  2. bun run typecheck foo and bun run build foo to confirm the bundle builds.
  3. bun run deploy foo and note the module_url it prints.
  4. Add the panel_custom entry to ../ha/configuration.yaml (outside this repo; see Registering the panel) with name: ha-foo-panel, then check the config and restart HA.

What you get for free

The root scripts discover workspaces from packages/* and dashboards/*, so a new dashboard is automatically picked up by bun run build, typecheck, test, lint, lint:fix, format and format:check, and therefore by CI. No root config changes are needed.

Known map-specific leftovers

These are not generalised yet; rename them when you add a second dashboard:

  • the compose project/container name ha-map (COMPOSE_PROJECT_NAME in .env.example, the container_name default in docker-compose.yml and the CMD ready line in Dockerfile.dev);
  • .devcontainer/devcontainer.json: the name "HA Map (...)" and the ha-map-vscode-server volume;
  • the node_modules volume comment in Dockerfile.dev (names only dashboards/map);
  • the bun run deploy map comments in docker-compose.yml;
  • the "HA Map" dev-preview title in dashboards/map/index.html.

Map markers and header tiles are not part of this list: they are discovered from every person.* entity with coordinates.

Commands

Run from the repo root.

Command What it does
bun run dev <dashboard> Dev harness with HMR (live or mock HA).
bun run build [dashboard] Builds the panel bundle (every dashboard when no name is given).
bun run build <dashboard> --watch Rebuilds the bundle on every source change.
bun run deploy <dashboard> Builds, then copies the bundle to $HA_WWW_DIR/<dashboard>/<panel>.<hash>.js.
bun run typecheck, test, lint, format:check Checks across every workspace and scripts/.
bun run lint:fix, format Auto-fix lint problems / rewrite files with Prettier.

Flags such as --watch or --mode staging are passed through to the dashboard's script, so put the workspace name before any flags (bun run build map --watch). deploy, lint, lint:fix, format and format:check reject flags.

CI

.forgejo/workflows/ci.yml runs on every pull request and every push to main, on the ubuntu-latest runner of the homelab Forgejo instance. It installs with bun install --frozen-lockfile, then runs format:check, lint, typecheck, test and build as separate steps, so a failing run shows which gate broke. Each is the root script from the table above, so new dashboards are covered automatically. No secrets or variables are needed.

Deploying

HA_WWW_DIR is Home Assistant's config/www folder on the host. Set it in the repo-root .env (see .env.example). deploy runs from the repo root so Bun loads that file.

The deployed filename carries the first 8 hex characters of the bundle's sha256, so a changed bundle is a new URL and HA and the browser cannot serve stale JavaScript. deploy removes older hashed files of the same panel and prints the exact module_url. The newest previous hashed file is kept, because ../ha/configuration.yaml still points at it until you update the URL and restart HA; only older hashes are removed, and only files named <panel>.<8 hex>.js. Redeploying unchanged code produces the same file and the same URL.

Home Assistant serves config/www at /local/, so www/map/ha-map-panel.1a2b3c4d.js is /local/map/ha-map-panel.1a2b3c4d.js.

Registering the panel

Add the panel to ../ha/configuration.yaml (outside this repo), using the module_url that bun run deploy <dashboard> printed:

panel_custom:
    - name: ha-map-panel
      sidebar_title: Map
      sidebar_icon: mdi:map
      module_url: /local/map/ha-map-panel.<hash>.js # exact URL printed by `bun run deploy map`
      require_admin: false
  • name must match the element name passed to definePanel.
  • require_admin: true hides the panel from non-admin users.

Because the YAML contains the hashed URL, the workflow after a code change is:

  1. bun run deploy <dashboard>.
  2. If the printed module_url differs from the one in ../ha/configuration.yaml, update it there.
  3. Check the config (Developer tools > YAML > Check configuration), then restart Home Assistant. panel_custom is not reloadable, so a restart is required whenever the URL changes. An unchanged bundle needs nothing.
  4. Hard-refresh the browser (Ctrl+Shift+R) so it drops the old module.

Migrating from the old deploy

The previous deploy script copied the bundle to $HA_WWW_DIR/ha-map/ha-map-panel.js. The new path is $HA_WWW_DIR/map/ with a hashed name. Delete the old $HA_WWW_DIR/ha-map/ folder from the install, and replace the /local/ha-map/ha-map-panel.js URL in ../ha/configuration.yaml with the one bun run deploy map prints.