max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
8 files changed,
+290 insertions,
-5 deletions
| @@ -10465,10 +10465,6 @@ | |||
| 10465 | 10465 | "pkg-config", | |
| 10466 | 10466 | ] | |
| 10467 | 10467 | ||
| 10468 | - | [[patch.unused]] | |
| 10469 | - | name = "supernote-push" | |
| 10470 | - | version = "0.1.0" | |
| 10471 | - | ||
| 10472 | 10468 | [[patch.unused]] | |
| 10473 | 10469 | name = "synckit-client" | |
| 10474 | 10470 | version = "0.6.0" | |
| @@ -10477,6 +10473,10 @@ | |||
| 10477 | 10473 | name = "synckit-config" | |
| 10478 | 10474 | version = "0.1.2" | |
| 10479 | 10475 | ||
| 10476 | + | [[patch.unused]] | |
| 10477 | + | name = "supernote-push" | |
| 10478 | + | version = "0.1.0" | |
| 10479 | + | ||
| 10480 | 10480 | [[patch.unused]] | |
| 10481 | 10481 | name = "kberg" | |
| 10482 | 10482 | version = "0.1.0" |
| @@ -21,3 +21,29 @@ | |||
| 21 | 21 | ``` | |
| 22 | 22 | ||
| 23 | 23 | See `CONTRIBUTING.md` for coding patterns, macros, and conventions. | |
| 24 | + | ||
| 25 | + | ## Contributing | |
| 26 | + | ||
| 27 | + | This project is licensed under PolyForm Noncommercial 1.0.0. Make Creative, LLC owns | |
| 28 | + | the codebase and needs to hold clear title to all of it, so contributions work a | |
| 29 | + | little differently here than on a permissively licensed project. | |
| 30 | + | ||
| 31 | + | **Found a bug, or something small and obviously wrong?** Open a bug report rather | |
| 32 | + | than sending a patch. A clear report of what you did, what happened, and what you | |
| 33 | + | expected is worth more to us than a diff, and it saves you the paperwork below. | |
| 34 | + | ||
| 35 | + | **Want to build something substantial?** Write to info@makenot.work first. We are | |
| 36 | + | open to paying for contributions that matter, and we would rather agree on scope and | |
| 37 | + | terms before you spend your evenings on it than after. | |
| 38 | + | ||
| 39 | + | **Sending code directly?** Any code you submit is covered by the Contributor License | |
| 40 | + | Agreement in `CLA.md`, which assigns copyright in the contribution to Make Creative, | |
| 41 | + | LLC. Read it before you send anything. Add these lines to your commit message to | |
| 42 | + | record that you accept it: | |
| 43 | + | ||
| 44 | + | Make-Creative-CLA: 1.0.0 | |
| 45 | + | Signed-off-by: Your Name <your@email> | |
| 46 | + | ||
| 47 | + | We may decline a contribution for any reason, including that it is not something we | |
| 48 | + | want to maintain. | |
| 49 | + |
| @@ -3615,6 +3615,12 @@ | |||
| 3615 | 3615 | text-align: left; | |
| 3616 | 3616 | } | |
| 3617 | 3617 | ||
| 3618 | + | .project-paywall-page .paywall-item-count { | |
| 3619 | + | font-size: var(--text-body); | |
| 3620 | + | opacity: 0.7; | |
| 3621 | + | margin-bottom: var(--gap-peer); | |
| 3622 | + | } | |
| 3623 | + | ||
| 3618 | 3624 | .project-paywall-page .paywall-tiers { | |
| 3619 | 3625 | display: grid; | |
| 3620 | 3626 | grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); |
| @@ -291,6 +291,24 @@ | |||
| 291 | 291 | Ok(()) | |
| 292 | 292 | } | |
| 293 | 293 | ||
| 294 | + | /// Active item count for one project, read from the denormalized | |
| 295 | + | /// `projects.item_count` column (migration 156, trigger-maintained). | |
| 296 | + | /// | |
| 297 | + | /// "Active" is exactly the storefront and discover filter (`is_public AND listed | |
| 298 | + | /// AND deleted_at IS NULL`), so this number agrees with both the item list on the | |
| 299 | + | /// project page and the count on a discover card. Use it wherever a page needs | |
| 300 | + | /// the count without the rows, notably the paywall, which shows how much is | |
| 301 | + | /// behind it but not what. | |
| 302 | + | #[tracing::instrument(skip_all, fields(%project_id))] | |
| 303 | + | pub async fn get_active_item_count(pool: &PgPool, project_id: ProjectId) -> Result<i32> { | |
| 304 | + | let count = sqlx::query_scalar::<_, i32>("SELECT item_count FROM projects WHERE id = $1") | |
| 305 | + | .bind(project_id) | |
| 306 | + | .fetch_one(pool) | |
| 307 | + | .await?; | |
| 308 | + | ||
| 309 | + | Ok(count) | |
| 310 | + | } | |
| 311 | + | ||
| 294 | 312 | /// Get public projects with item counts in a single query (avoids N+1) | |
| 295 | 313 | #[tracing::instrument(skip_all)] | |
| 296 | 314 | pub async fn get_public_projects_with_item_counts( |
| @@ -37,6 +37,10 @@ | |||
| 37 | 37 | <p class="paywall-description">{{ project.description }}</p> | |
| 38 | 38 | {% endif %} | |
| 39 | 39 | ||
| 40 | + | {% if project.item_count > 0 %} | |
| 41 | + | <p class="paywall-item-count">{{ project.item_count }} item{% if project.item_count != 1 %}s{% endif %} included</p> | |
| 42 | + | {% endif %} | |
| 43 | + | ||
| 40 | 44 | <div class="paywall-price">{{ price_display }}</div> | |
| 41 | 45 | ||
| 42 | 46 | {% match checkout_type %} |
| @@ -296,3 +296,79 @@ | |||
| 296 | 296 | ); | |
| 297 | 297 | } | |
| 298 | 298 | } | |
| 299 | + | ||
| 300 | + | /// A paywalled project page tells a visitor how much is behind the paywall. | |
| 301 | + | /// | |
| 302 | + | /// Hiding the count sells the project worse than showing it: a lot of items is | |
| 303 | + | /// the argument for paying. The items themselves stay gated, so this leaks | |
| 304 | + | /// nothing that was being sold. Before 2026-08-05 the paywall was built with a | |
| 305 | + | /// hardcoded count of 0 and said nothing at all. | |
| 306 | + | #[tokio::test] | |
| 307 | + | async fn paywalled_project_page_shows_its_item_count() { | |
| 308 | + | let mut h = TestHarness::new().await; | |
| 309 | + | ||
| 310 | + | let user_id = h | |
| 311 | + | .signup("paywalltest", "paywalltest@example.com", "password123") | |
| 312 | + | .await; | |
| 313 | + | h.grant_creator(user_id).await; | |
| 314 | + | h.client.post_form("/logout", "").await; | |
| 315 | + | h.login("paywalltest", "password123").await; | |
| 316 | + | ||
| 317 | + | let resp = h | |
| 318 | + | .client | |
| 319 | + | .post_form("/api/projects", "slug=paywall-proj&title=Paywall+Project") | |
| 320 | + | .await; | |
| 321 | + | let project: Value = resp.json(); | |
| 322 | + | let project_id = project["id"].as_str().unwrap(); | |
| 323 | + | ||
| 324 | + | h.client | |
| 325 | + | .put_json( | |
| 326 | + | &format!("/api/projects/{project_id}"), | |
| 327 | + | r#"{"is_public": true}"#, | |
| 328 | + | ) | |
| 329 | + | .await; | |
| 330 | + | ||
| 331 | + | for title in ["First+Piece", "Second+Piece"] { | |
| 332 | + | let resp = h | |
| 333 | + | .client | |
| 334 | + | .post_form( | |
| 335 | + | &format!("/api/projects/{project_id}/items"), | |
| 336 | + | &format!("title={title}&item_type=text&is_public=true&price_cents=0"), | |
| 337 | + | ) | |
| 338 | + | .await; | |
| 339 | + | assert!( | |
| 340 | + | resp.status.is_success(), | |
| 341 | + | "create item failed: {}", | |
| 342 | + | resp.text | |
| 343 | + | ); | |
| 344 | + | } | |
| 345 | + | ||
| 346 | + | // Price the project itself, which is what puts the page behind the paywall. | |
| 347 | + | let resp = h | |
| 348 | + | .client | |
| 349 | + | .put_json( | |
| 350 | + | &format!("/api/projects/{project_id}"), | |
| 351 | + | r#"{"pricing_model": "buy_once", "price_dollars": 9.0}"#, | |
| 352 | + | ) | |
| 353 | + | .await; | |
| 354 | + | assert!( | |
| 355 | + | resp.status.is_success(), | |
| 356 | + | "set project pricing failed: {}", | |
| 357 | + | resp.text | |
| 358 | + | ); | |
| 359 | + | ||
| 360 | + | h.client.post_form("/logout", "").await; | |
| 361 | + | ||
| 362 | + | let resp = h.client.get("/p/paywall-proj").await; | |
| 363 | + | assert_eq!(resp.status, 200, "paywalled project page should render"); | |
| 364 | + | assert!( | |
| 365 | + | resp.text.contains("2 items included"), | |
| 366 | + | "paywall should show how much is behind it; body was:\n{}", | |
| 367 | + | resp.text | |
| 368 | + | ); | |
| 369 | + | // Still a paywall: the item titles stay gated. | |
| 370 | + | assert!( | |
| 371 | + | !resp.text.contains("First Piece"), | |
| 372 | + | "paywall must not list the items themselves" | |
| 373 | + | ); | |
| 374 | + | } |
| @@ -88,7 +88,22 @@ | |||
| 88 | 88 | db::subscriptions::get_active_tiers_by_project(db, db_project.id).await?; | |
| 89 | 89 | let subscription_tiers: Vec<SubscriptionTier> = | |
| 90 | 90 | db_tiers.iter().map(SubscriptionTier::from).collect(); | |
| 91 | - | let project = Project::from_db(db_project, 0); | |
| 91 | + | // Show how much is behind the paywall. A project with a lot in it | |
| 92 | + | // sells better than a bare price, and the count is not the thing | |
| 93 | + | // being sold: the items themselves stay gated. Reads the | |
| 94 | + | // denormalized column, so it matches the discover card exactly and | |
| 95 | + | // costs one indexed lookup. | |
| 96 | + | let item_count = db::projects::get_active_item_count(db, db_project.id) | |
| 97 | + | .await | |
| 98 | + | .unwrap_or_else(|e| { | |
| 99 | + | tracing::warn!( | |
| 100 | + | project_id = %db_project.id, | |
| 101 | + | error = %e, | |
| 102 | + | "paywall item count lookup failed; rendering without a count" | |
| 103 | + | ); | |
| 104 | + | 0 | |
| 105 | + | }); | |
| 106 | + | let project = Project::from_db(db_project, item_count.max(0) as u32); | |
| 92 | 107 | return Ok(ProjectPaywallTemplate { | |
| 93 | 108 | csrf_token, | |
| 94 | 109 | session_user: maybe_user, |
| @@ -1,0 +1,140 @@ | |||
| 1 | + | # Contributor License Agreement | |
| 2 | + | ||
| 3 | + | Read this before sending code. Most people should not need to: if you have found a | |
| 4 | + | bug, a clear bug report is more useful to us than a patch, and it saves you the | |
| 5 | + | paperwork below. See the contributing section of the README. | |
| 6 | + | ||
| 7 | + | This agreement is version 1.0.0. You agree to the version in effect on the day you | |
| 8 | + | send us code. A later revision does not change what you already agreed to. | |
| 9 | + | ||
| 10 | + | ## Make Creative Contributor License Agreement, version 1.0.0 | |
| 11 | + | ||
| 12 | + | This agreement is between Make Creative, LLC, a Colorado limited liability company | |
| 13 | + | ("Make Creative", "we", "us"), and you, the person or entity submitting a | |
| 14 | + | Contribution ("you"). | |
| 15 | + | ||
| 16 | + | ### 1. Definitions | |
| 17 | + | ||
| 18 | + | **Project** means any software project owned by Make Creative that distributes this | |
| 19 | + | agreement in its source repository. | |
| 20 | + | ||
| 21 | + | **Contribution** means any work of authorship you submit to a Project, in any form | |
| 22 | + | and through any channel: a patch sent by email, a diff attached to an issue, a pull | |
| 23 | + | or merge request on any repository or mirror, a commit pushed to a branch we grant | |
| 24 | + | you access to, or code pasted into a message. It covers source code, documentation, | |
| 25 | + | configuration, test data, and build files. It does not cover a bug report, a feature | |
| 26 | + | request, or a description of a problem that contains no code we could copy. | |
| 27 | + | ||
| 28 | + | **Submit** means to deliver a Contribution to us or to a Project by any of the means | |
| 29 | + | above. | |
| 30 | + | ||
| 31 | + | ### 2. Assignment | |
| 32 | + | ||
| 33 | + | You assign to Make Creative all right, title, and interest in and to your | |
| 34 | + | Contribution worldwide, including all copyrights and all rights to sue for past, | |
| 35 | + | present, and future infringement. The assignment is effective the moment you Submit, | |
| 36 | + | is irrevocable, and requires no further action by either of us. | |
| 37 | + | ||
| 38 | + | You will, at our request and our expense, sign any document reasonably needed to | |
| 39 | + | record or perfect the assignment. | |
| 40 | + | ||
| 41 | + | ### 3. Fallback license | |
| 42 | + | ||
| 43 | + | To the extent any part of the assignment in section 2 is unenforceable in your | |
| 44 | + | jurisdiction, or is later found ineffective, you instead grant Make Creative a | |
| 45 | + | perpetual, worldwide, irrevocable, exclusive, transferable, sublicensable, | |
| 46 | + | royalty-free license to reproduce, prepare derivative works of, publicly display, | |
| 47 | + | publicly perform, distribute, and otherwise exploit your Contribution, by any means | |
| 48 | + | and in any medium now known or later devised, for any purpose, commercial or | |
| 49 | + | noncommercial. | |
| 50 | + | ||
| 51 | + | To the extent permitted by law, you waive, and agree not to assert against Make | |
| 52 | + | Creative or anyone receiving the Contribution from us, any moral rights or rights of | |
| 53 | + | attribution or integrity in your Contribution. Where such a waiver is not permitted, | |
| 54 | + | you agree not to enforce those rights in a way that would interfere with our | |
| 55 | + | exercise of the rights granted here. | |
| 56 | + | ||
| 57 | + | ### 4. Patent license | |
| 58 | + | ||
| 59 | + | You grant Make Creative a perpetual, worldwide, irrevocable, transferable, | |
| 60 | + | sublicensable, royalty-free patent license to make, have made, use, offer to sell, | |
| 61 | + | sell, import, and otherwise transfer your Contribution and any Project incorporating | |
| 62 | + | it. The license covers only those patent claims you can license that are necessarily | |
| 63 | + | infringed by your Contribution alone or by its combination with the Project it was | |
| 64 | + | Submitted to. | |
| 65 | + | ||
| 66 | + | If you initiate patent litigation alleging that a Project or a Contribution | |
| 67 | + | infringes a patent, every license you have received from us under section 5 | |
| 68 | + | terminates on the day the action is filed. | |
| 69 | + | ||
| 70 | + | ### 5. License back to you | |
| 71 | + | ||
| 72 | + | We grant you a perpetual, worldwide, irrevocable, non-exclusive, royalty-free | |
| 73 | + | license to use, reproduce, modify, and distribute your own Contribution, and | |
| 74 | + | derivative works of it, for any purpose, including commercially. This license covers | |
| 75 | + | your Contribution as you Submitted it. It does not extend to the rest of the | |
| 76 | + | Project, which remains available to you only under the Project's public license. | |
| 77 | + | ||
| 78 | + | ### 6. What you promise | |
| 79 | + | ||
| 80 | + | You represent that: | |
| 81 | + | ||
| 82 | + | 1. Each Contribution is your original work, or you have the right to Submit it and | |
| 83 | + | to grant the rights in this agreement. | |
| 84 | + | 2. If your employer has rights in work you produce, you have your employer's | |
| 85 | + | permission to Submit, or your employer has waived its rights in the Contribution. | |
| 86 | + | 3. Your Contribution does not contain, and is not derived from, code licensed under | |
| 87 | + | the GPL, LGPL, AGPL, or any other license that would impose conditions on how | |
| 88 | + | Make Creative may license the Project. If any part of your Contribution came from | |
| 89 | + | somewhere else, you identified the source and its license when you Submitted it. | |
| 90 | + | 4. If you used an automated tool to produce any part of your Contribution, you still | |
| 91 | + | hold or can grant the rights this agreement requires. | |
| 92 | + | 5. Your Contribution is not subject to any third-party agreement, license, or claim | |
| 93 | + | that conflicts with this agreement. | |
| 94 | + | ||
| 95 | + | If you learn that any of these is untrue, tell us at info@makenot.work promptly. | |
| 96 | + | ||
| 97 | + | ### 7. No warranty | |
| 98 | + | ||
| 99 | + | Except for section 6, you provide your Contribution as is, without warranty of any | |
| 100 | + | kind. You owe us no support, maintenance, or updates. | |
| 101 | + | ||
| 102 | + | ### 8. No obligation and no compensation | |
| 103 | + | ||
| 104 | + | We are not required to use, merge, distribute, or keep any Contribution. We may | |
| 105 | + | modify, relicense, or remove it. Submitting a Contribution creates no employment, | |
| 106 | + | partnership, agency, or joint venture between us. | |
| 107 | + | ||
| 108 | + | You are not entitled to compensation for a Contribution. If we agree to pay for | |
| 109 | + | one, that agreement will be separate and in writing, and it does not change the | |
| 110 | + | rights assigned here. | |
| 111 | + | ||
| 112 | + | ### 9. How you accept | |
| 113 | + | ||
| 114 | + | You accept this agreement when you Submit a Contribution. We also ask you to record | |
| 115 | + | that acceptance in each commit message: | |
| 116 | + | ||
| 117 | + | ``` | |
| 118 | + | Make-Creative-CLA: 1.0.0 | |
| 119 | + | Signed-off-by: Your Name <your@email> | |
| 120 | + | ``` | |
| 121 | + | ||
| 122 | + | Leaving the line out does not undo your acceptance. It only means the record of it | |
| 123 | + | lives outside the repository. | |
| 124 | + | ||
| 125 | + | If you are Submitting on behalf of an entity, the person who Submits represents that | |
| 126 | + | they are authorized to bind that entity, and "you" means the entity. | |
| 127 | + | ||
| 128 | + | ### 10. Terms | |
| 129 | + | ||
| 130 | + | This agreement is governed by the laws of the State of Colorado, USA, without regard | |
| 131 | + | to its conflict of laws rules. Any dispute will be brought in the state or federal | |
| 132 | + | courts sitting in Colorado, and you consent to their jurisdiction. | |
| 133 | + | ||
| 134 | + | This is the entire agreement between us about Contributions, and it replaces any | |
| 135 | + | earlier understanding on the subject. If a provision is held unenforceable, the rest | |
| 136 | + | stays in force. Our failure to enforce a provision is not a waiver of it. | |
| 137 | + | ||
| 138 | + | We may publish a revised version of this agreement. A revision applies only to | |
| 139 | + | Contributions Submitted on or after its publication date. Your earlier Contributions | |
| 140 | + | stay governed by the version in effect when you Submitted them. |