Skip to main content

max / makenotwork

Dashboard restructure: split Account tab, remove labels, rename jargon Dashboard: - Split monolithic Account tab into Profile, Account, and Plan tabs - Creators land on Projects tab; fans land on Payments - Progressive disclosure: gate seller sections, custom domain, creator notifications behind creator access - Move SyncKit to project-level tab (renamed "Cloud Sync") - Simplify Stripe status to single intent-driven display Labels: - Remove labels feature entirely (AI disclosure covers trust signals) - Drop routes, DB module, admin panel, discover filters, templates, tests - Migration 094 drops labels and project_labels tables Jargon renames: - Insertions → Clips / Dynamic Clips - Revenue Splits → Collaborator Payouts - SyncKit tab → Cloud Sync
Co-Authored-By
Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-05 17:52 UTC
Commit: 6e6eb06069944d6ad044d410ce073c266b1a7934
Parent: 55b1271
46 files changed, +1101 insertions, -1476 deletions
@@ -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.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.
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.
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,55 +12,92 @@
12 12
13 13 ## Remaining Audit Items
14 14
15 - ### Code Fuzz (Run 19, 2026-05-03) — DONE
16 - All high/serious/minor items fixed. Migration 090 deployed. Test failures fixed. CI fixed.
15 + ### Run 20 (2026-05-04) — Medium priority — DONE
17 16
18 - #### Low (previous)
19 - - [ ] Add README.md to server/
17 + ### Run 20 — Low priority — DONE
18 + Split checkout.rs (792 -> 434 + 308 helpers). yara-x bumped 1.14->1.15 (intaglio fix). aws-sdk-s3 bumped 1.119->1.131. rustls-webpki 0.101.7/0.103.10 remain upstream-blocked.
20 19
21 20 ### Deferred Code Quality
22 - - [ ] Extract inline SQL from route handlers to db/ (4 locations)
23 - - [ ] Remove `async-trait` in favor of Rust 2024 native async traits
24 - - [ ] Migrate inline `onclick` handlers to `addEventListener` for strict CSP
25 - - [ ] Monitor scheduler.rs (1249), git/mod.rs (624), license_keys.rs (684) for growth
21 + - [ ] Remove `async-trait` in favor of Rust 2024 native async traits (chronic)
22 + - [ ] Add README.md to server/
23 + - [ ] Split oversized route files: exports.rs (737), license_keys.rs (741), health.rs (844), tabs/user.rs (707)
24 + - [ ] Monitor scheduler.rs (1249), git/mod.rs (624) for growth
25 + - [x] [rust-fuzz] Replace `.unwrap()` with `.expect("tier passed is_none guard")` in db/creator_tiers.rs:607
26 + - [x] [rust-fuzz] Change `Config.host_url` from `String` to `Arc<str>` — eliminates 30+ String clones across codebase
26 27
27 - ### Dashboard Usability Audit (2026-05-03)
28 + ### Dashboard Restructure (2026-05-05)
28 29
29 - Grade: B-. Complexity B, Completeness B-, Learnability C+, Discoverability C.
30 + Redesign user dashboard around user intent rather than role. Split the monolithic Account tab into three purpose-driven tabs. Gate content by account state (fan → new creator → active creator).
30 31
31 - #### Discoverability (Critical)
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
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
35 - - [x] Surface Stripe requirement on Project Overview — persistent banner with direct link, shown until Stripe is connected
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
37 - - [x] Always show Blog tab with empty state — shows "Start writing to engage your audience" with context about RSS
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
39 - - [ ] Add "Embed & Share" quick action on Item Overview — embed codes only discoverable by navigating to specific item's Embed tab
32 + #### Tab Structure
40 33
41 - #### Learnability (High)
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
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
46 - - [x] Add AI Classification option descriptions in item_details dropdown
34 + New user-level tabs: **Projects, Payments, Analytics, Profile, Account, Plan** + overflow (Media, SSH Keys, Forums, Support).
47 35
48 - #### Complexity (Medium)
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)
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"
52 - - [ ] Simplify Stripe status display — replace raw onboarding states with user-intent language: "Ready to receive payments" (green) or "Action required: [task]" (red)
36 + Fan tab bar (no creator access): **Profile, Payments, Plan, Support** — 4 tabs, no overflow.
53 37
54 - #### Feature Completeness (Medium)
55 - - [ ] Add download count analytics per item — standard on Bandcamp, Gumroad; primary consumption metric for digital goods
56 - - [ ] Add cross-project item view — creator with 3 projects can't see all items in one place; no global search
57 - - [ ] Add refund initiation from dashboard — currently must go to Stripe dashboard to issue refunds
38 + | Tab | Contents | Mental model |
39 + |-----|----------|--------------|
40 + | Projects | Project list, create project | "My work" |
41 + | Payments | Purchases (fan), + sales/tips/splits (creator) | "My money" |
42 + | Analytics | Views, revenue, fans | "My numbers" |
43 + | Profile | Display name, bio, links, custom domain, personal feed | "How the world sees me" |
44 + | Account | Email/username, password, 2FA, passkeys, sessions, notifications, data export/import, account status, pause, delete | "My account's mechanics" |
45 + | Plan | Creator tier, storage, Stripe connect, invites, broadcast; or "Become a Creator" application for fans | "My relationship with the platform" |
46 +
47 + Overflow (creator only): Media (after first project), SSH Keys (when git enabled), Forums (when MT memberships exist), Support (always).
48 +
49 + #### Progressive Disclosure by Account State
50 +
51 + **Fan (no creator access):**
52 + - Profile: display name, bio, links, personal feed. No custom domain
53 + - Payments: purchases only. No seller sections
54 + - Plan: "Become a Creator" — explanation, tier comparison, application form
55 + - Account: full security (password, 2FA, passkeys, sessions), fan notification prefs (release alerts, login alerts), data export, delete
56 + - Hidden entirely: Projects, Analytics
57 +
58 + **New creator (approved, no projects/sales yet):**
59 + - All fan content, plus:
60 + - Projects: empty state → "Create your first project"
61 + - Analytics: empty state → "Publish an item to start tracking views and revenue"
62 + - Profile: + custom domain
63 + - Account: + pause creator, + creator notification prefs (sale, follower, issue alerts)
64 + - Payments: + empty seller section ("Once you make sales, they appear here")
65 + - Plan: current tier, storage, upgrade/downgrade, invites, broadcast
66 +
67 + **Active creator:**
68 + - Full content in all tabs
69 +
70 + #### Implementation Steps
71 +
72 + - [x] **Phase 1: Split templates** — Extract `user_details.html` into `user_profile.html`, `user_account.html`, `user_plan.html`
73 + - [x] **Phase 2: Route handlers** — Add `dashboard_tab_profile`, `dashboard_tab_account`; keep `dashboard_tab_creator` (served as "Plan" tab)
74 + - [x] **Phase 3: Tab bar** — Update `dashboard-user.html` with new tab layout. Fan vs creator logic for which tabs render
75 + - [x] **Phase 4: Progressive disclosure** — Gate sections within each tab by account state. Custom domain behind creator access. Seller payments behind creator access. Creator notifications behind creator access. Analytics empty state improved
76 + - [x] **Phase 5: Move SyncKit to project dashboard** — Project-level tab alongside Code, filtered to that project's apps. DB query `get_sync_apps_by_project` added. Removed from user-level overflow
77 + - [x] **Phase 6: Simplify Stripe status** — Single intent-driven status: "Ready to receive payments" (green check) or "Action required: [task]" (warning). Replaces 3-column grid + multiple warning banners
78 + - [ ] **Phase 7: Performance pass** — Deferred to a dedicated cross-site performance push. Pre-fetch tab data on hover, optimize perceived load times, aim for A+ responsiveness across every page
79 +
80 + #### Remaining Items (from previous audit, unchanged)
81 +
82 + Discoverability:
83 + - [ ] Add Media Library access from content editors — "Insert Image" button in blog/item editors
84 + - [ ] Add content search/filter to project Content tab — search by title, filter by status/type
85 + - [ ] Add "Embed & Share" quick action on Item Overview
86 +
87 + Learnability:
88 + - [x] Rename jargon: Insertions → "Clips"/"Dynamic Clips", Revenue Splits → "Collaborator Payouts", SyncKit tab → "Cloud Sync"
89 + - [x] Remove Labels feature — AI disclosure covers the trust signal use case. Deleted: db module, API routes, admin panel, discover filters, project settings, item publish confirmations, 12 tests. DB tables (labels, project_labels) still exist but are unused; drop in a future migration
90 + - [x] Add empty state context to analytics (done in Phase 4)
91 +
92 + Feature Completeness:
93 + - [ ] Add download count analytics per item
94 + - [ ] Add cross-project item view
95 + - [ ] Add refund initiation from dashboard
58 96 - [ ] Add "Export as CSV" button on item sales tables
59 97
60 - #### Discoverability (Lower)
61 - - [ ] Add bulk operations hint on Content tab — show "Select items for bulk actions" tip; show action bar in disabled state so feature is discoverable
62 - - [ ] Add contextual next-step suggestions after key actions — "Next: Set pricing" after creating item; "Next: Create item" after creating project
63 - - [ ] Show all conditional tabs (SyncKit, SSH Keys, Forums) always with empty states explaining prerequisites — instead of hiding them entirely
98 + Discoverability (Lower):
99 + - [ ] Add bulk operations hint on Content tab
100 + - [ ] Add contextual next-step suggestions after key actions
64 101
65 102 ### UX — Deferred (post-beta table stakes)
66 103 - [ ] Reviews/ratings system for items
@@ -17,7 +17,6 @@
17 17 pub search: Option<&'a str>,
18 18 pub item_type: Option<ItemType>,
19 19 pub tag: Option<&'a str>,
20 - pub label: Option<&'a str>,
21 20 pub min_price: Option<i32>,
22 21 pub max_price: Option<i32>,
23 22 pub sort_by: Option<DiscoverSort>,
@@ -77,7 +76,7 @@
77 76 }
78 77
79 78 /// Append discover-item filter clauses to a dynamic query.
80 - /// Parameter positions: $1=search, $2=item_type, $3=min_price, $4=max_price, $5=tag, $6=label, $7=ai_tier.
79 + /// Parameter positions: $1=search, $2=item_type, $3=min_price, $4=max_price, $5=tag, $6=ai_tier.
81 80 ///
82 81 /// When `short_query` is `true`, the ILIKE-only clause is used instead of the
83 82 /// full trigram + ILIKE clause (trigram matching is unreliable for 1-2 char terms).
@@ -113,21 +112,12 @@
113 112 )"#,
114 113 );
115 114 }
116 - if filters.label.is_some() {
117 - query.push_str(
118 - r#" AND EXISTS (
119 - SELECT 1 FROM project_labels pl
120 - JOIN labels l ON l.id = pl.label_id
121 - WHERE pl.project_id = i.project_id AND l.slug = $6
122 - )"#,
123 - );
124 - }
125 115 if filters.ai_tier.is_some() {
126 - query.push_str(" AND i.ai_tier = $7");
116 + query.push_str(" AND i.ai_tier = $6");
127 117 }
128 118 }
129 119
130 - /// Bind the 7 discover-filter parameters ($1-$7) to a sqlx query.
120 + /// Bind the 6 discover-filter parameters ($1-$6) to a sqlx query.
131 121 macro_rules! bind_item_discover_filters {
132 122 ($q:expr, $filters:expr, $search_term:expr) => {
133 123 $q.bind($search_term.unwrap_or(""))
@@ -135,7 +125,6 @@
135 125 .bind($filters.min_price.unwrap_or(0))
136 126 .bind($filters.max_price.unwrap_or(i32::MAX))
137 127 .bind($filters.tag.unwrap_or(""))
138 - .bind($filters.label.unwrap_or(""))
139 128 .bind($filters.ai_tier.map(|t| t.to_string()).unwrap_or_default())
140 129 };
141 130 }
@@ -262,7 +251,7 @@
262 251 }
263 252 };
264 253
265 - query.push_str(&format!(" ORDER BY {} LIMIT $8 OFFSET $9", order));
254 + query.push_str(&format!(" ORDER BY {} LIMIT $7 OFFSET $8", order));
266 255
267 256 let items = bind_item_discover_filters!(
268 257 sqlx::query_as::<_, DbDiscoverItemRow>(&query),
@@ -320,7 +309,6 @@
320 309 pool: &PgPool,
321 310 search: Option<&str>,
322 311 category_slug: Option<&str>,
323 - label_slug: Option<&str>,
324 312 limit: i64,
325 313 offset: i64,
326 314 ) -> Result<Vec<DbDiscoverProjectRow>> {
@@ -409,16 +397,6 @@
409 397 query.push_str(" AND pc.slug = $2");
410 398 }
411 399
412 - if label_slug.is_some() {
413 - query.push_str(
414 - r#" AND EXISTS (
415 - SELECT 1 FROM project_labels pl
416 - JOIN labels l ON l.id = pl.label_id
417 - WHERE pl.project_id = p.id AND l.slug = $3
418 - )"#,
419 - );
420 - }
421 -
422 400 query.push_str(" GROUP BY p.id, u.username, pc.name, pc.slug");
423 401
424 402 let order = if has_search {
@@ -427,12 +405,11 @@
427 405 "p.created_at DESC"
428 406 };
429 407
430 - query.push_str(&format!(" ORDER BY {} LIMIT $4 OFFSET $5", order));
408 + query.push_str(&format!(" ORDER BY {} LIMIT $3 OFFSET $4", order));
431 409
432 410 let projects = sqlx::query_as::<_, DbDiscoverProjectRow>(&query)
433 411 .bind(search_term.as_deref().unwrap_or(""))
434 412 .bind(category_slug.unwrap_or(""))
435 - .bind(label_slug.unwrap_or(""))
436 413 .bind(limit)
437 414 .bind(offset)
438 415 .fetch_all(pool)
@@ -447,7 +424,6 @@
447 424 pool: &PgPool,
448 425 search: Option<&str>,
449 426 category_slug: Option<&str>,
450 - label_slug: Option<&str>,
451 427 ) -> Result<i64> {
452 428 let search_term = normalize_search(search);
453 429 let has_search = search_term.is_some();
@@ -476,20 +452,9 @@
476 452 );
477 453 }
478 454
479 - if label_slug.is_some() {
480 - query.push_str(
481 - r#" AND EXISTS (
482 - SELECT 1 FROM project_labels pl
483 - JOIN labels l ON l.id = pl.label_id
484 - WHERE pl.project_id = p.id AND l.slug = $3
485 - )"#,
486 - );
487 - }
488 -
489 455 let count: i64 = sqlx::query_scalar(&query)
490 456 .bind(search_term.as_deref().unwrap_or(""))
491 457 .bind(category_slug.unwrap_or(""))
492 - .bind(label_slug.unwrap_or(""))
493 458 .fetch_one(pool)
494 459 .await?;
495 460
@@ -165,7 +165,6 @@
165 165 IssueId,
166 166 IssueCommentId,
167 167 IssueLabelId,
168 - LabelId,
169 168 ReportId,
170 169 FanPlusSubscriptionId,
171 170 CollectionId,
@@ -28,7 +28,6 @@
28 28 pub(crate) mod subscriptions;
29 29 pub(crate) mod tags;
30 30 pub(crate) mod categories;
31 - pub(crate) mod labels;
32 31 pub(crate) mod sessions;
33 32 pub(crate) mod totp;
34 33 pub(crate) mod passkeys;
@@ -118,6 +118,21 @@
118 118 Ok(apps)
119 119 }
120 120
121 + /// Get all sync apps linked to a specific project.
122 + pub async fn get_sync_apps_by_project(
123 + pool: &PgPool,
124 + project_id: ProjectId,
125 + ) -> Result<Vec<DbSyncApp>> {
126 + let apps = sqlx::query_as::<_, DbSyncApp>(
127 + "SELECT * FROM sync_apps WHERE project_id = $1 ORDER BY created_at DESC LIMIT 100",
128 + )
129 + .bind(project_id)
130 + .fetch_all(pool)
131 + .await?;
132 +
133 + Ok(apps)
134 + }
135 +
121 136 /// Regenerate an API key for a sync app. Stores the hashed key and prefix.
122 137 #[tracing::instrument(skip_all)]
123 138 pub async fn regenerate_sync_app_key(