homeos/handoff.md

425 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# handoff.md — HomeOS
> **Status: FINAL (2026-07-17).** This is the authoritative handoff — supersedes any earlier
> draft or chat summary.
>
> **Handoff for the Claude Code session on the server.** This document is the source of truth
> to build the project from scratch on the target VM. Read fully before the first command.
> Starting state of the VM: see §6 (user `nexxo` + sudo, Docker installed, otherwise empty).
> Companion file: `design-mockup.html` (interactive design reference — open in a browser;
> contains all three views: Dashboard, access gate, mobile/PWA layout).
>
> Meta-docs are English; **UI copy is German** (localized DE+EN, see rules). Conventions and
> STRICT RULES are carried over from the user's Clusev project and are **non-negotiable**.
---
## 1. Product
**HomeOS** *(working title — rename is an open decision, see §12)* — a **self-built smart-home
control plane**. Explicitly NOT Home Assistant and not a wrapper around it: own data model, own
integrations, own UI. Single household, self-hosted, LAN-first.
**Core capabilities (full vision):**
- Automatic **device discovery** on the network (mDNS/SSDP/ARP) with an "assign new device" flow
- **Device control** (switches, lights, plugs, power metering; later TVs, covers, …)
- **Rooms** and **persons**: every device/entity assignable to a room and optionally a person
- **Presence detection** (home/away per person) via UniFi + later geofencing, score-based fusion
- **Window/door contact sensors** with status + automations
- **Automations** (trigger → condition → action)
- Live dashboard (Reverb) + later **PWA** on the phone
- Later: settings area protected by **WebAuthn/passkeys** (deferred, see §11)
**v1 scope (build this first — do NOT boil the ocean):**
1. Foundation: Docker stack, auth, design system, device/entity/room model, MQTT ingest,
Reverb live updates, audit/command log
2. **Shelly integration** (the user's actual hardware today — see §4)
3. Discovery sidecar + "Neue Geräte" assignment flow
4. Presence via **UniFi API** (the user's actual network — see §5)
5. Automations engine (minimal TCA: state-change + time triggers, few actions)
**Deferred (keep OUT of v1):** face recognition entirely (later, maybe), WebAuthn gate,
TV/Chromecast control, Matter, Zigbee (until hardware decision), Ring bridge, native mobile app
(PWA later covers it), energy history charts beyond a simple sparkline.
---
## 2. Tech stack (hard requirements)
| Layer | Choice |
|---|---|
| Framework | **Laravel 13** (current major; user's newer projects run 13 — confirm once, see §12) |
| UI | **Livewire v3** — class-based, **NOT Volt** |
| CSS | **Tailwind v4** (CSS-first, `@theme` in `app.css`, no `tailwind.config.js`) |
| Build | **Vite** |
| Modals | **wire-elements/modal** |
| Realtime | **Laravel Reverb** + Echo — **every channel PRIVATE** |
| Queue/Cache | **Redis** (+ Horizon) |
| DB | **PostgreSQL 17 + TimescaleDB** recommended (telemetry hypertables, compression, retention). Fallback if user prefers stack consistency with Clusev: MariaDB + monthly partitioning + prune job. **Open decision §12 — ask before migrating.** |
| MQTT | **Eclipse Mosquitto 2** (auth + per-client ACLs from day one) |
| MQTT client | **php-mqtt/laravel-client** (^1.8) — subscriber runs as its own Artisan daemon container |
| Discovery | **Python sidecar** container (`network_mode: host`): python-zeroconf (mDNS), async-upnp-client (SSDP), arp-scan — reports via MQTT. PHP cannot do multicast listening from the bridge network. |
| Charts | JS lib as Alpine island (uPlot or ApexCharts) — v1 needs only sparklines |
| Icons | **Lucide**, inline SVG via `x-icon` Blade component — no emoji anywhere |
| Fonts | **Plus Jakarta Sans** (UI) · **IBM Plex Mono** (values, MACs, IPs, timestamps) — **self-hosted woff2 only** (R14) |
Composer: `livewire/livewire wire-elements/modal laravel/reverb laravel/horizon php-mqtt/laravel-client`
NPM: `tailwindcss @tailwindcss/vite laravel-echo pusher-js` (+ chart lib)
---
## 3. Architecture
**MQTT-first hybrid.** Mosquitto is the central bus. Everything that speaks MQTT natively
(Shelly Gen2+, later Zigbee2MQTT, Tasmota, WLED, ring-mqtt) connects directly. Devices without
MQTT get adapters later (HTTP drivers in PHP, or sidecars for TV/Cast/Matter). The UI never
touches protocol topics directly — **always through the DB mapping** (protocol-agnostic).
```
devices ──MQTT──► mosquitto ──► mqtt-listener (artisan daemon)
│ parse + dispatch only (NEVER heavy work in the loop)
Events / Horizon jobs ──► DB upsert (device_states)
Reverb (private channels) ──► Livewire UI (Echo)
UI action ──► Livewire ──► DeviceCommand job ──► driver (MqttDriver | HttpDriver) ──► device
└── every command written to command log (audit)
```
**Data model (core tables):**
- `users`, `rooms`, `persons` (person ↔ user optional link)
- `devices` — identity: uuid (route key, R11), name, vendor, model, protocol, connection config
(JSON: topic prefix / IP / auth ref), `room_id`, `person_id` nullable, first_seen/last_seen,
approval state (`discovered` | `active` | `ignored`)
- `entities` — one capability per row: type (`switch`, `light`, `power`, `contact`, `temperature`,
`humidity`, `motion`, …), capabilities JSON, `device_id`
- `device_states` — CURRENT state only: `entity_id` unique, state JSON, updated_at (pure upserts,
stays small and fast)
- `telemetry` — history (TimescaleDB hypertable; numeric value column + state JSON). Compression
+ retention policy **from day one** (a 30 s sensor ≈ 1 M rows/year)
- `commands` — audit: who/what switched which entity when (user | automation | physical),
payload, result. Unverzichtbar for debugging "warum ging das Licht an?"
- `automations` — name, enabled, trigger_type + trigger_config JSON, conditions JSON,
actions JSON, `cooldown_seconds`, `last_triggered_at` (cooldown from day one — motion sensors
without debounce flood the queue)
- `presence_signals` + computed person state (see §5)
- `discovery_findings` — raw sidecar reports before a device is approved
**Driver contract:** `app/Support/Drivers/DeviceDriver` interface (`turnOn`, `turnOff`,
`setState`, `capabilities`). v1 implements `ShellyMqttDriver`. Everything else is a later driver
behind the same contract — this is what keeps the system "für alle Produkte brauchbar".
**Broadcasting:** private channels only — `rooms.{uuid}`, `devices.{uuid}`, `presence`,
`discovery`. Authorize in `routes/channels.php`. State changes broadcast with
`ShouldBroadcastNow` (or a dedicated high-priority queue) — a slow job in the default queue must
never delay a light toggle in the UI.
**Long-runners:** `mqtt-listener`, `reverb`, `horizon`, `scheduler` are separate containers off
the same PHP image. MQTT loop rule: parse + dispatch only; reconnect with exponential backoff;
graceful SIGTERM shutdown via pcntl.
---
## 4. Hardware reality (drives the build order)
**Today the user owns: Shelly devices only, plus one Ring product. No Zigbee, no Hue.**
**Shelly (v1 target):**
- Gen2/Gen3 speak JSON-RPC over **MQTT** (enable MQTT in device settings → point at Mosquitto),
plus HTTP `POST /rpc` and WebSocket. Status arrives on `<device>/status/...`, notifications via
`rpc_ntf`; commands via RPC (`Switch.Set`, `Light.Set`).
- Discovery: Shellys announce via mDNS (`_shelly._tcp`) and expose `GET /shelly` (identity JSON:
model, gen, mac) — the sidecar uses both to classify.
- Power metering (PM devices) → `power` entity → dashboard energy tile.
- Docs: shelly-api-docs.shelly.cloud
**Window/door contacts (near future, still Shelly-native):** **Shelly BLU Door/Window**
(Bluetooth) received by any Shelly Gen3 device or a Shelly BLU Gateway acting as BT gateway —
events surface over the same MQTT path. This avoids buying into Zigbee just for contacts.
Zigbee (Zigbee2MQTT + LAN coordinator SLZB-06, never USB passthrough into the Proxmox VM)
remains the documented expansion path if/when the user buys Zigbee hardware.
**Ring (deferred, optional):** no official local API. Established bridge: **ring-mqtt**
(tsightler/ring-mqtt, Docker) — cloud-based, token auth, surfaces motion/ding/contact/camera
events on MQTT. Fits the bus as-is. Cloud dependency must be labeled in the UI (device badge
"Cloud"). Do not build a custom Ring client.
**Later expansion paths (documented, not built):** Hue via local CLIP v2 (HTTPS, HTTP driver in
PHP — existing PHP Hue packages are all deprecated v1 API, don't use them), TVs (LG SSAP /
Samsung Tizen websockets) + Chromecast + **Matter** (matter.js + @matter/mqtt) via a Node
sidecar, Tasmota/WLED native MQTT. The entity/driver model above must never assume Shelly.
---
## 5. Network topology & presence
**Actual network:** Fritz!Box (internet, NAT) → **UniFi Dream Machine** (own NAT — double NAT,
works fine, accepted) → LAN/WLAN with all devices and phones.
Consequences:
- **Presence source of truth = UniFi**, NOT Fritz!Box (phones associate with UniFi WLAN).
Package: **art-of-wifi/unifi-api-client** (`list_clients()` → MAC, IP, AP, last_seen). UDM =
UniFi OS console (port 443); create a dedicated read-only local user or API key for HomeOS.
Fritz!Box TR-064 is irrelevant here (it only sees the UDM as one client).
- Presence engine: "home" immediately on association; "away" only after **510 min debounce**
(`last_seen_at` + scheduler sweep). iPhone WLAN sleep causes false-aways otherwise. Never hang
a security automation on a single raw signal.
- MAC randomization: private MAC is stable per SSID — onboarding flow assigns "this client =
person X" from the live UniFi client list instead of asking for hardware MACs.
- Later additions: OwnTracks geofencing (MQTT, fastest clean "left home" signal), score-based
fusion of UniFi + GPS (+ BLE if ever). v1 = UniFi only.
- **Remote access:** double NAT makes port forwarding annoying (two chained forwards) — use
**Tailscale** (or WireGuard on the UDM) instead. No open ports. This is also the interim
answer for "PWA von unterwegs" until a public domain + TLS is set up.
- **WebAuthn (deferred)** will require HTTPS + a real domain (RP ID cannot be an IP; domain
change invalidates all passkeys) — plan `home.<domain>` + Let's Encrypt DNS-01 via Caddy when
that phase starts.
- Proxmox: VM NIC on vmbr0, **same L2/VLAN as the smart-home devices**, otherwise
mDNS/SSDP discovery is blind. If IoT ever moves to its own VLAN → mDNS reflector required.
---
## 6. Docker topology (dev = prod parity, R8 — host has only Docker)
**Starting point on the target VM (already prepared — this is where you begin):**
- User **`nexxo`** exists and has **sudo**. Work as this user.
- **Docker is already installed.** Verify with `docker compose version` as one of the first
commands; if the compose plugin is missing, report it.
- The VM is otherwise **empty**: no PHP, no Composer, no Node on the host — hence R8, every
project command runs in containers (bootstrap Laravel via a throwaway container).
- Project root: **`/home/nexxo/homeos`** (own git repo — does not exist yet, create it during
bootstrap; feature branches, not `main`). Read `HOST_UID`/`HOST_GID` from `id nexxo` into
`.env` — do not hardcode a UID.
Services in `docker-compose.yml`:
| Service | Role |
|---|---|
| `app` | php-fpm + nginx + Vite (dev) via supervisor — Laravel + Livewire |
| `horizon` | queue workers (same image, `php artisan horizon`) |
| `scheduler` | `php artisan schedule:work` (same image) — presence sweeps, retention, polling fallbacks |
| `reverb` | `php artisan reverb:start` (same image) |
| `mqtt-listener` | `php artisan mqtt:listen` (same image, `restart: always`) |
| `mosquitto` | eclipse-mosquitto:2 — password auth + ACL per client (laravel, sidecar, shelly, later z2m/ring) |
| `db` | timescale/timescaledb:latest-pg17 (or MariaDB — §12) |
| `redis` | redis:7-alpine |
| `discovery` | Python sidecar, **`network_mode: host`** + `cap_add: [NET_RAW]` |
Port planning (host-mode container shares the VM's ports — plan collisions explicitly):
app **:80**, Vite **:5173**, Reverb internal 8080 → host **:6001**, Mosquitto **:1883**
(LAN only), DB bound to 127.0.0.1. All ports env-driven, nothing hardcoded.
Ops non-negotiables:
- **Backups:** Proxmox vzdump for the VM + DB dump job; later `coordinator_backup.json` when
Zigbee exists. Mosquitto persistence volume.
- **Timezone:** app logic Europe/Berlin, storage UTC; VM NTP-synced (DST double/zero-fire bug
in time triggers otherwise).
- **Reverb behind proxy:** websocket upgrade headers + long `proxy_read_timeout`, or sockets
drop every 60 s.
- **Graceful degradation:** server down must never make the home unusable — physical switches
keep working because Shellys switch locally (detached/relay mode kept functional).
---
## 7. Folder map (R6 — follow exactly)
```
app/
Livewire/
Dashboard.php
Rooms/{Index.php, Show.php}
Devices/{Index.php, Show.php}
Discovery/Index.php
Persons/Index.php
Automations/{Index.php, Edit.php}
Settings/Index.php
Modals/ # wire-elements/modal (ConfirmDelete, AssignDevice, …)
Concerns/
Models/ # Device, Entity, DeviceState, Room, Person, Automation, Command, DiscoveryFinding
Support/
Mqtt/ # topic mapping, payload normalizers per vendor
Drivers/ # DeviceDriver contract + ShellyMqttDriver (+ later drivers)
Services/ # DeviceCommandService, PresenceEngine, AutomationEngine, DiscoveryService, UnifiClient
Events/ # DeviceStateChanged, DeviceDiscovered, PresenceChanged (ShouldBroadcastNow)
Jobs/
Console/Commands/MqttListenCommand.php
resources/
views/livewire/… # mirrors app/Livewire, kebab-case
views/components/ # x-panel, x-kpi, x-status-pill, x-status-dot, x-badge, x-icon,
# x-room-card, x-device-chip, x-toggle, sidebar, topbar
views/layouts/app.blade.php
css/app.css # @import "tailwindcss"; @theme { tokens §8 }
fonts/ # plus-jakarta-sans*.woff2, ibm-plex-mono*.woff2 (Vite-bundled)
js/app.js # Echo bootstrap + Alpine chart islands
lang/{de,en}/ # common, dashboard, rooms, devices, discovery, persons, automations, settings, modals
routes/web.php # full-page Livewire routes, English paths (R13)
routes/channels.php # private-channel auth
sidecar/ # Python discovery sidecar (own Dockerfile)
docker/ docker-compose.yml docker-compose.prod.yml
handoff.md rules.md CLAUDE.md design-mockup.html
```
---
## 8. Design system — "Enterprise Ops, dark OLED"
Reference: **`design-mockup.html`** (open it — it is the approved direction: UniFi/Grafana-class
ops console, NOT a consumer app). Port these tokens verbatim into `@theme` (R3); markup uses
token utilities only.
```css
@theme {
/* surfaces */
--color-base: #080D18; /* page bg, blue-biased OLED black */
--color-surface: #0D1424; /* cards */
--color-raised: #121C31; /* nested surfaces, chips */
--color-inset: #18233D; /* icon wells, kbd */
/* text ramp */
--color-ink: #EAF0FB;
--color-ink-2: #93A1BD;
--color-ink-3: #5E6C8A;
/* brand */
--color-accent: #4FC1FF; /* cyan — interaction, active nav, toggles; tints via /10 /25 */
/* status triad — reserved, never decorative */
--color-online: #34D399;
--color-warning: #FBBF24;
--color-offline: #F87171;
/* hairlines */
--color-line: #1C2846;
--color-line-soft: #141E36;
/* fonts */
--font-sans: "Plus Jakarta Sans", ui-sans-serif, system-ui;
--font-mono: "IBM Plex Mono", ui-monospace, monospace;
}
```
Rules of the look: status via colored dot/pill + text (never color alone, never emoji);
**every numeric value, MAC, IP, timestamp in `font-mono` with `tabular-nums`**; hairline borders
instead of shadows; accent used sparingly (one primary action per view); subtle pulse on the
live indicator; `prefers-reduced-motion` respected. Responsive 375/768/1280 (R7): sidebar →
drawer, KPI grid 5→3→2→1, right rail stacks below.
---
## 9. STRICT RULES (carried over — compact form)
> The user's Clusev `rules.md` applies 1:1 to this project (full examples live there).
> Copy this list into `rules.md` in the repo at bootstrap. On any conflict: **STOP and ask.**
- **R1** Pages = full-page class-based Livewire components mapped directly in `routes/web.php`. No page controllers.
- **R2** Livewire class + separate Blade view. **Volt forbidden** (never `make:volt`).
- **R3** All colors/design tokens in `@theme` (`app.css`); markup uses token utilities only — no raw hex/rgb.
- **R4** No inline `style=` — single exception: progress-bar `width`.
- **R5** Destructive/confirm actions via **wire-elements/modal** — never `confirm()`/Alpine popups.
- **R6** Files exactly per folder map (§7).
- **R7** Responsive verified at 375/768/1280; touch targets ≥ 44 px.
- **R8** Every PHP/Composer/Node/artisan command **inside the container** — host has only Docker.
- **R9** UI copy German, terse/operational, **no emoji**; status via color/dots/pills; technical tokens stay native.
- **R10** Reuse `@theme` tokens + Blade component kit — no ad-hoc colors/widgets.
- **R11** URL-exposed records addressed by **UUID**, never integer PK (`getRouteKeyName(): 'uuid'`).
- **R12** Every touched page verified in a real browser: **HTTP 200, zero console errors, zero failed requests**; loaded state of lazy pages, not the skeleton; inspect rendered DOM for leaked `@`/`{{ }}`/`group.key` literals. Green `Livewire::test` ≠ done.
- **R13** Route paths + names **English** (`/devices`, not `/geraete`); German only in visible labels.
- **R14** Fonts **self-hosted** (`resources/fonts/*.woff2`, relative `url('../fonts/…')` + `@font-face`) — no Google Fonts/CDN link or import.
- **R15** **Codex review** (`/codex:review`) after every change — no errors, no security findings, else fix and re-run. Task not done until clean.
- **R16** Every UI string localized `lang/{de,en}/<group>.php`, identical keys both languages, `__('group.key')` — no hard-coded copy anywhere.
- **R17** Blade: block `@php … @endphp` only (no inline `@php(...)`); never the literal directive tokens in comments/text.
- **Secrets:** `.env*`, tokens, keys never staged; check `git status` before every commit.
Git repo does not exist yet — create it at bootstrap (feature branches, not `main`).
**HomeOS-specific additions:**
- **H1** UI/Livewire never publishes to protocol topics directly — every command goes through
`DeviceCommandService` → driver, and every command is written to the `commands` audit table.
- **H2** MQTT subscriber callbacks: parse + dispatch only. No DB aggregation, no HTTP calls in
the loop.
- **H3** Vendor specifics (topics, payload shapes) live only in `Support/Mqtt` + `Support/Drivers`.
Models, Livewire and views stay protocol-agnostic.
- **H4** Telemetry writes go through one ingest path that enforces retention/compression policies.
- **H5** Automations always carry `cooldown_seconds`; the engine has a **dry-run mode** (log
instead of switch) for development — never develop automations against the live home.
---
## 10. Claude Code plugin setup (server session — do this FIRST, before Phase 1)
The server session must mirror the user's local Claude Code plugin set and **use** these
plugins during the build. At session start:
1. **Add the external marketplaces**, then install:
| Plugin | Marketplace / source | Install |
|---|---|---|
| superpowers | `claude-plugins-official` (built-in) | `/plugin install superpowers@claude-plugins-official` |
| frontend-design | `claude-plugins-official` | `/plugin install frontend-design@claude-plugins-official` |
| code-review | `claude-plugins-official` | `/plugin install code-review@claude-plugins-official` |
| context7 | `claude-plugins-official` | `/plugin install context7@claude-plugins-official` |
| codex | `/plugin marketplace add openai/codex-plugin-cc` | `/plugin install codex@openai-codex` |
| claude-mem | `/plugin marketplace add thedotmack/claude-mem` | `/plugin install claude-mem@thedotmack` |
| ui-ux-pro-max | `/plugin marketplace add nextlevelbuilder/ui-ux-pro-max-skill` | `/plugin install ui-ux-pro-max@ui-ux-pro-max-skill` |
| caveman | `/plugin marketplace add JuliusBrussee/caveman` | `/plugin install caveman@caveman` |
2. **Use them, don't just install them:** superpowers workflows (brainstorming before creative
work, TDD, systematic debugging, writing/executing plans), frontend-design + ui-ux-pro-max
for every UI task (§8 design system), code-review + **codex for R15** (mandatory review
gate), claude-mem for cross-session memory, context7 for current library docs
(Laravel/Livewire/Tailwind APIs — don't answer from training data), caveman optional.
3. **Anything missing → install it.** If an install fails, a plugin needs a runtime it can't
find, or **any tool requires a login/authentication** (Codex CLI login, API keys, tokens,
Gitea credentials, UniFi access) → **stop and report to the user**; never skip silently or
work around it. Because R15 depends on Codex, verify `/codex:setup` during bootstrap, not
when the first review is due.
4. Scope note: plugin runtimes (node/python for claude-mem, ui-ux-pro-max) belong to the Claude
Code session on the host — R8 (everything in containers) applies to **project** tooling, not
to session tooling.
---
## 11. Deferred decisions already researched (do not re-litigate, just don't build yet)
- **Face recognition:** postponed entirely. When it returns: **WebAuthn/passkeys**
(spatie/laravel-passkeys or asbiin/laravel-webauthn) is the security gate — browser-based
DIY liveness is not securable (virtual cameras, replay, deepfakes). Camera face recognition
only ever as comfort/personalization layer, never as the lock.
- **Zigbee:** if bought, Zigbee2MQTT + **SLZB-06 LAN coordinator** (PoE), never USB passthrough
into the Proxmox VM. Zigbee channel chosen against WLAN channels BEFORE first pairing.
- **Matter/TVs/Cast:** Node/Python sidecars speaking MQTT — never native PHP implementations.
- **Mobile:** PWA first (manifest + service worker + Web Push via
laravel-notification-channels/webpush; iOS supports Web Push for installed PWAs). Expo/React
Native only if a concrete PWA limit hurts. NativePHP rejected (runs a second Laravel on the
phone — wrong model).
---
## 12. Open decisions — ask the user before committing
1. **DB:** PostgreSQL+TimescaleDB (recommended for telemetry) vs MariaDB (Clusev consistency)?
2. **Laravel version:** 13 assumed (current, Clusev precedent) — original statement said 12.
3. **Project name:** "HomeOS" is a working title.
4. **Git remote:** Gitea like Clusev (`git.bave.dev`)? Repo must be created at bootstrap.
5. **UniFi auth:** local read-only user vs API key on the UDM — needs the user to create it.
6. **Which Ring product** the user owns (relevant only when the ring-mqtt phase starts).
---
## 13. Build order (each phase ends browser-verified, R12 + R15; §10 plugin setup comes first)
1. **Bootstrap** — compose stack up, Laravel installed in-container, git repo + `rules.md` +
`CLAUDE.md` created, tokens + fonts + layout shell (sidebar/topbar per mockup), auth + login.
2. **Domain core** — migrations for §3 tables, models, seeders with fake devices; dashboard
renders mock state (rooms, KPI row, live feed) — mock first, real hardware after.
3. **MQTT ingest** — mosquitto + mqtt-listener daemon; Shelly state flows into
`device_states`; Reverb pushes to the dashboard; toggle switches a real Shelly via
`ShellyMqttDriver`; commands audited.
4. **Discovery** — Python sidecar (mDNS/SSDP/ARP → MQTT), `discovery_findings`, "Neue Geräte"
panel with assign/ignore flow (per mockup).
5. **Presence** — UnifiClient poller (scheduler), person↔client mapping UI, debounce sweep,
presence panel + avatars live.
6. **Automations** — TCA engine (state_change + time triggers; actions: switch entity, notify),
cooldown, dry-run mode, audit integration.
7. **Polish + PWA prep** — empty states, `wire:init` loading states, breakpoints pass,
manifest groundwork.
Definition of done per phase: R12 browser proof (200, console clean, all 3 breakpoints) +
R15 Codex clean + commit on a feature branch.