Skip to main content

max / makenotwork

Move completed items to todo_done, trim todo files Cleaned mnw-cli (all phases done), synckit-client (fuzz items done), wam (phases 1-2 done), server (Run 24, docs, scheduler fixes done). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-10 02:37 UTC
Commit: 92a8a5175b0be79c7e7bc66dcd2a0d118a0442ea
Parent: 20d388b
5 files changed, +45 insertions, -198 deletions
@@ -1,59 +1,18 @@
1 1 # mnw-cli TODO
2 2
3 3 ## Status
4 - Done: Phases 1-8, Git proxy A-D (incl D5 DNS), UX audit fixes (8/8), PoM health check, astra test. Deployed 2026-05-05. Active: None. Next: Post-beta features.
4 + Done: Phases 1-8, Git proxy A-D, UX audit (8/8), all remaining features. Deployed 2026-05-05. Active: None. Next: Post-beta features.
5 5
6 6 ---
7 7
8 - ## UX Audit Fixes (2026-05-02)
8 + ## Deferred (Post-Beta)
9 9
10 - Priority order (highest impact first):
11 -
12 - - [x] Fix broadcast argument parsing — use `--subject`/`--body` flags instead of positional args
13 - - [x] Add confirmation to upload file deletion — double-press d to confirm, consistent with other destructive actions
14 - - [x] Improve blog post body input — Enter inserts newline, Ctrl+D advances to schedule step
15 - - [x] Add project creation — `ssh cli.makenot.work project create --title "X" --type audio` + server endpoint
16 - - [x] Show supported file types on upload rejection — lists all supported extensions in error message
17 - - [x] Allow direct field editing on upload screen — Enter saves current field, Tab advances to next
18 - - [x] Add empty state guidance — Home + Collections screens now point to makenot.work/dashboard
19 - - [x] Document git hosting in help text — added Git hosting and SFTP sections to help output
10 + - [ ] TUI tag management screen (currently inline on item detail only)
11 + - [ ] TUI tier creation (currently read-only — tier creation requires Stripe)
12 + - [ ] Collection create/delete from TUI (API wired, TUI screen not built)
20 13
21 14 ---
22 15
23 - ## Git Proxy — Part D: Server Configuration
24 -
25 - - [x] D1: Move sshd to port 2200, `ListenAddress 100.120.174.96` (Tailscale only) — done 2026-04-22
26 - - [x] D2: Update mnw-cli .env (`SSH_PORT=22`, `GIT_SUDO_USER=git`) — done 2026-04-22
27 - - [x] D3: Sudoers rule (`/etc/sudoers.d/mnw-cli-git` — git-upload-pack, git-receive-pack, git-upload-archive, git, tee, chmod) — done 2026-04-22, extended 2026-05-03
28 - - [x] D4: Firewall — removed 2222/tcp, added 2200/tcp — done 2026-04-22
29 - - [x] D5: DNS — `cli.makenot.work` A record -> `5.78.144.244`, proxy OFF — done 2026-05-03
30 - - [x] D6: Restart sequence verified — admin SSH on 2200, mnw-cli on 22, both running — done 2026-04-22
31 -
32 - ## Deploy
33 - - [x] Test on astra (full TUI + SFTP + git push/pull) — tested from both macbook and astra; registered astra SSH key; fixed bare repo HEAD refs
34 - - [x] Deploy to hetzner (cross-compile via deploy.sh) — done 2026-04-22
35 - - [x] Verified: SSH auth, TUI launch, git ls-remote, git clone all working — 2026-04-22
36 - - [x] Fixed: NoNewPrivileges blocking sudo for git ops — 2026-04-22
37 - - [x] Fixed: Tailscale SSH intercepting port 22 — disabled on hetzner — 2026-04-22
38 - - [x] Fixed: Git repo auto-create on first push — 2026-05-03
39 - - Repo creation delegated from server to mnw-cli (server only registers in DB)
40 - - mnw-cli runs `git init --bare --shared=group` directly (in git group, setgid parent dir)
41 - - `safe.directory=*` set for git service user
42 - - Post-receive hook installed by mnw-cli after init
43 - - Sudoers extended with git, tee, chmod
44 - - [x] Add PoM health check for mnw-cli (port 22 SSH banner check) — hetzner localhost + astra external, plus DNS for cli.makenot.work
45 -
46 - ## Remaining Features (from design doc)
47 - - [x] Bulk item operations — multi-select (Space) on project items screen, bulk publish/unpublish/delete with confirmation dialog. Selection count shown in status bar.
48 - - [x] Pipe mode uploads — `cat file.wav | ssh cli.makenot.work upload --filename track.wav --project my-slug [--title TITLE] [--price CENTS]`. Reads stdin via SSH channel, auto-creates item, uploads to S3, publishes.
49 - - [x] Broadcast to followers — SSH command: `broadcast "Subject" "Body"`. Server internal endpoint with 24h rate limit, fire-and-forget email delivery.
50 - - [x] Custom domain management — SSH commands: `domain`, `domain add`, `domain verify`, `domain remove`. Full flow: add domain, DNS TXT verification via Cloudflare DoH, removal with cache invalidation.
51 - - [x] Collection management — SSH command: `collections` (list). Server internal endpoints for create/delete. API client wired.
52 - - [x] Tag management — Server internal endpoints for list/add/remove item tags + tag search. API client wired. TUI screen deferred.
53 - - [x] Subscription tier management — Server internal endpoint for listing tiers by project. API client wired. TUI screen deferred (read-only — tier creation requires Stripe).
54 - - [x] Blog post scheduling — server internal blog create now accepts `publish_at` (ISO 8601). TUI blog create flow: Title -> Body -> Schedule (enter datetime or leave empty for draft). Scheduled posts shown as "sched YYYY-MM-DDTHH:MM" in post list. Server scheduler auto-publishes when time arrives.
55 - - [x] TUI screens — Tags: inline on item detail screen (shows current tags, `t` to search+add, Enter to confirm). Tiers: `t` from project screen opens read-only tier list. Collections: `c` from home screen opens collection list with navigation.
56 -
57 16 ## Key Paths
58 17 ```
59 18 mnw-cli/src/
@@ -19,14 +19,6 @@ Priority order. See `human_todo.md` for the full manual testing feature map.
19 19
20 20 ---
21 21
22 - ## Documentation: Undocumented Shipped Features
23 -
24 - These features are implemented but have no public-facing documentation yet. Add docs in the next documentation push.
25 -
26 - - [x] Shopping cart: `site-docs/public/guide/cart.md`
27 - - [x] Wishlist: `site-docs/public/guide/wishlist.md`
28 - - [x] Creator pause: `site-docs/public/guide/creator-pause.md`
29 -
30 22 ---
31 23
32 24 ## Deferred from Sprints
@@ -38,13 +30,8 @@ These features are implemented but have no public-facing documentation yet. Add
38 30
39 31 ---
40 32
41 - ## Ultra Fuzz Run 24 (2026-05-09) -- All Fixed
33 + ## Ultra Fuzz Run 24 (2026-05-09)
42 34
43 - - [x] **SERIOUS**: Add `starts_at` validation to guest checkout (`routes/api/guest_checkout.rs:105`). Copied from `cart.rs:150`.
44 - - [x] **SERIOUS**: Fix version replace storage counter rollback (`routes/storage/versions.rs`). Now uses atomic `try_replace_storage` for file replacements.
45 - - [x] MINOR: Change pending_uploads ON CONFLICT to DO NOTHING (`db/pending_uploads.rs:17`)
46 - - ~~MINOR: Blog editor: preserve form input on validation error~~ -- false positive; JS `fetch()` keeps form state on error
47 - - [x] MINOR: Idempotency middleware: avoid double allocation (`metrics.rs:232`)
48 35 - [ ] DEFERRED: Extract shared `validate_promo_code()` helper to prevent checkout path divergence
49 36
50 37 ---
@@ -66,18 +53,12 @@ Remaining open items from Runs 21-24 and Code Fuzz (2026-05-08). All SERIOUS ite
66 53 - [ ] MINOR: `item_slug_exists` considers soft-deleted items — intentional during 7-day recovery window (`db/items.rs:80-94`)
67 54
68 55 ### Scheduler & Infrastructure
69 - - [x] FIX: `integrity.rs` referenced non-existent `updated_at` on `creator_subscriptions` — changed to `current_period_end` (v0.5.11)
70 - - [x] FIX: `cleanup.rs` storage query had ungrouped column error — rewrote as LEFT JOIN LATERAL (v0.5.11)
71 56 - [ ] NOTE: Announcement emails lost on server restart — no delivery persistence (`scheduler/announcements.rs:59-85`)
72 57 - [ ] NOTE: Unconfigured S3 reports `s3_ok = true` — intentional but masks misconfig (`monitor.rs:56-65`)
73 58 - [ ] NOTE: `X-Forwarded-For` spoofable without Cloudflare — accepted risk (`rate_limit.rs:26-31`)
74 59
75 60 ---
76 61
77 - ## S3/DB Crash-Gap Fix: `pending_s3_deletions` Durable Queue -- DONE
78 -
79 - All 5 paths migrated, scheduler retry job wired. See `todo_done.md` for details.
80 -
81 62 ---
82 63
83 64 ## Backlog (no sprint assigned)
@@ -95,9 +76,6 @@ All 5 paths migrated, scheduler retry job wired. See `todo_done.md` for details.
95 76 - [ ] Video URL fallback on S3 presign failure (migration: `video_url` column)
96 77
97 78 ### Code Quality
98 - - ~~Remove `async-trait`~~ — kept: all 3 traits are used as `dyn` objects; Rust 2024 async fn in traits is not dyn-compatible. `async-trait` is the correct tool until `dyn async fn` stabilizes (RFC 3245)
99 - - [x] Add README.md to server/ (Run 23)
100 - - [x] Split oversized route files: exports.rs, health.rs, tabs/user.rs (Run 23)
101 79 - [ ] Monitor scheduler.rs, git/mod.rs for growth
102 80
103 81 ### Feature Completeness
@@ -4,6 +4,35 @@ Items moved from todo.md. See git history for implementation details.
4 4
5 5 ---
6 6
7 + ## v0.5.11 Deploy Fixes (2026-05-10)
8 +
9 + - [x] FIX: `integrity.rs` referenced non-existent `updated_at` on `creator_subscriptions` — changed to `current_period_end`
10 + - [x] FIX: `cleanup.rs` storage query had ungrouped column error — rewrote as LEFT JOIN LATERAL
11 + - [x] Add `/robots.txt` route (disallow /api/, /admin/, /settings/)
12 + - [x] Prometheus metrics auth configured (CLI_SERVICE_TOKEN)
13 + - [x] ALERT_EMAIL set (pom-alerts@makenot.work)
14 +
15 + ---
16 +
17 + ## Ultra Fuzz Run 24 (2026-05-09)
18 +
19 + - [x] **SERIOUS**: Add `starts_at` validation to guest checkout (`routes/api/guest_checkout.rs:105`)
20 + - [x] **SERIOUS**: Fix version replace storage counter rollback (`routes/storage/versions.rs`)
21 + - [x] MINOR: Change pending_uploads ON CONFLICT to DO NOTHING (`db/pending_uploads.rs:17`)
22 + - [x] MINOR: Idempotency middleware: avoid double allocation (`metrics.rs:232`)
23 +
24 + ---
25 +
26 + ## Documentation (2026-05-09)
27 +
28 + - [x] Shopping cart: `site-docs/public/guide/cart.md`
29 + - [x] Wishlist: `site-docs/public/guide/wishlist.md`
30 + - [x] Creator pause: `site-docs/public/guide/creator-pause.md`
31 + - [x] Add README.md to server/ (Run 23)
32 + - [x] Split oversized route files: exports.rs, health.rs, tabs/user.rs (Run 23)
33 +
34 + ---
35 +
7 36 ## Unified Media Player — Phase 1 (2026-05-05)
8 37
9 38 - [x] Extract `static/media-player.js` (~450 lines) — full state machine: simple mode, segment mode (dual-element gapless), insertions, chapters, seek, speed, volume, progress persistence. Keyboard shortcuts: Space (play/pause), arrows (seek/volume), M (mute), F (fullscreen/video), S (skip insertion)
@@ -1,91 +1,35 @@
1 1 # SyncKit Client SDK — Todo
2 2
3 - Done: All phases (S1-S5), key rotation. Active: None. Next: Post-beta items below.
3 + Done: All phases (S1-S5), key rotation, fuzz remediations (Runs 1-2). Active: None. Next: Post-beta items below.
4 4
5 5 v0.3.1. Audit grade A. 340 tests (249 unit + integration). Rust 2024 edition (2026-05-06). rand 0.9.
6 6
7 - ## Fuzz Findings (2026-04-27)
7 + ---
8 8
9 - ### Serious
10 - - [x] SSE buffer can grow unbounded — added 1MB `MAX_SSE_BUFFER` limit in `subscribe.rs`.
11 - - [x] `resp.json()` failure after successful server-side push — added `retry_request_json` that deserializes inside the retry loop. Push now uses it.
12 - - [x] OAuth code exchange retry permanently fails on single-use codes — removed retry wrapper from `authenticate_with_code`, sends exactly once.
13 -
14 - ### Medium
15 - - [x] Normalized password string not zeroized after Argon2 derivation — `derive_wrapping_key` now zeroizes the normalized `String` before returning.
16 - - [x] `detect_conflicts` silently drops duplicate local pending changes — documented precondition and behavior in doc comment.
17 - - [x] Multiple remote changes for same row produce multiple ConflictPairs — documented in doc comment that caller must resolve in seq order.
18 - - [x] `PullFilter { tables: Some(vec![]) }` sends empty array — added `tables_is_empty` skip-if predicate; `Some(vec![])` now serializes same as `None`.
19 -
20 - ### Minor
21 - - [x] 429 `Retry-After` header ignored — `check_response` now parses `Retry-After` into `Server.retry_after_secs`; retry loop uses it (capped at 60s) instead of default backoff.
22 - - [x] `is_transient` misclassifies redirect loops and decode errors as retryable — added `!e.is_redirect() && !e.is_decode()` checks.
23 - - [x] `resolve_field_merge` with non-object base silently returns `KeepRemote` — improved doc comment to note `Value::Null` case and suggest LWW fallback.
24 - - [x] Trailing slash in `server_url` creates `//api/v1/...` double-slash paths — `Endpoints::new` now trims trailing slash.
25 - - [x] SSE error swallowed — `Err` branch now logs the error via `tracing::debug` before returning `None`.
26 - - [x] Invalid UTF-8 in SSE stream replaced silently — changed from `from_utf8_lossy` to `from_utf8`; invalid UTF-8 now closes the stream with a warning.
27 - - [x] Decrypted master key `Vec<u8>` in `unwrap_master_key` not zeroized before drop — added `plaintext.zeroize()`.
28 - - [x] `size_bytes: i64` accepts negative values — added validation in `blob_upload_url` and `blob_confirm`, returns `Internal` error for negative values.
29 -
30 - ### Notes
31 - - [x] Dummy `[0u8; 32]` key passed when no data — replaced with dedicated `encrypt_change_no_data`/`decrypt_change_no_data` that skip the key entirely.
32 - - [x] Field merge is shallow (top-level keys only) — documented as "top-level keys only" in function doc.
33 - - [x] Concurrent `authenticate()` calls silently overwrite each other — documented last-writer-wins behavior in `authenticate` doc comment.
34 - - [x] Keychain errors always classified non-transient — added comment in `is_transient` explaining keychain ops are synchronous and not wrapped by retry_request.
35 - - [x] `ChangeOp::from_str_opt` is case-sensitive but undocumented as such — doc comment now states "case-sensitive" and lists accepted values.
36 -
37 - ## Fuzz Findings (2026-04-29)
38 -
39 - ### Serious
40 - - [x] Push retry creates duplicate sync log entries — added `batch_id` UUID to push request + server dedup via `idx_sync_log_batch_id`. Migration 083.
41 - - [x] Auth timing oracle leaks account status — moved `verify_password` before account status checks. All failures now return uniform 401.
42 -
43 - ### Medium
44 - - [x] `change_password` TOCTOU race — added `expected_version` to `PutKeyRequest`. Server rejects with 409 on version mismatch. Client sends version from `get_server_key`.
45 - - [x] Pull/status JSON deserialization outside retry loop — reads use `retry_request` then `resp.json()` outside loop. Body-read failures not retried for idempotent reads. Switched to `retry_request_json`.
46 - - [x] Keystore doesn't zeroize base64 intermediates — `store_key`/`load_key` leave base64 strings on heap without zeroizing.
47 -
48 - ### Minor
49 - - [x] `prune_sync_log` accepts negative `retain_days` — would delete all entries. Added guard.
50 - - [x] Client `GetKeyResponse` missing `key_version` field — server returns it but client drops it.
51 - - [x] Token can expire during push retry sequence — mitigated by push idempotency key (retries are now safe).
52 - - [x] Blob size mismatch on concurrent upload — changed `ON CONFLICT DO NOTHING` to `DO UPDATE SET size_bytes = EXCLUDED.size_bytes`.
53 - - [x] `validate-app` endpoint uses GET with API key in query string — changed to POST with JSON body.
54 -
55 - ### Notes
56 - - SSE buffer can overshoot MAX_SSE_BUFFER by one chunk size before check triggers — bounded, not unbounded.
57 - - SSE parser only handles `\n\n` delimiters, not `\r\n\r\n` — fine since MNW server sends `\n`.
58 - - Integer vs float (`1` vs `1.0`) treated as different in `resolve_field_merge` — serde_json Value semantics.
59 - - Null base in `resolve_field_merge` returns KeepRemote — documented, callers should use LWW fallback.
60 -
61 - ## Business Sustainability (from 2026-05-02 audit)
9 + ## Business Sustainability — Remaining
62 10
63 11 ### Pricing & Revenue
64 - - [x] Reconcile pricing models — consolidated into Simple/Builder model in `server/docs/synckit_pricing.md`. Old Shared Pool/Per-User tiers archived in synckit-plan.md. Weight/Burst is the billing engine; Simple mode = Builder with fixed 5x burst ratio.
65 - - [x] Re-evaluate free tier — replaced with application-based access + 14-day unbilled trial. Aligns with MNW no-free-tier philosophy.
66 - - [x] Charge for sync in GO, BB, AF — server: migration 098 `app_sync_subscriptions`, sync gate (402 for GO/BB metadata, 402 for AF blobs), inline Stripe checkout (no pre-created products). Client SDK: `get_subscription_status()`, `create_subscription_checkout()`. GO/BB: Tauri commands + settings UI. AF: SyncManager methods + egui subscription panel with tier selector + storage bar.
67 - - [x] Clarify SyncKit relationship to MNW creator tiers — no relationship. SyncKit is a separate product for developers. App sync pricing is independent of creator tiers. MNW may offer creators a separate cloud storage budget in the future, but that's a MNW feature, not SyncKit.
68 12 - [ ] Implement developer application flow — short form (app description, expected usage), manual approval, 14-day unbilled trial starts on approval.
69 13 - [ ] Cancellation grace period — decide whether `past_due` subscribers can still sync (probably yes, for a few days). After cancellation, sync stops at billing period end, data retained 30 days before cleanup.
70 14 - [ ] Subscription management UI on makenot.work dashboard — show active app sync subscriptions with cancel/change-tier options.
71 15 - [ ] Test end-to-end against live Stripe — subscribe via each app, verify webhook, verify gate.
72 16
73 17 ### Operational Prerequisites (before first external customer)
74 - - [x] Automate sync_log compaction — already wired in server monitor.rs maintenance loop
75 - - [x] Per-app rate limiting — dual-layer: per-IP (burst 30, 10/sec) + per-app via JWT app claim (burst 60, 20/sec). `SyncAppKeyExtractor` decodes JWT payload without signature verification for rate limiting only. 6 unit tests.
76 18 - [ ] Separate VPS for SyncKit — $5-30/mo depending on tier. Prevents resource contention. Technical spec at `server/docs/internal/strategy/synckit_vps_separation.md`. Trigger: first external developer accepted.
77 19
78 20 ### Documentation & Accuracy
79 - - [x] Update synckit-plan.md economics — archived stale sections with correction notes (R2→Hetzner, $1 support→$5-15 realistic). Business Model section wrapped in `<details>`. Stage 5 roadmap updated (free tier→application-based access).
80 21 - [ ] Monitor Turso and PowerSync — track whether either ships E2E encryption or flat pricing (SyncKit's main differentiators).
81 22
23 + ---
24 +
82 25 ## Deferred (Post-Beta)
83 - - [ ] Conflict resolution helpers — LWW, field-level merge, custom resolver callback in the SDK. Reduces client-side boilerplate. (Gap vs Ditto, Couchbase)
84 - - [x] Key rotation mechanism — `rotate_key(device_id, password)` method. Server-coordinated via `sync_key_rotations` table; client-driven re-encryption in batches. Migration 089.
85 26
27 + - [ ] Conflict resolution helpers — LWW, field-level merge, custom resolver callback in the SDK. Reduces client-side boilerplate. (Gap vs Ditto, Couchbase)
86 28 - [ ] WASM web client — compile SyncKit to WASM for browser use. Only if a consumer app ships a web companion.
87 29 - [ ] C FFI layer — enables Swift/Kotlin/Python bindings. Only if non-Tauri consumers appear.
88 30
31 + ---
32 +
89 33 ## Key Paths
90 34 - Client: `MNW/shared/synckit-client/src/client/` (mod, auth, encryption, sync, rotation, subscribe, blob, helpers)
91 35 - Crypto: `MNW/shared/synckit-client/src/crypto.rs`
M wam/docs/todo.md +2 -65
@@ -1,70 +1,7 @@
1 1 # WAM (Whack-a-Mole) TODO
2 2
3 3 ## Status
4 - v0.1.0 complete. Phase 1 done. ~800 lines, 5 tests.
5 -
6 - ---
7 -
8 - ## Overview
9 - WAM (Whack-a-Mole) is a ratatui-based CLI ticket manager for internal MNW operations. Other MNW services (server, PoM, mnw-cli) can create tickets programmatically via HTTP API or CLI, and an operator manages them through the TUI. Security: HTTP API binds on tailnet only -- tailnet membership is the auth boundary.
10 -
11 - ---
12 -
13 - ## Phase 1: Core (v0.1.0) -- DONE
14 -
15 - ### Data Layer
16 - - [x] SQLite database with WAL mode, inline migration
17 - - [x] Ticket model: id (UUID), title, body, priority, status, source, source_ref, created_at, updated_at, resolved_at
18 - - [x] CRUD: create, get (prefix match), list (with filters), update status
19 - - [x] DB path: `~/.local/share/wam/wam.db` (via `directories` crate)
20 - - [x] 5 unit tests (in-memory SQLite)
21 -
22 - ### CLI
23 - - [x] `wam` -- launch TUI (default command)
24 - - [x] `wam create --title "..." [--body "..."] [--priority high] [--source manual] [--source-ref X]`
25 - - [x] `wam list [--status open] [--priority high] [--source X]`
26 - - [x] `wam show <id>` -- git-style prefix matching
27 - - [x] `wam resolve <id>`
28 - - [x] `wam close <id>`
29 -
30 - ### TUI
31 - - [x] List view: table sorted by priority desc, then date desc
32 - - [x] Color coding by priority (low=DarkGray, medium=White, high=Yellow, critical=Red)
33 - - [x] Status indicators (* open, > in-progress, v resolved, x closed)
34 - - [x] Detail view: full body, all metadata
35 - - [x] Inline status changes (o/i/r/c keys)
36 - - [x] Create new ticket (n key, popup with title input)
37 - - [x] Filter toggles: f (status), p (priority), s (source -- cycles through known sources)
38 - - [x] Search (/ key, filters by title)
39 - - [x] q to quit, Esc to go back
40 -
41 - ---
42 -
43 - ## Phase 2: Integration API (v0.2.0) -- HTTP API DONE, integration pending
44 -
45 - ### HTTP Listener -- DONE
46 - - [x] `wam serve [--port 7890]` -- axum HTTP server, binds 0.0.0.0
47 - - [x] `POST /tickets` -- create ticket (JSON body: title, body, priority, source, source_ref)
48 - - [x] `GET /tickets` -- list tickets (query params: status, priority, source, search)
49 - - [x] `GET /tickets/:id` -- get single ticket (prefix match)
50 - - [x] `PATCH /tickets/:id` -- update ticket status
51 - - [x] Security: tailnet-only (no auth layer needed -- tailnet membership is the trust boundary)
52 - - [x] Shared state via Arc<Mutex<Connection>>
53 -
54 - ### MNW Server Integration -- DONE
55 - - [x] `wam_client.rs` in MNW server -- fire-and-forget reqwest POST, 5s timeout
56 - - [x] `WAM_URL` env var in Config, `WamClient` in AppState (optional)
57 - - [x] Refund escalation: creates critical ticket alongside alert email (`source: refund-escalation`)
58 - - [x] Webhook dead letter: creates high-priority ticket when retries exhausted (`source: webhook-dead-letter`)
59 - - [x] 6 user-facing proactive tickets (license key, Fan+ credit, quarantine, payment failed, Stripe Connect, build failed)
60 - - [x] 2 infrastructure tickets (health status, DB pool pressure)
61 - - [x] 3 periodic integrity checks (sales count drift, stale subscriptions, bounce spike)
62 -
63 - ### PoM Integration -- DONE
64 - - [x] `wam_url` field in AlertConfig, WAM client in Alerter
65 - - [x] All non-recovery alerts now create WAM tickets instead of emails
66 - - [x] Recovery alerts remain email-only (transient good news)
67 - - [x] 12 alert types wired: health, TLS expiry, TLS error, peer missing, route failure, DNS mismatch, WHOIS expiry, WHOIS error, CORS failure, latency drift, test duration drift, backup stale, monitoring offline
4 + v0.2.0. Phases 1-2 complete. ~800 lines, 5 tests. MNW + PoM integration done.
68 5
69 6 ---
70 7
@@ -84,7 +21,7 @@ WAM (Whack-a-Mole) is a ratatui-based CLI ticket manager for internal MNW operat
84 21
85 22 ---
86 23
87 - ## Future
24 + ## Deferred
88 25 - [ ] Standalone distribution (outside MNW monorepo) if it proves generally useful
89 26 - [ ] Paid hosted version with team features, SLA tracking, escalation chains
90 27 - [ ] Webhook-based integrations (Slack, Discord, email)