Skip to main content

max / makenotwork

G2: repo visibility, resolve_repo refactor, repo creation API, maxmj→max rename - Add visibility column (public/unlisted/private) to git_repos - Refactor all git routes through resolve_repo() helper: DB-driven user lookup, auto-registration, visibility enforcement - Add MaybeUser to smart HTTP handlers so private repos block clones - POST /api/repos creates bare repos on disk from dashboard - PUT /api/repos/{id}/visibility updates repo visibility - Dashboard "Create Repository" UI in project settings tab - Rename /opt/git/maxmj/ to /opt/git/max/, update all references - Update local git remotes from maxmj to max - Onboarding emails, blog scheduling, text editor improvements Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-03-11 05:24 UTC
Commit: 3fb6820a3708f06eff5966d7035fbe6f369f9202
Parent: 6db86ed
20 files changed, +656 insertions, -89 deletions
@@ -3453,7 +3453,7 @@
3453 3453
3454 3454 [[package]]
3455 3455 name = "makenotwork"
3456 - version = "0.1.7"
3456 + version = "0.1.9"
3457 3457 dependencies = [
3458 3458 "ammonia",
3459 3459 "anyhow",
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makenotwork"
3 - version = "0.1.8"
3 + version = "0.1.9"
4 4 edition = "2024"
5 5 license-file = "../../LICENSE"
6 6
@@ -13,7 +13,7 @@
13 13 ## Repository
14 14
15 15 ```
16 - makenot.work/git/maxmj/
16 + makenot.work/git/max/
17 17 ```
18 18
19 19 Available for review:
@@ -170,7 +170,7 @@
170 170 - Check for vulnerabilities
171 171 - Report issues responsibly
172 172
173 - Repository: [makenot.work/git/maxmj/](https://makenot.work/git/maxmj/)
173 + Repository: [makenot.work/git/max/](https://makenot.work/git/max/)
174 174
175 175 ---
176 176
@@ -2,8 +2,8 @@
2 2 # Setup SSH git push access on the production VPS.
3 3 #
4 4 # Creates a `git` system user with git-shell, home directory at /opt/git/
5 - # so that `git@makenot.work:maxmj/makenotwork.git` resolves to
6 - # /opt/git/maxmj/makenotwork.git.
5 + # so that `git@makenot.work:max/makenotwork.git` resolves to
6 + # /opt/git/max/makenotwork.git.
7 7 #
8 8 # Run as root on the production VPS.
9 9
@@ -62,7 +62,7 @@
62 62 echo "=== Git SSH setup complete ==="
63 63 echo ""
64 64 echo "Test with:"
65 - echo " git clone git@makenot.work:maxmj/makenotwork.git"
65 + echo " git clone git@makenot.work:max/makenotwork.git"
66 66 echo " cd makenotwork && git push"
67 67 echo ""
68 68 echo "To add more SSH keys, edit: $GIT_HOME/.ssh/authorized_keys"
@@ -561,6 +561,96 @@
561 561 self.send_email(to, subject, body).await
562 562 }
563 563
564 + // ── Onboarding email sequence ──
565 +
566 + /// Step 1: Welcome email sent immediately after signup.
567 + pub async fn send_onboarding_welcome(
568 + &self,
569 + to_email: &str,
570 + to_name: Option<&str>,
571 + host_url: &str,
572 + ) -> Result<()> {
573 + let subject = "Welcome to Makenot.work";
574 + let body = format!(
575 + r#"Hi{name},
576 +
577 + Welcome to Makenot.work — the fair creator platform.
578 +
579 + Here's what makes us different:
580 + - 0% platform fee. You keep everything (minus Stripe's ~3% processing).
581 + - No lock-in. Export your data, cancel anytime.
582 + - Source-available. You can see how everything works.
583 +
584 + To get started, head to your dashboard:
585 + {host_url}/dashboard
586 +
587 + You'll find a checklist there to help you set up your profile, connect Stripe, create your first project, and publish your first item.
588 +
589 + If you have questions, reply to this email — it goes straight to a human.
590 +
591 + - Makenotwork"#,
592 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
593 + host_url = host_url,
594 + );
595 +
596 + self.send_email(to_email, subject, &body).await
597 + }
598 +
599 + /// Step 2: Profile setup tips (sent ~24h after signup).
600 + pub async fn send_onboarding_profile(
601 + &self,
602 + to_email: &str,
603 + to_name: Option<&str>,
604 + host_url: &str,
605 + ) -> Result<()> {
606 + let subject = "Set up your creator profile";
607 + let body = format!(
608 + r#"Hi{name},
609 +
610 + A quick tip: creators with a display name and bio get more attention from fans.
611 +
612 + Head to your account settings to add them:
613 + {host_url}/dashboard (Account tab)
614 +
615 + Your display name appears on all your projects and items. Your bio tells fans who you are and what you create.
616 +
617 + Once your profile is set, create your first project — that's where your items live.
618 +
619 + - Makenotwork"#,
620 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
621 + host_url = host_url,
622 + );
623 +
624 + self.send_email(to_email, subject, &body).await
625 + }
626 +
627 + /// Step 3: Stripe connection guide (sent ~72h after signup).
628 + pub async fn send_onboarding_stripe(
629 + &self,
630 + to_email: &str,
631 + to_name: Option<&str>,
632 + host_url: &str,
633 + ) -> Result<()> {
634 + let subject = "Start receiving payments";
635 + let body = format!(
636 + r#"Hi{name},
637 +
638 + To sell on Makenot.work, connect your Stripe account. It takes about 5 minutes.
639 +
640 + {host_url}/dashboard (Payments tab)
641 +
642 + Stripe handles payment processing at ~3% per transaction. That's the only fee — Makenot.work takes 0%.
643 +
644 + Once connected, you can set prices on your items and start selling immediately.
645 +
646 + - Makenotwork"#,
647 + name = to_name.map(|n| format!(" {}", n)).unwrap_or_default(),
648 + host_url = host_url,
649 + );
650 +
651 + self.send_email(to_email, subject, &body).await
652 + }
653 +
564 654 /// Send an email (implementation)
565 655 async fn send_email(&self, to: &str, subject: &str, body: &str) -> Result<()> {
566 656 self.send_email_inner(to, subject, body, &[], None).await
@@ -65,6 +65,59 @@
65 65 }
66 66 }
67 67
68 + /// Process the getting-started email drip sequence.
69 + ///
70 + /// Step 1 (welcome) is sent at signup in the auth handler.
71 + /// Step 2 (profile tips) fires 24h after welcome, skipped if display_name is set.
72 + /// Step 3 (Stripe guide) fires 72h after welcome, skipped if Stripe is connected.
73 + async fn send_onboarding_emails(state: &AppState) {
74 + let host_url = &state.config.host_url;
75 +
76 + // Step 1→2: profile tips (24h after welcome)
77 + if let Ok(users) =
78 + db::users::get_onboarding_candidates(&state.db, 1, chrono::Duration::hours(24)).await
79 + {
80 + for user in users {
81 + // Skip if they already set a display name
82 + if user.display_name.is_some() {
83 + let _ = db::users::advance_onboarding_step(&state.db, user.id, 2).await;
84 + continue;
85 + }
86 + if let Err(e) = state
87 + .email
88 + .send_onboarding_profile(&user.email, user.display_name.as_deref(), host_url)
89 + .await
90 + {
91 + tracing::error!(error = ?e, user_id = %user.id, "failed to send onboarding profile email");
92 + continue;
93 + }
94 + let _ = db::users::advance_onboarding_step(&state.db, user.id, 2).await;
95 + }
96 + }
97 +
98 + // Step 2→3: Stripe guide (72h after welcome)
99 + if let Ok(users) =
100 + db::users::get_onboarding_candidates(&state.db, 2, chrono::Duration::hours(48)).await
101 + {
102 + for user in users {
103 + // Skip if they already connected Stripe
104 + if user.stripe_account_id.is_some() {
105 + let _ = db::users::advance_onboarding_step(&state.db, user.id, 3).await;
106 + continue;
107 + }
108 + if let Err(e) = state
109 + .email
110 + .send_onboarding_stripe(&user.email, user.display_name.as_deref(), host_url)
111 + .await
112 + {
113 + tracing::error!(error = ?e, user_id = %user.id, "failed to send onboarding stripe email");
114 + continue;
115 + }
116 + let _ = db::users::advance_onboarding_step(&state.db, user.id, 3).await;
117 + }
118 + }
119 + }
120 +
68 121 /// Spawn the background scheduler loop. Drop `shutdown_tx` to stop it.
69 122 pub fn spawn_scheduler(
70 123 state: AppState,
@@ -104,6 +157,9 @@
104 157 }
105 158 }
106 159
160 + // Send onboarding drip emails
161 + send_onboarding_emails(&state).await;
162 +
107 163 // Publish scheduled blog posts
108 164 match db::blog_posts::publish_scheduled_blog_posts(&state.db).await {
109 165 Ok(posts) => {