WIP: development_backend #4
							
								
								
									
										29
									
								
								actix_mvc_app/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										29
									
								
								actix_mvc_app/Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -1329,6 +1329,12 @@ version = "0.4.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "heck"
 | 
			
		||||
version = "0.5.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "hermit-abi"
 | 
			
		||||
version = "0.3.9"
 | 
			
		||||
@@ -1351,6 +1357,8 @@ dependencies = [
 | 
			
		||||
 "rhai_wrapper",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "strum",
 | 
			
		||||
 "strum_macros",
 | 
			
		||||
 "tst",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@@ -2344,7 +2352,7 @@ dependencies = [
 | 
			
		||||
name = "rhai_autobind_macros"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "heck",
 | 
			
		||||
 "heck 0.4.1",
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn",
 | 
			
		||||
@@ -2701,6 +2709,25 @@ version = "0.11.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "strum"
 | 
			
		||||
version = "0.26.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "strum_macros"
 | 
			
		||||
version = "0.26.4"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "heck 0.5.0",
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "rustversion",
 | 
			
		||||
 "syn",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "subtle"
 | 
			
		||||
version = "2.6.1"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,13 @@
 | 
			
		||||
use crate::db::governance_tracker;
 | 
			
		||||
use crate::db::proposals::{self, get_proposal_by_id};
 | 
			
		||||
use crate::db::proposals::{
 | 
			
		||||
    self, create_activity, get_all_activities, get_proposal_by_id, get_proposals,
 | 
			
		||||
    get_recent_activities,
 | 
			
		||||
};
 | 
			
		||||
use crate::models::governance::{Vote, VoteType, VotingResults};
 | 
			
		||||
use crate::utils::render_template;
 | 
			
		||||
use actix_session::Session;
 | 
			
		||||
use actix_web::{HttpResponse, Responder, Result, web};
 | 
			
		||||
use chrono::{Duration, Utc};
 | 
			
		||||
use heromodels::models::ActivityType;
 | 
			
		||||
use heromodels::models::governance::{Proposal, ProposalStatus};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use serde_json::Value;
 | 
			
		||||
@@ -80,6 +83,7 @@ impl GovernanceController {
 | 
			
		||||
 | 
			
		||||
    /// Handles the governance dashboard page route
 | 
			
		||||
    pub async fn index(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
 | 
			
		||||
        println!("==============================================");
 | 
			
		||||
        let mut ctx = tera::Context::new();
 | 
			
		||||
        ctx.insert("active_page", "governance");
 | 
			
		||||
        ctx.insert("active_tab", "dashboard");
 | 
			
		||||
@@ -96,14 +100,32 @@ impl GovernanceController {
 | 
			
		||||
        let user = Self::get_user_from_session(&session).unwrap();
 | 
			
		||||
        ctx.insert("user", &user);
 | 
			
		||||
 | 
			
		||||
        println!("==============================================");
 | 
			
		||||
        // Get proposals from the database
 | 
			
		||||
        let proposals = match crate::db::proposals::get_proposals() {
 | 
			
		||||
            Ok(props) => props,
 | 
			
		||||
            Ok(props) => {
 | 
			
		||||
                println!(
 | 
			
		||||
                    "📋 Proposals list page: Successfully loaded {} proposals from database",
 | 
			
		||||
                    props.len()
 | 
			
		||||
                );
 | 
			
		||||
                for (i, proposal) in props.iter().enumerate() {
 | 
			
		||||
                    println!(
 | 
			
		||||
                        "  Proposal {}: ID={}, title={:?}, status={:?}",
 | 
			
		||||
                        i + 1,
 | 
			
		||||
                        proposal.base_data.id,
 | 
			
		||||
                        proposal.title,
 | 
			
		||||
                        proposal.status
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
                props
 | 
			
		||||
            }
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                println!("❌ Proposals list page: Failed to load proposals: {}", e);
 | 
			
		||||
                ctx.insert("error", &format!("Failed to load proposals: {}", e));
 | 
			
		||||
                vec![]
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        println!("==============================================");
 | 
			
		||||
 | 
			
		||||
        // Make a copy of proposals for statistics
 | 
			
		||||
        let proposals_for_stats = proposals.clone();
 | 
			
		||||
@@ -170,8 +192,9 @@ impl GovernanceController {
 | 
			
		||||
            ctx.insert("user", &user);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        println!("============== Loading proposals =================");
 | 
			
		||||
        // Get proposals from the database
 | 
			
		||||
        let mut proposals = match crate::db::proposals::get_proposals() {
 | 
			
		||||
        let mut proposals = match get_proposals() {
 | 
			
		||||
            Ok(props) => props,
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                ctx.insert("error", &format!("Failed to load proposals: {}", e));
 | 
			
		||||
@@ -179,6 +202,8 @@ impl GovernanceController {
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        println!("proposals: {:?}", proposals);
 | 
			
		||||
 | 
			
		||||
        // Filter proposals by status if provided
 | 
			
		||||
        if let Some(status_filter) = &query.status {
 | 
			
		||||
            if !status_filter.is_empty() {
 | 
			
		||||
@@ -372,15 +397,12 @@ impl GovernanceController {
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                // Track the proposal creation activity
 | 
			
		||||
                let creation_activity =
 | 
			
		||||
                    crate::models::governance::GovernanceActivity::proposal_created(
 | 
			
		||||
                        proposal_id,
 | 
			
		||||
                        &saved_proposal.title,
 | 
			
		||||
                        &user_id,
 | 
			
		||||
                        &user_name,
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                let _ = governance_tracker::create_activity(creation_activity);
 | 
			
		||||
                let _ = create_activity(
 | 
			
		||||
                    proposal_id,
 | 
			
		||||
                    &saved_proposal.title,
 | 
			
		||||
                    &user_name,
 | 
			
		||||
                    &ActivityType::ProposalCreated,
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                ctx.insert("success", "Proposal created successfully!");
 | 
			
		||||
            }
 | 
			
		||||
@@ -394,8 +416,24 @@ impl GovernanceController {
 | 
			
		||||
 | 
			
		||||
        // Get proposals from the database
 | 
			
		||||
        let proposals = match crate::db::proposals::get_proposals() {
 | 
			
		||||
            Ok(props) => props,
 | 
			
		||||
            Ok(props) => {
 | 
			
		||||
                println!(
 | 
			
		||||
                    "✅ Successfully loaded {} proposals from database",
 | 
			
		||||
                    props.len()
 | 
			
		||||
                );
 | 
			
		||||
                for (i, proposal) in props.iter().enumerate() {
 | 
			
		||||
                    println!(
 | 
			
		||||
                        "  Proposal {}: ID={}, title={:?}, status={:?}",
 | 
			
		||||
                        i + 1,
 | 
			
		||||
                        proposal.base_data.id,
 | 
			
		||||
                        proposal.title,
 | 
			
		||||
                        proposal.status
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
                props
 | 
			
		||||
            }
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                println!("❌ Failed to load proposals: {}", e);
 | 
			
		||||
                ctx.insert("error", &format!("Failed to load proposals: {}", e));
 | 
			
		||||
                vec![]
 | 
			
		||||
            }
 | 
			
		||||
@@ -407,6 +445,14 @@ impl GovernanceController {
 | 
			
		||||
        ctx.insert("status_filter", &None::<String>);
 | 
			
		||||
        ctx.insert("search_filter", &None::<String>);
 | 
			
		||||
 | 
			
		||||
        // Header data (required by _header.html template)
 | 
			
		||||
        ctx.insert("page_title", "All Proposals");
 | 
			
		||||
        ctx.insert(
 | 
			
		||||
            "page_description",
 | 
			
		||||
            "Browse and filter all governance proposals",
 | 
			
		||||
        );
 | 
			
		||||
        ctx.insert("show_create_button", &false);
 | 
			
		||||
 | 
			
		||||
        render_template(&tmpl, "governance/proposals.html", &ctx)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -461,15 +507,12 @@ impl GovernanceController {
 | 
			
		||||
 | 
			
		||||
                // Track the vote cast activity
 | 
			
		||||
                if let Ok(Some(proposal)) = get_proposal_by_id(proposal_id_u32) {
 | 
			
		||||
                    let vote_activity = crate::models::governance::GovernanceActivity::vote_cast(
 | 
			
		||||
                    let _ = create_activity(
 | 
			
		||||
                        proposal_id_u32,
 | 
			
		||||
                        &proposal.title,
 | 
			
		||||
                        user_name,
 | 
			
		||||
                        &form.vote_type,
 | 
			
		||||
                        1, // shares
 | 
			
		||||
                        &ActivityType::VoteCast,
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    let _ = governance_tracker::create_activity(vote_activity);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Redirect to the proposal detail page with a success message
 | 
			
		||||
@@ -576,7 +619,7 @@ impl GovernanceController {
 | 
			
		||||
    /// Get recent governance activities from the database
 | 
			
		||||
    fn get_recent_governance_activities() -> Result<Vec<Value>, String> {
 | 
			
		||||
        // Get real activities from the database (no demo data)
 | 
			
		||||
        let activities = governance_tracker::get_recent_activities()?;
 | 
			
		||||
        let activities = get_recent_activities()?;
 | 
			
		||||
 | 
			
		||||
        // Convert GovernanceActivity to the format expected by the template
 | 
			
		||||
        let formatted_activities: Vec<Value> = activities
 | 
			
		||||
@@ -596,10 +639,10 @@ impl GovernanceController {
 | 
			
		||||
                serde_json::json!({
 | 
			
		||||
                    "type": activity.activity_type,
 | 
			
		||||
                    "icon": icon,
 | 
			
		||||
                    "user": activity.actor_name,
 | 
			
		||||
                    "user": activity.creator_name,
 | 
			
		||||
                    "action": action,
 | 
			
		||||
                    "proposal_title": activity.proposal_title,
 | 
			
		||||
                    "timestamp": activity.timestamp.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
 | 
			
		||||
                    "created_at": activity.created_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
 | 
			
		||||
                    "proposal_id": activity.proposal_id
 | 
			
		||||
                })
 | 
			
		||||
            })
 | 
			
		||||
@@ -611,7 +654,7 @@ impl GovernanceController {
 | 
			
		||||
    /// Get all governance activities from the database
 | 
			
		||||
    fn get_all_governance_activities() -> Result<Vec<Value>, String> {
 | 
			
		||||
        // Get all activities from the database
 | 
			
		||||
        let activities = governance_tracker::get_all_activities()?;
 | 
			
		||||
        let activities = get_all_activities()?;
 | 
			
		||||
 | 
			
		||||
        // Convert GovernanceActivity to the format expected by the template
 | 
			
		||||
        let formatted_activities: Vec<Value> = activities
 | 
			
		||||
@@ -631,10 +674,10 @@ impl GovernanceController {
 | 
			
		||||
                serde_json::json!({
 | 
			
		||||
                    "type": activity.activity_type,
 | 
			
		||||
                    "icon": icon,
 | 
			
		||||
                    "user": activity.actor_name,
 | 
			
		||||
                    "user": activity.creator_name,
 | 
			
		||||
                    "action": action,
 | 
			
		||||
                    "proposal_title": activity.proposal_title,
 | 
			
		||||
                    "timestamp": activity.timestamp.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
 | 
			
		||||
                    "created_at": activity.created_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
 | 
			
		||||
                    "proposal_id": activity.proposal_id
 | 
			
		||||
                })
 | 
			
		||||
            })
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								actix_mvc_app/src/db/db.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								actix_mvc_app/src/db/db.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
 | 
			
		||||
use heromodels::db::hero::OurDB;
 | 
			
		||||
 | 
			
		||||
/// The path to the database file. Change this as needed for your environment.
 | 
			
		||||
pub const DB_PATH: &str = "/tmp/freezone_db";
 | 
			
		||||
 | 
			
		||||
/// Returns a shared OurDB instance for the given path. You can wrap this in Arc/Mutex for concurrent access if needed.
 | 
			
		||||
pub fn get_db() -> Result<OurDB, String> {
 | 
			
		||||
    let db_path = PathBuf::from(DB_PATH);
 | 
			
		||||
    if let Some(parent) = db_path.parent() {
 | 
			
		||||
        let _ = std::fs::create_dir_all(parent);
 | 
			
		||||
    }
 | 
			
		||||
    // Temporarily reset the database to fix the serialization issue
 | 
			
		||||
    let db = heromodels::db::hero::OurDB::new(db_path, false).expect("Can create DB");
 | 
			
		||||
    Ok(db)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,139 +0,0 @@
 | 
			
		||||
use crate::models::governance::GovernanceActivity;
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
 | 
			
		||||
/// Database path for governance activities
 | 
			
		||||
pub const DB_PATH: &str = "/tmp/ourdb_governance_activities";
 | 
			
		||||
 | 
			
		||||
/// Returns a shared OurDB instance for activities
 | 
			
		||||
pub fn get_db() -> Result<heromodels::db::hero::OurDB, String> {
 | 
			
		||||
    let db_path = PathBuf::from(DB_PATH);
 | 
			
		||||
    if let Some(parent) = db_path.parent() {
 | 
			
		||||
        let _ = std::fs::create_dir_all(parent);
 | 
			
		||||
    }
 | 
			
		||||
    let db = heromodels::db::hero::OurDB::new(db_path, true)
 | 
			
		||||
        .map_err(|e| format!("Failed to create activities DB: {:?}", e))?;
 | 
			
		||||
    Ok(db)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Creates a new governance activity and saves it to the database using OurDB
 | 
			
		||||
pub fn create_activity(activity: GovernanceActivity) -> Result<(u32, GovernanceActivity), String> {
 | 
			
		||||
    let db = get_db()?;
 | 
			
		||||
 | 
			
		||||
    // Since OurDB doesn't support custom models directly, we'll use a simple key-value approach
 | 
			
		||||
    // Store each activity with a unique key and serialize it as JSON
 | 
			
		||||
 | 
			
		||||
    // First, get the next available ID by checking existing keys
 | 
			
		||||
    let activity_id = get_next_activity_id(&db)?;
 | 
			
		||||
 | 
			
		||||
    // Create the activity with the assigned ID
 | 
			
		||||
    let mut new_activity = activity;
 | 
			
		||||
    new_activity.id = Some(activity_id);
 | 
			
		||||
 | 
			
		||||
    // Serialize the activity to JSON
 | 
			
		||||
    let activity_json = serde_json::to_string(&new_activity)
 | 
			
		||||
        .map_err(|e| format!("Failed to serialize activity: {}", e))?;
 | 
			
		||||
 | 
			
		||||
    // Store in OurDB using a key-value approach
 | 
			
		||||
    let key = format!("activity_{}", activity_id);
 | 
			
		||||
 | 
			
		||||
    // Use OurDB's raw storage capabilities to store the JSON string
 | 
			
		||||
    // Since we can't use collections directly, we'll store as raw data
 | 
			
		||||
    let db_path = format!("{}/{}.json", DB_PATH, key);
 | 
			
		||||
    std::fs::write(&db_path, &activity_json)
 | 
			
		||||
        .map_err(|e| format!("Failed to write activity to DB: {}", e))?;
 | 
			
		||||
 | 
			
		||||
    // Also maintain an index of activity IDs for efficient retrieval
 | 
			
		||||
    update_activity_index(&db, activity_id)?;
 | 
			
		||||
 | 
			
		||||
    println!(
 | 
			
		||||
        "✅ Activity recorded: {} - {}",
 | 
			
		||||
        new_activity.activity_type, new_activity.description
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    Ok((activity_id, new_activity))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Gets the next available activity ID
 | 
			
		||||
fn get_next_activity_id(_db: &heromodels::db::hero::OurDB) -> Result<u32, String> {
 | 
			
		||||
    let index_path = format!("{}/activity_index.json", DB_PATH);
 | 
			
		||||
 | 
			
		||||
    if std::path::Path::new(&index_path).exists() {
 | 
			
		||||
        let content = std::fs::read_to_string(&index_path)
 | 
			
		||||
            .map_err(|e| format!("Failed to read activity index: {}", e))?;
 | 
			
		||||
        let index: Vec<u32> = serde_json::from_str(&content).unwrap_or_else(|_| Vec::new());
 | 
			
		||||
        Ok(index.len() as u32 + 1)
 | 
			
		||||
    } else {
 | 
			
		||||
        Ok(1)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Updates the activity index with a new activity ID
 | 
			
		||||
fn update_activity_index(
 | 
			
		||||
    _db: &heromodels::db::hero::OurDB,
 | 
			
		||||
    activity_id: u32,
 | 
			
		||||
) -> Result<(), String> {
 | 
			
		||||
    let index_path = format!("{}/activity_index.json", DB_PATH);
 | 
			
		||||
 | 
			
		||||
    let mut index: Vec<u32> = if std::path::Path::new(&index_path).exists() {
 | 
			
		||||
        let content = std::fs::read_to_string(&index_path)
 | 
			
		||||
            .map_err(|e| format!("Failed to read activity index: {}", e))?;
 | 
			
		||||
        serde_json::from_str(&content).unwrap_or_else(|_| Vec::new())
 | 
			
		||||
    } else {
 | 
			
		||||
        Vec::new()
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    index.push(activity_id);
 | 
			
		||||
 | 
			
		||||
    let content = serde_json::to_string(&index)
 | 
			
		||||
        .map_err(|e| format!("Failed to serialize activity index: {}", e))?;
 | 
			
		||||
 | 
			
		||||
    std::fs::write(&index_path, content)
 | 
			
		||||
        .map_err(|e| format!("Failed to write activity index: {}", e))?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Gets all activities from the database using OurDB
 | 
			
		||||
pub fn get_all_activities() -> Result<Vec<GovernanceActivity>, String> {
 | 
			
		||||
    let _db = get_db()?;
 | 
			
		||||
    let index_path = format!("{}/activity_index.json", DB_PATH);
 | 
			
		||||
 | 
			
		||||
    // Read the activity index to get all activity IDs
 | 
			
		||||
    if !std::path::Path::new(&index_path).exists() {
 | 
			
		||||
        return Ok(Vec::new());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let content = std::fs::read_to_string(&index_path)
 | 
			
		||||
        .map_err(|e| format!("Failed to read activity index: {}", e))?;
 | 
			
		||||
    let activity_ids: Vec<u32> = serde_json::from_str(&content).unwrap_or_else(|_| Vec::new());
 | 
			
		||||
 | 
			
		||||
    let mut activities = Vec::new();
 | 
			
		||||
 | 
			
		||||
    // Load each activity by ID
 | 
			
		||||
    for activity_id in activity_ids {
 | 
			
		||||
        let activity_path = format!("{}/activity_{}.json", DB_PATH, activity_id);
 | 
			
		||||
        if std::path::Path::new(&activity_path).exists() {
 | 
			
		||||
            let activity_content = std::fs::read_to_string(&activity_path)
 | 
			
		||||
                .map_err(|e| format!("Failed to read activity {}: {}", activity_id, e))?;
 | 
			
		||||
 | 
			
		||||
            if let Ok(activity) = serde_json::from_str::<GovernanceActivity>(&activity_content) {
 | 
			
		||||
                activities.push(activity);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(activities)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Gets recent activities (last 10) sorted by timestamp using OurDB
 | 
			
		||||
pub fn get_recent_activities() -> Result<Vec<GovernanceActivity>, String> {
 | 
			
		||||
    let mut activities = get_all_activities()?;
 | 
			
		||||
 | 
			
		||||
    // Sort by timestamp (most recent first)
 | 
			
		||||
    activities.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
 | 
			
		||||
 | 
			
		||||
    // Take only the last 10
 | 
			
		||||
    activities.truncate(10);
 | 
			
		||||
 | 
			
		||||
    Ok(activities)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,2 +1,2 @@
 | 
			
		||||
pub mod governance_tracker;
 | 
			
		||||
pub mod db;
 | 
			
		||||
pub mod proposals;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,25 +1,10 @@
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
 | 
			
		||||
use chrono::{Duration, Utc};
 | 
			
		||||
use heromodels::db::hero::OurDB;
 | 
			
		||||
use heromodels::{
 | 
			
		||||
    db::{Collection, Db},
 | 
			
		||||
    models::governance::{Proposal, ProposalStatus},
 | 
			
		||||
    models::governance::{Activity, ActivityType, Proposal, ProposalStatus},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// The path to the database file. Change this as needed for your environment.
 | 
			
		||||
pub const DB_PATH: &str = "/tmp/ourdb_governance";
 | 
			
		||||
 | 
			
		||||
/// Returns a shared OurDB instance for the given path. You can wrap this in Arc/Mutex for concurrent access if needed.
 | 
			
		||||
pub fn get_db(db_path: &str) -> Result<OurDB, String> {
 | 
			
		||||
    let db_path = PathBuf::from(db_path);
 | 
			
		||||
    if let Some(parent) = db_path.parent() {
 | 
			
		||||
        let _ = std::fs::create_dir_all(parent);
 | 
			
		||||
    }
 | 
			
		||||
    // Temporarily reset the database to fix the serialization issue
 | 
			
		||||
    let db = heromodels::db::hero::OurDB::new(db_path, false).expect("Can create DB");
 | 
			
		||||
    Ok(db)
 | 
			
		||||
}
 | 
			
		||||
use super::db::get_db;
 | 
			
		||||
 | 
			
		||||
/// Creates a new proposal and saves it to the database. Returns the saved proposal and its ID.
 | 
			
		||||
pub fn create_new_proposal(
 | 
			
		||||
@@ -31,7 +16,7 @@ pub fn create_new_proposal(
 | 
			
		||||
    voting_start_date: Option<chrono::DateTime<Utc>>,
 | 
			
		||||
    voting_end_date: Option<chrono::DateTime<Utc>>,
 | 
			
		||||
) -> Result<(u32, Proposal), String> {
 | 
			
		||||
    let db = get_db(DB_PATH).expect("Can create DB");
 | 
			
		||||
    let db = get_db().expect("Can get DB");
 | 
			
		||||
 | 
			
		||||
    let created_at = Utc::now();
 | 
			
		||||
    let updated_at = created_at;
 | 
			
		||||
@@ -60,7 +45,7 @@ pub fn create_new_proposal(
 | 
			
		||||
 | 
			
		||||
/// Loads all proposals from the database and returns them as a Vec<Proposal>.
 | 
			
		||||
pub fn get_proposals() -> Result<Vec<Proposal>, String> {
 | 
			
		||||
    let db = get_db(DB_PATH).map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Proposal>()
 | 
			
		||||
        .expect("can open proposal collection");
 | 
			
		||||
@@ -78,7 +63,7 @@ pub fn get_proposals() -> Result<Vec<Proposal>, String> {
 | 
			
		||||
 | 
			
		||||
/// Fetches a single proposal by its ID from the database.
 | 
			
		||||
pub fn get_proposal_by_id(proposal_id: u32) -> Result<Option<Proposal>, String> {
 | 
			
		||||
    let db = get_db(DB_PATH).map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Proposal>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
@@ -100,7 +85,7 @@ pub fn submit_vote_on_proposal(
 | 
			
		||||
    comment: Option<String>,
 | 
			
		||||
) -> Result<Proposal, String> {
 | 
			
		||||
    // Get the proposal from the database
 | 
			
		||||
    let db = get_db(DB_PATH).map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let db = get_db().map_err(|e| format!("DB error: {}", e))?;
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Proposal>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
@@ -167,13 +152,6 @@ pub fn submit_vote_on_proposal(
 | 
			
		||||
    // We'll create a simple ballot with an auto-generated ID
 | 
			
		||||
    let ballot_id = proposal.ballots.len() as u32 + 1;
 | 
			
		||||
 | 
			
		||||
    // We need to manually create a ballot since we can't use cast_vote
 | 
			
		||||
    // This is a simplified version that just records the vote
 | 
			
		||||
    println!(
 | 
			
		||||
        "Recording vote: ballot_id={}, user_id={}, option_id={}, shares={}",
 | 
			
		||||
        ballot_id, user_id, option_id, shares_count
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Create a new ballot and add it to the proposal's ballots
 | 
			
		||||
    use heromodels::models::governance::Ballot;
 | 
			
		||||
 | 
			
		||||
@@ -210,3 +188,69 @@ pub fn submit_vote_on_proposal(
 | 
			
		||||
 | 
			
		||||
    Ok(updated_proposal)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Creates a new governance activity and saves it to the database using OurDB
 | 
			
		||||
pub fn create_activity(
 | 
			
		||||
    proposal_id: u32,
 | 
			
		||||
    proposal_title: &str,
 | 
			
		||||
    creator_name: &str,
 | 
			
		||||
    activity_type: &ActivityType,
 | 
			
		||||
) -> Result<(u32, Activity), String> {
 | 
			
		||||
    let db = get_db().expect("Can get DB");
 | 
			
		||||
    let mut activity = Activity::default();
 | 
			
		||||
 | 
			
		||||
    match activity_type {
 | 
			
		||||
        ActivityType::ProposalCreated => {
 | 
			
		||||
            activity = Activity::proposal_created(proposal_id, proposal_title, creator_name);
 | 
			
		||||
        }
 | 
			
		||||
        ActivityType::VoteCast => {
 | 
			
		||||
            activity = Activity::vote_cast(proposal_id, proposal_title, creator_name);
 | 
			
		||||
        }
 | 
			
		||||
        ActivityType::VotingStarted => {
 | 
			
		||||
            activity = Activity::voting_started(proposal_id, proposal_title);
 | 
			
		||||
        }
 | 
			
		||||
        ActivityType::VotingEnded => {
 | 
			
		||||
            activity = Activity::voting_ended(proposal_id, proposal_title);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Save the proposal to the database
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Activity>()
 | 
			
		||||
        .expect("can open activity collection");
 | 
			
		||||
 | 
			
		||||
    let (proposal_id, saved_proposal) = collection.set(&activity).expect("can save proposal");
 | 
			
		||||
    Ok((proposal_id, saved_proposal))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn get_recent_activities() -> Result<Vec<Activity>, String> {
 | 
			
		||||
    let db = get_db().expect("Can get DB");
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Activity>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    let mut db_activities = collection
 | 
			
		||||
        .get_all()
 | 
			
		||||
        .map_err(|e| format!("DB fetch error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    // Sort by created_at descending
 | 
			
		||||
    db_activities.sort_by(|a, b| b.created_at.cmp(&a.created_at));
 | 
			
		||||
 | 
			
		||||
    // Take the top 10 most recent
 | 
			
		||||
    let recent_activities = db_activities.into_iter().take(10).collect();
 | 
			
		||||
 | 
			
		||||
    Ok(recent_activities)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn get_all_activities() -> Result<Vec<Activity>, String> {
 | 
			
		||||
    let db = get_db().expect("Can get DB");
 | 
			
		||||
    let collection = db
 | 
			
		||||
        .collection::<Activity>()
 | 
			
		||||
        .map_err(|e| format!("Collection error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    let db_activities = collection
 | 
			
		||||
        .get_all()
 | 
			
		||||
        .map_err(|e| format!("DB fetch error: {:?}", e))?;
 | 
			
		||||
 | 
			
		||||
    Ok(db_activities)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -208,108 +208,8 @@ pub struct VotingResults {
 | 
			
		||||
    pub total_votes: usize,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Represents a governance activity in the system
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct GovernanceActivity {
 | 
			
		||||
    /// Unique identifier for the activity
 | 
			
		||||
    pub id: Option<u32>,
 | 
			
		||||
    /// Type of activity (proposal_created, vote_cast, etc.)
 | 
			
		||||
    pub activity_type: String,
 | 
			
		||||
    /// ID of the related proposal
 | 
			
		||||
    pub proposal_id: u32,
 | 
			
		||||
    /// Title of the related proposal
 | 
			
		||||
    pub proposal_title: String,
 | 
			
		||||
    /// Name of the user who performed the action
 | 
			
		||||
    pub actor_name: String,
 | 
			
		||||
    /// Description of the activity
 | 
			
		||||
    pub description: String,
 | 
			
		||||
    /// Date and time when the activity occurred
 | 
			
		||||
    pub timestamp: DateTime<Utc>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl GovernanceActivity {
 | 
			
		||||
    /// Creates a new governance activity
 | 
			
		||||
    pub fn new(
 | 
			
		||||
        activity_type: &str,
 | 
			
		||||
        proposal_id: u32,
 | 
			
		||||
        proposal_title: &str,
 | 
			
		||||
        actor_name: &str,
 | 
			
		||||
        description: &str,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            id: None,
 | 
			
		||||
            activity_type: activity_type.to_string(),
 | 
			
		||||
            proposal_id,
 | 
			
		||||
            proposal_title: proposal_title.to_string(),
 | 
			
		||||
            actor_name: actor_name.to_string(),
 | 
			
		||||
            description: description.to_string(),
 | 
			
		||||
            timestamp: Utc::now(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Creates a proposal creation activity
 | 
			
		||||
    pub fn proposal_created(
 | 
			
		||||
        proposal_id: u32,
 | 
			
		||||
        proposal_title: &str,
 | 
			
		||||
        _creator_id: &str,
 | 
			
		||||
        creator_name: &str,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        Self::new(
 | 
			
		||||
            "proposal_created",
 | 
			
		||||
            proposal_id,
 | 
			
		||||
            proposal_title,
 | 
			
		||||
            creator_name,
 | 
			
		||||
            &format!("Proposal '{}' created by {}", proposal_title, creator_name),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Creates a vote cast activity
 | 
			
		||||
    pub fn vote_cast(
 | 
			
		||||
        proposal_id: u32,
 | 
			
		||||
        proposal_title: &str,
 | 
			
		||||
        voter_name: &str,
 | 
			
		||||
        vote_option: &str,
 | 
			
		||||
        shares: i64,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        Self::new(
 | 
			
		||||
            "vote_cast",
 | 
			
		||||
            proposal_id,
 | 
			
		||||
            proposal_title,
 | 
			
		||||
            voter_name,
 | 
			
		||||
            &format!(
 | 
			
		||||
                "{} voted '{}' with {} shares",
 | 
			
		||||
                voter_name, vote_option, shares
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Creates a proposal status change activity
 | 
			
		||||
    pub fn proposal_status_changed(
 | 
			
		||||
        proposal_id: u32,
 | 
			
		||||
        proposal_title: &str,
 | 
			
		||||
        new_status: &ProposalStatus,
 | 
			
		||||
        reason: Option<&str>,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        let description = format!(
 | 
			
		||||
            "Proposal '{}' status changed to {}{}",
 | 
			
		||||
            proposal_title,
 | 
			
		||||
            new_status,
 | 
			
		||||
            reason.map(|r| format!(": {}", r)).unwrap_or_default()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        Self::new(
 | 
			
		||||
            "proposal_status_changed",
 | 
			
		||||
            proposal_id,
 | 
			
		||||
            proposal_title,
 | 
			
		||||
            "System",
 | 
			
		||||
            &description,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
impl VotingResults {
 | 
			
		||||
    /// Creates a new empty voting results object
 | 
			
		||||
    /// Creates a new VotingResults instance
 | 
			
		||||
    pub fn new(proposal_id: String) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            proposal_id,
 | 
			
		||||
@@ -319,38 +219,4 @@ impl VotingResults {
 | 
			
		||||
            total_votes: 0,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Adds a vote to the results
 | 
			
		||||
    pub fn add_vote(&mut self, vote_type: &VoteType) {
 | 
			
		||||
        match vote_type {
 | 
			
		||||
            VoteType::Yes => self.yes_count += 1,
 | 
			
		||||
            VoteType::No => self.no_count += 1,
 | 
			
		||||
            VoteType::Abstain => self.abstain_count += 1,
 | 
			
		||||
        }
 | 
			
		||||
        self.total_votes += 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Calculates the percentage of yes votes
 | 
			
		||||
    pub fn yes_percentage(&self) -> f64 {
 | 
			
		||||
        if self.total_votes == 0 {
 | 
			
		||||
            return 0.0;
 | 
			
		||||
        }
 | 
			
		||||
        (self.yes_count as f64 / self.total_votes as f64) * 100.0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Calculates the percentage of no votes
 | 
			
		||||
    pub fn no_percentage(&self) -> f64 {
 | 
			
		||||
        if self.total_votes == 0 {
 | 
			
		||||
            return 0.0;
 | 
			
		||||
        }
 | 
			
		||||
        (self.no_count as f64 / self.total_votes as f64) * 100.0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Calculates the percentage of abstain votes
 | 
			
		||||
    pub fn abstain_percentage(&self) -> f64 {
 | 
			
		||||
        if self.total_votes == 0 {
 | 
			
		||||
            return 0.0;
 | 
			
		||||
        }
 | 
			
		||||
        (self.abstain_count as f64 / self.total_votes as f64) * 100.0
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -92,17 +92,17 @@
 | 
			
		||||
                    <div class="mt-auto">
 | 
			
		||||
                        <h5><i class="bi bi-calendar-event me-2"></i>Voting Period</h5>
 | 
			
		||||
                        <div class="d-flex justify-content-between align-items-center p-3 bg-light rounded">
 | 
			
		||||
                            {% if proposal.voting_starts_at and proposal.voting_ends_at %}
 | 
			
		||||
                            {% if proposal.vote_start_date and proposal.vote_end_date %}
 | 
			
		||||
                            <div>
 | 
			
		||||
                                <div class="text-muted mb-1">Start Date</div>
 | 
			
		||||
                                <div class="fw-bold">{{ proposal.voting_starts_at | date(format="%Y-%m-%d") }}</div>
 | 
			
		||||
                                <div class="fw-bold">{{ proposal.vote_start_date | date(format="%Y-%m-%d") }}</div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="text-center">
 | 
			
		||||
                                <i class="bi bi-arrow-right fs-4 text-muted"></i>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div>
 | 
			
		||||
                                <div class="text-muted mb-1">End Date</div>
 | 
			
		||||
                                <div class="fw-bold">{{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}</div>
 | 
			
		||||
                                <div class="fw-bold">{{ proposal.vote_end_date | date(format="%Y-%m-%d") }}</div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            {% else %}
 | 
			
		||||
                            <div class="text-center w-100">Not set</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -106,9 +106,9 @@
 | 
			
		||||
                                </td>
 | 
			
		||||
                                <td>{{ proposal.created_at | date(format="%Y-%m-%d") }}</td>
 | 
			
		||||
                                <td>
 | 
			
		||||
                                    {% if proposal.voting_starts_at and proposal.voting_ends_at %}
 | 
			
		||||
                                    {{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} to {{
 | 
			
		||||
                                    proposal.voting_ends_at | date(format="%Y-%m-%d") }}
 | 
			
		||||
                                    {% if proposal.vote_start_date and proposal.vote_end_date %}
 | 
			
		||||
                                    {{ proposal.vote_start_date | date(format="%Y-%m-%d") }} to {{
 | 
			
		||||
                                    proposal.vote_end_date | date(format="%Y-%m-%d") }}
 | 
			
		||||
                                    {% else %}
 | 
			
		||||
                                    Not set
 | 
			
		||||
                                    {% endif %}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user