Browse Source

feat(docker): package laundry status service with Docker and README

main
Domagoj Zecevic 3 weeks ago
parent
commit
6a67a5864e
  1. 17
      .ai/HANDOFF.md
  2. 13
      .ai/REVIEW.md
  3. 2
      .ai/TASKS.md
  4. 17
      .dockerignore
  5. 13
      Dockerfile
  6. 142
      README.md
  7. 13
      docker-compose.yml

17
.ai/HANDOFF.md

@ -43,3 +43,20 @@ Append-only role handoff log. Each role adds one entry when its step is complete
### T-002 — commit_task (all_task) — 2026-10-12T11:17:06Z ### T-002 — commit_task (all_task) — 2026-10-12T11:17:06Z
- Status: done. Commit message read from the T-002 next_task entry. - Status: done. Commit message read from the T-002 next_task entry.
### T-003 — next_task (dev/all_task) — 2026-10-12T11:22:18Z
| Field | Value |
|-------|-------|
| Agent | codex |
| Summary | Added a static non-root Docker image, Compose configuration, build exclusions, and deployment/status documentation while preserving the AI Workflow section |
| Validation | fmt/vet/test PASS; Docker build/runtime and Compose overrides PASS; review PASS |
| Commit | feat(docker): package laundry status service with Docker and README |
| Next Role | dev commit |
- PR limitation: `aide pr --dry-run` refuses main; the repository instructions require staying on the current branch.
### T-003 — commit_task (all_task) — 2026-10-12T11:22:18Z
- Status: done. Commit message read from the T-003 next_task entry.
- All three tasks completed and reviewed; one separate Conventional Commit per task.

13
.ai/REVIEW.md

@ -22,3 +22,16 @@
- Browser: 360px viewport, all four messages/colors, white background, large status, black readings, 47.70W/-- formatting, 60s timer, failed fetch/recovery, and no horizontal overflow PASS. Screenshot: `/tmp/mqtt-washingmachine-mobile.png`. - Browser: 360px viewport, all four messages/colors, white background, large status, black readings, 47.70W/-- formatting, 60s timer, failed fetch/recovery, and no horizontal overflow PASS. Screenshot: `/tmp/mqtt-washingmachine-mobile.png`.
- Implementation smoke retry: Docker reassigned a dynamically published broker port after restart; fixed the temporary harness to reserve an explicit port. All assertions retained; rerun passed. No product-code or review validation retries. - Implementation smoke retry: Docker reassigned a dynamically published broker port after restart; fixed the temporary harness to reserve an explicit port. All assertions retained; rerun passed. No product-code or review validation retries.
- Verdict: PASS, approved for the per-task commit under `all_task`. - Verdict: PASS, approved for the per-task commit under `all_task`.
## Task: T-003
### Review Round 1 — 2026-10-12T11:22:18Z
- Findings: no required fixes; multi-stage static image, Compose variables, build context exclusions, and README match the plan. No application behavior changed in this task.
- Validation: `go fmt ./...`, `go vet ./...`, `go test ./...`, `docker compose config --quiet`, Docker build, and diff checks PASS.
- Packaging smoke: `node /tmp/mqtt-washingmachine-docker-smoke.mjs` PASS. Verified non-root scratch image (6,754,466 bytes), custom HTTP port/broker/topic/sample interval, actual Docker-network MQTT sampling, embedded page/API, missing samples, graceful stop with exit 0, rejection of zero duration, and all Compose environment overrides. Temporary containers/network removed.
- Documentation: original README content from `## AI Workflow` onward is byte-for-byte unchanged. Setup, rules, all six env vars, TelePeriod, API, manual validation, and limitations documented.
- E2E: NOT_CONFIGURED as a repository command; temporary packaging smoke PASS. T-002 browser/MQTT smoke also passed.
- Environment retry during implementation: Docker build metadata was read-only under the sandbox; approved rerun built successfully. No review validation retries or weakened assertions.
- Workflow limitation: `aide pr --dry-run` rejected the current `main` branch. AGENTS.md requires working in the current branch, so no branch change or PR creation was performed.
- Verdict: PASS, approved for the per-task commit under `all_task`.

2
.ai/TASKS.md

@ -22,4 +22,4 @@ Command expectations:
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| T-001 | Status evaluation logic (3-sample window, 3.00 W threshold, waiting/incomplete/running/done) and Tasmota payload parsing, with tests first | done | All state/boundary tests pass (2.99 idle, 3.00 active, gap -> incomplete, <3 samples -> waiting); payload parser handles valid, missing ENERGY/Power, and invalid JSON; fmt/vet/test pass | 2026-10-12T11:03:20Z: tests first, fmt/vet/test PASS; review PASS | none | | T-001 | Status evaluation logic (3-sample window, 3.00 W threshold, waiting/incomplete/running/done) and Tasmota payload parsing, with tests first | done | All state/boundary tests pass (2.99 idle, 3.00 active, gap -> incomplete, <3 samples -> waiting); payload parser handles valid, missing ENERGY/Power, and invalid JSON; fmt/vet/test pass | 2026-10-12T11:03:20Z: tests first, fmt/vet/test PASS; review PASS | none |
| T-002 | Per-minute sampler, MQTT subscription, HTTP `/api/status` and phone-first page, `main.go` env config | done | Sampler records valid/missing samples per tick; `/api/status` matches JSON contract; page has white background, large status, `47.70W` readings, viewport meta, 60 s refresh; fmt/vet/test pass | 2026-10-12T11:17:06Z: fmt/vet/test/race PASS; MQTT and 360px Chrome smoke PASS | none | | T-002 | Per-minute sampler, MQTT subscription, HTTP `/api/status` and phone-first page, `main.go` env config | done | Sampler records valid/missing samples per tick; `/api/status` matches JSON contract; page has white background, large status, `47.70W` readings, viewport meta, 60 s refresh; fmt/vet/test pass | 2026-10-12T11:17:06Z: fmt/vet/test/race PASS; MQTT and 360px Chrome smoke PASS | none |
| T-003 | Dockerfile, docker-compose example, .dockerignore, README documentation | ready_for_implement | Image builds (or noted if Docker unavailable); README documents rules, env vars, deployment, limitations; existing AI Workflow section untouched | n/a | implement | | T-003 | Dockerfile, docker-compose example, .dockerignore, README documentation | done | Image builds (or noted if Docker unavailable); README documents rules, env vars, deployment, limitations; existing AI Workflow section untouched | 2026-10-12T11:22:18Z: fmt/vet/test PASS; Docker build/runtime PASS; README workflow preserved | none |

17
.dockerignore

@ -0,0 +1,17 @@
.git
.ai
.agents
.codex
.claude
.env
.env.*
bin
build
dist
node_modules
vendor
target
*.test
*.out
*.exe
*.md

13
Dockerfile

@ -0,0 +1,13 @@
FROM golang:1.26.8-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY main.go ./
COPY internal ./internal
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /laundry-status .
FROM scratch
COPY --from=builder /laundry-status /laundry-status
USER 65532:65532
EXPOSE 8080
ENTRYPOINT ["/laundry-status"]

142
README.md

@ -2,11 +2,149 @@
## Overview ## Overview
TODO: Describe this project. A small Go service that shows whether the laundry is done using the Tasmota plug's
MQTT `ENERGY.Power` readings. The phone-first page has a white background, a large
status message, and the last three power readings, such as `47.70W`. Missing
readings appear as `--`.
The backend runs continuously, even with no page open. Every 60 seconds it records
the latest valid message received since the previous tick, or a missing sample if
none arrived. It keeps only the last three samples in memory, oldest first. The
page fetches the current status on load and every 60 seconds.
### Status rules
Rules apply in this order:
| Condition | Message | Color |
|-----------|---------|-------|
| Fewer than three samples recorded since startup | Waiting for data... | Grey |
| Any of the last three samples is missing | Incomplete data | Grey |
| Any sample is at least 3.00 W | Laundry not yet done | Red |
| All three samples are below 3.00 W | Laundry done | Default text color |
Exactly **3.00 W is active**; `2.99 W` and `0 W` are idle. Malformed JSON and
payloads without numeric `ENERGY.Power` are logged and ignored; they do not replace
the last valid message or make a missing interval valid. If the HTTP request fails,
the page shows a grey "Status unavailable" message and retries at the next refresh.
## Getting Started ## Getting Started
TODO: Add setup instructions. ### Docker Compose
Install Docker with the Compose plugin on a server that can reach your MQTT
broker. From this repository, run:
```bash
docker compose up --build -d
docker compose logs -f laundry-status
```
Open `http://<your-server>:8080/` on a phone connected to your home Wi-Fi. By
default the service connects to `192.168.1.12:1883` and subscribes to
`tele/tasmota_washingmashine_C21183/SENSOR`. No broker is included in the Compose
file; it connects to your existing one.
To override settings, export the environment variables below or place them in a
local `.env` file before running Compose. For example:
```bash
MQTT_BROKER=tcp://192.168.1.12:1883 docker compose up --build -d
```
Keep credential files local. The Docker build context excludes `.env` files.
The container runs as an unprivileged user, with a static Go binary and embedded
page; no volume or database is required. Stop it with `docker compose down`.
### Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `MQTT_BROKER` | `tcp://192.168.1.12:1883` | Broker URL, including scheme and port |
| `MQTT_TOPIC` | `tele/tasmota_washingmashine_C21183/SENSOR` | Tasmota sensor topic |
| `MQTT_USERNAME` | Empty | Optional MQTT username |
| `MQTT_PASSWORD` | Empty | Optional MQTT password |
| `HTTP_ADDR` | `:8080` | HTTP listen address |
| `SAMPLE_INTERVAL` | `60s` | Positive Go duration between samples, for example `60s` or `1m` |
Empty variables use the defaults. In Docker, keep the HTTP listen address on all
interfaces (for example `:8080`). If you change its port, also update the target
port in the Compose `ports` mapping. To change only the published host port, edit
the left side of `8080:8080` and leave `HTTP_ADDR` unchanged.
Changing `SAMPLE_INTERVAL` affects backend sampling and the three-sample window;
the browser still refreshes every 60 seconds. Invalid or nonpositive intervals
stop startup with an error. The service retries an unavailable broker in the
background and subscribes again after reconnecting. HTTP remains available during
broker outages; intervals without valid messages become missing data.
### Tasmota setup
Configure the plug to publish sensor telemetry to the topic above, with a payload
containing a numeric power measurement, for example:
```json
{"ENERGY":{"Power":139.60}}
```
The plug currently uses `TelePeriod 10` (about six messages per minute). Sampling
does not depend on that exact setting: any telemetry interval works, but an
interval longer than `SAMPLE_INTERVAL` can leave gaps and show "Incomplete data".
Only the latest message within each sampling interval is used; this is not an
average of all measurements.
### Run and validate locally
Install Go 1.26 or later, then run:
```bash
go run .
```
Validation commands:
```bash
go fmt ./...
go vet ./...
go test ./...
go test -race ./...
docker build -t laundry-status .
```
No automated E2E command is configured. For a manual MQTT smoke check, run the
service against a test broker and topic with `SAMPLE_INTERVAL=1s`. Use
`mosquitto_pub` to send a message repeatedly, at least once per second:
```bash
mosquitto_pub -h <test-broker> -t <test-topic> -m '{"ENERGY":{"Power":2.99}}'
```
After three valid samples, `/api/status` should report `done`. Publish `3.00` to
get `running`, then stop publishing for a sample interval to get `incomplete`.
Reload the page to inspect each result immediately. Check the page at 360 px width
for readable text and no horizontal scrolling.
### Status API
`GET /api/status` returns JSON with a state, display message, and zero to three
readings ordered oldest first. Responses are not cached. For example:
```json
{"state":"incomplete","message":"Incomplete data","readings":[{"power":47.7,"valid":true},{"power":0,"valid":false},{"power":2.99,"valid":true}]}
```
Use `valid` to distinguish a missing sample from a real zero-watt reading. Startup
returns `{"state":"waiting","message":"Waiting for data...","readings":[]}`.
### Limitations
- In-memory state only: a restart clears the window. With the default interval,
the service shows "Waiting for data..." for approximately three minutes.
- A single device and three samples only; no persistent history or notifications.
- This is an idle-power heuristic. An unused machine or a sufficiently long pause
below 3 W also results in "Laundry done"; it does not track wash-cycle phases.
- Use on a trusted home LAN only. HTTP has no TLS or authentication; keep the
published port off the public internet.
## AI Workflow ## AI Workflow

13
docker-compose.yml

@ -0,0 +1,13 @@
services:
laundry-status:
build: .
restart: unless-stopped
ports:
- "8080:8080"
environment:
MQTT_BROKER: "${MQTT_BROKER:-tcp://192.168.1.12:1883}"
MQTT_TOPIC: "${MQTT_TOPIC:-tele/tasmota_washingmashine_C21183/SENSOR}"
MQTT_USERNAME: "${MQTT_USERNAME:-}"
MQTT_PASSWORD: "${MQTT_PASSWORD:-}"
HTTP_ADDR: "${HTTP_ADDR:-:8080}"
SAMPLE_INTERVAL: "${SAMPLE_INTERVAL:-60s}"
Loading…
Cancel
Save