Skip to main content

Creator trust audit: AI tiers, export fixes, doc corrections, availability commitment Implement per-item AI tier system (Handmade/Assisted/Generated) with migration, enum, DB queries, API routes, discover filters, and template UI including disclosure textarea for Assisted tier. Fix export gaps: add follower emails (with consent) to CSV, add play/download counts to project JSON, add per-project content export via project_id query param. Clarify payout records are in Stripe. Correct doc inaccuracies: SameSite Strict->Lax in security.md, remove Sentry from privacy policy, fix ZIP export "planned" to "available", remove Stripe "standard account" label, fix appeal timeline mismatch. Raise storage limits to match docs (50/250/500/500 GB). Add pricing table to creators page. Add Status link to site footer. Update monitoring docs to describe actual health dashboard. Add 99.5% availability guarantee with 99.9% as beta exit target. Mark subscription pause on suspension as not yet enforced.

  • Co-Authored-ByClaude Opus 4.6 (1M context) <noreply@anthropic.com>

Author: Max J. <87768334+MaxJMath@users.noreply.github.com> - 2026-04-25 23:31 UTC

Commit:

a6b721f1ba49b4d860b4fe4f1be0585aab2a4e43

Parent:

61ee4ef

31 files changed,

+384 insertions,

-50 deletions

OldNewLine
@@ -3385,7 +3385,7 @@
3385
3385
3386
3386
[[package]]
3387
3387
name = "makenotwork"
3388
version = "0.3.26"
3388
version = "0.4.0"
3389
3389
dependencies = [
3390
3390
"anyhow",
3391
3391
"argon2",
OldNewLine
@@ -677,6 +677,8 @@
677
677
listed: true,
678
678
license_preset: None,
679
679
custom_license_text: None,
680
ai_tier: db::AiTier::Handmade,
681
ai_disclosure: None,
680
682
}
681
683
}
682
684
OldNewLine
@@ -26,6 +26,7 @@
26
26
<a href="/policy">Policy</a>
27
27
<a href="/docs/terms-of-service">Terms</a>
28
28
<a href="/docs/privacy-policy">Privacy</a>
29
<a href="/health">Status</a>
29
30
</div>
30
31
<p>&copy; 2026 Makenotwork</p>
31
32
</footer>
OldNewLine
@@ -4,7 +4,7 @@
4
4
5
5
use sqlx::{FromRow, PgPool};
6
6
7
use super::enums::{DiscoverSort, ItemType};
7
use super::enums::{AiTier, DiscoverSort, ItemType};
8
8
use super::models::*;
9
9
use crate::error::Result;
10
10
@@ -21,6 +21,7 @@
21
21
pub min_price: Option<i32>,
22
22
pub max_price: Option<i32>,
23
23
pub sort_by: Option<DiscoverSort>,
24
pub ai_tier: Option<AiTier>,
24
25
}
25
26
26
27
// Shared SQL fragments for fuzzy search (trigram + ILIKE fallback).
@@ -82,7 +83,7 @@
82
83
}
83
84
84
85
/// Append discover-item filter clauses to a dynamic query.
85
/// Parameter positions: $1=search, $2=item_type, $3=min_price, $4=max_price, $5=tag, $6=label.
86
/// Parameter positions: $1=search, $2=item_type, $3=min_price, $4=max_price, $5=tag, $6=label, $7=ai_tier.
86
87
///
87
88
/// When `short_query` is `true`, the ILIKE-only clause is used instead of the
88
89
/// full trigram + ILIKE clause (trigram matching is unreliable for 1-2 char terms).
@@ -127,9 +128,12 @@
127
128
)"#,
128
129
);
129
130
}
131
if filters.ai_tier.is_some() {
132
query.push_str(" AND i.ai_tier = $7");
133
}
130
134
}
131
135
132
/// Bind the 6 discover-filter parameters ($1-$6) to a sqlx query.
136
/// Bind the 7 discover-filter parameters ($1-$7) to a sqlx query.
133
137
macro_rules! bind_item_discover_filters {
134
138
($q:expr, $filters:expr, $search_term:expr) => {
135
139
$q.bind($search_term.unwrap_or(""))
@@ -138,6 +142,7 @@
138
142
.bind($filters.max_price.unwrap_or(i32::MAX))
139
143
.bind($filters.tag.unwrap_or(""))
140
144
.bind($filters.label.unwrap_or(""))
145
.bind($filters.ai_tier.map(|t| t.to_string()).unwrap_or_default())
141
146
};
142
147
}
143
148
@@ -263,7 +268,7 @@
263
268
}
264
269
};
265
270
266
query.push_str(&format!(" ORDER BY {} LIMIT $7 OFFSET $8", order));
271
query.push_str(&format!(" ORDER BY {} LIMIT $8 OFFSET $9", order));
267
272
268
273
let items = bind_item_discover_filters!(
269
274
sqlx::query_as::<_, DbDiscoverItemRow>(&query),
OldNewLine
@@ -515,10 +515,10 @@
515
515
/// Maximum total storage in bytes.
516
516
pub fn max_storage_bytes(&self) -> i64 {
517
517
match self {
518
Self::Basic => 500 * 1024 * 1024, // 500 MB
519
Self::SmallFiles => 10 * 1024 * 1024 * 1024, // 10 GB
520
Self::BigFiles => 50 * 1024 * 1024 * 1024, // 50 GB
521
Self::Streaming => 200 * 1024 * 1024 * 1024, // 200 GB
518
Self::Basic => 50 * 1024 * 1024 * 1024, // 50 GB
519
Self::SmallFiles => 250 * 1024 * 1024 * 1024, // 250 GB
520
Self::BigFiles => 500 * 1024 * 1024 * 1024, // 500 GB
521
Self::Streaming => 500 * 1024 * 1024 * 1024, // 500 GB
522
522
}
523
523
}
524
524
@@ -529,6 +529,32 @@
529
529
}
530
530
}
531
531
532
// ── AI Tiers ──
533
534
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
535
#[serde(rename_all = "snake_case")]
536
pub enum AiTier {
537
Handmade,
538
Assisted,
539
Generated,
540
}
541
542
impl_str_enum!(AiTier {
543
Handmade => "handmade",
544
Assisted => "assisted",
545
Generated => "generated",
546
});
547
548
impl AiTier {
549
pub fn label(&self) -> &'static str {
550
match self {
551
Self::Handmade => "Handmade",
552
Self::Assisted => "Assisted",
553
Self::Generated => "Generated",
554
}
555
}
556
}
557
532
558
// ── Project Features ──
533
559
534
560
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -1031,10 +1057,10 @@
1031
1057
1032
1058
#[test]
1033
1059
fn creator_tier_storage_limits() {
1034
assert_eq!(CreatorTier::Basic.max_storage_bytes(), 500 * 1024 * 1024);
1035
assert_eq!(CreatorTier::SmallFiles.max_storage_bytes(), 10 * 1024 * 1024 * 1024);
1036
assert_eq!(CreatorTier::BigFiles.max_storage_bytes(), 50 * 1024 * 1024 * 1024);
1037
assert_eq!(CreatorTier::Streaming.max_storage_bytes(), 200 * 1024 * 1024 * 1024);
1060
assert_eq!(CreatorTier::Basic.max_storage_bytes(), 50 * 1024 * 1024 * 1024);
1061
assert_eq!(CreatorTier::SmallFiles.max_storage_bytes(), 250 * 1024 * 1024 * 1024);
1062
assert_eq!(CreatorTier::BigFiles.max_storage_bytes(), 500 * 1024 * 1024 * 1024);
1063
assert_eq!(CreatorTier::Streaming.max_storage_bytes(), 500 * 1024 * 1024 * 1024);
1038
1064
}
1039
1065
1040
1066
#[test]
@@ -1312,6 +1338,21 @@
1312
1338
assert_eq!(cards.len(), 5);
1313
1339
}
1314
1340
1341
#[test]
1342
fn ai_tier_round_trip() {
1343
assert_eq!(AiTier::Handmade.to_string(), "handmade");
1344
assert_eq!("assisted".parse::<AiTier>().unwrap(), AiTier::Assisted);
1345
assert_eq!("generated".parse::<AiTier>().unwrap(), AiTier::Generated);
1346
assert!("bogus".parse::<AiTier>().is_err());
1347
}
1348
1349
#[test]
1350
fn ai_tier_label() {
1351
assert_eq!(AiTier::Handmade.label(), "Handmade");
1352
assert_eq!(AiTier::Assisted.label(), "Assisted");
1353
assert_eq!(AiTier::Generated.label(), "Generated");
1354
}
1355
1315
1356
#[test]
1316
1357
fn import_source_round_trip() {
1317
1358
assert_eq!(ImportSource::GenericCsv.to_string(), "generic_csv");