Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## Ownership and scope

- This `AGENTS.md` file is written and maintained by the human.
- You (the agent) may **read** this file but must **never modify it under any circumstances**.
- You are allowed to create and edit documentation **only** under the `agent_docs/` directory in this repository.
- Treat everything inside `agent_docs/` as your documentation workspace and Obsidian vault.

### Your responsibilities:

- Use existing documentation under `agent_docs/` before guessing.
- Keep documentation in `agent_docs/` in sync with the behavior of the code.
- Record meaningful domain knowledge there.

---

## Obsidian markdown conventions

You must treat `agent_docs/` as an Obsidian vault and follow these rules strictly:

- **File names are titles**
- The file name is the note title. Do not add a level‑1 heading to restate the title.
- Use natural, human‑readable names with spaces and standard capitalisation.
- Example: `MQTT Topics and Payloads.md`, `Backend Overview.md`, `2026-03-08 Refactoring MQTT.md`.
- Never use kebab‑case or snake_case file names (e.g. `mqtt-topics-and-payloads.md` is wrong).

- **Links between notes**
- Always use Obsidian wiki links: `[[Note Name]]`.
- The note name inside `[[...]]` must exactly match the file name (without the `.md` extension).
- Example: `[[MQTT Topics and Payloads]]`, `[[Backend Overview]]`.
- Never use standard markdown links for cross‑vault references.

---

## Documentation workspace: `agent_docs/`

- The root of your docs workspace is: `agent_docs/`.
- You **must not** create or edit docs outside `agent_docs/` unless explicitly asked.
- When you need to persist knowledge, decisions, or plans, write them into `agent_docs/`, not only into code comments (except for small, local notes).

You are expected to maintain at least:

- `agent_docs/index.md` - main index of all important notes.
- `agent_docs/structure.md` - where to find things in this repository
- `agent_docs/architecture.md` - the high level architecture of the project
150 changes: 150 additions & 0 deletions agent_docs/Backend Signalling Contracts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
## Purpose

This note documents the current backend signalling contracts and where they are implemented.

## Source-of-Truth Files

- `golang/cmd/backend/server.go`
- `golang/cmd/backend/Connection.go`
- `golang/cmd/backend/MQTTSync.go`
- `golang/internal/connectionstoresync/Sync.go`
- `golang/internal/connections/Event.go`

## Active Signalling Endpoints

Registered in `golang/cmd/backend/server.go`:

- `GET /clients/{client_id}/websocket`
- `GET /clients/{client_id}/connections/{connection_id}`
- `PUT /sessions/{session_id}`
- `DELETE /sessions/{session_id}` (no-op)

Both routes are behind auth middleware under `/clients`.

## Legacy WebSocket Signalling Route

`GET /clients/{client_id}/websocket` in `server.go` + `HandleWebsocket`:

- Legacy peer-to-peer relay via backend in-memory `ClientStore`.
- Inbound JSON:
- `to_peer_id: string`
- `data: any JSON`
- Outbound JSON:
- `from_peer_id: string`
- `data: any JSON`

This is the legacy transport targeted for deprecation after MQTT migration.

## Connection Route: WebSocket Transport + MQTT Relay

`GET /clients/{client_id}/connections/{connection_id}` in `Connection.go`:

- Upgrades to WebSocket.
- Publishes connect status to:
- `accounts/{account_id}/clients/{client_id}/status`
- payload: `{"type":"connected","connection_id":"...","at_millis":<int>}`
- Publishes disconnect status to:
- `accounts/{account_id}/clients/{client_id}/status`
- payload: `{"type":"disconnected","connection_id":"...","disconnected":{"reason":"clean|unexpected"},"at_millis":<int>}`
- Incoming websocket payload from client:
- `{"type":"signal","signal":{"to_connection_id":"...","data":<json>}}`
- backend republishes to MQTT topic:
- `accounts/{account_id}/clients/{to_client_id|sender_client_id}/webrtc_inbox`
- payload: `{"from_client_id":"...","from_connection_id":"...","connection_id":"...","data":<json>}`
- Keepalive:
- websocket ping/pong with 30s ping period
- pong timeout 10s
- Active websocket connections are tracked in-memory via `ConnectionHub`.

## MQTT Topics

Defined in `Connection.go` and `MQTTSync.go`:

- Status topic format:
- `accounts/%s/clients/%s/status`
- WebRTC inbox:
- `accounts/%s/clients/%s/webrtc_inbox`
- Baby stations:
- `accounts/%s/baby_stations`
- Parent stations:
- `accounts/%s/parent_stations`
- Control inbox:
- `accounts/%s/clients/%s/control_inbox`

Status sync subscriber in `connectionstoresync/Sync.go`:

- Subscribes wildcard:
- `accounts/+/clients/+/status`
- Parses account and client IDs from topic, then uses payload `connection_id`.

## MQTT Status Payloads

Published from `Connection.go` on connect/disconnect:

- Connected:
- `{"type":"connected","connection_id":"...","at_millis":<int>}`
- Disconnected:
- `{"type":"disconnected","connection_id":"...","disconnected":{"reason":"clean|unexpected"},"at_millis":<int>}`

Event payloads are defined in `golang/internal/connections/Event.go`.

## Session Ingress/Egress

- `PUT /sessions/{session_id}` now publishes session announcements to:
- `accounts/{account_id}/baby_stations`
- `PUT /sessions/{session_id}` continues to accept the legacy payload only:
- `id`, `name`, `host_connection_id`, `started_at`
- Backend derives MQTT announcement fields from the legacy request body:
- `session_id` comes from path/body `id`
- `connection_id` comes from `host_connection_id`
- `started_at_millis` comes from `started_at`
- Backend resolves `client_id` from the in-memory connection map (`connection_id -> client_id`).
- If mapping is not available yet, backend queues session start in-memory and publishes after websocket connect registers that connection.
- Queue policy: per-account LRU with max 2 pending session starts.
- Old-client compatibility detail:
- after publishing the MQTT announcement, backend also appends `session.started` locally before returning from `PUT /sessions/{session_id}`
- backend still subscribes its own `baby_stations` topic; that later delivery is treated as a duplicate by `sessionstartdecider`
- this avoids a race where old clients could call stop before the backend had observed its own published start announcement
- `DELETE /sessions/{session_id}` appends `session.ended` using the real session id if the session is still active.
- `MQTTSync` subscribes `accounts/+/baby_stations` and appends `session.started` with idempotency via `sessionstartdecider`.
- Clean disconnect compatibility:
- `connectionstoresync` still appends `client.disconnected`
- if disconnect reason is `clean` and the connection owns an active session, backend also appends `session.ended`
- this preserves existing frontend session teardown behavior without changing frontend code
- `sessionstartdecider` releases a connection id for reuse after `session.ended`, so the same long-lived frontend connection id can start a later session again

## Identity Model

- `account_id`:
- stable account scope
- `client_id`:
- stable device/browser identity
- `session_id`:
- explicit baby-station session lifecycle id
- preserved for compatibility and event history
- `connection_id`:
- runtime identity for legacy websocket transport
- survives reconnects of the same runtime
- still needed as a bridge while websocket clients coexist with MQTT-native clients

## Parent Station Discovery

- `MQTTSync` subscribes `accounts/+/parent_stations`.
- Expected payload includes `client_id` of requesting parent station.
- For each active baby station snapshot entry, backend publishes control message:
- topic: `accounts/{account_id}/clients/{client_id}/control_inbox`
- payload:
- `{"type":"baby_station_announcement","at_millis":<int>,"baby_station_announcement":{"client_id":"...","connection_id":"...","name":"...","started_at_millis":<int>}}`

Client status subscription remains in `connectionstoresync`:

- `accounts/+/clients/+/status`

## Auth and Token Durations

Configured in `golang/cmd/backend/server.go`:

- Access token duration: `1 * time.Hour`
- Refresh token duration: `30 * 24 * time.Hour`

Refresh token scope handling is in `golang/internal/accounts/Handlers.go`.
32 changes: 32 additions & 0 deletions agent_docs/Frontend Service Architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## Frontend Service Architecture

The frontend codebase generally works best when domain behavior lives in services and React stays focused on rendering.

## Preferred Split

- Services own domain state and transitions.
- Services own HTTP calls and long-running async workflows.
- Services own timers, retries, event subscriptions, and derived domain status.
- React components render state and invoke service commands.
- Hooks are thin adapters that subscribe React to service state.

## What Belongs in React

Keep state in React only when it is truly view-local:

- modal open or closed
- active tab
- local draft input before submit
- focus, hover, or animation state

If the state affects workflow, survives across screens, or mirrors backend/application behavior, it should usually move into a service.

## Existing Repository Patterns

- `frontend/src/services/Service.ts` provides a common base for stateful frontend services.
- `frontend/src/hooks/useServiceState.ts` is the standard hook shape for subscribing React to service state.
- `frontend/src/services/ParentStation/CountUpTimer.ts` is a concrete example where timer behavior is outside React.

## Practical Rule

If a React component starts accumulating fetch calls, retry logic, state-machine transitions, or several interdependent domain `useState` values, stop and extract a service.
14 changes: 14 additions & 0 deletions agent_docs/Open Questions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
## Open Questions

These are useful for tightening architecture docs and future onboarding.

## High Priority

- What is the endpoint removal order for legacy WebSocket routes?
- What is the intended deprecation timeline and compatibility policy for old cached PWAs?

## Useful Clarifications

- Which backend endpoints are considered stable public contract vs internal implementation detail?
- What anonymous analytics events are currently emitted from frontend and backend?
- Are there explicit SLOs for connection setup success/reconnect time in local-network conditions?
43 changes: 43 additions & 0 deletions agent_docs/Product and Privacy Model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
## Product Model

BeddyBytes is for parents monitoring children during sleep using existing devices with modern browsers.

Two runtime roles exist:

- Baby Station: captures audio/video and streams.
- Parent Station: connects to and monitors one or more baby station sessions.

## Privacy Model

Privacy priorities:

- Primary: privacy.
- Secondary: simplicity.

Privacy guarantees (intended behavior):

- No video/audio leaves the home network.
- Backend never receives media payloads.
- No server-side recording.
- Analytics are anonymous.

Data collected by service:

- Email address (account identity).
- Payment processing via Stripe (one-off checkout, no subscription model).

## Recording Behavior

- Recording is initiated on Parent Station.
- Browser `MediaRecorder` captures incoming stream.
- Recording is downloaded immediately to local device.
- Recording is never uploaded to backend.

## Product Constraints

- No STUN/TURN means no off-LAN fallback for media.
- Connection intentionally fails when local-network conditions are not met.

## Operational Reality

Because this is a PWA, users may run cached app versions for weeks before refresh/update. Any migration strategy must tolerate mixed-version clients.
37 changes: 37 additions & 0 deletions agent_docs/Signalling Migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## Current State

- Legacy signalling path uses backend WebSockets.
- MQTT migration is active for backend ingress/egress topic contracts.
- Backend now:
- publishes client status to `accounts/{account_id}/clients/{client_id}/status`
- publishes WebRTC signalling to `accounts/{account_id}/clients/{client_id}/webrtc_inbox`
- subscribes `baby_stations`, `parent_stations`, `webrtc_inbox`, and status wildcard topics
- keeps `PUT /sessions/{session_id}` on the legacy request payload and derives the MQTT announcement fields server-side, including `session_id`
- queues `PUT /sessions/{session_id}` session starts in-memory when `client_id` is unresolved and flushes on websocket connect
- appends `session.ended` on explicit delete for old clients and on clean disconnect for compatibility with future clients that end sessions by disconnecting
- Current direction:
- `session_id` stays
- `connection_id` is being reduced to compatibility/runtime identity rather than long-term primary identity
- Current implementation is backend-centric; no finalized frontend MQTT path yet.

See [[Backend Signalling Contracts]] for current endpoint/topic details.

## Direction

- Move clients to direct MQTT-first signalling behavior.
- Deprecate WebSocket endpoints.
- Remove WebSocket signalling paths after migration is complete and safe.

## Risk Factors

- PWA caching causes version skew across active clients.
- Mixed fleets (WebSocket-era and MQTT-era clients) must interoperate during rollout.
- Session continuity and reconnection behavior must remain stable while transports coexist.

## Documentation Impact

As migration advances, update:

- [[architecture]] for control-plane transport changes.
- [[structure]] for source-of-truth files handling signalling.
- [[Backend Signalling Contracts]] as backend transport behavior changes.
13 changes: 13 additions & 0 deletions agent_docs/Working Agreement with Human.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
The human prefers immediate escalation when implementation issues arise.

## Escalation Preference

- If a refactor exposes missing data, ordering assumptions, or contract ambiguity, raise it immediately.
- Do not silently patch around issues.
- Present the issue and let the human choose the resolution approach.

## Current Applied Example (2026-03-10)

- MQTT `BabyStationAnnouncement` requires `client_id`, but legacy `PUT /sessions/{session_id}` payload does not contain it.
- Frontend Baby Station currently calls session create before opening the websocket connection, so connection-to-client mapping is not guaranteed at create time.
- Human-selected resolution: maintain an in-memory connection map (`connection_id` -> `client_id`) and handle session-create timing accordingly.
Loading