slack status without the slack status.zzstoatzz.io/
quickslice

fix: resolve merge conflict and combine following filter with dev mode

- Merge dev mode features into following-filter branch
- Resolve conflicts in feed.html template
- Combine feed-header-with-indicator for both features
- Keep all functionality from both branches intact

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

+3
.env.template
··· 4 4 PUBLIC_URL="" # Set when deployed publicly, e.g. "https://mysite.com". Informs OAuth client id. 5 5 # DB_PATH="./statusphere.sqlite3" # The SQLite database path. Leave commented out to use a temporary in-memory database. 6 6 7 + # Dev Mode Configuration 8 + DEV_MODE="false" # Enable dev mode for testing with dummy data. Access via ?dev=true query parameter when enabled. 9 + 7 10
+1
Cargo.lock
··· 2277 2277 "env_logger", 2278 2278 "hickory-resolver", 2279 2279 "log", 2280 + "rand 0.8.5", 2280 2281 "rocketman", 2281 2282 "serde", 2282 2283 "serde_json",
+1
Cargo.toml
··· 27 27 thiserror = "1.0.69" 28 28 async-sqlite = "0.5.0" 29 29 async-trait = "0.1.88" 30 + rand = "0.8" 30 31 31 32 [build-dependencies] 32 33 askama = "0.13"
+1
fly.review.toml
··· 9 9 SERVER_HOST = "0.0.0.0" 10 10 DATABASE_URL = "sqlite:///data/status.db" 11 11 ENABLE_FIREHOSE = "true" 12 + DEV_MODE = "true" 12 13 # OAUTH_REDIRECT_BASE will be set dynamically by the workflow 13 14 14 15 [http_service]
+7
src/config.rs
··· 28 28 29 29 /// Log level 30 30 pub log_level: String, 31 + 32 + /// Dev mode for testing with dummy data 33 + pub dev_mode: bool, 31 34 } 32 35 33 36 impl Config { ··· 53 56 .parse() 54 57 .unwrap_or(false), 55 58 log_level: env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()), 59 + dev_mode: env::var("DEV_MODE") 60 + .unwrap_or_else(|_| "false".to_string()) 61 + .parse() 62 + .unwrap_or(false), 56 63 }) 57 64 } 58 65 }
+87
src/dev_utils.rs
··· 1 + use crate::db::StatusFromDb; 2 + use chrono::{Duration, Utc}; 3 + use rand::prelude::*; 4 + 5 + /// Generate dummy status data for development testing 6 + pub fn generate_dummy_statuses(count: usize) -> Vec<StatusFromDb> { 7 + let mut rng = thread_rng(); 8 + let mut statuses = Vec::new(); 9 + 10 + // Sample data pools 11 + let emojis = vec![ 12 + "🚀", "💭", "☕", "🎨", "📚", "🎵", "🏃", "😴", "🍕", "💻", "🌟", "🔥", "✨", "🌙", "☀️", 13 + "🌈", "⚡", "🎯", "🎮", "📝", 14 + ]; 15 + 16 + let texts = [ 17 + Some("working on something cool"), 18 + Some("deep in flow state"), 19 + Some("taking a break"), 20 + Some("debugging..."), 21 + Some("shipping it"), 22 + None, 23 + Some("coffee time"), 24 + Some("in a meeting"), 25 + Some("focused"), 26 + None, 27 + Some("learning rust"), 28 + Some("reading docs"), 29 + ]; 30 + 31 + let handles = [ 32 + "alice.test", 33 + "bob.test", 34 + "charlie.test", 35 + "dana.test", 36 + "eve.test", 37 + "frank.test", 38 + "grace.test", 39 + "henry.test", 40 + "iris.test", 41 + "jack.test", 42 + "karen.test", 43 + "leo.test", 44 + ]; 45 + 46 + let now = Utc::now(); 47 + 48 + for i in 0..count { 49 + // Generate random timestamps going back up to 48 hours 50 + let hours_ago = rng.gen_range(0..48); 51 + let minutes_ago = rng.gen_range(0..60); 52 + let started_at = now - Duration::hours(hours_ago) - Duration::minutes(minutes_ago); 53 + 54 + // Random chance of having an expiration 55 + let expires_at = if rng.gen_bool(0.3) { 56 + // 30% chance of having expiration 57 + let expire_hours = rng.gen_range(1..24); 58 + Some(started_at + Duration::hours(expire_hours)) 59 + } else { 60 + None 61 + }; 62 + 63 + let mut status = StatusFromDb::new( 64 + format!("at://did:plc:dummy{}/xyz.statusphere.status/dummy{}", i, i), 65 + format!("did:plc:dummy{}", i % handles.len()), 66 + emojis.choose(&mut rng).unwrap().to_string(), 67 + ); 68 + 69 + status.text = texts.choose(&mut rng).unwrap().map(|s| s.to_string()); 70 + status.started_at = started_at; 71 + status.expires_at = expires_at; 72 + status.indexed_at = started_at; 73 + status.handle = Some(handles[i % handles.len()].to_string()); 74 + 75 + statuses.push(status); 76 + } 77 + 78 + // Sort by started_at desc (newest first) 79 + statuses.sort_by(|a, b| b.started_at.cmp(&a.started_at)); 80 + 81 + statuses 82 + } 83 + 84 + /// Check if dev mode is requested via query parameter 85 + pub fn is_dev_mode_requested(query: &str) -> bool { 86 + query.contains("dev=true") || query.contains("dev=1") 87 + }
+55 -12
src/main.rs
··· 44 44 45 45 mod config; 46 46 mod db; 47 + mod dev_utils; 47 48 mod error_handler; 48 49 mod ingester; 49 50 #[allow(dead_code)] ··· 573 574 query: web::Query<HashMap<String, String>>, 574 575 db_pool: web::Data<Arc<Pool>>, 575 576 handle_resolver: web::Data<HandleResolver>, 577 + config: web::Data<config::Config>, 576 578 ) -> Result<impl Responder> { 577 579 let offset = query 578 580 .get("offset") ··· 584 586 .unwrap_or(20) 585 587 .min(50); // Cap at 50 items per request 586 588 587 - let mut statuses = StatusFromDb::load_statuses_paginated(&db_pool, offset, limit) 588 - .await 589 - .unwrap_or_else(|err| { 590 - log::error!("Error loading statuses: {err}"); 591 - vec![] 592 - }); 589 + // Check if dev mode is requested 590 + let use_dev_mode = config.dev_mode && query.get("dev").is_some_and(|v| v == "true" || v == "1"); 591 + 592 + let mut statuses = if use_dev_mode && offset == 0 { 593 + // For first page in dev mode, mix dummy data with real data 594 + let mut real_statuses = StatusFromDb::load_statuses_paginated(&db_pool, 0, limit / 2) 595 + .await 596 + .unwrap_or_else(|err| { 597 + log::error!("Error loading paginated statuses: {err}"); 598 + vec![] 599 + }); 600 + let dummy_statuses = dev_utils::generate_dummy_statuses((limit / 2) as usize); 601 + real_statuses.extend(dummy_statuses); 602 + real_statuses.sort_by(|a, b| b.started_at.cmp(&a.started_at)); 603 + real_statuses 604 + } else { 605 + StatusFromDb::load_statuses_paginated(&db_pool, offset, limit) 606 + .await 607 + .unwrap_or_else(|err| { 608 + log::error!("Error loading statuses: {err}"); 609 + vec![] 610 + }) 611 + }; 593 612 594 613 // Resolve handles for each status 595 614 let mut quick_resolve_map: HashMap<Did, String> = HashMap::new(); ··· 843 862 /// Feed page - shows all users' statuses 844 863 #[get("/feed")] 845 864 async fn feed( 865 + request: HttpRequest, 846 866 session: Session, 847 867 oauth_client: web::Data<OAuthClientType>, 848 868 db_pool: web::Data<Arc<Pool>>, 849 869 handle_resolver: web::Data<HandleResolver>, 870 + config: web::Data<config::Config>, 850 871 ) -> Result<impl Responder> { 851 872 // This is essentially the old home function 852 873 const TITLE: &str = "status feed"; 853 - let mut statuses = StatusFromDb::load_latest_statuses(&db_pool) 854 - .await 855 - .unwrap_or_else(|err| { 856 - log::error!("Error loading statuses: {err}"); 857 - vec![] 858 - }); 874 + 875 + // Check if dev mode is active 876 + let query = request.query_string(); 877 + let use_dev_mode = config.dev_mode && dev_utils::is_dev_mode_requested(query); 878 + 879 + let mut statuses = if use_dev_mode { 880 + // Mix dummy data with real data for testing 881 + let mut real_statuses = StatusFromDb::load_latest_statuses(&db_pool) 882 + .await 883 + .unwrap_or_else(|err| { 884 + log::error!("Error loading statuses: {err}"); 885 + vec![] 886 + }); 887 + let dummy_statuses = dev_utils::generate_dummy_statuses(15); 888 + real_statuses.extend(dummy_statuses); 889 + // Resort by started_at 890 + real_statuses.sort_by(|a, b| b.started_at.cmp(&a.started_at)); 891 + real_statuses 892 + } else { 893 + StatusFromDb::load_latest_statuses(&db_pool) 894 + .await 895 + .unwrap_or_else(|err| { 896 + log::error!("Error loading statuses: {err}"); 897 + vec![] 898 + }) 899 + }; 859 900 860 901 let mut quick_resolve_map: HashMap<Did, String> = HashMap::new(); 861 902 for db_status in &mut statuses { ··· 931 972 }, 932 973 statuses, 933 974 is_admin, 975 + dev_mode: use_dev_mode, 934 976 } 935 977 .render() 936 978 .expect("template should be valid"); ··· 956 998 profile: None, 957 999 statuses, 958 1000 is_admin: false, 1001 + dev_mode: use_dev_mode, 959 1002 } 960 1003 .render() 961 1004 .expect("template should be valid");
+1
src/templates.rs
··· 47 47 pub profile: Option<Profile>, 48 48 pub statuses: Vec<StatusFromDb>, 49 49 pub is_admin: bool, 50 + pub dev_mode: bool, 50 51 }
+20 -4
templates/feed.html
··· 65 65 66 66 <!-- Feed --> 67 67 <div class="feed-container"> 68 - <div class="feed-header"> 68 + <div class="feed-header-with-indicator"> 69 69 <h2>recent updates</h2> 70 + {% if dev_mode %} 71 + <div class="dev-indicator" title="dev mode: showing dummy data mixed with real posts">dev</div> 72 + {% endif %} 70 73 </div> 71 74 72 75 {% if !statuses.is_empty() %} ··· 120 123 121 124 <!-- End of feed indicator --> 122 125 <div id="end-of-feed" style="display: none; text-align: center; padding: 2rem;"> 123 - <span style="color: var(--text-tertiary);">You've reached the beginning of time ✨</span> 126 + <span style="color: var(--text-tertiary);">you've reached the beginning of time ✨</span> 124 127 </div> 125 128 </div> 126 129 ··· 415 418 transform: translateX(20px); 416 419 } 417 420 418 - .feed-header { 421 + .feed-header-with-indicator { 422 + display: flex; 423 + justify-content: space-between; 424 + align-items: center; 419 425 margin-bottom: 1.5rem; 420 426 } 421 427 422 - .feed-header h2 { 428 + .feed-container h2 { 423 429 font-size: 1rem; 424 430 font-weight: 500; 425 431 color: var(--text-secondary); 426 432 text-transform: uppercase; 427 433 letter-spacing: 0.05em; 428 434 margin: 0; 435 + } 436 + 437 + .dev-indicator { 438 + font-size: 0.75rem; 439 + color: var(--text-tertiary); 440 + background: var(--bg-secondary); 441 + border: 1px solid var(--border-color); 442 + border-radius: var(--radius-sm); 443 + padding: 0.25rem 0.5rem; 444 + opacity: 0.7; 429 445 } 430 446 431 447 /* Status List */