+3
.env.template
+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
+1
Cargo.lock
+1
Cargo.toml
+1
Cargo.toml
+1
fly.review.toml
+1
fly.review.toml
+7
src/config.rs
+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
+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
+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
+1
src/templates.rs
+20
-4
templates/feed.html
+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 */