# BOARD — every open piece of work (2026-09-25)

One line per item: what, why, and the proof that closes it. Closed items move to `plans/closed.csv`.
Capped at 150 lines by test. UNCONFIRMED items stand only once the operator keeps them.

## Now, in order
0. **Audit and fix the whole app** (operator, 2026-09-25): everything, broken or seemingly broken, UI/UX,
   functionality, security, code quality; this board's items are leads, never its boundary. Main plus at most
   five child lanes. Proof: every finding fixed or boarded with evidence; every full suite and typecheck green.
1. **WhatsApp fully working, with its capabilities and integrations** (his top priority, 2026-09-25).
   Blocked by Meta: WABA 141008 not active (HANDOFF 1). Proof left: the AI answers a real question, a
   paused contact gets nothing, and a flow reaches a closed window with a template.
2. **Documentation rebuild.** Finish re-confirming rulings; write `specs/screens/` from the
   live app and walk them through with him. Proof: every screen spec carries his date.
3. **Flow node positions** (`e65696f2`): never seen on prod. Proof: a dragged step survives a reload.
4. **Fold `design/CONVENTIONS.md` and `design/craft.md` into the screen specs**, then delete both (ruling 35).
5. **The Sagepilot rework** (his directive 2026-09-26; plan and proof per stage: the Claude Doc "Channels Manager:
   improve and extend"). Live: stages 0 to 3, 2b (roles, `02283d74`) and 4 (draft and publish, manage mode, `601e40e3`).
   Next: item 8, then stage 5. Stage 4 left: Knowledge sources (file, URL, site crawl, an AI summary per card), the Playground's
   attach; the Log lists Behaviour's publishes only (`prompt-log.service.ts`); manage mode spend has no trace row.
6. **Comment moderation by one score** (his design 2026-09-27): a 0-1 score (how sure removing is right) and a reason on
   a written scale; two lines pick leave, hide or delete; retry, a second model, a keyword hide, "not checked by AI". Live,
   switched off (`838b3ad2`). His test 09-27 (0.40/0.70, $0.0073): "Is this a scam.." and noise like "Xx" must hide: the scale now lists them, scores snap to anchor points (`bf7ef7d3`, live); he reruns;
   the switch-over, the moderation-events contract doc saying confidence is then the removal score. Proof: a week live
   with no delete he marks wrong; rulings 7 and 8 rewritten in the same commit.
7. **Flows on every channel, then reliable** (main 09-27, audited). Live `83b0b582`: reliability, a builder test run,
   date-and-time answers, real media and reply buttons on Instagram and Messenger, Messenger replies reaching a flow (the AI
   does not). Live `703c4691`: quick-reply follow-up, website-button actions (a signed `/api/r/f/` link). Left, after item 8
   (ruling 33): sequences; story-reply, story-mention, reaction and contact-event triggers. Proof: a real flow run per channel.
8. **Usability round** (his yes 2026-09-27, ruling 33): no new features until it runs. Per screen: apple-design review, 2-3 mockups
   for his pick, rebuild, before/after shots; the flow builder also gets one real task built end to end. Proof: his pick per screen.

## Audit findings not fixed yet (2026-09-25; the file's lane in brackets)
- **Flow and questionnaire URLs are checked on the literal host only** (`shared/src/flows/ssrf.ts`, `operator-fetch.ts`): a name
  resolving to a private address passes. Fix: check the address it resolves to at fetch time, connect to that one. Proof: a name for 127.0.0.1 refused. [automation]
- Activity ignores a role's "only assigned" access (Roles says so): scope the feed, review and trace routes. [ux-pass]
- **A flow saves a LeadRat lead with no name** (`flow-crm.service.ts:298` checks the phone only; the agent refuses without one,
  `tools.ts:512`). Flag it like a missing phone. Proof: a nameless contact gives crm_no_name, red in the test run. [automation]
- **nginx sends no security headers** (CSP, HSTS, nosniff, frame-ancestors), serves media same-origin. A prod
  change; the blocks: hub `ccef0c0` lanes/audit-security.md; `public/partner.html:179`. Proof: `curl -I`.
- **The WhatsApp verify token is a guessable word, logged in plain text** by nginx on Meta's check
  (`/var/log/nginx/access.log`, 09-25). Rotate to a random value: connection card + Meta app (his). Low.
- Low: a hand replay of a job BullMQ calls completed while its row reads unprocessed may send twice
  (`webhook-ingest.service.ts` replay, admin-only).
- **Analytics cuts days at UTC midnight, the Dashboard at the operator's**, so "today"
  differs by 4 h (`analytics/dto.ts` parseRange, `questions.service.ts:129`). Ruling 35: last.
- **CSV import has no row cap**: 3-4 queries per row inside the request, a 25 MB body taken
  (`contacts/csv-import.service.ts`). Cap it or queue it. [api, unowned]
- **The open Inbox thread reloads every message every 8 s**, no limit (`inbox.service.ts:296`
  findMany with no take; `Inbox.tsx:229`). A page or a since-cursor on the api and the timeline.
- **A password change does not revoke sessions** (deactivation does): tokens signed before it
  live 8 h. `passwordChangedAt` on users; JwtStrategy refuses an older `iat` (migration).
- **`JWT_SECRET` accepts 16 characters** (`env.schema.ts:14`; HS256 wants 32+): read prod's length first.
- **Dependency floors** (F15): api nodemailer ^9.1.1, @types/nodemailer ^8.0.2, @nestjs/* ^11.2.6; web react-router ^7.18.4.
- Low: `web-chat/config.ts:194` `originList` duplicates `cors-policy.ts` `siteOriginsFrom`; keep one.
- **Activity's `total` counts raw turns while its items fold retries** (contract doc :77-80); the
  count belongs in `turn-trace-admin.service.ts`. [api, unowned]
- Dead: web `listModerationConsole` (`moderation-api.ts:130`) calls a removed route. [ux-pass]
- **`resilience.ts`'s JSON reprompt misfits a plain-text caller** (the opener): a `reprompt` option (low). [agent]
- **`search_site_properties` has no property-type argument**: "townhouse" goes into the name
  query and matches 0 (prod log 09-17..24). Needs the website proxy's type parameter.
- **Turnstile `timeout-or-duplicate` answers `POST /session` 403**; the re-challenge is the website repo's.
- **LeadRat notes vs ruling 28**: `leadrat.service.ts:393-425` PUTs existing + new notes. On a
  test lead PUT "A" then "B": both shown → send the new line only; else rewrite ruling 28.
- **Ruling 31 half met**: LeadRat source defaults are constants (`leadrat.service.ts:729`), ""
  cannot unmap a channel, and no screen edits the maps. Fields on the LeadRat card.
- **Ruling 42**: FlowRule, AutomationRule "engine v2" and the questionnaire engine run beside Flows (prod: 0
  flow_rules, 2 reminder rules, 2 questionnaires never run). The questionnaire records a failed send as sent.
- **8 foreign keys have no index** (ConversationNote.authorId, Message/Campaign.templateId, Campaign.segmentId,
  CampaignRecipient.contactId, QuestionnaireSession, FlowSession.flowVersionId, AuditLog.userId). Low.
- **Campaign send is send-then-record** (`campaign-send.service.ts:876/895`, `message.service.ts:
  107/211`): a restart in the gap double-sends. Claim first, or a `sending` status. [automation]
- **Reminders never stop on a LeadRat status change**: `reminders.service.ts:369` hardcodes
  "New" and no status is stored. Needs a status source first.
- **A template text header may hold a variable nothing fills** (`templates/types.ts:9`): every
  send of one fails at Meta. Refuse it at create. [automation]
- **Replay runs its rows inside the request** (`replay.service.ts:118`, 2-3 model calls each)
  behind nginx's 60 s: ~5 rows 504 while spend continues. A job and a poll, or a cap.
- **Flow request address and body can hold keys marketing reads**, and marketing may change the body
  of a request whose header is hidden (headers masked, live `1dfdade3`); the canvas does not pre-lock its URL. [automation]
- Low: `scripts/backfill-button-optout.sql:37` lists the old opt-out words. [automation]
- Low: shared `reminders/config.test.ts:12` imports `parity-pin.test.ts`, so 9 tests run twice: move `REMINDER_PARITY_GOLDEN` out.
- **The web app ships as one 1.19 MB chunk** (340 KB gzip): no route code-splitting (low).
- **Web halves of live api fixes** [ux-pass]: the Posts toast shows sync `errors` (`b8c1e4e0`); the
  grid and a post's comments page with `listPostsPage` / `getPostDetail` (`cf48a58a`); Triggers reads
  `notLiveMarker` from `GET /api/manychat/keywords` (`6336e49f`).
- **Figures that wait on fields** (one migration lane; spec in hub `lanes/audit-automation.md` For main): `Contact.crmLeadCreatedAt`
  ("Leads created"), `Message.flowId`/`flowNodeId` + `ShortLink.flowId` (per step delivered, read, clicked), `Flow.archivedAt`; one draft per flow as a partial unique index.
- Social's popup video needs the sync to store IG `media_url`/`media_type`. Low: two invalid flow answers racing can re-ask twice. [automation]
- Low [ux-pass]: Triggers' dates differ from the trace's format; the trace reads "Replied: replied";
  TracePanel says "Reply sent" on a sandboxed Playground turn (agent's F30).

## Operator orders from 2026-09-04 and 09-05, still open
- **The Log shows direct database edits to the prompt row**: the screen is live (`70eb4574`); a
  database trigger must record them (agent's SQL, hub `lanes/audit-agent.md`). A migration: his word.
- **"AI stays out of this post" switch** (`9c450e7e`, live 09-12): closes when one post's next comment is skipped.
- **The figures rail judges formatting, not provenance** (`mandate.ts`); ruling: provenance at the
  tool boundary (`fd6d4b00`/`76624f8d` partial). Proof: an invented spelled figure is held.
- **Four web-chat api halves** (currency, page filters, vote on GET, `[[label: noun]]`): his live-site check.
- **The opener falls back to the seeded wording** (13 in 09-17..24). After `bd80e624` deploys,
  `empty_output:max_tokens` in `decision_reason` = thinking budget; else filter. Proof: a clean week.
- **Instagram and Facebook conversation switches** (`243debbd`): closes on `instagram:dms: live` on prod.
- **Facebook has no AI DM arm** (`fb-dm.service.ts`): build it, or film Facebook as manual-only. Its switch saves a gate nothing reads.
- **Prove the typed CTA starts the ManyChat flow**: one live comment whose opener reply reaches ManyChat and starts the flow.
- **One language system on both channels** (ruling 26): "مهتم" reaches the AI on Instagram,
  ManyChat's English flow on Facebook. He builds an Arabic ManyChat flow; CM's hand-off is open.
- **Moderation misses veiled creepy intent** ("I want to meet you"): obvious → delete;
  plausible → escalate, never answer. A stored-policy change and an escalation route.
- **Activity to its spec** (`activity.md`): one screen, a detail popup, a page selector, its kind chips. [ux-pass]
- **Remove the corrections / verdict feature.** Alive: `decision-label.service.ts`, `PUT/DELETE
  /api/moderation/labels`, `labelsForTurns`, Activity's glyphs, `comment-draft.service.ts:569`,
  labelled replay sets, `questions.service.ts`, Home's queue kind `label` (`dashboard-words.ts`).
  40 `label` rows on prod need his word.
- **The reply pass gets eyes on the thread**: our last replies on the post, the commenter's earlier ones.
- **A human's Page-inbox reply makes our opener fail forever**: an "already answered" outcome, his word.
- **Meta tester role**: does his personal Instagram inherit it? One DM from our UI decides.

## Blocked on the operator
- Comment author id == conversation sender id per channel (a prod count, his word): assumed by `opener-inbox.ts:52-55`.
- The Posts spec promises "reply to any comment by id"; nothing builds it (`Posts.tsx:908`). Recommend: cut it.
- Record the App Review screencast (recipe `docs/SCREENCAST-DM-TESTS.md`). Does the Meta webhook subscribe to
  `messaging_referrals`? Without it an ad click with no message is never stored (Meta configuration).
- Flows' typing pause: build it (WhatsApp's typing indicator also marks the message read) or keep it out of the palette
  (out since item 4, the canvas says it does nothing). Recommend: build it for WhatsApp only.
- n8n hygiene: deactivate the old WhatsApp bot; move the `/internal/*` secret (24 nodes) and LeadRat's
  key and secret into credentials, rotate; five webhooks take unauthenticated POSTs. Proof: no literals.
- Rotate ManyChat's secret: the card's new `inboundSecret` into the External Request step, then `callbackSecret`.
- An off-box copy of the nightly dumps (all 14 days sit on the VPS), then one rehearsed
  restore into a scratch database. Proof: a dump under 24 h old off the VPS restores.
- Prod deletes: Redis queues `funnel-flow-sweep`, `flow-dispatch`, `funnel-flow-delay`; settings rows
  `corrections.reviewedUpTo`, `igCommentPoll` (nothing reads them).
- Flip the Instagram DM gate; paste the nine ManyChat fallback messages; rule on
  `design/tiles/sweep-phase1-round-4/SHEET-STATUS-2026-09-05.md` §1 and §5.
- Client interaction flow: the ManyChat button URLs and one test Calendly booking.
- One lead is unanswered: Ben Zino, 2026-08-10 21:54 UTC, "Do you have 200 own??".

## UNCONFIRMED — older items, operator to keep or kill
- Rewrite every knowledge document from zero; 9 hand-written rows on prod (07-27).
- Public comment replies must not say "DM us"; the seed says "steer them into our DMs" and a
  golden test pins it (07-27). Comment→DM itself was built 08-19.
- Redesign the review detail panel (mockups asked for, none exist) (07-27).
- Retire raw form controls screen by screen, Settings first: 244 on 07-28, 200 on 09-25 (47 in Settings).
- WhatsApp silence as a standing UI status instead of repeated notifications (07-24).
- 21st.dev and outside design systems: reference only, rebuilt as our own (07-29).
- Ten drawn-not-built mockups in `design/mockups/`: keep or kill each.
- Decision override is scoped to `approve` only; `moderate` deletes are irreversible.
- Flaky under load, pass alone: `Posts.test.tsx` "View full trace", `ThreadStream.test.tsx` "renders a system line" (8/8
  at one fork), `source-hygiene.test.ts` (5.5 s loaded).
