From 6dbd0c80e6c50f92fac8fa6620523cddf928e0e5 Mon Sep 17 00:00:00 2001 From: Froidefond Julien Date: Mon, 23 Mar 2026 18:46:25 +0100 Subject: [PATCH] feat: improve Telegram notification UI with better formatting Add visual separators, contextual emojis, bold labels, structured result sections, and conditional error lines for cleaner messages. Co-Authored-By: Claude Opus 4.6 --- crates/notifications/src/lib.rs | 194 +++++++++++++++++++------------- 1 file changed, 116 insertions(+), 78 deletions(-) diff --git a/crates/notifications/src/lib.rs b/crates/notifications/src/lib.rs index 9b3e1fe..e415e6c 100644 --- a/crates/notifications/src/lib.rs +++ b/crates/notifications/src/lib.rs @@ -161,7 +161,13 @@ async fn send_telegram_photo(config: &TelegramConfig, caption: &str, photo_path: /// Send a test message. Returns the result directly (not fire-and-forget). pub async fn send_test_message(config: &TelegramConfig) -> Result<()> { - send_telegram(config, "🔔 Stripstream Librarian\nTest notification — connection OK!").await + send_telegram( + config, + "🔔 Stripstream Librarian\n\ + ━━━━━━━━━━━━━━━━━━━━\n\ + ✅ Test notification — connection OK!", + ) + .await } // --------------------------------------------------------------------------- @@ -265,22 +271,23 @@ fn format_event(event: &NotificationEvent) -> String { } => { let lib = library_name.as_deref().unwrap_or("All libraries"); let duration = format_duration(*duration_seconds); - format!( - "📚 Scan completed\n\ - Library: {lib}\n\ - Type: {job_type}\n\ - New books: {}\n\ - New series: {}\n\ - Files scanned: {}\n\ - Removed: {}\n\ - Errors: {}\n\ - Duration: {duration}", - stats.indexed_files, - stats.new_series, - stats.scanned_files, - stats.removed_files, - stats.errors, - ) + let mut lines = vec![ + format!("✅ Scan completed"), + format!("━━━━━━━━━━━━━━━━━━━━"), + format!("📂 Library: {lib}"), + format!("🏷 Type: {job_type}"), + format!("⏱ Duration: {duration}"), + String::new(), + format!("📊 Results"), + format!(" 📗 New books: {}", stats.indexed_files), + format!(" 📚 New series: {}", stats.new_series), + format!(" 🔎 Files scanned: {}", stats.scanned_files), + format!(" 🗑 Removed: {}", stats.removed_files), + ]; + if stats.errors > 0 { + lines.push(format!(" ⚠️ Errors: {}", stats.errors)); + } + lines.join("\n") } NotificationEvent::ScanFailed { job_type, @@ -289,23 +296,28 @@ fn format_event(event: &NotificationEvent) -> String { } => { let lib = library_name.as_deref().unwrap_or("All libraries"); let err = truncate(error, 200); - format!( - "❌ Scan failed\n\ - Library: {lib}\n\ - Type: {job_type}\n\ - Error: {err}" - ) + [ + format!("🚨 Scan failed"), + format!("━━━━━━━━━━━━━━━━━━━━"), + format!("📂 Library: {lib}"), + format!("🏷 Type: {job_type}"), + String::new(), + format!("💬 {err}"), + ] + .join("\n") } NotificationEvent::ScanCancelled { job_type, library_name, } => { let lib = library_name.as_deref().unwrap_or("All libraries"); - format!( - "⏹ Scan cancelled\n\ - Library: {lib}\n\ - Type: {job_type}" - ) + [ + format!("⏹ Scan cancelled"), + format!("━━━━━━━━━━━━━━━━━━━━"), + format!("📂 Library: {lib}"), + format!("🏷 Type: {job_type}"), + ] + .join("\n") } NotificationEvent::ThumbnailCompleted { job_type, @@ -314,12 +326,14 @@ fn format_event(event: &NotificationEvent) -> String { } => { let lib = library_name.as_deref().unwrap_or("All libraries"); let duration = format_duration(*duration_seconds); - format!( - "🖼 Thumbnails completed\n\ - Library: {lib}\n\ - Type: {job_type}\n\ - Duration: {duration}" - ) + [ + format!("✅ Thumbnails completed"), + format!("━━━━━━━━━━━━━━━━━━━━"), + format!("📂 Library: {lib}"), + format!("🏷 Type: {job_type}"), + format!("⏱ Duration: {duration}"), + ] + .join("\n") } NotificationEvent::ThumbnailFailed { job_type, @@ -328,12 +342,15 @@ fn format_event(event: &NotificationEvent) -> String { } => { let lib = library_name.as_deref().unwrap_or("All libraries"); let err = truncate(error, 200); - format!( - "❌ Thumbnails failed\n\ - Library: {lib}\n\ - Type: {job_type}\n\ - Error: {err}" - ) + [ + format!("🚨 Thumbnails failed"), + format!("━━━━━━━━━━━━━━━━━━━━"), + format!("📂 Library: {lib}"), + format!("🏷 Type: {job_type}"), + String::new(), + format!("💬 {err}"), + ] + .join("\n") } NotificationEvent::ConversionCompleted { library_name, @@ -342,11 +359,13 @@ fn format_event(event: &NotificationEvent) -> String { } => { let lib = library_name.as_deref().unwrap_or("Unknown"); let title = book_title.as_deref().unwrap_or("Unknown"); - format!( - "🔄 CBR→CBZ conversion completed\n\ - Library: {lib}\n\ - Book: {title}" - ) + [ + format!("✅ CBR → CBZ conversion completed"), + format!("━━━━━━━━━━━━━━━━━━━━"), + format!("📂 Library: {lib}"), + format!("📖 Book: {title}"), + ] + .join("\n") } NotificationEvent::ConversionFailed { library_name, @@ -357,23 +376,28 @@ fn format_event(event: &NotificationEvent) -> String { let lib = library_name.as_deref().unwrap_or("Unknown"); let title = book_title.as_deref().unwrap_or("Unknown"); let err = truncate(error, 200); - format!( - "❌ CBR→CBZ conversion failed\n\ - Library: {lib}\n\ - Book: {title}\n\ - Error: {err}" - ) + [ + format!("🚨 CBR → CBZ conversion failed"), + format!("━━━━━━━━━━━━━━━━━━━━"), + format!("📂 Library: {lib}"), + format!("📖 Book: {title}"), + String::new(), + format!("💬 {err}"), + ] + .join("\n") } NotificationEvent::MetadataApproved { series_name, provider, .. } => { - format!( - "🔗 Metadata linked\n\ - Series: {series_name}\n\ - Provider: {provider}" - ) + [ + format!("✅ Metadata linked"), + format!("━━━━━━━━━━━━━━━━━━━━"), + format!("📚 Series: {series_name}"), + format!("🔗 Provider: {provider}"), + ] + .join("\n") } NotificationEvent::MetadataBatchCompleted { library_name, @@ -381,11 +405,13 @@ fn format_event(event: &NotificationEvent) -> String { processed, } => { let lib = library_name.as_deref().unwrap_or("All libraries"); - format!( - "🔍 Metadata batch completed\n\ - Library: {lib}\n\ - Series processed: {processed}/{total_series}" - ) + [ + format!("✅ Metadata batch completed"), + format!("━━━━━━━━━━━━━━━━━━━━"), + format!("📂 Library: {lib}"), + format!("📊 Processed: {processed}/{total_series} series"), + ] + .join("\n") } NotificationEvent::MetadataBatchFailed { library_name, @@ -393,11 +419,14 @@ fn format_event(event: &NotificationEvent) -> String { } => { let lib = library_name.as_deref().unwrap_or("All libraries"); let err = truncate(error, 200); - format!( - "❌ Metadata batch failed\n\ - Library: {lib}\n\ - Error: {err}" - ) + [ + format!("🚨 Metadata batch failed"), + format!("━━━━━━━━━━━━━━━━━━━━"), + format!("📂 Library: {lib}"), + String::new(), + format!("💬 {err}"), + ] + .join("\n") } NotificationEvent::MetadataRefreshCompleted { library_name, @@ -406,13 +435,19 @@ fn format_event(event: &NotificationEvent) -> String { errors, } => { let lib = library_name.as_deref().unwrap_or("All libraries"); - format!( - "🔄 Metadata refresh completed\n\ - Library: {lib}\n\ - Updated: {refreshed}\n\ - Unchanged: {unchanged}\n\ - Errors: {errors}" - ) + let mut lines = vec![ + format!("✅ Metadata refresh completed"), + format!("━━━━━━━━━━━━━━━━━━━━"), + format!("📂 Library: {lib}"), + String::new(), + format!("📊 Results"), + format!(" 🔄 Updated: {refreshed}"), + format!(" ▪️ Unchanged: {unchanged}"), + ]; + if *errors > 0 { + lines.push(format!(" ⚠️ Errors: {errors}")); + } + lines.join("\n") } NotificationEvent::MetadataRefreshFailed { library_name, @@ -420,11 +455,14 @@ fn format_event(event: &NotificationEvent) -> String { } => { let lib = library_name.as_deref().unwrap_or("All libraries"); let err = truncate(error, 200); - format!( - "❌ Metadata refresh failed\n\ - Library: {lib}\n\ - Error: {err}" - ) + [ + format!("🚨 Metadata refresh failed"), + format!("━━━━━━━━━━━━━━━━━━━━"), + format!("📂 Library: {lib}"), + String::new(), + format!("💬 {err}"), + ] + .join("\n") } } }