Skip to content

Commit 3697ebe

Browse files
committed
exports
1 parent d4aade5 commit 3697ebe

File tree

5 files changed

+174
-42
lines changed

5 files changed

+174
-42
lines changed

src/web/routes.rs

Lines changed: 161 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,30 @@ use axum::{
66
Form,
77
};
88
use rusqlite::params;
9-
use serde::Deserialize;
9+
use serde::{Deserialize, Serialize};
1010
use tera::Context;
1111

1212
use super::server::AppState;
1313

14+
#[derive(Debug, Serialize)]
15+
struct UserExport {
16+
id: String,
17+
name: String,
18+
level: i32,
19+
xp: i32,
20+
social_credit: i64,
21+
relationship: String,
22+
example_input: String,
23+
example_output: String,
24+
memories: Vec<MemoryExport>,
25+
}
26+
27+
#[derive(Debug, Serialize)]
28+
struct MemoryExport {
29+
key: String,
30+
content: String,
31+
}
32+
1433
#[derive(Debug, Deserialize)]
1534
pub struct UserUpdateForm {
1635
pub name: String,
@@ -25,19 +44,6 @@ pub struct MemoryForm {
2544
pub content: String,
2645
}
2746

28-
pub async fn index(State(state): State<AppState>) -> Response {
29-
let mut context = Context::new();
30-
context.insert("title", "Memory Manager");
31-
32-
match state.templates.render("index.html", &context) {
33-
Ok(html) => Html(html).into_response(),
34-
Err(e) => {
35-
tracing::error!("Template error: {:?}", e);
36-
(StatusCode::INTERNAL_SERVER_ERROR, format!("Template error: {}", e)).into_response()
37-
}
38-
}
39-
}
40-
4147
pub async fn list_users(State(state): State<AppState>) -> Response {
4248
let conn = match state.db.get() {
4349
Ok(conn) => conn,
@@ -489,3 +495,144 @@ pub async fn serve_css() -> Response {
489495
)
490496
.into_response()
491497
}
498+
499+
// Helper function to get all user exports with memories
500+
fn get_all_user_exports(conn: &rusqlite::Connection) -> Result<Vec<UserExport>, String> {
501+
// Get all users with their data
502+
let mut stmt = conn.prepare(
503+
"SELECT id, level, xp, social_credit, name, relationship, example_input, example_output FROM user ORDER BY id"
504+
).map_err(|e| format!("Database error: {}", e))?;
505+
506+
let users_result: Result<Vec<(String, i32, i32, i64, String, String, String, String)>, _> = stmt
507+
.query_map([], |row| {
508+
Ok((
509+
row.get(0)?,
510+
row.get(1)?,
511+
row.get(2)?,
512+
row.get(3)?,
513+
row.get(4)?,
514+
row.get(5)?,
515+
row.get(6)?,
516+
row.get(7)?,
517+
))
518+
})
519+
.and_then(|mapped| mapped.collect());
520+
521+
let users = users_result.map_err(|e| format!("Database error: {}", e))?;
522+
523+
// Create user exports without memories
524+
let exports: Vec<UserExport> = users
525+
.into_iter()
526+
.map(|(id, level, xp, social_credit, name, relationship, example_input, example_output)| {
527+
UserExport {
528+
id,
529+
name,
530+
level,
531+
xp,
532+
social_credit,
533+
relationship,
534+
example_input,
535+
example_output,
536+
memories: Vec::new(),
537+
}
538+
})
539+
.collect();
540+
541+
Ok(exports)
542+
}
543+
544+
pub async fn export_prompts_json(State(state): State<AppState>) -> Response {
545+
let conn = match state.db.get() {
546+
Ok(conn) => conn,
547+
Err(e) => {
548+
return (StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response()
549+
}
550+
};
551+
552+
let exports = match get_all_user_exports(&conn) {
553+
Ok(exports) => exports,
554+
Err(e) => {
555+
return (StatusCode::INTERNAL_SERVER_ERROR, e).into_response()
556+
}
557+
};
558+
559+
match serde_json::to_string_pretty(&exports) {
560+
Ok(json) => (
561+
[
562+
(axum::http::header::CONTENT_TYPE, "application/json"),
563+
(
564+
axum::http::header::CONTENT_DISPOSITION,
565+
"attachment; filename=\"prompts_export.json\"",
566+
),
567+
],
568+
json,
569+
)
570+
.into_response(),
571+
Err(e) => {
572+
(StatusCode::INTERNAL_SERVER_ERROR, format!("JSON error: {}", e)).into_response()
573+
}
574+
}
575+
}
576+
577+
pub async fn export_users_csv(State(state): State<AppState>) -> Response {
578+
let conn = match state.db.get() {
579+
Ok(conn) => conn,
580+
Err(e) => {
581+
return (StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)).into_response()
582+
}
583+
};
584+
585+
let exports = match get_all_user_exports(&conn) {
586+
Ok(exports) => exports,
587+
Err(e) => {
588+
return (StatusCode::INTERNAL_SERVER_ERROR, e).into_response()
589+
}
590+
};
591+
592+
// Helper to escape CSV fields
593+
let escape_csv = |s: &str| -> String {
594+
if s.contains(',') || s.contains('"') || s.contains('\n') {
595+
format!("\"{}\"", s.replace('"', "\"\""))
596+
} else {
597+
s.to_string()
598+
}
599+
};
600+
601+
// Build CSV with memories
602+
let mut csv = String::from("user_id,name,level,xp,social_credit,relationship,example_input,example_output,memories\n");
603+
604+
for user in exports {
605+
// Serialize memories as JSON for the CSV field
606+
let memories_json = match serde_json::to_string(&user.memories) {
607+
Ok(json) => json,
608+
Err(e) => {
609+
return (StatusCode::INTERNAL_SERVER_ERROR, format!("JSON error: {}", e)).into_response()
610+
}
611+
};
612+
613+
csv.push_str(&format!(
614+
"{},{},{},{},{},{},{},{},{}\n",
615+
escape_csv(&user.id),
616+
escape_csv(&user.name),
617+
user.level,
618+
user.xp,
619+
user.social_credit,
620+
escape_csv(&user.relationship),
621+
escape_csv(&user.example_input),
622+
escape_csv(&user.example_output),
623+
escape_csv(&memories_json),
624+
));
625+
}
626+
627+
(
628+
[
629+
(axum::http::header::CONTENT_TYPE, "text/csv"),
630+
(
631+
axum::http::header::CONTENT_DISPOSITION,
632+
"attachment; filename=\"users_export.csv\"",
633+
),
634+
],
635+
csv,
636+
)
637+
.into_response()
638+
}

src/web/server.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ pub async fn run_web_server(db: Pool<SqliteConnectionManager>, port: u16) -> Res
3939
};
4040

4141
let app = Router::new()
42-
.route("/", get(super::routes::index))
42+
.route("/", get(super::routes::list_users))
4343
.route("/users", get(super::routes::list_users))
4444
.route("/user/{id}", get(super::routes::view_user))
4545
.route("/user/{id}/edit", get(super::routes::edit_user_form))
@@ -50,6 +50,8 @@ pub async fn run_web_server(db: Pool<SqliteConnectionManager>, port: u16) -> Res
5050
.route("/memory/{id}/edit", get(super::routes::edit_memory_form))
5151
.route("/memory/{id}/edit", post(super::routes::update_memory))
5252
.route("/memory/{id}/delete", post(super::routes::delete_memory))
53+
.route("/export/prompts.json", get(super::routes::export_prompts_json))
54+
.route("/export/users.csv", get(super::routes::export_users_csv))
5355
.route("/static/style.css", get(super::routes::serve_css))
5456
.with_state(state);
5557

web/templates/base.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
<div class="container">
1212
<a href="/" class="nav-brand">Memory Manager</a>
1313
<ul class="nav-links">
14-
<li><a href="/">Home</a></li>
15-
<li><a href="/users">Users</a></li>
14+
<li><a href="/">Users</a></li>
15+
<li><a href="/export/prompts.json" download>Export JSON</a></li>
16+
<li><a href="/export/users.csv" download>Export CSV</a></li>
1617
</ul>
1718
</div>
1819
</nav>

web/templates/index.html

Lines changed: 0 additions & 24 deletions
This file was deleted.

web/templates/users.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
{% extends "base.html" %}
22

33
{% block content %}
4-
<h1>Users</h1>
4+
<div class="page-header">
5+
<h1>Users</h1>
6+
<div class="actions">
7+
<a href="/export/prompts.json" class="btn btn-primary" download>Export Prompts (JSON)</a>
8+
<a href="/export/users.csv" class="btn btn-secondary" download>Export Users (CSV)</a>
9+
</div>
10+
</div>
511

612
<div class="users-grid">
713
{% for user in users %}

0 commit comments

Comments
 (0)