Skip to main content

max / makenotwork

Dashboard usability improvements: discoverability, learnability, complexity Discoverability: - Promote Analytics and Creator tabs to main tab bar, move Support to overflow - Add Stripe requirement banner on project overview until connected - Always show Blog tab with improved empty state messaging Learnability: - Make breadcrumbs link to #tab-projects for correct HTMX navigation - Add explanatory context to onboarding checklist steps - Add descriptive labels to AI Classification dropdown options Complexity: - Replace UUID display on item header with Copy ID button - Hide Stripe account ID behind disclosure toggle Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-04 19:14 UTC
Commit: 55b127149bc7bf1604e7a10061becf18def0628b
Parent: 99db513
13 files changed, +80 insertions, -58 deletions
@@ -3385,7 +3385,7 @@ dependencies = [
3385 3385
3386 3386 [[package]]
3387 3387 name = "makenotwork"
3388 - version = "0.4.9"
3388 + version = "0.4.10"
3389 3389 dependencies = [
3390 3390 "anyhow",
3391 3391 "argon2",
@@ -3,7 +3,7 @@
3 3 ## Status
4 4 Done: All pre-beta phases, UX audit remediation, creator trust audit remediation. Active: Creator setup (Stripe), manual testing. Next: Soft launch.
5 5
6 - v0.4.8. Audit grade A (Run 18, 2026-05-01). Code fuzz Run 19 complete (2026-05-03, 17 bugs fixed). ~1,220 unit + ~679 integration = ~1,930 tests (all passing with `--features fast-tests` as of 2026-05-04). Mutation kill rate 99.4%. Property-based testing active (proptest). `cargo test --features fast-tests` for fast runs.
6 + v0.4.10 deployed 2026-05-04. Audit grade A (Run 18, 2026-05-01). Code fuzz Run 19 complete (2026-05-03, 17 bugs fixed). ~1,220 unit + ~679 integration = ~1,930 tests (all passing). CI on astra operational. Mutation kill rate 99.4%. Property-based testing active (proptest). `cargo test --features fast-tests` for fast runs.
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`.
@@ -12,21 +12,8 @@ Completed items moved to `todo_done.md`.
12 12
13 13 ## Remaining Audit Items
14 14
15 - ### Code Fuzz (Run 19, 2026-05-03)
16 - All high/serious/minor items fixed. Migration 090 required. Needs deploy.
17 -
18 - ### Pre-deploy
19 - - [ ] Run migration 090 on production after deploy
20 -
21 - ### Test Failures — FIXED (2026-05-04)
22 - All 10 previously-failing integration tests resolved. Three bugs fixed:
23 - - **SyncKit 500→401**: `SyncAppKeyExtractor` returned `GovernorError::UnableToExtractKey` on missing bearer token, causing tower_governor to respond 500 before `SyncUser` could return 401. Fix: return nil sentinel `SyncAppId` to pass through to handler.
24 - - **CSRF double-slash**: Exempt prefix `/postmark/` caused matching to produce `/postmark//`. Fix: use `/postmark` without trailing slash.
25 - - **CSRF missing guest-free**: `/api/checkout/guest-free` was not in the CSRF exempt list. Fix: added it alongside `/api/checkout/guest`.
26 - - **Sandbox rate limit** (2 tests): Not a code bug — requires `--features fast-tests` as documented.
27 - - Removed dead `changelog_page` function (superseded by blog-based `/changelog` route). Zero warnings.
28 -
29 - Also: CI on astra has compilation errors (separate issue, likely stale paths from monorepo restructure).
15 + ### Code Fuzz (Run 19, 2026-05-03) — DONE
16 + All high/serious/minor items fixed. Migration 090 deployed. Test failures fixed. CI fixed.
30 17
31 18 #### Low (previous)
32 19 - [ ] Add README.md to server/
@@ -42,26 +29,26 @@ Also: CI on astra has compilation errors (separate issue, likely stale paths fro
42 29 Grade: B-. Complexity B, Completeness B-, Learnability C+, Discoverability C.
43 30
44 31 #### Discoverability (Critical)
45 - - [ ] Reorganize user dashboard tabs — move Analytics and Creator to visible tab bar by default; only keep SSH Keys, Forums in overflow. Move SyncKit from user dashboard to project dashboard (apps are linked to projects, not users). 10 user tabs (4 visible + 6 hidden) is too many hidden
46 - - [ ] Add dashboard tab customization setting — let users choose which tabs are always visible in the tab bar and which go into the overflow menu. Store preference per user. Sensible defaults (Account, Projects, Payments, Analytics, Creator visible; SSH Keys, Media, Forums, Support in overflow) but fully user-configurable
32 + - [x] Reorganize user dashboard tabs — Analytics and Creator moved to visible tab bar; Support moved to overflow. SyncKit, Media, SSH Keys, Forums remain in overflow
33 + - [ ] Add dashboard tab customization setting — let users choose which tabs are always visible in the tab bar and which go into the overflow menu. Store preference per user
47 34 - [ ] Move SyncKit tab to project dashboard — SyncKit apps are tied to projects (Linked To column). Show as a project-level tab alongside Code, filtered to that project's apps. Keep a user-level summary view (or link) for creators managing apps across multiple projects
48 - - [ ] Surface Stripe requirement on Project Overview — persistent banner "Connect Stripe to sell items" with direct link until connected and charges enabled
35 + - [x] Surface Stripe requirement on Project Overview — persistent banner with direct link, shown until Stripe is connected
49 36 - [ ] Add Media Library access from content editors — "Insert Image" button in blog editor and item content editor that opens media library. Currently completely disconnected from where users need it
50 - - [ ] Always show Blog tab with empty state — currently only appears if posts exist. Show "No blog posts yet. Start writing to engage your audience." with "New Post" button
37 + - [x] Always show Blog tab with empty state — shows "Start writing to engage your audience" with context about RSS
51 38 - [ ] Add content search/filter to project Content tab — search by title, filter by status (Draft/Published/Scheduled) and type. Table stakes for any content management interface
52 39 - [ ] Add "Embed & Share" quick action on Item Overview — embed codes only discoverable by navigating to specific item's Embed tab
53 40
54 41 #### Learnability (High)
55 - - [ ] Make breadcrumbs clickable navigation links — currently display-only text; users can't click back to parent; broken with HTMX tab state
56 - - [ ] Add explanatory text to jargon terms: SyncKit ("Cloud sync for indie apps" subtitle), Insertions (rename to "Dynamic Clips" in storage display), AI Classification (add option descriptions: "Handmade — no AI tools", "AI-Assisted — AI tools with human creation", "AI-Generated — primarily created by AI"), Labels ("Platform-curated tags describing your project's commitments"), Revenue Splits (add setup instructions linking to Project Members tab)
57 - - [ ] Improve onboarding checklist context — add brief explanations to each step: "Connect Stripe — required to receive payments, 3% processing only", "Create a project — blog, podcast, course, etc."
42 + - [x] Make breadcrumbs clickable navigation links — link to `#tab-projects` for correct tab navigation
43 + - [ ] Add explanatory text to jargon terms: SyncKit ("Cloud sync for indie apps" subtitle), Insertions (rename to "Dynamic Clips" in storage display), Labels ("Platform-curated tags describing your project's commitments"), Revenue Splits (add setup instructions linking to Project Members tab)
44 + - [x] Improve onboarding checklist context — each step now has a brief explanation
58 45 - [ ] Add empty state context to analytics — change "No revenue data yet" to "Once you publish items and make sales, revenue data will appear here" with link to publish
59 - - [ ] Add AI Classification option descriptions in item_details dropdown — "Handmade (no AI tools)", "AI-Assisted (AI tools with human creation)", "AI-Generated (primarily created by AI)" with examples
46 + - [x] Add AI Classification option descriptions in item_details dropdown
60 47
61 48 #### Complexity (Medium)
62 49 - [ ] Split Account Details tab into sub-sections — currently 13 sections in one scroll. Group into: Profile (name, bio, links, domain), Security (password, 2FA, passkeys, sessions), Notifications, Data & Privacy (export, import, deletion)
63 - - [ ] Hide UUID from item dashboard header — remove or put behind a "Copy ID" button. Creators don't need to see UUIDs
64 - - [ ] Hide Stripe account ID behind disclosure toggle — currently shown in monospace on Payments tab. Collapse behind "Show details"
50 + - [x] Hide UUID from item dashboard header — replaced with "Copy ID" button
51 + - [x] Hide Stripe account ID behind disclosure toggle — collapsed behind `<details>` "Show account ID"
65 52 - [ ] Simplify Stripe status display — replace raw onboarding states with user-intent language: "Ready to receive payments" (green) or "Action required: [task]" (red)
66 53
67 54 #### Feature Completeness (Medium)
@@ -4,6 +4,18 @@ Items moved from todo.md. See git history for implementation details.
4 4
5 5 ---
6 6
7 + ## v0.4.10 Deploy (2026-05-04)
8 + - [x] Fix CSRF exempt path matching: `/postmark/` trailing slash → double-slash in starts_with. Changed to `/postmark`.
9 + - [x] Add `/api/checkout/guest-free` to CSRF exempt list.
10 + - [x] Fix SyncKit rate limiter: return nil sentinel for unauthenticated requests instead of 500.
11 + - [x] Remove dead `changelog_page` (superseded by blog-based `/changelog` route). Zero warnings.
12 + - [x] Run migration 090 on production (auto-ran on startup).
13 + - [x] CI on astra: fix SSH config (port 22 for git), switch remote to Tailscale path, install sqlx-cli, run migrations on staging DB, add `--features fast-tests`, replace `SQLX_OFFLINE` with live `DATABASE_URL`.
14 + - [x] Kill stuck Apr-10 SSE test process on astra.
15 + - [x] All 1,930 tests passing (local + astra CI).
16 +
17 + ---
18 +
7 19 ## File Scanning Hardening
8 20 - [x] Add timeout to YARA scanning. Fixed: `scanner.set_timeout(30s)` via yara-x native API.
9 21 - [x] Nested archive detection: check magic bytes, not just file extensions. Fixed: magic bytes check for ZIP, gzip, 7z, RAR in archive.rs.
@@ -30,25 +30,25 @@ fn build_onboarding_checklist(
30 30 ) -> OnboardingChecklist {
31 31 let steps = vec![
32 32 OnboardingStep {
33 - label: "Set up your profile",
33 + label: "Set up your profile — name, bio, and links",
34 34 done: profile_done,
35 35 link_tab: "tab-details",
36 36 link_label: "Go to Account",
37 37 },
38 38 OnboardingStep {
39 - label: "Connect Stripe",
39 + label: "Connect Stripe — required to receive payments, 3% processing only",
40 40 done: stripe_done,
41 41 link_tab: "tab-payments",
42 42 link_label: "Go to Payments",
43 43 },
44 44 OnboardingStep {
45 - label: "Create your first project",
45 + label: "Create your first project — blog, podcast, course, etc.",
46 46 done: projects_done,
47 47 link_tab: "tab-projects",
48 48 link_label: "Go to Projects",
49 49 },
50 50 OnboardingStep {
51 - label: "Publish your first item",
51 + label: "Publish your first item — upload files, set pricing, go live",
52 52 done: publish_done,
53 53 link_tab: "tab-projects",
54 54 link_label: "Go to Projects",
@@ -130,9 +130,14 @@ pub(super) async fn project_tab_overview(
130 130 },
131 131 ];
132 132
133 + let db_user = db::users::get_user_by_id(&state.db, session_user.id)
134 + .await?
135 + .ok_or(AppError::NotFound)?;
136 +
133 137 Ok(helpers::with_etag(generation, ProjectOverviewTabTemplate {
134 138 stats,
135 139 project_slug: db_project.slug.to_string(),
140 + stripe_connected: db_user.stripe_account_id.is_some(),
136 141 }))
137 142 }
138 143
@@ -287,6 +287,7 @@ pub struct UserCreatorTabTemplate {
287 287 pub struct ProjectOverviewTabTemplate {
288 288 pub stats: Vec<StatCard>,
289 289 pub project_slug: String,
290 + pub stripe_connected: bool,
290 291 }
291 292
292 293 /// Dashboard tab: project content items list.
@@ -30,7 +30,7 @@
30 30 <div class="container">
31 31 <header>
32 32 <div class="breadcrumb">
33 - <a href="/dashboard">Dashboard</a> / <a href="/dashboard">Projects</a> / <a href="/dashboard/project/{{ project_slug }}">{{ project_title }}</a> / {{ item.title }}
33 + <a href="/dashboard#tab-projects">Dashboard</a> / <a href="/dashboard#tab-projects">Projects</a> / <a href="/dashboard/project/{{ project_slug }}">{{ project_title }}</a> / {{ item.title }}
34 34 </div>
35 35 <h1>{{ item.title }}</h1>
36 36 <div class="item-meta">
@@ -42,7 +42,8 @@
42 42 <span class="badge inactive">Draft</span>
43 43 {% endif %}
44 44 <span>{{ item.sales_count }} sales</span>
45 - <span class="uuid">UUID: {{ item.id }}</span>
45 + <button class="secondary small" style="font-size: 0.8rem; padding: 0.15rem 0.5rem; opacity: 0.6;"
46 + onclick="navigator.clipboard.writeText('{{ item.id }}'); this.textContent='Copied!'; setTimeout(()=>this.textContent='Copy ID', 1500);">Copy ID</button>
46 47 </div>
47 48 </header>
48 49
@@ -31,7 +31,7 @@
31 31 <div class="container">
32 32 <header>
33 33 <div class="breadcrumb">
34 - <a href="/dashboard">Dashboard</a> / <a href="/dashboard">Projects</a> / {{ project.title }}
34 + <a href="/dashboard#tab-projects">Dashboard</a> / <a href="/dashboard#tab-projects">Projects</a> / {{ project.title }}
35 35 </div>
36 36 <h1>{{ project.title }}<span class="dot">.</span></h1>
37 37 <div class="project-meta">
@@ -72,7 +72,6 @@
72 72 hx-swap="innerHTML"
73 73 hx-indicator="#tab-spinner"
74 74 onclick="setActiveTab(this)">Analytics</button>
75 - {% if has_blog %}
76 75 <button class="tab"
77 76 role="tab"
78 77 aria-selected="false"
@@ -83,7 +82,6 @@
83 82 hx-swap="innerHTML"
84 83 hx-indicator="#tab-spinner"
85 84 onclick="setActiveTab(this)">Blog</button>
86 - {% endif %}
87 85 <button class="tab"
88 86 role="tab"
89 87 aria-selected="false"
@@ -142,34 +142,32 @@
142 142 hx-swap="innerHTML"
143 143 hx-indicator="#tab-spinner"
144 144 onclick="setActiveTab(this)">Payments</button>
145 + {% if let Some(su) = session_user %}{% if su.can_create_projects %}
145 146 <button class="tab"
146 147 role="tab"
147 148 aria-selected="false"
148 149 aria-controls="tab-content"
149 - id="tab-support"
150 - hx-get="/dashboard/tabs/support"
150 + id="tab-analytics"
151 + hx-get="/dashboard/tabs/analytics"
151 152 hx-target="#tab-content"
152 153 hx-swap="innerHTML"
153 154 hx-indicator="#tab-spinner"
154 - onclick="setActiveTab(this)">Support</button>
155 + onclick="setActiveTab(this)">Analytics</button>
156 + {% endif %}{% endif %}
157 + <button class="tab"
158 + role="tab"
159 + aria-selected="false"
160 + aria-controls="tab-content"
161 + id="tab-creator"
162 + hx-get="/dashboard/tabs/creator"
163 + hx-target="#tab-content"
164 + hx-swap="innerHTML"
165 + hx-indicator="#tab-spinner"
166 + onclick="setActiveTab(this)">Creator</button>
155 167
156 168 <div class="tab-overflow" style="position: relative; display: inline-block;">
157 169 <button class="tab" onclick="var m=this.nextElementSibling; m.style.display=m.style.display==='block'?'none':'block';" type="button">More</button>
158 170 <div class="tab-overflow-menu" style="display: none; position: absolute; top: 100%; left: 0; z-index: 10; background: var(--background); border: 1px solid var(--border); min-width: 160px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
159 - {% if let Some(su) = session_user %}{% if su.can_create_projects %}
160 - <button class="tab" style="display: block; width: 100%; text-align: left; padding: 0.5rem 1rem;"
161 - hx-get="/dashboard/tabs/analytics"
162 - hx-target="#tab-content"
163 - hx-swap="innerHTML"
164 - hx-indicator="#tab-spinner"
165 - onclick="setActiveTab(this); this.closest('.tab-overflow-menu').style.display='none';">Analytics</button>
166 - {% endif %}{% endif %}
167 - <button class="tab" style="display: block; width: 100%; text-align: left; padding: 0.5rem 1rem;"
168 - hx-get="/dashboard/tabs/creator"
169 - hx-target="#tab-content"
170 - hx-swap="innerHTML"
171 - hx-indicator="#tab-spinner"
172 - onclick="setActiveTab(this); this.closest('.tab-overflow-menu').style.display='none';">Creator</button>
173 171 {% if let Some(su) = session_user %}{% if su.can_create_projects && !projects.is_empty() %}
174 172 <button class="tab" style="display: block; width: 100%; text-align: left; padding: 0.5rem 1rem;"
175 173 hx-get="/dashboard/tabs/synckit"
@@ -200,6 +198,12 @@
200 198 hx-indicator="#tab-spinner"
201 199 onclick="setActiveTab(this); this.closest('.tab-overflow-menu').style.display='none';">Forums</button>
202 200 {% endif %}
201 + <button class="tab" style="display: block; width: 100%; text-align: left; padding: 0.5rem 1rem;"
202 + hx-get="/dashboard/tabs/support"
203 + hx-target="#tab-content"
204 + hx-swap="innerHTML"
205 + hx-indicator="#tab-spinner"
206 + onclick="setActiveTab(this); this.closest('.tab-overflow-menu').style.display='none';">Support</button>
203 207 </div>
204 208 </div>
205 209 <span id="tab-spinner" class="htmx-indicator" style="margin-left: 1rem;" aria-live="polite"> Loading...</span>
@@ -38,9 +38,9 @@
38 38 <div class="form-group">
39 39 <label for="ai_tier">AI Classification</label>
40 40 <select id="ai_tier" name="ai_tier">
41 - <option value="handmade" {% if item.ai_tier == "handmade" %}selected{% endif %}>Handmade</option>
42 - <option value="assisted" {% if item.ai_tier == "assisted" %}selected{% endif %}>Assisted</option>
43 - <option value="generated" {% if item.ai_tier == "generated" %}selected{% endif %}>Generated</option>
41 + <option value="handmade" {% if item.ai_tier == "handmade" %}selected{% endif %}>Handmade — no AI tools used</option>
42 + <option value="assisted" {% if item.ai_tier == "assisted" %}selected{% endif %}>AI-Assisted — AI tools with human creation</option>
43 + <option value="generated" {% if item.ai_tier == "generated" %}selected{% endif %}>AI-Generated — primarily created by AI</option>
44 44 </select>
45 45 </div>
46 46 </div>
@@ -7,7 +7,8 @@
7 7
8 8 {% if posts.is_empty() %}
9 9 <div class="empty-state" style="padding: 2rem 0;">
10 - <p>No blog posts yet. Click "New Post" to create your first post.</p>
10 + <p>No blog posts yet. Start writing to engage your audience.</p>
11 + <p style="opacity: 0.7; font-size: 0.9rem; margin-top: 0.5rem;">Blog posts appear on your project page and in your RSS feed.</p>
11 12 </div>
12 13 {% else %}
13 14 <table class="data-table">
@@ -1,3 +1,13 @@
1 + {% if !stripe_connected %}
2 + <div style="margin-bottom: 1.5rem; padding: 1rem 1.25rem; background: var(--surface-muted); border: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.75rem;">
3 + <div>
4 + <strong>Connect Stripe to sell items</strong>
5 + <span style="opacity: 0.7; margin-left: 0.5rem;">Only 3% processing — no platform fee.</span>
6 + </div>
7 + <a href="/dashboard#tab-payments"><button class="primary small">Connect Stripe</button></a>
8 + </div>
9 + {% endif %}
10 +
1 11 <div class="tab-docs"><a href="/docs/projects">Docs: Projects &rarr;</a></div>
2 12
3 13 <div class="stats-grid">
@@ -10,7 +10,10 @@
10 10 <span class="badge {{ user.stripe_status_class() }}">{{ user.stripe_status_text() }}</span>
11 11 </div>
12 12 {% if let Some(account_id) = user.stripe_account_id %}
13 - <div class="text-xs" style="opacity: 0.5; font-family: monospace;">{{ account_id }}</div>
13 + <details style="margin-top: 0.25rem;">
14 + <summary style="font-size: 0.8rem; opacity: 0.5; cursor: pointer;">Show account ID</summary>
15 + <div class="text-xs" style="opacity: 0.5; font-family: monospace; margin-top: 0.25rem;">{{ account_id }}</div>
16 + </details>
14 17 {% endif %}
15 18 </div>
16 19 {% if !user.stripe_onboarding_complete || !user.stripe_charges_enabled || !user.stripe_payouts_enabled %}