From c185ea102ce32b8206ae8061c01809d7bd385cc6 Mon Sep 17 00:00:00 2001 From: rustmailer Date: Tue, 21 Apr 2026 18:15:54 +0800 Subject: [PATCH] update --- Cargo.lock | 49 +- Cargo.toml | 14 +- src/modules/account/migration.rs | 9 +- src/modules/dashboard/mod.rs | 60 +- src/modules/mailbox/delete.rs | 7 +- src/modules/message/delete.rs | 8 +- src/modules/message/search.rs | 2 + src/modules/store/tantivy/attachment.rs | 20 +- src/modules/store/tantivy/envelope.rs | 6 + web/src/api/system/api.ts | 9 + .../components/running-state-dialog.tsx | 156 +++--- web/src/features/attachment/filter-reset.tsx | 10 +- .../features/attachment/mail-list-table.tsx | 4 +- web/src/features/dashboard/index.tsx | 526 +++++++++++------- web/src/features/search/filter-reset.tsx | 10 +- web/src/features/search/mail-list-table.tsx | 4 +- web/src/index.css | 163 +++--- web/src/locales/ar.json | 1 + web/src/locales/da.json | 1 + web/src/locales/de.json | 1 + web/src/locales/en.json | 1 + web/src/locales/es.json | 1 + web/src/locales/fi.json | 1 + web/src/locales/fr.json | 1 + web/src/locales/it.json | 1 + web/src/locales/jp.json | 1 + web/src/locales/ko.json | 1 + web/src/locales/nl.json | 1 + web/src/locales/no.json | 1 + web/src/locales/pl.json | 1 + web/src/locales/pt.json | 1 + web/src/locales/ru.json | 1 + web/src/locales/sv.json | 1 + web/src/locales/zh-tw.json | 1 + web/src/locales/zh.json | 1 + 35 files changed, 652 insertions(+), 423 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4629da6..7621423 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -794,9 +794,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -816,9 +816,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2013,9 +2013,9 @@ dependencies = [ [[package]] name = "html2text" -version = "0.16.7" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12d23156ea4dbe6b37ad48fab2da56ff27b0f6192fb5db210c44eb07bfe6e787" +checksum = "d1c97eea9d3e6524d8d2a4643ce0e3135500c4c7ca1d02393a485f871bef69d8" dependencies = [ "html5ever", "tendril", @@ -2025,9 +2025,9 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.38.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" dependencies = [ "log", "markup5ever", @@ -2607,12 +2607,11 @@ checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "libmimalloc-sys" -version = "0.1.44" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" +checksum = "bc89deee4af0429081d2a518c0431ae068222a5a262a3bc6ff4d8535ec2e02fe" dependencies = [ "cc", - "libc", ] [[package]] @@ -2799,9 +2798,9 @@ dependencies = [ [[package]] name = "markup5ever" -version = "0.38.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" dependencies = [ "log", "tendril", @@ -2855,9 +2854,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.48" +version = "0.1.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" +checksum = "aca3c01a711f395b4257b81674c0e90e8dd1f1e62c4b7db45f684cc7a4fcb18a" dependencies = [ "libmimalloc-sys", ] @@ -3271,9 +3270,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.113" +version = "0.9.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad2f2c0eba47118757e4c6d2bff2838f3e0523380021356e7875e858372ce644" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" dependencies = [ "cc", "libc", @@ -5218,9 +5217,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes 1.11.1", "libc", @@ -5724,9 +5723,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.1", "js-sys", @@ -5946,9 +5945,9 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.2.1" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e588f10c7bc3465f5fc1ab087fc97877ec1064a7ec89fb685ac4ee998dac4a" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" dependencies = [ "phf 0.13.1", "phf_codegen", @@ -5967,9 +5966,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] diff --git a/Cargo.toml b/Cargo.toml index 20e82dc..9175525 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,8 +28,8 @@ codegen-units = 1 [dependencies] chrono = "0.4.44" -clap = { version = "4.6.0", features = ["derive", "env"] } -mimalloc = "0.1.48" +clap = { version = "4.6.1", features = ["derive", "env"] } +mimalloc = "0.1.49" native_db = "0.8.2" itertools = "0.14.0" native_model = "0.4.20" @@ -46,7 +46,7 @@ poem-openapi = { version = "5.1.16", features = [ ring = { version = "0.17.14", features = ["std"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" -tokio = { version = "1.52.0", features = ["full"] } +tokio = { version = "1.52.1", features = ["full"] } tracing = "0.1.44" tracing-appender = "0.2.3" tracing-subscriber = { version = "0.3.23", features = ["env-filter", "json"] } @@ -85,7 +85,7 @@ async-imap = { version = "0.11.2", default-features = false, features = [ "runtime-tokio", "compress", ] } -webpki-roots = "1.0.6" +webpki-roots = "1.0.7" rustls = { version = "0.23.38", default-features = false, features = ["ring"] } rustls-pki-types = "1.14.0" tokio-io-timeout = "1.2.1" @@ -105,10 +105,10 @@ autoconfig = "0.4.0" urlencoding = "2.1.3" dashmap = "6.1.0" # Statically links OpenSSL by compiling from source, avoiding system library dependencies -openssl-sys = { version = "0.9.113", optional = true, features = ["vendored"] } +openssl-sys = { version = "0.9.114", optional = true, features = ["vendored"] } gethostname = "1.1.0" itoa = "1.0.18" -html2text = "0.16.7" +html2text = "0.17.1" bytes = "1.11.1" dialoguer = "0.12.0" console = "0.16.3" @@ -121,7 +121,7 @@ mail-send = "0.6.0" rcgen = "0.14.7" rustls-pemfile = "2.2.0" blake3 = "1.8.4" -uuid = { version = "1.23.0", features = ["v4", "serde"] } +uuid = { version = "1.23.1", features = ["v4", "serde"] } fjall = { version = "3.1.4", features = ["lz4", "metrics", "bytes_1"] } tantivy = { version = "0.26.0", features = ["zstd-compression"] } tracing-log = "0.2.0" diff --git a/src/modules/account/migration.rs b/src/modules/account/migration.rs index 2edae04..ea0ba41 100644 --- a/src/modules/account/migration.rs +++ b/src/modules/account/migration.rs @@ -35,7 +35,7 @@ use crate::{ cache::imap::mailbox::MailBox, database::{list_all_impl, secondary_find_impl, with_transaction}, error::BichonResult, - store::tantivy::envelope::ENVELOPE_MANAGER, + store::tantivy::{attachment::ATTACHMENT_MANAGER, envelope::ENVELOPE_MANAGER}, users::{role::DEFAULT_ACCOUNT_MANAGER_ROLE_ID, UserModel, DEFAULT_ADMIN_USER_ID}, }, utc_now, @@ -372,7 +372,12 @@ impl AccountV4 { OAuth2AccessToken::try_delete(account.id).await?; UserModel::cleanup_account(account.id).await?; MailBox::clean(account.id).await?; - ENVELOPE_MANAGER.delete_account_envelopes(account.id).await?; + ENVELOPE_MANAGER + .delete_account_envelopes(account.id) + .await?; + ATTACHMENT_MANAGER + .delete_account_attachments(account.id) + .await?; Self::delete_account(account.id).await?; info!("Sequential cleanup completed for account: {}", account.id); Ok(()) diff --git a/src/modules/dashboard/mod.rs b/src/modules/dashboard/mod.rs index 60cacef..4903e0b 100644 --- a/src/modules/dashboard/mod.rs +++ b/src/modules/dashboard/mod.rs @@ -17,7 +17,12 @@ // along with this program. If not, see . use crate::modules::{ - store::tantivy::{envelope::ENVELOPE_MANAGER, fields::F_ID, schema::SchemaTools}, + store::tantivy::{ + attachment::ATTACHMENT_MANAGER, + envelope::ENVELOPE_MANAGER, + fields::{F_CONTENT_HASH, F_ID}, + schema::SchemaTools, + }, users::permissions::Permission, }; use poem_openapi::Object; @@ -39,17 +44,18 @@ use crate::{ #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize, Object)] pub struct DashboardStats { - pub account_count: usize, // Number of accounts - pub email_count: u64, // Total number of emails - pub total_size_bytes: u64, // Total size of all emails (in bytes) - pub storage_usage_bytes: u64, // Actual storage used (in bytes) - pub index_usage_bytes: u64, // Index storage size (in bytes) - pub recent_activity: Vec, // Email activity over recent days - pub top_senders: Vec, // Top 10 senders - pub top_accounts: Vec, // Top 10 accounts - pub with_attachment_count: u64, // Emails with attachments - pub without_attachment_count: u64, // Emails without attachments - pub top_largest_emails: Vec, // Top 10 largest emails + pub account_count: usize, // Number of accounts + pub email_count: u64, // Total number of emails + pub total_size_bytes: u64, // Total size of all emails (in bytes) + pub storage_usage_bytes: u64, // Actual storage used (in bytes) + pub index_usage_bytes: u64, // Index storage size (in bytes) + pub recent_activity: Vec, // Email activity over recent days + pub top_senders: Vec, // Top 10 senders + pub top_accounts: Vec, // Top 10 accounts + pub with_attachment_count: u64, // Emails with attachments + pub without_attachment_count: u64, // Emails without attachments + pub top_largest_emails: Vec, // Top 10 largest emails + pub top_largest_attachments: Vec, // Top 10 largest attachments pub system_version: String, // The semantic version string of the currently running backend service pub commit_hash: String, // Git commit hash used to build this system version } @@ -66,9 +72,16 @@ impl DashboardStats { Some(context.user.account_access_map.keys().cloned().collect()) }; - let mut stat = ENVELOPE_MANAGER.get_dashboard_stats(&authorized_ids).await?; + let mut stat = ENVELOPE_MANAGER + .get_dashboard_stats(&authorized_ids) + .await?; - stat.top_largest_emails = ENVELOPE_MANAGER.top_10_largest_emails(&authorized_ids).await?; + stat.top_largest_emails = ENVELOPE_MANAGER + .top_10_largest_emails(&authorized_ids) + .await?; + stat.top_largest_attachments = ATTACHMENT_MANAGER + .top_10_largest_attachments(&authorized_ids) + .await?; stat.account_count = if has_all_accounts { AccountModel::count().await? @@ -165,6 +178,7 @@ pub struct LargestAttachment { pub name: String, // Attachment name pub size_bytes: u64, // Attachment size in bytes pub id: String, + pub content_hash: String, } impl LargestAttachment { @@ -205,12 +219,26 @@ impl LargestAttachment { ) })?; - let envelope = LargestAttachment { + let value = document.get_first(fields.f_content_hash).ok_or_else(|| { + raise_error!( + format!("'{}' field not found", F_CONTENT_HASH), + ErrorCode::InternalError + ) + })?; + let content_hash = value.as_str().map(|s| s.to_string()).ok_or_else(|| { + raise_error!( + format!("'{}' field is not a string", F_CONTENT_HASH), + ErrorCode::InternalError + ) + })?; + + let attachment = LargestAttachment { name, size_bytes, id, + content_hash, }; - Ok(envelope) + Ok(attachment) } } diff --git a/src/modules/mailbox/delete.rs b/src/modules/mailbox/delete.rs index ace21e4..ea19f38 100644 --- a/src/modules/mailbox/delete.rs +++ b/src/modules/mailbox/delete.rs @@ -17,7 +17,9 @@ // along with this program. If not, see . use crate::modules::{ - cache::imap::mailbox::MailBox, error::BichonResult, store::tantivy::envelope::ENVELOPE_MANAGER, + cache::imap::mailbox::MailBox, + error::BichonResult, + store::tantivy::{attachment::ATTACHMENT_MANAGER, envelope::ENVELOPE_MANAGER}, }; pub async fn delete_mailbox_impl(account_id: u64, mailbox_id: u64) -> BichonResult<()> { @@ -45,5 +47,8 @@ pub async fn delete_mailbox_impl(account_id: u64, mailbox_id: u64) -> BichonResu ENVELOPE_MANAGER .delete_mailbox_envelopes(account_id, ids_to_delete.clone()) .await?; + ATTACHMENT_MANAGER + .delete_mailbox_attachments(account_id, ids_to_delete.clone()) + .await?; Ok(()) } diff --git a/src/modules/message/delete.rs b/src/modules/message/delete.rs index 6171521..f998fdd 100644 --- a/src/modules/message/delete.rs +++ b/src/modules/message/delete.rs @@ -17,9 +17,15 @@ // along with this program. If not, see . use crate::modules::error::BichonResult; +use crate::modules::store::tantivy::attachment::ATTACHMENT_MANAGER; use crate::modules::store::tantivy::envelope::ENVELOPE_MANAGER; use std::collections::HashMap; pub async fn delete_messages_impl(request: HashMap>) -> BichonResult<()> { - ENVELOPE_MANAGER.delete_envelopes_multi_account(request).await + ENVELOPE_MANAGER + .delete_envelopes_multi_account(request.clone()) + .await?; + ATTACHMENT_MANAGER + .delete_envelopes_multi_account(request) + .await } diff --git a/src/modules/message/search.rs b/src/modules/message/search.rs index 8121b01..85c82a9 100644 --- a/src/modules/message/search.rs +++ b/src/modules/message/search.rs @@ -38,6 +38,7 @@ use crate::{ pub struct EmailSearchFilter { pub text: Option, pub subject: Option, + pub id: Option, pub body: Option, pub from: Option, pub to: Option, @@ -111,6 +112,7 @@ pub async fn search_messages_impl( #[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, Object)] pub struct AttachmentSearchFilter { + pub id: Option, pub text: Option, pub subject: Option, pub from: Option, diff --git a/src/modules/store/tantivy/attachment.rs b/src/modules/store/tantivy/attachment.rs index 7472df7..e5ce6b8 100644 --- a/src/modules/store/tantivy/attachment.rs +++ b/src/modules/store/tantivy/attachment.rs @@ -343,13 +343,18 @@ impl IndexManager { subqueries.push((Occur::Must, Box::new(query))); } - if let Some(content_hash) = &filter.content_hash { let term = Term::from_field_text(f.f_content_hash, content_hash); let query = TermQuery::new(term, IndexRecordOption::Basic); subqueries.push((Occur::Must, Box::new(query))); } + if let Some(id) = &filter.id { + let term = Term::from_field_text(f.f_id, id); + let query = TermQuery::new(term, IndexRecordOption::Basic); + subqueries.push((Occur::Must, Box::new(query))); + } + if let Some(ref name) = filter.attachment_name { let query_parser = QueryParser::for_index(&self.index, vec![f.f_name_text, f.f_name_exact]); @@ -539,17 +544,24 @@ impl IndexManager { let attachment_docs: Vec<(Option, DocAddress)> = searcher .search( &query, - &TopDocs::with_limit(10).order_by_fast_field(F_SIZE, Order::Desc), + &TopDocs::with_limit(200).order_by_fast_field(F_SIZE, Order::Desc), ) .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; let mut result = Vec::new(); + let mut seen_hashes = std::collections::HashSet::new(); + for (_, doc_address) in attachment_docs { let doc: TantivyDocument = searcher .doc(doc_address) .map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?; let att = LargestAttachment::from_tantivy_doc(&doc)?; - result.push(att); + if seen_hashes.insert(att.content_hash.clone()) { + result.push(att); + } + if result.len() >= 10 { + break; + } } Ok(result) } @@ -566,7 +578,7 @@ impl IndexManager { Ok(()) } - pub async fn delete_mailbox_envelopes( + pub async fn delete_mailbox_attachments( &self, account_id: u64, mailbox_ids: Vec, diff --git a/src/modules/store/tantivy/envelope.rs b/src/modules/store/tantivy/envelope.rs index 695ca66..fd4e24f 100644 --- a/src/modules/store/tantivy/envelope.rs +++ b/src/modules/store/tantivy/envelope.rs @@ -390,6 +390,12 @@ impl IndexManager { } } + if let Some(ref id) = filter.id { + let term = Term::from_field_text(f.f_id, id); + let query = TermQuery::new(term, IndexRecordOption::Basic); + subqueries.push((Occur::Must, Box::new(query))); + } + if let Some(ref extension) = filter.attachment_extension { let term = Term::from_field_text(f.f_attachment_ext, extension); let query = TermQuery::new(term, IndexRecordOption::Basic); diff --git a/web/src/api/system/api.ts b/web/src/api/system/api.ts index e5ee845..9c06027 100644 --- a/web/src/api/system/api.ts +++ b/web/src/api/system/api.ts @@ -53,6 +53,7 @@ export interface DashboardStats { with_attachment_count: number; // Emails with attachments without_attachment_count: number; // Emails without attachments top_largest_emails: LargestEmail[]; // Top 10 largest emails + top_largest_attachments: LargestAttachment[]; // Top 10 largest attachments system_version: string, //The semantic version string of the currently running backend service commit_hash: string //Git commit hash used to build this system version } @@ -69,6 +70,7 @@ export const INITIAL_DASHBOARD_STATS: DashboardStats = { with_attachment_count: 0, without_attachment_count: 0, top_largest_emails: [], + top_largest_attachments: [], system_version: '0.0.0', commit_hash: 'n/a' }; @@ -87,6 +89,13 @@ export interface Group { export interface LargestEmail { subject: string; // Email subject size_bytes: number; // Email size in bytes + id: string +} + +export interface LargestAttachment { + name: string; // Attachment name + size_bytes: number; // Email size in bytes + id: String // attachment id } export interface Proxy { diff --git a/web/src/features/accounts/components/running-state-dialog.tsx b/web/src/features/accounts/components/running-state-dialog.tsx index 683464d..ca4028e 100644 --- a/web/src/features/accounts/components/running-state-dialog.tsx +++ b/web/src/features/accounts/components/running-state-dialog.tsx @@ -55,13 +55,14 @@ interface Props { } function StatusBadge({ status }: { status: string }) { + // 保持颜色逻辑,但在暗色模式下这些颜色也相对友好,如果需要完全适配可调整为 bg-primary/10 等 const map: Record = { - Running: 'bg-blue-100 text-blue-700', - Downloading: 'bg-blue-100 text-blue-700', - Success: 'bg-green-100 text-green-700', - Failed: 'bg-red-100 text-red-700', - Cancelled: 'bg-gray-100 text-gray-700', - Pending: 'bg-amber-100 text-amber-700', + Running: 'bg-blue-500/10 text-blue-600', + Downloading: 'bg-blue-500/10 text-blue-600', + Success: 'bg-green-500/10 text-green-600', + Failed: 'bg-red-500/10 text-red-600', + Cancelled: 'bg-muted text-muted-foreground', + Pending: 'bg-amber-500/10 text-amber-600', } return ( @@ -73,37 +74,36 @@ function StatusBadge({ status }: { status: string }) { function TriggerBadge({ trigger }: { trigger: string }) { const isScheduled = trigger === 'Scheduled' return ( - + {trigger} ) } - function FolderDetailItem({ f, t }: { f: FolderProgress, t: (key: string) => string }) { const percentage = Math.min(Math.round((f.current / f.planned) * 100), 100) || 0; return ( -
-
+
+
- + {f.folder_name}
-
+
- {percentage}% + {percentage}%
-
- {f.current} +
+ {f.current} / {f.planned}
@@ -116,11 +116,11 @@ function FolderDetailItem({ f, t }: { f: FolderProgress, t: (key: string) => str {f.message && (
-
- +
+
-

{t('accounts.runningState.message')}:

-

{f.message}

+

{t('accounts.runningState.message')}:

+

{f.message}

@@ -148,7 +148,7 @@ export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) {
- + {currentRow.email} @@ -159,27 +159,27 @@ export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) { -
+
- + {t('accounts.runningState.tabs.active_session')} - + {t('accounts.runningState.tabs.history')} {history.length} - + {t('accounts.runningState.tabs.global_errors')} {globalErrors.length > 0 && {globalErrors.length}}
-
+
{isLoading ? (
- -

{t('accounts.runningState.loading.fetching_account_state')}

+ +

{t('accounts.runningState.loading.fetching_account_state')}

) : ( <> @@ -194,29 +194,29 @@ export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) { ) : (
-
+

{t('accounts.runningState.session.status')}

-
+

{t('accounts.runningState.session.trigger')}

-
+

{t('accounts.runningState.session.started_at')}

-
+
{format(new Date(session.start_time), 'yyyy-MM-dd HH:mm:ss')}
- + {t('accounts.runningState.tabs.folders')} {t('accounts.runningState.tabs.errors')} ({session.errors?.length || 0}) @@ -228,12 +228,12 @@ export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) { {(!session.errors || session.errors.length === 0) ? ( -
+
{t('accounts.runningState.empty.no_errors_current')}
) : (
-
+
{[...session.errors] .sort((a, b) => new Date(b.at).getTime() - new Date(a.at).getTime()) @@ -242,38 +242,38 @@ export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) {
{ei === 0 ? ( - - + + ) : ( -
+
)}
{ei === 0 && ( - + {t('accounts.runningState.latest')} )} - + {format(new Date(err.at), 'yyyy-MM-dd HH:mm:ss')}
-

+

{err.error}

@@ -293,21 +293,21 @@ export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) {
{history.length === 0 ? ( -
{t('accounts.runningState.empty.no_history')}
+
{t('accounts.runningState.empty.no_history')}
) : ( {[...history].reverse().map((h, i) => ( - +
-
+
{format(new Date(h.start_time), 'yyyy-MM-dd HH:mm:ss')}
- + {Object.keys(h.folder_details).length} {t('accounts.runningState.folders')} · {Object.values(h.folder_details).reduce((sum, f) => sum + (f.current || 0), 0)} @@ -316,11 +316,11 @@ export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) {
- + - + {t('accounts.runningState.tabs.folders')} - + {t('accounts.runningState.tabs.errors')} ({h.errors?.length || 0}) @@ -331,12 +331,12 @@ export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) { {(!h.errors || h.errors.length === 0) ? ( -
+
{t('accounts.runningState.empty.no_errors_session')}
) : (
-
+
{[...h.errors] .sort((a, b) => new Date(b.at).getTime() - new Date(a.at).getTime()) @@ -345,38 +345,38 @@ export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) {
{ei === 0 ? ( - - + + ) : ( -
+
)}
{ei === 0 && ( - + {t('accounts.runningState.latest')} )} - + {format(new Date(err.at), 'yyyy-MM-dd HH:mm:ss')}
-

+

{err.error}

@@ -399,13 +399,13 @@ export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) {
{globalErrors.length === 0 ? ( -
+

{t('accounts.runningState.empty.no_global_errors')}

) : (
-
+
{[...globalErrors] .sort((a, b) => new Date(b.at).getTime() - new Date(a.at).getTime()) @@ -414,30 +414,30 @@ export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) {
{i === 0 ? ( - - + + ) : ( -
+
)}
{i === 0 && ( - + {t('accounts.runningState.latest')} )} -
+
{format(new Date(e.at), 'HH:mm')} @@ -448,7 +448,7 @@ export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) {
@@ -456,7 +456,7 @@ export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) {

{e.error} @@ -477,7 +477,7 @@ export function RunningStateDialog({ currentRow, open, onOpenChange }: Props) { - + diff --git a/web/src/features/attachment/filter-reset.tsx b/web/src/features/attachment/filter-reset.tsx index c0e70bf..368f74d 100644 --- a/web/src/features/attachment/filter-reset.tsx +++ b/web/src/features/attachment/filter-reset.tsx @@ -38,20 +38,20 @@ export function FilterResetButton() { return ( ); } \ No newline at end of file diff --git a/web/src/features/attachment/mail-list-table.tsx b/web/src/features/attachment/mail-list-table.tsx index 7023b86..97beb84 100644 --- a/web/src/features/attachment/mail-list-table.tsx +++ b/web/src/features/attachment/mail-list-table.tsx @@ -223,8 +223,8 @@ export function AttachmentListTable({ ) }, meta: { className: 'text-left text-xs' }, - minSize: 100, - maxSize: 100, + minSize: 130, + maxSize: 130, }, { id: 'actions', diff --git a/web/src/features/dashboard/index.tsx b/web/src/features/dashboard/index.tsx index 69b86ac..49e9d22 100644 --- a/web/src/features/dashboard/index.tsx +++ b/web/src/features/dashboard/index.tsx @@ -1,5 +1,5 @@ // -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) +// Copyright (c) 2025-2026 rustmailer.com[](https://rustmailer.com) // // This file is part of the Bichon Email Archiving Project // @@ -10,7 +10,6 @@ // import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Skeleton } from '@/components/ui/skeleton'; import { XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, BarChart, Bar } from 'recharts'; @@ -25,6 +24,7 @@ import { getToken } from '@/stores/authStore'; import { useNavigate } from '@tanstack/react-router'; import useMinimalAccountList from '@/hooks/use-minimal-account-list'; import { Badge } from '@/components/ui/badge'; +import LongText from '@/components/long-text'; interface DailyActivity { date: string; @@ -75,8 +75,6 @@ const formatTooltipDate = (timestamp_ms: number, locale: string): string => { }).format(date); }; -const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444']; - const MetricCardSkeleton = () => ( @@ -91,7 +89,7 @@ const MetricCardSkeleton = () => ( ); const EmptyChart = ({ title }: { title: string }) => ( -

+

{title}

@@ -157,13 +155,27 @@ export default function MailArchiveDashboard() { }); }; + + const handleQuickAttachmentSearch = (filter: Record) => { + navigate({ + to: '/attachment', + search: (prev: any) => ({ + page: 1, + pageSize: prev.pageSize ?? 50, + sortBy: prev.sortBy ?? "DATE", + sortOrder: prev.sortOrder ?? "desc", + q: JSON.stringify(filter), + }), + }); + }; + const attachmentData = totalAttachments > 0 ? [ - { name: 'With Attachments', value: attachmentRatio, fill: COLORS[1] }, - { name: 'No Attachments', value: 1 - attachmentRatio, fill: '#e5e7eb' }, + { name: 'With Attachments', value: attachmentRatio, fill: 'hsl(var(--primary))' }, + { name: 'No Attachments', value: 1 - attachmentRatio, fill: 'hsl(var(--muted))' }, ] : [ - { name: 'No Data', value: 1, fill: '#e5e7eb' }, + { name: 'No Data', value: 1, fill: 'hsl(var(--muted))' }, ]; if (isLoading) { @@ -172,7 +184,7 @@ export default function MailArchiveDashboard() {
{[...Array(4)].map((_, i) => )}
- +
); } @@ -182,37 +194,55 @@ export default function MailArchiveDashboard() {
+ {/* Top Metrics */}
- {t('dashboard.mailAccounts')} + {t('dashboard.mailAccounts')} -
{formatNumber(stats!.account_count)}
+
{formatNumber(stats!.account_count)}

{t('dashboard.connected')}

- + - {t('dashboard.totalEmails')} + {t('dashboard.totalEmails')} -
{formatNumber(stats!.email_count)}
+
{formatNumber(stats!.email_count)}

{t('dashboard.syncedLocally')}

- - + - - - {t('dashboard.efficiency', 'Efficiency')} + {t('dashboard.systemVersion')} + + +
+ {stats!.system_version ? ( + + {stats!.system_version} + + ) : 'N/A'} +
+
+ +

{stats!.commit_hash?.substring(0, 7) ?? 'N/A'}

+
+
+
+ + + + + {t('dashboard.efficiency')}
- {savingsPercent}% + {savingsPercent}% {t('dashboard.saved', 'Saved')}
@@ -250,198 +280,282 @@ export default function MailArchiveDashboard() {
- - - - {t('dashboard.systemVersion')} - Community +
+
+ + + {t('dashboard.newEmails')} + {t('dashboard.messageDistribution')} - -
- {stats!.system_version ? ( - - {stats!.system_version} - - ) : 'N/A'} -
-
- -

{stats!.commit_hash?.substring(0, 7) ?? 'N/A'}

-
+ + {hasRecentActivity ? ( + + + + + + formatNumber(v as number)} + content={({ payload }) => { + if (payload && payload.length) { + const dataPoint = payload[0].payload; + const fullDate = formatTooltipDate(dataPoint.timestamp_ms, currentLocale); + return ( +
+

{fullDate}

+

{t('dashboard.emails')}: {formatNumber(dataPoint.count)}

+
+ ); + } + return null; + }} + /> + +
+
+ ) : ( + + )} +
+
+ + + {t('dashboard.attachmentRatio')} + + {totalAttachments > 0 + ? t('dashboard.attachmentRatioDesc', { percent: (attachmentRatio * 100).toFixed(1) }) + : t('dashboard.noEmailsSynced')} + + + + + + + {attachmentData.map((entry, i) => ( + + ))} + + + totalAttachments > 0 ? `${((v as number) * 100).toFixed(1)}%` : '0%' + } + contentStyle={{ + fontSize: '12px', + padding: '4px 8px', + borderRadius: '6px', + }} + itemStyle={{ + fontSize: '12px', + }} + labelStyle={{ + fontSize: '12px', + }} /> + + + + +
+
+ + + {t('dashboard.top10Senders')} + + + {hasTopSenders ? ( + + + + {t('dashboard.sender')} + {t('dashboard.count')} + + + + {stats!.top_senders.map((s) => ( + + +
+
+
+ + + +
+
+ + {formatNumber(s.count)} + + ))} + +
+ ) : ( + + )} +
+
+ + + {t('dashboard.top10LargestEmails')} + + + {hasTopEmails ? ( + + + + {t('dashboard.subject')} + {t('dashboard.size')} + + + + {stats!.top_largest_emails.map((m, i) => ( + + +
+
+
+ + + +
+
+ + {formatBytes(m.size_bytes)} + + ))} + +
+ ) : ( + + )} +
+
+ + + {t('dashboard.top10Accounts')} + + + {hasTopAccounts ? ( + + + + {t('dashboard.account')} + {t('dashboard.emails')} + + + + {stats!.top_accounts.map((acc) => ( + + +
+
+
+ + + +
+
+ + {formatNumber(acc.count)} + + ))} + +
+ ) : ( + + )} +
+
+ + + + {t('dashboard.top10LargestAttachments')} + + + + {stats?.top_largest_attachments?.length ? ( + + + + Name + {t('dashboard.size')} + + + + {stats.top_largest_attachments.slice(0, 10).map((a, i) => ( + handleQuickAttachmentSearch({ id: a.id })} + > + +
+
+
+ + + +
+
+ + + {formatBytes(a.size_bytes)} + + + ))} + +
+ ) : ( + + )}
- - - - {t('dashboard.dayTrend')} - {t('dashboard.attachments')} - {t('dashboard.topLists')} - - - - - - {t('dashboard.newEmails')} - {t('dashboard.messageDistribution')} - - - {hasRecentActivity ? ( - - - - - - formatNumber(v as number)} - content={({ payload }) => { - if (payload && payload.length) { - const dataPoint = payload[0].payload; - const fullDate = formatTooltipDate(dataPoint.timestamp_ms, currentLocale); - return ( -
-

{fullDate}

-

{t('dashboard.emails')}: {formatNumber(dataPoint.count)}

-
- ); - } - return null; - }} - /> - -
-
- ) : ( - - )} -
-
-
- - - - - {t('dashboard.attachmentRatio')} - - {totalAttachments > 0 - ? t('dashboard.attachmentRatioDesc', { percent: (attachmentRatio * 100).toFixed(1) }) - : t('dashboard.noEmailsSynced')} - - - - - - - {attachmentData.map((entry, i) => ( - - ))} - - totalAttachments > 0 ? `${((v as number) * 100).toFixed(1)}%` : '0%'} /> - - - - - - - -
- - - {t('dashboard.top10Senders')} - - - {hasTopSenders ? ( - - - - {t('dashboard.sender')} - {t('dashboard.count')} - - - - {stats!.top_senders.map((s) => ( - handleQuickSearch({ from: s.key })}> - {s.key} - {formatNumber(s.count)} - - ))} - -
- ) : ( - - )} -
-
- - - - {t('dashboard.top10LargestEmails')} - - - {hasTopEmails ? ( - - - - {t('dashboard.subject')} - {t('dashboard.size')} - - - - {stats!.top_largest_emails.map((m, i) => ( - handleQuickSearch({ subject: m.subject })}> - {m.subject || t('dashboard.noSubject')} - {formatBytes(m.size_bytes)} - - ))} - -
- ) : ( - - )} -
-
- - - - {t('dashboard.top10Accounts')} - - - {hasTopAccounts ? ( - - - - {t('dashboard.account')} - {t('dashboard.emails')} - - - - {stats!.top_accounts.map((acc) => ( - handleQuickSearch({ account_ids: [getAccountIdByEmail(acc.key) || 0] })}> - {acc.key} - {formatNumber(acc.count)} - - ))} - -
- ) : ( - - )} -
-
-
-
-
+
© 2025-2026 rustmailer.com - Bichon Email Archiving Project
diff --git a/web/src/features/search/filter-reset.tsx b/web/src/features/search/filter-reset.tsx index da8aec2..ffe73f1 100644 --- a/web/src/features/search/filter-reset.tsx +++ b/web/src/features/search/filter-reset.tsx @@ -38,20 +38,20 @@ export function FilterResetButton() { return ( ); } \ No newline at end of file diff --git a/web/src/features/search/mail-list-table.tsx b/web/src/features/search/mail-list-table.tsx index 4386edc..0c85421 100755 --- a/web/src/features/search/mail-list-table.tsx +++ b/web/src/features/search/mail-list-table.tsx @@ -249,8 +249,8 @@ export function MailListTable({ ) }, meta: { className: 'text-left text-xs' }, - minSize: 100, - maxSize: 100, + minSize: 130, + maxSize: 130, }, { id: 'actions', diff --git a/web/src/index.css b/web/src/index.css index 1d71aa4..233f4d3 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -5,76 +5,101 @@ @layer base { :root { - --background: 271 8% 98%; - --foreground: 271 56% 6%; - --muted: 271 8% 92%; - --muted-foreground: 271 10% 35%; - --popover: 271 8% 98%; - --popover-foreground: 271 56% 6%; - --card: 271 8% 97%; - --card-foreground: 271 56% 5%; - --border: 271 6% 88%; - --input: 271 6% 88%; - --primary: 271 60% 66%; - --primary-foreground: 0 0% 0%; - --secondary: 91 55% 58%; - --secondary-foreground: 91 50% 10%; - --accent: 91 55% 58%; - --accent-foreground: 91 50% 10%; - --destructive: 17 85% 48%; - --destructive-foreground: 0 0% 100%; - --ring: 271 60% 66%; - --chart-1: 271 60% 66%; - --chart-2: 91 55% 58%; - --chart-3: 91 55% 58%; - --chart-4: 91 55% 62%; - --chart-5: 271 63% 66%; - --radius: 0.5rem; - /* Sidebar */ - --sidebar-background: 271 8% 98%; - --sidebar-foreground: 271 56% 6%; - --sidebar-primary: 271 60% 66%; - --sidebar-primary-foreground: 0 0% 0%; - --sidebar-accent: 91 55% 58%; - --sidebar-accent-foreground: 91 50% 10%; - --sidebar-border: 271 6% 88%; - --sidebar-ring: 271 60% 66%; - } - .dark { - --background: 271 20% 10%; - --foreground: 271 10% 96%; - --muted: 271 10% 14%; - --muted-foreground: 271 10% 65%; - --popover: 271 20% 10%; - --popover-foreground: 271 10% 96%; - --card: 271 20% 13%; - --card-foreground: 0 0% 100%; - --border: 271 12% 22%; - --input: 271 12% 22%; - --primary: 271 50% 66%; - --primary-foreground: 0 0% 0%; - --secondary: 91 45% 58%; - --secondary-foreground: 91 40% 10%; - --accent: 91 45% 58%; - --accent-foreground: 91 40% 10%; - --destructive: 17 85% 55%; - --destructive-foreground: 0 0% 100%; - --ring: 271 50% 66%; - --chart-1: 271 50% 66%; - --chart-2: 91 45% 58%; - --chart-3: 91 45% 58%; - --chart-4: 91 45% 62%; - --chart-5: 271 55% 66%; + /* 亮色模式:保持清爽,略微加深边框对比度 */ + --background: 0 0% 100%; + --foreground: 215 28% 17%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215 16% 47%; + + --popover: 0 0% 100%; + --popover-foreground: 215 28% 17%; + --card: 0 0% 100%; + --card-foreground: 215 28% 17%; + + /* 提升亮色下的边框清晰度 */ + --border: 214 32% 88%; + --input: 214 32% 88%; + + --primary: 221 83% 53%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222 47% 11%; + --accent: 210 40% 96.1%; + --accent-foreground: 222 47% 11%; + + --destructive: 0 84% 60%; + --destructive-foreground: 210 40% 98%; + + --ring: 221 83% 53%; + --radius: 0.35rem; - /* Sidebar */ - --sidebar-background: 271 20% 10%; - --sidebar-foreground: 271 10% 96%; - --sidebar-primary: 271 50% 66%; - --sidebar-primary-foreground: 0 0% 0%; - --sidebar-accent: 91 45% 58%; - --sidebar-accent-foreground: 91 40% 10%; - --sidebar-border: 271 12% 22%; - --sidebar-ring: 271 50% 66%; + --chart-1: 221 83% 53%; + --chart-2: 200 90% 40%; + --chart-3: 35 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + + --sidebar-background: 210 40% 98%; + --sidebar-foreground: 215 28% 17%; + --sidebar-primary: 221 83% 53%; + --sidebar-primary-foreground: 210 40% 98%; + --sidebar-accent: 214 32% 91%; + --sidebar-accent-foreground: 215 28% 17%; + --sidebar-border: 214 32% 88%; + --sidebar-ring: 221 83% 53%; + } + + .dark { + /* 背景:采用质感深灰 (Slate 900级别) 而不是纯黑 */ + --background: 215 20% 12%; + --foreground: 210 40% 98%; /* 更明亮的纯白主文本 */ + + /* 卡片和悬浮层:比背景明显提亮一个色阶 (Slate 800级别),形成物理层次感 */ + --card: 215 20% 16%; + --card-foreground: 210 40% 98%; + --popover: 215 20% 16%; + --popover-foreground: 210 40% 98%; + + /* 弱化背景:用于 Table 表头、Hover 状态,再次提亮 */ + --muted: 215 20% 22%; + /* 弱化文字:明度提高到 70%,确保在深色背景上不费眼 */ + --muted-foreground: 215 15% 70%; + + /* 边框和输入框:使用高光轮廓线,对比度极强,切分区域极其清晰 */ + --border: 215 20% 28%; + --input: 215 20% 28%; + + /* 主色调:采用穿透力更强的高明度冰蓝色 */ + --primary: 217 90% 70%; + --primary-foreground: 215 20% 12%; + + --secondary: 215 20% 22%; + --secondary-foreground: 210 40% 98%; + --accent: 215 20% 22%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 70% 60%; + --destructive-foreground: 210 40% 98%; + + --ring: 217 90% 70%; + + --chart-1: 217 90% 70%; + --chart-2: 200 90% 60%; + --chart-3: 35 90% 65%; + --chart-4: 280 75% 75%; + --chart-5: 340 85% 70%; + + /* 侧边栏:略微比主区域深一点点,起到画框包裹的视觉效果 */ + --sidebar-background: 215 20% 10%; + --sidebar-foreground: 210 40% 98%; + --sidebar-primary: 217 90% 70%; + --sidebar-primary-foreground: 215 20% 12%; + --sidebar-accent: 215 20% 22%; + --sidebar-accent-foreground: 210 40% 98%; + --sidebar-border: 215 20% 28%; + --sidebar-ring: 217 90% 70%; } diff --git a/web/src/locales/ar.json b/web/src/locales/ar.json index b17a23b..81b0f38 100644 --- a/web/src/locales/ar.json +++ b/web/src/locales/ar.json @@ -485,6 +485,7 @@ "tantivyDatabase": "تعتمد على محرك البحث Tantivy", "title": "لوحة التحكم", "top10Accounts": "أفضل 10 حسابات", + "top10LargestAttachments": "أكبر 10 مرفقات", "top10LargestEmails": "أكبر 10 رسائل بريد إلكتروني", "top10Senders": "أفضل 10 مرسلين", "topLists": "القوائم الأعلى", diff --git a/web/src/locales/da.json b/web/src/locales/da.json index 015fa8e..8899385 100644 --- a/web/src/locales/da.json +++ b/web/src/locales/da.json @@ -485,6 +485,7 @@ "tantivyDatabase": "Baseret på Tantivy-søgemaskine", "title": "Oversigt", "top10Accounts": "Top 10 Konti", + "top10LargestAttachments": "Top 10 største vedhæftede filer", "top10LargestEmails": "Top 10 Største E-mails", "top10Senders": "Top 10 Afsendere", "topLists": "Toplister", diff --git a/web/src/locales/de.json b/web/src/locales/de.json index 12aac9e..2c00d39 100644 --- a/web/src/locales/de.json +++ b/web/src/locales/de.json @@ -485,6 +485,7 @@ "tantivyDatabase": "Basiert auf der Tantivy-Suchmaschine", "title": "Dashboard", "top10Accounts": "Top 10 Konten", + "top10LargestAttachments": "Top 10 der größten Anhänge", "top10LargestEmails": "Top 10 größte E-Mails", "top10Senders": "Top 10 Absender", "topLists": "Top-Listen", diff --git a/web/src/locales/en.json b/web/src/locales/en.json index c5b51c0..461d930 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -485,6 +485,7 @@ "tantivyDatabase": "Based on Tantivy Search Engine", "title": "Dashboard", "top10Accounts": "Top 10 Accounts", + "top10LargestAttachments": "Top 10 largest attachments", "top10LargestEmails": "Top 10 Largest Emails", "top10Senders": "Top 10 Senders", "topLists": "Top Lists", diff --git a/web/src/locales/es.json b/web/src/locales/es.json index 11f345d..c5b5514 100644 --- a/web/src/locales/es.json +++ b/web/src/locales/es.json @@ -485,6 +485,7 @@ "tantivyDatabase": "Basado en el motor de búsqueda Tantivy", "title": "Panel de control", "top10Accounts": "Las 10 principales cuentas", + "top10LargestAttachments": "Top 10 archivos adjuntos más grandes", "top10LargestEmails": "Los 10 correos más grandes", "top10Senders": "Los 10 principales remitentes", "topLists": "Listas principales", diff --git a/web/src/locales/fi.json b/web/src/locales/fi.json index 01cefeb..6a10d9e 100644 --- a/web/src/locales/fi.json +++ b/web/src/locales/fi.json @@ -485,6 +485,7 @@ "tantivyDatabase": "Perustuu Tantivy-hakukoneeseen", "title": "Kojelauta", "top10Accounts": "10 parasta tiliä", + "top10LargestAttachments": "10 suurinta liitetiedostoa", "top10LargestEmails": "10 suurinta sähköpostia", "top10Senders": "10 parasta lähettäjää", "topLists": "Parhaat listat", diff --git a/web/src/locales/fr.json b/web/src/locales/fr.json index 13b3f3e..7b35840 100644 --- a/web/src/locales/fr.json +++ b/web/src/locales/fr.json @@ -485,6 +485,7 @@ "tantivyDatabase": "Basé sur le moteur de recherche Tantivy", "title": "Tableau de bord", "top10Accounts": "Top 10 des Comptes", + "top10LargestAttachments": "Top 10 des plus grosses pièces jointes", "top10LargestEmails": "Top 10 des E-mails les Plus Volumineux", "top10Senders": "Top 10 des Expéditeurs", "topLists": "Top Listes", diff --git a/web/src/locales/it.json b/web/src/locales/it.json index c7ea973..eaa593e 100644 --- a/web/src/locales/it.json +++ b/web/src/locales/it.json @@ -485,6 +485,7 @@ "tantivyDatabase": "Basato sul motore di ricerca Tantivy", "title": "Dashboard", "top10Accounts": "I 10 Account Principali", + "top10LargestAttachments": "Top 10 allegati più grandi", "top10LargestEmails": "Le 10 Email più Grandi", "top10Senders": "I 10 Mittenti Principali", "topLists": "Liste Top", diff --git a/web/src/locales/jp.json b/web/src/locales/jp.json index 0e867fc..09e601c 100644 --- a/web/src/locales/jp.json +++ b/web/src/locales/jp.json @@ -485,6 +485,7 @@ "tantivyDatabase": "Tantivy 検索エンジンを搭載", "title": "ダッシュボード", "top10Accounts": "アカウントトップ10", + "top10LargestAttachments": "添付ファイルトップ10", "top10LargestEmails": "容量の大きいメールトップ10", "top10Senders": "送信者トップ10", "topLists": "トップリスト", diff --git a/web/src/locales/ko.json b/web/src/locales/ko.json index 636966c..ef784bb 100644 --- a/web/src/locales/ko.json +++ b/web/src/locales/ko.json @@ -485,6 +485,7 @@ "tantivyDatabase": "Tantivy 검색 엔진 기반", "title": "대시보드", "top10Accounts": "상위 10개 계정", + "top10LargestAttachments": "가장 큰 첨부 파일 10개", "top10LargestEmails": "상위 10개 최대 크기 이메일", "top10Senders": "상위 10명 발신자", "topLists": "상위 목록", diff --git a/web/src/locales/nl.json b/web/src/locales/nl.json index 6d291d4..29ed239 100644 --- a/web/src/locales/nl.json +++ b/web/src/locales/nl.json @@ -485,6 +485,7 @@ "tantivyDatabase": "Gebaseerd op de zoekmachine Tantivy", "title": "Dashboard", "top10Accounts": "Top 10 Accounts", + "top10LargestAttachments": "Top 10 grootste bijlagen", "top10LargestEmails": "Top 10 Grootste e-mails", "top10Senders": "Top 10 Afzenders", "topLists": "Toplijsten", diff --git a/web/src/locales/no.json b/web/src/locales/no.json index 2094bc3..906b112 100644 --- a/web/src/locales/no.json +++ b/web/src/locales/no.json @@ -485,6 +485,7 @@ "tantivyDatabase": "Basert på søkemotoren Tantivy", "title": "Oversikt", "top10Accounts": "Topp 10 kontoer", + "top10LargestAttachments": "Topp 10 største vedlegg", "top10LargestEmails": "Topp 10 største e-poster", "top10Senders": "Topp 10 avsendere", "topLists": "Topplister", diff --git a/web/src/locales/pl.json b/web/src/locales/pl.json index 151463f..80e37c7 100644 --- a/web/src/locales/pl.json +++ b/web/src/locales/pl.json @@ -485,6 +485,7 @@ "tantivyDatabase": "Oparte na silniku wyszukiwania Tantivy", "title": "Panel", "top10Accounts": "Top 10 kont", + "top10LargestAttachments": "10 największych załączników", "top10LargestEmails": "Top 10 największych wiadomości", "top10Senders": "Top 10 Nadawców", "topLists": "Lista TOP", diff --git a/web/src/locales/pt.json b/web/src/locales/pt.json index 1244c79..e1e4715 100644 --- a/web/src/locales/pt.json +++ b/web/src/locales/pt.json @@ -485,6 +485,7 @@ "tantivyDatabase": "Baseado no motor de busca Tantivy", "title": "Painel", "top10Accounts": "Top 10 Contas", + "top10LargestAttachments": "Top 10 maiores anexos", "top10LargestEmails": "Top 10 Maiores Emails", "top10Senders": "Top 10 Remetentes", "topLists": "Principais Listas", diff --git a/web/src/locales/ru.json b/web/src/locales/ru.json index d008d96..c82947b 100644 --- a/web/src/locales/ru.json +++ b/web/src/locales/ru.json @@ -485,6 +485,7 @@ "tantivyDatabase": "На базе поискового движка Tantivy", "title": "Дашборд", "top10Accounts": "Топ 10 аккаунтов", + "top10LargestAttachments": "10 самых больших вложений", "top10LargestEmails": "Топ 10 самых больших писем", "top10Senders": "Топ 10 отправителей", "topLists": "Топ списки", diff --git a/web/src/locales/sv.json b/web/src/locales/sv.json index f544e52..1aabf5e 100644 --- a/web/src/locales/sv.json +++ b/web/src/locales/sv.json @@ -485,6 +485,7 @@ "tantivyDatabase": "Baserat på sökmotorn Tantivy", "title": "Översikt", "top10Accounts": "Topp 10 konton", + "top10LargestAttachments": "Topp 10 största bilagorna", "top10LargestEmails": "Topp 10 största e-postmeddelanden", "top10Senders": "Topp 10 avsändare", "topLists": "Topplistor", diff --git a/web/src/locales/zh-tw.json b/web/src/locales/zh-tw.json index 75d0b30..628c734 100644 --- a/web/src/locales/zh-tw.json +++ b/web/src/locales/zh-tw.json @@ -485,6 +485,7 @@ "tantivyDatabase": "基于 Tantivy 搜索引擎", "title": "儀表板", "top10Accounts": "前 10 名帳號", + "top10LargestAttachments": "前10個最大附件", "top10LargestEmails": "前 10 封最大郵件", "top10Senders": "前 10 名寄件人", "topLists": "排行榜", diff --git a/web/src/locales/zh.json b/web/src/locales/zh.json index a95ab39..52d36d9 100644 --- a/web/src/locales/zh.json +++ b/web/src/locales/zh.json @@ -485,6 +485,7 @@ "tantivyDatabase": "基於 Tantivy 搜尋引擎", "title": "仪表板", "top10Accounts": "前10名账户", + "top10LargestAttachments": "前10个最大附件", "top10LargestEmails": "前10大邮件", "top10Senders": "前10名发件人", "topLists": "排行榜",