max / makenotwork
- Co-Authored-By
- Claude Opus 4.6 (1M context) <noreply@anthropic.com>
62 files changed,
+2053 insertions,
-126 deletions
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | [package] | |
| 2 | 2 | name = "makenotwork" | |
| 3 | - | version = "0.4.12" | |
| 3 | + | version = "0.5.0" | |
| 4 | 4 | edition = "2024" | |
| 5 | 5 | license-file = "LICENSE" | |
| 6 | 6 |
| @@ -1,9 +1,9 @@ | |||
| 1 | 1 | # Makenotwork TODO | |
| 2 | 2 | ||
| 3 | 3 | ## Status | |
| 4 | - | Done: All pre-beta phases, UX audit remediation, creator trust audit remediation. Active: Creator setup (Stripe), manual testing. Next: Soft launch. | |
| 4 | + | Done: All pre-beta phases, UX audit remediation, creator trust audit remediation, shopping cart + wishlist checkout. Active: Final pre-launch testing. Next: Soft launch (target 2026-05-09). | |
| 5 | 5 | ||
| 6 | - | v0.4.10 deployed 2026-05-04. Audit grade A (Run 20, 2026-05-04). ~83K LOC, 1,214+ test annotations, 0 cargo warnings, 2 cold spots. CI on astra operational. Mutation kill rate 99.4%. Property-based testing active (proptest). `cargo test --features fast-tests` for fast runs. Doc fuzz (2026-05-06): deleted stale database_schema.md, fixed MT README, updated SyncKit version in docs. Usability audit (2026-05-06): grade B-, 18 easy wins + 9 medium items + 3 deferred added. | |
| 6 | + | v0.5.0 deployed 2026-05-06. Audit grade A (Run 20, 2026-05-04). ~85K LOC, 1,912 tests (1220 unit + 664 integration + 28 load), 0 cargo warnings, 2 flaky sandbox tests. CI on astra operational. Mutation kill rate 99.4%. Property-based testing active (proptest). UX audit (2026-05-06): Learnability A-, Complexity B+, Discoverability B+, Feature Completeness B. Shopping cart implemented: multi-item Stripe checkout sessions, wishlist-to-cart flow, per-seller grouping, savings nudges. Migration 096. | |
| 7 | 7 | ||
| 8 | 8 | Human tasks (manual testing, outreach, legal, infrastructure) moved to `human_todo.md`. | |
| 9 | 9 | Completed items moved to `todo_done.md`. | |
| @@ -86,12 +86,28 @@ | |||
| 86 | 86 | - Conversion analytics: Views + Conversion stat cards on both user and project Analytics tabs. Period-over-period % for views. | |
| 87 | 87 | - Subscription → Membership terminology: project dashboard tab, wizard, project page, 7 docs files. | |
| 88 | 88 | ||
| 89 | - | ### UX — Deferred (post-beta table stakes) | |
| 89 | + | ### UX Audit Remediation (2026-05-06) — DONE (4 rounds) | |
| 90 | + | ||
| 91 | + | **Learnability (B+ → A-):** Join preamble, TOTP/passkey labels, SyncKit jargon fix, empty state CTAs (subscriptions, collections), project onboarding checklist, tab hover hints (all dashboards), passkey login label. | |
| 92 | + | ||
| 93 | + | **Complexity (B- → B+):** Pitch step simplified (removed trial/tier fields), item wizard sections step removed (6→5 steps), item Settings merged into Details, item Embed folded into Overview (7→5 tabs), item tab hints. | |
| 94 | + | ||
| 95 | + | **Upload flow:** Client-side file size pre-validation with tier limit from presign response, tier-aware error messages (HTTP 413), retry preserves file selection, upload speed + ETA display. | |
| 96 | + | ||
| 97 | + | **Discoverability (B- → B+):** Wishlists tab in Library (new endpoint + template), data export link in dashboard header, changelog link de-muted, RSS/copy-link moved to profile header, library purchases search filter, quick-edit links confirmed present. | |
| 98 | + | ||
| 99 | + | **Shopping cart + wishlist checkout (Feature Completeness B → B+):** Migration 096 (cart_items table), cart DB module, cart API (toggle/remove/count), multi-line-item Stripe Checkout Sessions per seller (Direct Charges), cart checkout route, cart webhook handler (batch transaction completion), cart page grouped by seller with savings nudge, wishlist-to-cart flow, "Add to Cart" on item pages + wishlists + purchase page, cart link in nav, scheduler cleanup. | |
| 100 | + | ||
| 101 | + | ### UX — Deferred (post-launch) | |
| 102 | + | - [ ] Storefront customization (creator themes/colors — 24 TOML themes exist in shared/themes/) | |
| 90 | 103 | - [ ] Reviews/ratings system for items | |
| 104 | + | - [ ] Wishlist price-drop alerts | |
| 91 | 105 | - [ ] Gift purchases at checkout | |
| 92 | 106 | - [ ] HTML rich email for creator broadcasts (currently plain text only) | |
| 93 | - | - [ ] Creator-to-fan broadcast email composer (full system, not just contact export) | |
| 94 | 107 | - [ ] In-app notification center (beyond email-only notifications) | |
| 108 | + | - [ ] Cross-seller cart checkout (currently one Stripe session per seller) | |
| 109 | + | - [ ] Promo codes in cart checkout (currently single-item only) | |
| 110 | + | - [ ] PWYW custom amounts in cart (currently uses minimum price) | |
| 95 | 111 | ||
| 96 | 112 | --- | |
| 97 | 113 | ||
| @@ -274,7 +290,7 @@ | |||
| 274 | 290 | import/ (CSV converter, pipeline, intermediate format) | |
| 275 | 291 | MNW/server/tests/ | |
| 276 | 292 | integration.rs, harness/, workflows/*.rs | |
| 277 | - | MNW/server/migrations/ (001-090) | |
| 293 | + | MNW/server/migrations/ (001-096) | |
| 278 | 294 | MNW/server/templates/ | |
| 279 | 295 | MNW/server/deploy/ | |
| 280 | 296 | MNW/server/site-docs/public/, MNW/server/site-docs/unpublished/ |
| @@ -23,6 +23,7 @@ | |||
| 23 | 23 | filenameEl: document.getElementById('upload-filename'), | |
| 24 | 24 | percentEl: document.getElementById('upload-percent'), | |
| 25 | 25 | progressBar: document.getElementById('progress-bar'), | |
| 26 | + | speedEl: document.getElementById('upload-speed'), | |
| 26 | 27 | }); | |
| 27 | 28 | ||
| 28 | 29 | initDropzone( | |
| @@ -45,14 +46,24 @@ | |||
| 45 | 46 | }); | |
| 46 | 47 | } | |
| 47 | 48 | ||
| 49 | + | var lastFile = null; | |
| 50 | + | ||
| 48 | 51 | document.getElementById('cancel-upload-btn').addEventListener('click', function() { | |
| 49 | 52 | uploader.cancel(); | |
| 50 | 53 | resetUpload(); | |
| 51 | 54 | }); | |
| 52 | 55 | ||
| 53 | - | document.getElementById('retry-upload-btn').addEventListener('click', resetUpload); | |
| 56 | + | document.getElementById('retry-upload-btn').addEventListener('click', function() { | |
| 57 | + | if (lastFile) { | |
| 58 | + | document.getElementById('upload-error').classList.add('hidden'); | |
| 59 | + | uploadAudio(lastFile); | |
| 60 | + | } else { | |
| 61 | + | resetUpload(); | |
| 62 | + | } | |
| 63 | + | }); | |
| 54 | 64 | ||
| 55 | 65 | function resetUpload() { | |
| 66 | + | lastFile = null; | |
| 56 | 67 | document.getElementById('audio-dropzone').classList.remove('hidden'); | |
| 57 | 68 | document.getElementById('upload-progress').classList.add('hidden'); | |
| 58 | 69 | document.getElementById('upload-success').classList.add('hidden'); | |
| @@ -61,6 +72,7 @@ | |||
| 61 | 72 | } | |
| 62 | 73 | ||
| 63 | 74 | function uploadAudio(file) { | |
| 75 | + | lastFile = file; | |
| 64 | 76 | document.getElementById('audio-dropzone').classList.add('hidden'); | |
| 65 | 77 | document.getElementById('upload-progress').classList.remove('hidden'); | |
| 66 | 78 | ||
| @@ -81,6 +93,11 @@ | |||
| 81 | 93 | return res.json(); | |
| 82 | 94 | }) | |
| 83 | 95 | .then(function(data) { | |
| 96 | + | if (data.max_file_bytes && file.size > data.max_file_bytes) { | |
| 97 | + | var limitMB = (data.max_file_bytes / (1024 * 1024)).toFixed(0); | |
| 98 | + | var fileMB = (file.size / (1024 * 1024)).toFixed(1); | |
| 99 | + | throw new Error('File is ' + fileMB + ' MB but your plan allows up to ' + limitMB + ' MB per file. Upgrade your tier or use a smaller file.'); | |
| 100 | + | } | |
| 84 | 101 | return uploader.upload(data.upload_url, file, data.s3_key, 'audio/mpeg', data.cache_control); | |
| 85 | 102 | }) | |
| 86 | 103 | .then(function(s3Key) { | |
| @@ -125,6 +142,7 @@ | |||
| 125 | 142 | filenameEl: document.getElementById('version-upload-filename'), | |
| 126 | 143 | percentEl: document.getElementById('version-upload-percent'), | |
| 127 | 144 | progressBar: document.getElementById('version-progress-bar'), | |
| 145 | + | speedEl: document.getElementById('version-upload-speed'), | |
| 128 | 146 | }); | |
| 129 | 147 | ||
| 130 | 148 | var versionDropzone = document.getElementById('version-dropzone'); | |
| @@ -223,6 +241,11 @@ | |||
| 223 | 241 | return res.json(); | |
| 224 | 242 | }) | |
| 225 | 243 | .then(function(data) { | |
| 244 | + | if (data.max_file_bytes && file.size > data.max_file_bytes) { | |
| 245 | + | var limitMB = (data.max_file_bytes / (1024 * 1024)).toFixed(0); | |
| 246 | + | var fileMB = (file.size / (1024 * 1024)).toFixed(1); | |
| 247 | + | throw new Error('File is ' + fileMB + ' MB but your plan allows up to ' + limitMB + ' MB per file. Upgrade your tier or use a smaller file.'); | |
| 248 | + | } | |
| 226 | 249 | return uploader.upload(data.upload_url, file, data.s3_key, 'application/octet-stream', data.cache_control); | |
| 227 | 250 | }) | |
| 228 | 251 | .then(function(s3Key) { |
| @@ -17,16 +17,19 @@ | |||
| 17 | 17 | this.filenameEl = opts.filenameEl; | |
| 18 | 18 | this.percentEl = opts.percentEl; | |
| 19 | 19 | this.progressBar = opts.progressBar; | |
| 20 | + | this.speedEl = opts.speedEl || null; | |
| 20 | 21 | this._xhr = null; | |
| 21 | 22 | } | |
| 22 | 23 | ||
| 23 | 24 | /** Start an upload. Returns a Promise resolving to the s3Key. */ | |
| 24 | 25 | S3Uploader.prototype.upload = function(url, file, s3Key, fallbackContentType, cacheControl) { | |
| 25 | 26 | var self = this; | |
| 27 | + | var startTime = Date.now(); | |
| 26 | 28 | ||
| 27 | 29 | if (self.filenameEl) self.filenameEl.textContent = file.name; | |
| 28 | 30 | if (self.percentEl) self.percentEl.textContent = '0%'; | |
| 29 | 31 | if (self.progressBar) self.progressBar.style.width = '0%'; | |
| 32 | + | if (self.speedEl) self.speedEl.textContent = ''; | |
| 30 | 33 | ||
| 31 | 34 | return new Promise(function(resolve, reject) { | |
| 32 | 35 | var xhr = new XMLHttpRequest(); | |
| @@ -37,6 +40,20 @@ | |||
| 37 | 40 | var percent = Math.round((e.loaded / e.total) * 100); | |
| 38 | 41 | if (self.percentEl) self.percentEl.textContent = percent + '%'; | |
| 39 | 42 | if (self.progressBar) self.progressBar.style.width = percent + '%'; | |
| 43 | + | if (self.speedEl && e.loaded > 0) { | |
| 44 | + | var elapsed = (Date.now() - startTime) / 1000; | |
| 45 | + | if (elapsed > 0.5) { | |
| 46 | + | var speed = e.loaded / elapsed; | |
| 47 | + | var remaining = (e.total - e.loaded) / speed; | |
| 48 | + | var speedStr = speed > 1024 * 1024 | |
| 49 | + | ? (speed / (1024 * 1024)).toFixed(1) + ' MB/s' | |
| 50 | + | : (speed / 1024).toFixed(0) + ' KB/s'; | |
| 51 | + | var etaStr = remaining < 60 | |
| 52 | + | ? Math.ceil(remaining) + 's' | |
| 53 | + | : Math.ceil(remaining / 60) + 'm ' + Math.ceil(remaining % 60) + 's'; | |
| 54 | + | self.speedEl.textContent = speedStr + ' \u2014 ' + etaStr + ' remaining'; | |
| 55 | + | } | |
| 56 | + | } | |
| 40 | 57 | } | |
| 41 | 58 | }); | |
| 42 | 59 |