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 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 18:46:25 +01:00
parent 0c42a9ed04
commit 6dbd0c80e6

View File

@@ -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). /// Send a test message. Returns the result directly (not fire-and-forget).
pub async fn send_test_message(config: &TelegramConfig) -> Result<()> { pub async fn send_test_message(config: &TelegramConfig) -> Result<()> {
send_telegram(config, "🔔 <b>Stripstream Librarian</b>\nTest notification — connection OK!").await send_telegram(
config,
"🔔 <b>Stripstream Librarian</b>\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 lib = library_name.as_deref().unwrap_or("All libraries");
let duration = format_duration(*duration_seconds); let duration = format_duration(*duration_seconds);
format!( let mut lines = vec![
"📚 <b>Scan completed</b>\n\ format!(" <b>Scan completed</b>"),
Library: {lib}\n\ format!("━━━━━━━━━━━━━━━━━━━━"),
Type: {job_type}\n\ format!("📂 <b>Library:</b> {lib}"),
New books: {}\n\ format!("🏷 <b>Type:</b> {job_type}"),
New series: {}\n\ format!("⏱ <b>Duration:</b> {duration}"),
Files scanned: {}\n\ String::new(),
Removed: {}\n\ format!("📊 <b>Results</b>"),
Errors: {}\n\ format!(" 📗 New books: <b>{}</b>", stats.indexed_files),
Duration: {duration}", format!(" 📚 New series: <b>{}</b>", stats.new_series),
stats.indexed_files, format!(" 🔎 Files scanned: <b>{}</b>", stats.scanned_files),
stats.new_series, format!(" 🗑 Removed: <b>{}</b>", stats.removed_files),
stats.scanned_files, ];
stats.removed_files, if stats.errors > 0 {
stats.errors, lines.push(format!(" ⚠️ Errors: <b>{}</b>", stats.errors));
) }
lines.join("\n")
} }
NotificationEvent::ScanFailed { NotificationEvent::ScanFailed {
job_type, job_type,
@@ -289,23 +296,28 @@ fn format_event(event: &NotificationEvent) -> String {
} => { } => {
let lib = library_name.as_deref().unwrap_or("All libraries"); let lib = library_name.as_deref().unwrap_or("All libraries");
let err = truncate(error, 200); let err = truncate(error, 200);
format!( [
" <b>Scan failed</b>\n\ format!("🚨 <b>Scan failed</b>"),
Library: {lib}\n\ format!("━━━━━━━━━━━━━━━━━━━━"),
Type: {job_type}\n\ format!("📂 <b>Library:</b> {lib}"),
Error: {err}" format!("🏷 <b>Type:</b> {job_type}"),
) String::new(),
format!("💬 <code>{err}</code>"),
]
.join("\n")
} }
NotificationEvent::ScanCancelled { NotificationEvent::ScanCancelled {
job_type, job_type,
library_name, library_name,
} => { } => {
let lib = library_name.as_deref().unwrap_or("All libraries"); let lib = library_name.as_deref().unwrap_or("All libraries");
format!( [
"⏹ <b>Scan cancelled</b>\n\ format!("⏹ <b>Scan cancelled</b>"),
Library: {lib}\n\ format!("━━━━━━━━━━━━━━━━━━━━"),
Type: {job_type}" format!("📂 <b>Library:</b> {lib}"),
) format!("🏷 <b>Type:</b> {job_type}"),
]
.join("\n")
} }
NotificationEvent::ThumbnailCompleted { NotificationEvent::ThumbnailCompleted {
job_type, job_type,
@@ -314,12 +326,14 @@ fn format_event(event: &NotificationEvent) -> String {
} => { } => {
let lib = library_name.as_deref().unwrap_or("All libraries"); let lib = library_name.as_deref().unwrap_or("All libraries");
let duration = format_duration(*duration_seconds); let duration = format_duration(*duration_seconds);
format!( [
"🖼 <b>Thumbnails completed</b>\n\ format!(" <b>Thumbnails completed</b>"),
Library: {lib}\n\ format!("━━━━━━━━━━━━━━━━━━━━"),
Type: {job_type}\n\ format!("📂 <b>Library:</b> {lib}"),
Duration: {duration}" format!("🏷 <b>Type:</b> {job_type}"),
) format!("⏱ <b>Duration:</b> {duration}"),
]
.join("\n")
} }
NotificationEvent::ThumbnailFailed { NotificationEvent::ThumbnailFailed {
job_type, job_type,
@@ -328,12 +342,15 @@ fn format_event(event: &NotificationEvent) -> String {
} => { } => {
let lib = library_name.as_deref().unwrap_or("All libraries"); let lib = library_name.as_deref().unwrap_or("All libraries");
let err = truncate(error, 200); let err = truncate(error, 200);
format!( [
" <b>Thumbnails failed</b>\n\ format!("🚨 <b>Thumbnails failed</b>"),
Library: {lib}\n\ format!("━━━━━━━━━━━━━━━━━━━━"),
Type: {job_type}\n\ format!("📂 <b>Library:</b> {lib}"),
Error: {err}" format!("🏷 <b>Type:</b> {job_type}"),
) String::new(),
format!("💬 <code>{err}</code>"),
]
.join("\n")
} }
NotificationEvent::ConversionCompleted { NotificationEvent::ConversionCompleted {
library_name, library_name,
@@ -342,11 +359,13 @@ fn format_event(event: &NotificationEvent) -> String {
} => { } => {
let lib = library_name.as_deref().unwrap_or("Unknown"); let lib = library_name.as_deref().unwrap_or("Unknown");
let title = book_title.as_deref().unwrap_or("Unknown"); let title = book_title.as_deref().unwrap_or("Unknown");
format!( [
"🔄 <b>CBRCBZ conversion completed</b>\n\ format!(" <b>CBRCBZ conversion completed</b>"),
Library: {lib}\n\ format!("━━━━━━━━━━━━━━━━━━━━"),
Book: {title}" format!("📂 <b>Library:</b> {lib}"),
) format!("📖 <b>Book:</b> {title}"),
]
.join("\n")
} }
NotificationEvent::ConversionFailed { NotificationEvent::ConversionFailed {
library_name, library_name,
@@ -357,23 +376,28 @@ fn format_event(event: &NotificationEvent) -> String {
let lib = library_name.as_deref().unwrap_or("Unknown"); let lib = library_name.as_deref().unwrap_or("Unknown");
let title = book_title.as_deref().unwrap_or("Unknown"); let title = book_title.as_deref().unwrap_or("Unknown");
let err = truncate(error, 200); let err = truncate(error, 200);
format!( [
" <b>CBRCBZ conversion failed</b>\n\ format!("🚨 <b>CBRCBZ conversion failed</b>"),
Library: {lib}\n\ format!("━━━━━━━━━━━━━━━━━━━━"),
Book: {title}\n\ format!("📂 <b>Library:</b> {lib}"),
Error: {err}" format!("📖 <b>Book:</b> {title}"),
) String::new(),
format!("💬 <code>{err}</code>"),
]
.join("\n")
} }
NotificationEvent::MetadataApproved { NotificationEvent::MetadataApproved {
series_name, series_name,
provider, provider,
.. ..
} => { } => {
format!( [
"🔗 <b>Metadata linked</b>\n\ format!(" <b>Metadata linked</b>"),
Series: {series_name}\n\ format!("━━━━━━━━━━━━━━━━━━━━"),
Provider: {provider}" format!("📚 <b>Series:</b> {series_name}"),
) format!("🔗 <b>Provider:</b> {provider}"),
]
.join("\n")
} }
NotificationEvent::MetadataBatchCompleted { NotificationEvent::MetadataBatchCompleted {
library_name, library_name,
@@ -381,11 +405,13 @@ fn format_event(event: &NotificationEvent) -> String {
processed, processed,
} => { } => {
let lib = library_name.as_deref().unwrap_or("All libraries"); let lib = library_name.as_deref().unwrap_or("All libraries");
format!( [
"🔍 <b>Metadata batch completed</b>\n\ format!(" <b>Metadata batch completed</b>"),
Library: {lib}\n\ format!("━━━━━━━━━━━━━━━━━━━━"),
Series processed: {processed}/{total_series}" format!("📂 <b>Library:</b> {lib}"),
) format!("📊 <b>Processed:</b> {processed}/{total_series} series"),
]
.join("\n")
} }
NotificationEvent::MetadataBatchFailed { NotificationEvent::MetadataBatchFailed {
library_name, library_name,
@@ -393,11 +419,14 @@ fn format_event(event: &NotificationEvent) -> String {
} => { } => {
let lib = library_name.as_deref().unwrap_or("All libraries"); let lib = library_name.as_deref().unwrap_or("All libraries");
let err = truncate(error, 200); let err = truncate(error, 200);
format!( [
" <b>Metadata batch failed</b>\n\ format!("🚨 <b>Metadata batch failed</b>"),
Library: {lib}\n\ format!("━━━━━━━━━━━━━━━━━━━━"),
Error: {err}" format!("📂 <b>Library:</b> {lib}"),
) String::new(),
format!("💬 <code>{err}</code>"),
]
.join("\n")
} }
NotificationEvent::MetadataRefreshCompleted { NotificationEvent::MetadataRefreshCompleted {
library_name, library_name,
@@ -406,13 +435,19 @@ fn format_event(event: &NotificationEvent) -> String {
errors, errors,
} => { } => {
let lib = library_name.as_deref().unwrap_or("All libraries"); let lib = library_name.as_deref().unwrap_or("All libraries");
format!( let mut lines = vec![
"🔄 <b>Metadata refresh completed</b>\n\ format!(" <b>Metadata refresh completed</b>"),
Library: {lib}\n\ format!("━━━━━━━━━━━━━━━━━━━━"),
Updated: {refreshed}\n\ format!("📂 <b>Library:</b> {lib}"),
Unchanged: {unchanged}\n\ String::new(),
Errors: {errors}" format!("📊 <b>Results</b>"),
) format!(" 🔄 Updated: <b>{refreshed}</b>"),
format!(" ▪️ Unchanged: <b>{unchanged}</b>"),
];
if *errors > 0 {
lines.push(format!(" ⚠️ Errors: <b>{errors}</b>"));
}
lines.join("\n")
} }
NotificationEvent::MetadataRefreshFailed { NotificationEvent::MetadataRefreshFailed {
library_name, library_name,
@@ -420,11 +455,14 @@ fn format_event(event: &NotificationEvent) -> String {
} => { } => {
let lib = library_name.as_deref().unwrap_or("All libraries"); let lib = library_name.as_deref().unwrap_or("All libraries");
let err = truncate(error, 200); let err = truncate(error, 200);
format!( [
" <b>Metadata refresh failed</b>\n\ format!("🚨 <b>Metadata refresh failed</b>"),
Library: {lib}\n\ format!("━━━━━━━━━━━━━━━━━━━━"),
Error: {err}" format!("📂 <b>Library:</b> {lib}"),
) String::new(),
format!("💬 <code>{err}</code>"),
]
.join("\n")
} }
} }
} }