Ver Fonte

M1(8/8): M1 verification doc + SPEC update + PROMPT log

- M1_VERIFICATION.md: 9-step smoke test
  1. docker compose up -d (seed runs first as one-shot sidecar)
  2. verify the seed populated the 3 tables
  3. fakefcmd /health
  4. signed POST -> 202
  5. deliveries row appears within ~2s
  6. fakefcmd log shows the corresponding send
  7. M0 protections still fire (401/413)
  8. loadgen burst test
  9. dedupe end-to-end
- SPEC §23 M1 row: 'code complete 2026-06-13 (awaiting live smoke)'
- README status line updated
- PROMPT: M1 entry, port-convention entry
- 5 commits pushed for M1: 264d284..9cf68a1

Total state at end of M1 code-complete:
  - 4 services + loadgen + seed + fakefcmd (7 binaries)
  - 23+ sections of SPEC, ARCHITECTURE, M0/M1 verification
  - All unit tests green, all mermaid diagrams valid
Luis Rosales há 2 meses atrás
pai
commit
d5a211622a
4 ficheiros alterados com 251 adições e 5 exclusões
  1. 189 0
      M1_VERIFICATION.md
  2. 55 0
      PROMPT.md
  3. 6 4
      README.md
  4. 1 1
      SPEC.md

+ 189 - 0
M1_VERIFICATION.md

@@ -0,0 +1,189 @@
+# M1 Verification
+
+How to prove the M1 milestone is done.
+
+## What "M1 done" means
+
+SPEC §23: "HTTP POST ingest end-to-end — send a signed webhook →
+FCM test push to a fake device; layers 1, 3, 4, 5 in."
+
+Concretely, the **M1 exit criteria** are:
+
+1. `docker compose up -d` brings up the full M0+M1 stack (the new
+   `seed` sidecar runs once, applies migrations + seed, exits).
+2. `seed` finishes `ok` and the `companies`/`individuals`/
+   `fcm_tokens` tables are populated.
+3. `fakefcmd` is healthy on `:8820` (its `/health` returns
+   `{"status":"ok",...}`).
+4. A signed POST to ingestd on `:8800` returns `202` (M0 still works).
+5. Within ~2s, a row appears in the `deliveries` table with
+   `status='sent'` and `channel='fcm'`.
+6. fakefcmd's stderr log shows the corresponding `fakefcmd send`
+   line with the right `alert_id`.
+7. M0 protections still fire: bad signature → 401, payload too large
+   → 413, invalid JSON → 400, rate-limit on burst → 429.
+
+## Step-by-step
+
+### 1. Bring up the stack
+
+```bash
+cd /root/.openclaw/workspace/broad-announce
+docker compose build         # build all images
+docker compose up -d seed    # run migrations + seed
+docker compose up -d         # bring up the rest
+docker compose ps            # all "Up (healthy)" except loadgen
+```
+
+Expected: the `seed` container exits 0 within ~5s after Postgres is
+healthy. The other services come up.
+
+### 2. Verify the seed
+
+```bash
+docker compose logs seed
+```
+
+Expected last line: `seed: ok`.
+
+```bash
+docker compose exec postgres psql -U ba -d ba -c \
+  "SELECT id, name FROM companies; SELECT id, full_name FROM individuals; SELECT count(*) FROM fcm_tokens;"
+```
+
+Expected:
+```
+   id    |   name
+---------+----------
+ acme-001 | Acme Corp
+   id       |   full_name
+------------+---------------
+ ind-acme-001 | Alice Operator
+ count
+-------
+     1
+```
+
+### 3. fakefcmd health
+
+```bash
+curl -fsS http://localhost:8820/health
+```
+
+Expected: `{"failed":0,"received":0,"service":"fakefcmd","status":"ok"}`.
+
+### 4. Signed POST to ingestd
+
+```bash
+SECRET='s3cret-acme'
+BODY='{"company_id":"acme-001","source_id":"prom-prod","severity":"critical","category":"storage","title":"Disk full on db-prod-03","body":"92% used","data":{"host":"db-prod-03"},"dedupe_key":"m1-test-001"}'
+TS=$(date +%s)
+SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')
+
+curl -fsS -X POST http://localhost:8800/v1/ingest \
+  -H "Content-Type: application/json" \
+  -H "X-BA-Signature: t=$TS,v1=$SIG" \
+  --data "$BODY"
+```
+
+Expected: `{"alert_id":"...","dedupe_count":1,"received_at":"..."}`.
+
+### 5. delivery row appears
+
+Within ~2s:
+
+```bash
+docker compose exec postgres psql -U ba -d ba -c \
+  "SELECT alert_id, individual_id, channel, target, status, attempts
+   FROM deliveries ORDER BY id DESC LIMIT 5;"
+```
+
+Expected: one row with `channel='fcm'`, `target='fake-fcm-token-acme-alice-001'`,
+`status='sent'`, `attempts=1`, `alert_id` matching step 4.
+
+### 6. fakefcmd logged the send
+
+```bash
+docker compose logs fakefcmd | tail -3
+```
+
+Expected last line:
+```
+fakefcmd send path=/v1/projects/fakefcmd/messages:send token=fake-fcm-token-acme-alice-001…
+  title=Disk full on db-prod-03 data_keys=8 alert_id=018f…
+```
+
+### 7. M0 protections still fire (regression check)
+
+Bad signature:
+```bash
+curl -i -X POST http://localhost:8800/v1/ingest \
+  -H "Content-Type: application/json" \
+  -H "X-BA-Signature: t=1,v1=deadbeef" \
+  --data "$BODY"
+```
+Expected: `401 {"error":"bad_signature"}`.
+
+Payload too large:
+```bash
+python3 -c 'import sys; sys.stdout.write("{\"x\":\"" + "a"*300000 + "\"}")' \
+  | curl -i -X POST http://localhost:8800/v1/ingest \
+      -H "Content-Type: application/json" --data-binary @-
+```
+Expected: `413 {"error":"payload_too_large"}`.
+
+### 8. Burst through loadgen
+
+```bash
+docker compose --profile loadgen up loadgen-http
+```
+
+Expected: 30s of 50/s; 1500 messages accepted; ~1500 deliveries
+written (1:1 with the single seeded token in M1); no failures.
+
+```bash
+docker compose exec postgres psql -U ba -d ba -c \
+  "SELECT status, count(*) FROM deliveries GROUP BY status;"
+```
+
+Expected: ~1500 `sent`, 0 `failed`, 0 `dlq`.
+
+### 9. Dedupe still works end-to-end
+
+Send the same `dedupe_key` twice in quick succession. Both
+deliveries succeed (one per the dedupe window), but
+`dedupe_count` in the 202 response is 2 on the second call.
+
+## What's NOT in M1 (the honest list)
+
+- **Recipient resolution rules** (subscriptions, opt-in per
+  source/severity, quiet hours, routing rules). M1 is broadcast.
+  **M2**.
+- **Real FCM** (Google service account, real HTTP v1 endpoint).
+  M1 talks to fakefcmd. **M3**.
+- **Other channels** (Telegram, SMS, email, Slack, Teams,
+  webhook). **M3+**.
+- **Retry with exponential backoff** (SPEC §9). M1 marks
+  terminal status on the first attempt. **M3** with the per-channel
+  retry worker.
+- **DLQ table reads, replay UI**. **M8**.
+- **Per-source `allowed_targets`** in the schema. **M2**.
+- **ClickHouse archive, Timescale hypertables** for `alerts` and
+  `deliveries`. **M7**.
+- **HMAC secret in DB** (M1 reads from env). **M2**.
+- **Multi-tenant query filters** at every query site. M1 has
+  `WHERE company_id = $1` in routing. M2 audits every other query.
+- **Per-IP concurrency cap, circuit breaker, quarantine** (M0
+  honest scope). **M5 / M9**.
+
+## If something fails
+
+| symptom | first thing to check |
+|---|---|
+| seed: `connect: dial tcp ...:5432: connect: connection refused` | is postgres healthy? `docker compose ps postgres` |
+| seed: `extension pgcrypto does not exist` | wrong image; we use `timescale/timescaledb:latest-pg16` which has it |
+| ingestd returns 401 on a "valid" call | openssl pipe vs Go HMAC mismatch; recheck `printf '%s.%s'` |
+| no rows in `deliveries` | is routerd healthy? `docker compose logs routerd` — it might not have created the consumer yet |
+| `deliveries` row but `status=failed` | is fakefcmd up? `curl localhost:8820/health` |
+| `last_error` says `status 503: UNAVAILABLE` | fakefcmd fault injection; check its log |
+| consumer never picks up messages | NATS JetStream state retained from a previous run with different consumer config; `docker compose down -v` to wipe |

+ 55 - 0
PROMPT.md

@@ -151,3 +151,58 @@ What's NOT in M0 (and not supposed to be):
 Module path: `git3.techno-world.net/lrosales/broad-announce`.
 Loadgen module path: `git3.techno-world.net/lrosales/broad-announce/loadgen`.
 All pushed: 7cd922c..49b2dba on master.
+
+**2026-06-13 — Port convention**
+
+Project rule: app HTTP services use 8800–8899 (ingestd 8800,
+routerd 8801, deliverd 8802, admind 8803, loadgen metrics 8891,
+fakefcmd 8820). Canonical ports stay (5432, 4222, 6379, 1883,
+9090, 3000). Reason: 8080+ collides with workstation tooling.
+SPEC §18 now has a port-conventions sub-section. Commit: d76aa0b.
+
+**2026-06-13 — M1 code-complete (8 commits, awaiting live smoke)**
+
+What landed:
+- `migrations/001_init.up.sql` — companies, individuals,
+  fcm_tokens (the M1 minimum schema; full SPEC §4 lands in M2
+  as additional migrations)
+- `migrations/002_deliveries.up.sql` — deliveries table
+  (status: pending|sent|failed|dlq; payload jsonb for M8 replay)
+- `migrations/seed.sql` — idempotent; 1 company, 1 individual,
+  1 token
+- `internal/postgres` — pgxpool wrapper with retry-on-startup
+- `cmd/seed` — applies *.up.sql in lexical order, then seed.sql
+- `testfakes/fakefcmd` — 70 lines, /health + /v1/.../messages:send,
+  --fail-rate knob
+- `internal/routing` — Resolver with ResolveTokens (M1 broadcast
+  via single SQL join; M2 swaps for the rules engine)
+- `cmd/routerd` — M1 main: subscribes to alerts.>, resolves
+  recipients, publishes one deliveries.fcm.<co> per token
+- `cmd/deliverd` — M1 main: subscribes to deliveries.fcm.>,
+  builds FCM HTTP v1 message body (M3 swap is a no-op at this
+  layer), posts to BA_FAKECMD_URL, writes a deliveries row
+- Dockerfile builds 7 binaries
+- docker-compose adds fakefcmd + seed (one-shot sidecar),
+  wires BA_FAKECMD_URL into deliverd
+- M1_VERIFICATION.md: 9-step smoke test
+
+What we agreed to defer (per the user):
+- Q1 seed: 1 company / 1 individual / 1 token — DONE
+- Q2 router: M1 broadcast (no subscriptions) — DONE
+- Q3 testfakes: only fakefcmd for M1 — DONE
+- Q4 migrations: 3 tables in M1, expand in M2 — DONE
+
+What's NOT in M1 (and not supposed to be):
+- subscriptions, quiet hours, routing rules (M2)
+- per-source allowed_targets (M2)
+- real FCM (M3)
+- other delivery channels (M3+)
+- retry + DLQ from SPEC §9 (M3)
+- ClickHouse / Timescale hypertables (M7)
+- HMAC secret in DB (M2)
+- per-IP cap, circuit breaker, quarantine (M5/M9)
+
+Pushed: 264d284..9cf68a1 on master (5 commits for M1 code,
+plus the d76aa0b port shift).
+**The user still has to actually run `docker compose up` and the
+M1 verification steps before M1 is fully done.**

+ 6 - 4
README.md

@@ -7,10 +7,12 @@ normalizes them, resolves recipients via `companies` → `groups` →
 Telegram, SMS, email, voice, Slack, MS Teams, and arbitrary outbound
 webhooks.
 
-> **Status**: M0 shipped 2026-06-13. Single-host docker-compose stack
-> + 4 Go services + loadgen-http + alert schema. See
-> `M0_VERIFICATION.md` for the smoke test. Spec is in `SPEC.md`,
-> diagrams in `ARCHITECTURE.md`, build log in `PROMPT.md`.
+> **Status**: M0 + M1 code-complete 2026-06-13. M0 is the single-host
+> docker-compose stack + 4 Go services + loadgen-http + alert schema.
+> M1 is the end-to-end: signed webhook → broker → router → deliverd
+> → fakefcmd. See `M0_VERIFICATION.md` and `M1_VERIFICATION.md`
+> for the smoke tests. Spec is in `SPEC.md`, diagrams in
+> `ARCHITECTURE.md`, build log in `PROMPT.md`.
 
 ## v1 in one paragraph
 

+ 1 - 1
SPEC.md

@@ -883,7 +883,7 @@ ingestd_rejection_latency_seconds_bucket{transport,reason} histogram
 | # | milestone | exit criterion |
 |---|---|---|
 | M0 | Skeleton + docker-compose up | Postgres + NATS + Redis up, all 4 services start, /health green | **✅ shipped 2026-06-13** |
-| M1 | HTTP POST ingest end-to-end | send a signed webhook → FCM test push to a fake device; layers 1, 3, 4, 5 in |
+| M1 | HTTP POST ingest end-to-end | send a signed webhook → FCM test push to a fake device; layers 1, 3, 4, 5 in | **✅ code complete 2026-06-13** (awaiting live smoke test) |
 | M2 | Recipient resolution | per-source `allowed_targets` honored, subscriptions applied |
 | M3 | Telegram delivery + bot commands | user can `/subscribe` and receive an alert via Telegram |
 | M4 | MQTT ingest | EMQX up, QoS 1, per-company topic ACLs |