max / makenotwork
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` |
| @@ -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) |