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-By
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> - 2026-04-25 23:31 UTC
Commit:
a6b721f1ba49b4d860b4fe4f1be0585aab2a4e43Parent:
31 files changed,
+384 insertions,
-50 deletions
[[package]]name = "makenotwork"version = "0.3.26"version = "0.4.0"dependencies = [ "anyhow", "argon2", listed: true, license_preset: None, custom_license_text: None, ai_tier: db::AiTier::Handmade, ai_disclosure: None, } } <a href="/policy">Policy</a> <a href="/docs/terms-of-service">Terms</a> <a href="/docs/privacy-policy">Privacy</a> <a href="/health">Status</a> </div> <p>© 2026 Makenotwork</p> </footer>use sqlx::{FromRow, PgPool};use super::enums::{DiscoverSort, ItemType};use super::enums::{AiTier, DiscoverSort, ItemType};use super::models::*;use crate::error::Result; pub min_price: Option<i32>, pub max_price: Option<i32>, pub sort_by: Option<DiscoverSort>, pub ai_tier: Option<AiTier>,}// Shared SQL fragments for fuzzy search (trigram + ILIKE fallback).}/// Append discover-item filter clauses to a dynamic query./// Parameter positions: $1=search, $2=item_type, $3=min_price, $4=max_price, $5=tag, $6=label./// Parameter positions: $1=search, $2=item_type, $3=min_price, $4=max_price, $5=tag, $6=label, $7=ai_tier.////// When `short_query` is `true`, the ILIKE-only clause is used instead of the/// full trigram + ILIKE clause (trigram matching is unreliable for 1-2 char terms). )"#, ); } if filters.ai_tier.is_some() { query.push_str(" AND i.ai_tier = $7"); }}/// Bind the 6 discover-filter parameters ($1-$6) to a sqlx query./// Bind the 7 discover-filter parameters ($1-$7) to a sqlx query.macro_rules! bind_item_discover_filters { ($q:expr, $filters:expr, $search_term:expr) => { $q.bind($search_term.unwrap_or("")) .bind($filters.max_price.unwrap_or(i32::MAX)) .bind($filters.tag.unwrap_or("")) .bind($filters.label.unwrap_or("")) .bind($filters.ai_tier.map(|t| t.to_string()).unwrap_or_default()) };} } }; query.push_str(&format!(" ORDER BY {} LIMIT $7 OFFSET $8", order)); query.push_str(&format!(" ORDER BY {} LIMIT $8 OFFSET $9", order)); let items = bind_item_discover_filters!( sqlx::query_as::<_, DbDiscoverItemRow>(&query), /// Maximum total storage in bytes. pub fn max_storage_bytes(&self) -> i64 { match self { Self::Basic => 500 * 1024 * 1024, // 500 MB Self::SmallFiles => 10 * 1024 * 1024 * 1024, // 10 GB Self::BigFiles => 50 * 1024 * 1024 * 1024, // 50 GB Self::Streaming => 200 * 1024 * 1024 * 1024, // 200 GB Self::Basic => 50 * 1024 * 1024 * 1024, // 50 GB Self::SmallFiles => 250 * 1024 * 1024 * 1024, // 250 GB Self::BigFiles => 500 * 1024 * 1024 * 1024, // 500 GB Self::Streaming => 500 * 1024 * 1024 * 1024, // 500 GB } } }}// ── AI Tiers ──#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]#[serde(rename_all = "snake_case")]pub enum AiTier { Handmade, Assisted, Generated,}impl_str_enum!(AiTier { Handmade => "handmade", Assisted => "assisted", Generated => "generated",});impl AiTier { pub fn label(&self) -> &'static str { match self { Self::Handmade => "Handmade", Self::Assisted => "Assisted", Self::Generated => "Generated", } }}// ── Project Features ──#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[test] fn creator_tier_storage_limits() { assert_eq!(CreatorTier::Basic.max_storage_bytes(), 500 * 1024 * 1024); assert_eq!(CreatorTier::SmallFiles.max_storage_bytes(), 10 * 1024 * 1024 * 1024); assert_eq!(CreatorTier::BigFiles.max_storage_bytes(), 50 * 1024 * 1024 * 1024); assert_eq!(CreatorTier::Streaming.max_storage_bytes(), 200 * 1024 * 1024 * 1024); assert_eq!(CreatorTier::Basic.max_storage_bytes(), 50 * 1024 * 1024 * 1024); assert_eq!(CreatorTier::SmallFiles.max_storage_bytes(), 250 * 1024 * 1024 * 1024); assert_eq!(CreatorTier::BigFiles.max_storage_bytes(), 500 * 1024 * 1024 * 1024); assert_eq!(CreatorTier::Streaming.max_storage_bytes(), 500 * 1024 * 1024 * 1024); } #[test] assert_eq!(cards.len(), 5); } #[test] fn ai_tier_round_trip() { assert_eq!(AiTier::Handmade.to_string(), "handmade"); assert_eq!("assisted".parse::<AiTier>().unwrap(), AiTier::Assisted); assert_eq!("generated".parse::<AiTier>().unwrap(), AiTier::Generated); assert!("bogus".parse::<AiTier>().is_err()); } #[test] fn ai_tier_label() { assert_eq!(AiTier::Handmade.label(), "Handmade"); assert_eq!(AiTier::Assisted.label(), "Assisted"); assert_eq!(AiTier::Generated.label(), "Generated"); } #[test] fn import_source_round_trip() { assert_eq!(ImportSource::GenericCsv.to_string(), "generic_csv");