From 0eb069fa3754ac930b4f93f930a46fbee6aa50f3 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Mon, 2 Nov 2026 21:46:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(app-iced):=20M95=20r4=20=E2=80=94=E2=80=94?= =?UTF-8?q?=20=E5=BD=92=E5=B1=9E=E5=88=A4=E6=8D=AE=E5=9B=9E=E5=BD=92=20/?= =?UTF-8?q?=20=E7=BD=AE=E4=BF=A1=E5=BA=A6=E6=A1=A3=20/=20=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E4=B8=AD=E9=A1=B5=E5=A4=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 代码复审 r3 的四条 p2,修三条、登记一条(P2-4 页脚余量 13px,后置)。 **P2-1(回归修复)**:r3 把归属判据写成「有选中就只认选中」,把**导入崩溃文件**与 `open_session` 这两条路径的诊断一并关掉了——`559ba20` 是**无条件**画的,所以这是 r3 引入的**代码级回归**, 不是既有缺口(今天 iced 侧没有 dispatch 站点,那些还在遗留的 Slint shell 里,故复审未判 p1)。 判据改成三分支(`app/view/logs.rs`): | live_session | selected_session | 报告算数的条件 | |---|---|---| | Some(s) | 任意 | `report.session == s`(只有它在线时日志行是它在追加,别人的报告挂上来就是串场) | | None | Some(s) | `report.session == s` **或** 报告不属于任何已知会话(导入用 `file:<文件名>`,永不在索引里) | | None | None | 全部算数(此时日志行只可能来自 `open_session`/导入,与报告同一次调用发出) | 新增两条测试(都走真实 reducer):`an_imported_crash_file_still_shows_its_findings`、 `a_live_run_says_it_is_running_instead_of_nothing_runs`。 残留(今天不可达,已登记):`open_session(A)` 在 `selected_session` 仍停在上场 B 时会被挡掉—— 真要修得让 presenter 在 `LogLinesChanged` 记下「日志行属于哪一场」。 **P2-2 置信度档(视觉变更)**:`Severity`(借 core 的 `critical/warning/notice` 词汇)→ `Confidence`(`High/Medium/Low`)+ 新 key `LogsConfidenceHigh/Medium/Low`(中英齐,标题明确写 「高/中/低置信度」)。阈值仍是 80/50,但它现在是**档位定义**而不是「严重度代理」。 摘要里的 `findings=rule:high(85)` 同步换档位 slug。DTO 补齐 `severity` 之后应换回真严重度(finding)。 **P2-3 运行中的页头**:session 索引只在**结束**时追加,运行中的那一场不在 `log_sessions` 里 ⇒ 拿不到实例名与结局 ⇒ r3 之前页头在日志滚动时一直说「当前没有运行中的实例」(demo 伪造了身份, 截图照不出来)。`Logs` 新增 `live: bool`(`live_session.is_some()`),`with_status` 三级回退: 有实例+结局 → `实例 · 时间 · 结局`;运行中但没结局 → 新 key `LogsSubtitleLive`(「Running now — logs stream as they arrive」);都没有 → `LogsSubtitleIdle`。 新增 `a_live_run_says_it_is_running_instead_of_nothing_runs`(真实 reducer:只有 `LogAppended(running=true)`,断言副标题不是 Idle 文案且含 `Running now`)。 新增 demo 状态 `ULL_LOGS_FIXTURE=running` 用于出图。 **变异实测**(每条:回退 → 红 → 放回 → 绿):见报告。 验证:`fmt --check` 通过;`clippy -p unlucky-app-iced -p unlucky-i18n --all-targets -D warnings` 干净; `cargo test` **280 + 6 + 23 passed / 0 failed**;行数最大 `pages/mod.rs` 367、`app/view.rs` 324、 `app/view/logs.rs` 337。 --- crates/app-iced/src/app/view/logs.rs | 234 ++++-------------- crates/app-iced/src/app/view/logs/tests.rs | 242 +++++++++++++++++++ crates/app-iced/src/pages/logs.rs | 6 +- crates/app-iced/src/pages/logs/demo.rs | 22 +- crates/app-iced/src/pages/logs/diagnostic.rs | 32 +-- crates/app-iced/src/pages/logs/model.rs | 86 ++++--- crates/app-iced/src/pages/logs/tests.rs | 50 ++-- crates/app-iced/src/pages/mod.rs | 2 +- crates/i18n/src/catalog.rs | 7 +- 9 files changed, 414 insertions(+), 267 deletions(-) create mode 100644 crates/app-iced/src/app/view/logs/tests.rs diff --git a/crates/app-iced/src/app/view/logs.rs b/crates/app-iced/src/app/view/logs.rs index 7c13e1b..136c635 100644 --- a/crates/app-iced/src/app/view/logs.rs +++ b/crates/app-iced/src/app/view/logs.rs @@ -19,16 +19,28 @@ //! | 概念 | 取值 | //! |---|---| //! | **身份**(这一场是谁) | `live_session`(正在跑)→ `selected_session`(打开/刚结束的),**不从 `diagnosis` 反推** | -//! | 诊断结论 | 只有 `diagnosis.session == 身份` 才画;身份为空时才信任报告(那时日志行只可能来自 `open_session`,它和报告是同一次调用发的) | +//! | 诊断结论 | 见下表的三个分支(r4 修正了 r3 的一条**回归**:r3 只认身份,把「导入崩溃文件」与「打开会话」两条路径的诊断一并关掉了) | //! | 实例/时间/结局(给页头描述) | 身份优先;身份为空才退到报告的 session | //! +//! 归属判据(`report.session` 与身份的关系): +//! +//! | `live_session` | `selected_session` | 报告算数的条件 | +//! |---|---|---| +//! | `Some(live)` | 任意 | `report.session == live` —— 只有它在线时日志行是它自己在追加,别人的报告挂上来就是串场 | +//! | `None` | `Some(sel)` | `report.session == sel` **或** 报告不属于任何已知会话(`ImportCrashLog` 用 `file:<文件名>` 当会话 id,永远不在索引里) | +//! | `None` | `None` | 全部算数 —— 此时日志行只可能来自 `open_session` 或导入,两者都与报告同一次调用发出 | +//! +//! 已知残留(今天 iced 侧不可达,登记在报告里):`open_session(A)` 在 `selected_session` 仍停在上 +//! 一场 B 时会被第三行判据挡掉(A 是已知会话且 ≠ B)——真正要修得让 presenter 在 +//! `LogLinesChanged` 时把「日志行属于哪一场」记下来(`AppState` 今天没有这个字段)。 +//! //! 「没有命中规则」的**占位报告**也不画:host 的 `open_session` 在规则引擎无结论时会发一份 //! `rule: ""` / `confidence: 0` / `title: "该会话没有命中已知规则"` 的假报告(`CoreEvent::DiagnosisReady` //! 是非 Option,host 无法表达「没有结论」)——它是标记不是结论,画出来等于给用户看一条「严重度提示」。 //! //! 已知缺口(已报 finding,不在本 mission 修):`clear_logs` 之后 `selected_session` 不清, -//! 身份会继续指向那一场;导入崩溃文件(`session: "file:…"`)没有对应的日志行载入,所以那份 -//! 报告在本页永远不会显示(r2 会把它挂到不相干的日志行上——两害相权取不显示)。 +//! 身份会继续指向那一场;导入崩溃文件只发 `DiagnosisReady`(不带日志行),所以页面上会出现 +//! 「结论 + 上一场的日志正文」——行由 `open_session` 负责,那是 presenter 的缺口。 use unlucky_presenter::dto::{Diagnosis, LogLevel, LogOutcome}; use unlucky_presenter::state::AppState; @@ -38,16 +50,34 @@ use crate::pages; /// 日志页视图模型:presenter 快照 → 要画的东西。 /// /// 两处 DTO 侧的**信息丢失**(已报 finding):`dto::Diagnosis` 没有 core 的 `severity` -/// (严重度只能按置信度折算,阈值是债)与 `cause.key`(`title`/`hint` 只能当数据画)。 +/// (所以页面按置信度档分组,不冒称严重度)与 `cause.key`(`title`/`hint` 只能当数据画)。 pub(super) fn logs_model(state: &AppState) -> pages::Logs { - let identity = state - .live_session - .as_deref() - .or(state.selected_session.as_deref()); + let live = state.live_session.as_deref(); + let selected = state.selected_session.as_deref(); + let identity = live.or(selected); - let report = state.diagnosis.as_ref().filter(|report| match identity { - Some(id) => report.session == id, - None => true, + // 归属判据(r4 修正 r3 的**回归**:r3 的「有选中就只认选中」把导入/打开这两条路径的诊断 + // 一起关掉了)。三条命中任一即认: + // + // 1. 报告属于**正在跑**的那一场 —— 这一条必须单独成立:只有它在线时日志行是它自己在追加, + // 别的会话的报告挂上来就是串场(r3 修的正是这个); + // 2. 报告属于**当前选中**的那一场(没有 live 会话时); + // 3. 报告的会话**不在会话索引里** —— `ImportCrashLog` 用 `file:<文件名>` 当会话 id, + // `open_session` 也不一定把 id 写进 `selected_session`;这两条路径与报告是同一次调用发出的, + // 把它们一并关掉就是把「导入崩溃日志」这条功能画成空(r3 的实测回归)。 + let known = |id: &str| state.log_sessions.iter().any(|session| session.id == id); + let report = state.diagnosis.as_ref().filter(|report| { + let id = report.session.as_str(); + match (live, selected) { + // 有会话在跑:日志行是它自己在追加,别的会话的报告挂上来就是串场(r3 修的 P1)。 + (Some(live), _) => id == live, + // 没有在跑、但有选中项:报告要么是它的,要么是「会话索引里没有的那一份」 + // (`ImportCrashLog` 用 `file:<文件名>`;`open_session` 不写 `selected_session`, + // 所以只能靠「不在索引里」认它)。 + (None, Some(selected)) => id == selected || !known(id), + // 什么身份都没有:日志行只可能来自 `open_session` 或导入,两者与报告同一次发出。 + (None, None) => true, + } }); // 身份为空时,描述的来源只能退到报告(见模块文档的表格)。 @@ -59,6 +89,9 @@ pub(super) fn logs_model(state: &AppState) -> pages::Logs { instance: session.map(|session| session.instance.clone()), when: session.map(|session| session.when.clone()), outcome: session.map(|session| log_outcome(session.outcome)), + // 运行中的会话不在 `log_sessions` 里(索引只在结束时追加),所以页头要靠这个标记 + // 走「正在运行」那条文案,而不是「当前没有运行中的实例」。 + live: live.is_some(), lines: state .log_lines .iter() @@ -95,11 +128,11 @@ fn log_outcome(outcome: LogOutcome) -> pages::Outcome { } } -/// 一条诊断结论 → 页面结论(严重度折算见 [`logs_model`] 的文档)。 +/// 一条诊断结论 → 页面结论(置信度档见 [`pages::Confidence`])。 fn diagnosis_of(diagnosis: &Diagnosis) -> pages::Finding { pages::Finding { rule: diagnosis.rule.clone(), - severity: pages::Severity::of_confidence(diagnosis.confidence), + confidence_band: pages::Confidence::of(diagnosis.confidence), title: diagnosis.title.clone(), hint: diagnosis.hint.clone(), confidence: diagnosis.confidence, @@ -108,177 +141,4 @@ fn diagnosis_of(diagnosis: &Diagnosis) -> pages::Finding { } #[cfg(test)] -mod tests { - use unlucky_i18n::Lang; - use unlucky_presenter::dto::{ - CoreEvent, DiagnosisReport, LaunchFinished, LiveLogBatch, LogLevel, LogLine, LogOutcome, - LogSession, - }; - use unlucky_presenter::state::AppState; - - use crate::pages; - - use super::logs_model; - - /// 崩溃的那一场。 - const CRASH: &str = "s-1029-1840"; - /// 崩溃之后又启动的、正常跑的一场。 - const OK_RUN: &str = "s-1029-1922"; - - fn session(id: &str, when: &str, outcome: LogOutcome) -> LogSession { - LogSession { - id: id.to_string(), - instance: "开发测试".to_string(), - when: when.to_string(), - outcome, - duration_secs: 62, - } - } - - fn line(text: &str, level: LogLevel) -> LogLine { - LogLine { - text: text.to_string(), - level, - } - } - - fn report(session: &str, rule: &str) -> DiagnosisReport { - DiagnosisReport { - session: session.to_string(), - diagnosis: pages_dto(rule), - } - } - - fn pages_dto(rule: &str) -> unlucky_presenter::dto::Diagnosis { - unlucky_presenter::dto::Diagnosis { - rule: rule.to_string(), - title: "显卡驱动过旧(OpenGL 版本不足)".to_string(), - confidence: 92, - hint: "升级显卡驱动".to_string(), - fix: Some("change_graphics.driver".to_string()), - } - } - - /// **真实数据路径**(复审要求:不许造注入源绕过去): - /// 崩溃一场跑完 → 再启动一场正常跑。`state.diagnosis` 在这里**故意不清**, - /// 因为 presenter 里根本没有清零点——这就是线上发生串场的条件。 - fn crashed_only() -> AppState { - let mut state = AppState::new(); - state.apply_core(CoreEvent::LogSessionsChanged(vec![ - session(CRASH, "10-29 18:40", LogOutcome::Crashed), - session(OK_RUN, "10-29 19:22", LogOutcome::Ok), - ])); - - // 第一场跑起来(live_session = CRASH)→ 日志 → 规则结论 → 收尾(selected_session = CRASH) - state.apply_core(CoreEvent::LogAppended(LiveLogBatch { - session: CRASH.to_string(), - lines: vec![line("[18:40] boom: GLFW error", LogLevel::Error)], - running: true, - })); - state.apply_core(CoreEvent::DiagnosisReady(report(CRASH, "gpu.driver_old"))); - state.apply_core(CoreEvent::LaunchFinished(LaunchFinished { - id: "dev".to_string(), - session: CRASH.to_string(), - outcome: LogOutcome::Crashed, - summary: "崩溃(exit 1)".to_string(), - })); - - state - } - - /// 崩溃之后又启动一场正常跑:`live_session` 前进,`diagnosis` 仍是上一场的。 - fn after_a_new_run() -> AppState { - let mut state = crashed_only(); - state.apply_core(CoreEvent::LogAppended(LiveLogBatch { - session: OK_RUN.to_string(), - lines: vec![line("[19:22] launching 1.20.4", LogLevel::Info)], - running: true, - })); - state - } - - #[test] - fn a_new_run_does_not_inherit_the_previous_crash() { - let state = after_a_new_run(); - let logs = logs_model(&state).with_status(Lang::En); - - // 抓得住:身份被上一场的 `diagnosis.session` 顶掉(r2 的 bug)——那时 outcome 会是 - // Crashed、findings 挂着 gpu.driver_old、摘要里 `lines=` 与结论不同源。 - // 抓不住:presenter 自己 `clear_logs` 后 `selected_session` 不清的缺口(已报 finding), - // 以及多场并行会话(本仓一次只跑一场)。 - assert_eq!( - logs.outcome, - Some(pages::Outcome::Ok), - "身份应该是正在跑的这一场" - ); - - let status = logs.status.clone().expect("有会话身份就该有副标题"); - assert!(status.contains("Finished normally"), "{status}"); - assert!( - !status.contains("The game crashed"), - "页头把新会话标成了上一场的崩溃:{status}" - ); - - assert!( - logs.findings.is_empty(), - "上一场的结论不能挂到这一场:{:?}", - logs.findings - ); - - // 摘要必须与身份同源:outcome 是 ok、不含旧规则、行数就是当前这些行。 - let summary = logs.summary(); - assert!(summary.contains("outcome=ok"), "{summary}"); - assert!(!summary.contains("gpu.driver_old"), "{summary}"); - assert!( - summary.contains(&format!("lines={}", logs.lines.len())), - "{summary}" - ); - assert!(!summary.contains("findings="), "{summary}"); - } - - #[test] - fn the_crash_itself_is_still_reported() { - // 上一条测试的**对照**:崩溃当场必须画结论,否则那条断言「findings 为空」是空的。 - // 抓得住:把判别写成「永远不画诊断」(用空断言骗过 P2)。 - // 抓不住:结论排序/去重(那是 core 的规则引擎的事)。 - let state = crashed_only(); - let logs = logs_model(&state).with_status(Lang::En); - - assert_eq!(logs.outcome, Some(pages::Outcome::Crashed)); - assert_eq!(logs.findings.len(), 1, "{:?}", logs.findings); - assert_eq!(logs.findings[0].rule, "gpu.driver_old"); - let status = logs.status.clone().unwrap_or_default(); - assert!(status.contains("The game crashed"), "{status}"); - assert!(logs.summary().contains("gpu.driver_old:critical(92)")); - } - - #[test] - fn the_hosts_no_rule_matched_placeholder_is_not_a_finding() { - // host 的 `open_session` 在规则引擎无结论时发一份 `rule: ""` 的假报告(非 Option 逼的)。 - // 抓得住:把「没命中」画成一条严重度结论(用户会去修一个不存在的毛病)。 - // 抓不住:host 该不该改发 `None`(那是 presenter 的 finding)。 - let mut state = AppState::new(); - state.apply_core(CoreEvent::LogSessionsChanged(vec![session( - CRASH, - "10-29 18:40", - LogOutcome::Crashed, - )])); - state.apply_core(CoreEvent::LogLinesChanged(vec![line( - "[18:40] nothing to see", - LogLevel::Info, - )])); - - state.apply_core(CoreEvent::DiagnosisReady(report(CRASH, ""))); - assert!( - logs_model(&state).findings.is_empty(), - "空 rule 是『没命中』的占位,不是结论" - ); - - state.apply_core(CoreEvent::DiagnosisReady(report(CRASH, "gpu.driver_old"))); - assert_eq!( - logs_model(&state).findings.len(), - 1, - "换成真规则就得画出来(证明上面的过滤不是把诊断一起关了)" - ); - } -} +mod tests; diff --git a/crates/app-iced/src/app/view/logs/tests.rs b/crates/app-iced/src/app/view/logs/tests.rs new file mode 100644 index 0000000..624732a --- /dev/null +++ b/crates/app-iced/src/app/view/logs/tests.rs @@ -0,0 +1,242 @@ +//! 日志页展示转换的守门测试(从 `app/view/logs.rs` 拆出:那边已经 382 行,贴着 400 硬上限)。 +//! +//! 全部走**真实 reducer 路径**(`AppState::apply_core`),不造注入源——r3 的教训是 +//! 「测试全走注入源而真实路径零覆盖」,所以这里的夹具就是线上那串事件本身。 + +use unlucky_i18n::Lang; +use unlucky_presenter::dto::{ + CoreEvent, DiagnosisReport, LaunchFinished, LiveLogBatch, LogLevel, LogLine, LogOutcome, + LogSession, +}; +use unlucky_presenter::state::AppState; + +use crate::pages; + +use super::logs_model; + +/// 崩溃的那一场。 +const CRASH: &str = "s-1029-1840"; +/// 崩溃之后又启动的、正常跑的一场。 +const OK_RUN: &str = "s-1029-1922"; + +fn session(id: &str, when: &str, outcome: LogOutcome) -> LogSession { + LogSession { + id: id.to_string(), + instance: "开发测试".to_string(), + when: when.to_string(), + outcome, + duration_secs: 62, + } +} + +fn line(text: &str, level: LogLevel) -> LogLine { + LogLine { + text: text.to_string(), + level, + } +} + +fn report(session: &str, rule: &str) -> DiagnosisReport { + DiagnosisReport { + session: session.to_string(), + diagnosis: pages_dto(rule), + } +} + +fn pages_dto(rule: &str) -> unlucky_presenter::dto::Diagnosis { + unlucky_presenter::dto::Diagnosis { + rule: rule.to_string(), + title: "显卡驱动过旧(OpenGL 版本不足)".to_string(), + confidence: 92, + hint: "升级显卡驱动".to_string(), + fix: Some("change_graphics.driver".to_string()), + } +} + +/// **真实数据路径**(复审要求:不许造注入源绕过去): +/// 崩溃一场跑完 → 再启动一场正常跑。`state.diagnosis` 在这里**故意不清**, +/// 因为 presenter 里根本没有清零点——这就是线上发生串场的条件。 +fn crashed_only() -> AppState { + let mut state = AppState::new(); + state.apply_core(CoreEvent::LogSessionsChanged(vec![ + session(CRASH, "10-29 18:40", LogOutcome::Crashed), + session(OK_RUN, "10-29 19:22", LogOutcome::Ok), + ])); + + // 第一场跑起来(live_session = CRASH)→ 日志 → 规则结论 → 收尾(selected_session = CRASH) + state.apply_core(CoreEvent::LogAppended(LiveLogBatch { + session: CRASH.to_string(), + lines: vec![line("[18:40] boom: GLFW error", LogLevel::Error)], + running: true, + })); + state.apply_core(CoreEvent::DiagnosisReady(report(CRASH, "gpu.driver_old"))); + state.apply_core(CoreEvent::LaunchFinished(LaunchFinished { + id: "dev".to_string(), + session: CRASH.to_string(), + outcome: LogOutcome::Crashed, + summary: "崩溃(exit 1)".to_string(), + })); + + state +} + +/// 崩溃之后又启动一场正常跑:`live_session` 前进,`diagnosis` 仍是上一场的。 +fn after_a_new_run() -> AppState { + let mut state = crashed_only(); + state.apply_core(CoreEvent::LogAppended(LiveLogBatch { + session: OK_RUN.to_string(), + lines: vec![line("[19:22] launching 1.20.4", LogLevel::Info)], + running: true, + })); + state +} + +#[test] +fn a_new_run_does_not_inherit_the_previous_crash() { + let state = after_a_new_run(); + let logs = logs_model(&state).with_status(Lang::En); + + // 抓得住:身份被上一场的 `diagnosis.session` 顶掉(r2 的 bug)——那时 outcome 会是 + // Crashed、findings 挂着 gpu.driver_old、摘要里 `lines=` 与结论不同源。 + // 抓不住:presenter 自己 `clear_logs` 后 `selected_session` 不清的缺口(已报 finding), + // 以及多场并行会话(本仓一次只跑一场)。 + assert_eq!( + logs.outcome, + Some(pages::Outcome::Ok), + "身份应该是正在跑的这一场" + ); + + let status = logs.status.clone().expect("有会话身份就该有副标题"); + assert!(status.contains("Finished normally"), "{status}"); + assert!( + !status.contains("The game crashed"), + "页头把新会话标成了上一场的崩溃:{status}" + ); + + assert!( + logs.findings.is_empty(), + "上一场的结论不能挂到这一场:{:?}", + logs.findings + ); + + // 摘要必须与身份同源:outcome 是 ok、不含旧规则、行数就是当前这些行。 + let summary = logs.summary(); + assert!(summary.contains("outcome=ok"), "{summary}"); + assert!(!summary.contains("gpu.driver_old"), "{summary}"); + assert!( + summary.contains(&format!("lines={}", logs.lines.len())), + "{summary}" + ); + assert!(!summary.contains("findings="), "{summary}"); +} + +#[test] +fn the_crash_itself_is_still_reported() { + // 上一条测试的**对照**:崩溃当场必须画结论,否则那条断言「findings 为空」是空的。 + // 抓得住:把判别写成「永远不画诊断」(用空断言骗过 P2)。 + // 抓不住:结论排序/去重(那是 core 的规则引擎的事)。 + let state = crashed_only(); + let logs = logs_model(&state).with_status(Lang::En); + + assert_eq!(logs.outcome, Some(pages::Outcome::Crashed)); + assert_eq!(logs.findings.len(), 1, "{:?}", logs.findings); + assert_eq!(logs.findings[0].rule, "gpu.driver_old"); + let status = logs.status.clone().unwrap_or_default(); + assert!(status.contains("The game crashed"), "{status}"); + assert!(logs.summary().contains("gpu.driver_old:high(92)")); +} + +#[test] +fn an_imported_crash_file_still_shows_its_findings() { + // r3 的**回归**:r3 把归属判据写成「有选中就只认选中」,于是导入崩溃日志 + // (`ImportCrashLog` → `DiagnosisReady` 带 `file:<文件名>` 的会话 id,且**不发** + // `LogLinesChanged`)被整条丢掉。今天 iced 侧还没有 dispatch 站点(那些还在遗留的 + // Slint shell 里),所以复审没判 p1;这条测试是为了**将来接线的人**不再踩。 + // 抓得住:只有「属于当前选中/live 会话」才画的窄判据(导入的 `file:` 会话不等于任何 + // 索引里的会话,会被误杀)。 + // 抓不住:导入后日志正文该显示什么(`ImportCrashLog` 不载入日志行,那是 presenter 的事)。 + let mut state = AppState::new(); + state.apply_core(CoreEvent::LogSessionsChanged(vec![session( + CRASH, + "10-29 18:40", + LogOutcome::Crashed, + )])); + // 上一场刚结束 ⇒ selected_session = CRASH(过期选中项),随后用户导入一份崩溃文件 + state.apply_core(CoreEvent::LaunchFinished(LaunchFinished { + id: "dev".to_string(), + session: CRASH.to_string(), + outcome: LogOutcome::Crashed, + summary: "崩溃(exit 1)".to_string(), + })); + state.apply_core(CoreEvent::DiagnosisReady(report( + "file:crash-2026-11-02_19.40.15-client.txt", + "integrity.unsatisfied_link", + ))); + + let logs = logs_model(&state).with_status(Lang::En); + assert_eq!( + logs.findings.len(), + 1, + "导入的崩溃日志必须画出它的结论:{:?}", + logs.findings + ); + assert_eq!(logs.findings[0].rule, "integrity.unsatisfied_link"); +} + +#[test] +fn a_live_run_says_it_is_running_instead_of_nothing_runs() { + // 视觉复审 r3 的 P2-3:session 索引只在**结束**时追加(`host::logs::sessions`), + // 所以运行中的那一场永远不在 `log_sessions` 里 ⇒ 拿不到实例名与结局 ⇒ r3 的页头在 + // 每次实时运行都说「当前没有运行中的实例」。demo 伪造了身份,截图上根本照不出来。 + // 抓得住:页头退回 Idle 文案(这次走真实 reducer:只有 `LogAppended(running=true)`)。 + // 抓不住:文案长度在某尺寸下会不会撞右侧动作区(那要看截图)。 + let mut state = AppState::new(); + state.apply_core(CoreEvent::LogSessionsChanged(vec![session( + CRASH, + "10-29 18:40", + LogOutcome::Crashed, + )])); + state.apply_core(CoreEvent::LogAppended(LiveLogBatch { + session: OK_RUN.to_string(), + lines: vec![line("[19:22] launching", LogLevel::Info)], + running: true, + })); + + let logs = logs_model(&state).with_status(Lang::En); + assert!(logs.live, "live_session 非空就该标记为运行中"); + let status = logs.status.clone().expect("运行中必须有副标题"); + let idle = unlucky_i18n::catalog::Catalog::of(Lang::En) + .text(unlucky_i18n::catalog::Key::LogsSubtitleIdle); + assert_ne!(status, idle, "运行中的页头不能说「没有运行」"); + assert!(status.contains("Running now"), "{status}"); +} + +#[test] +fn the_hosts_no_rule_matched_placeholder_is_not_a_finding() { + // host 的 `open_session` 在规则引擎无结论时发一份 `rule: ""` 的假报告(非 Option 逼的)。 + // 抓得住:把「没命中」画成一条严重度结论(用户会去修一个不存在的毛病)。 + // 抓不住:host 该不该改发 `None`(那是 presenter 的 finding)。 + let mut state = AppState::new(); + state.apply_core(CoreEvent::LogSessionsChanged(vec![session( + CRASH, + "10-29 18:40", + LogOutcome::Crashed, + )])); + state.apply_core(CoreEvent::LogLinesChanged(vec![line( + "[18:40] nothing to see", + LogLevel::Info, + )])); + + state.apply_core(CoreEvent::DiagnosisReady(report(CRASH, ""))); + assert!( + logs_model(&state).findings.is_empty(), + "空 rule 是『没命中』的占位,不是结论" + ); + + state.apply_core(CoreEvent::DiagnosisReady(report(CRASH, "gpu.driver_old"))); + assert_eq!( + logs_model(&state).findings.len(), + 1, + "换成真规则就得画出来(证明上面的过滤不是把诊断一起关了)" + ); +} diff --git a/crates/app-iced/src/pages/logs.rs b/crates/app-iced/src/pages/logs.rs index eb7da7d..e9e46bd 100644 --- a/crates/app-iced/src/pages/logs.rs +++ b/crates/app-iced/src/pages/logs.rs @@ -4,7 +4,7 @@ //! //! ```text //! ┌ 页头(两行 84:标题 + 运行状态;右侧 Open folder / Clear)──────────┐ -//! ├ 诊断卡片(只在有结论时出现,**按严重度分组**)──────────────────────┤ +//! ├ 诊断卡片(只在有结论时出现,**按置信度档分组**:HIGH / MEDIUM / LOW)──┤ //! │ DIAGNOSIS 3 CRITICAL 1 / WARNING 2 │ //! │ integrity.unsatisfied_link(规则 id,等宽) │ //! │ 结论(规则表原文) / 说明 / Suggested fix / Confidence 85% │ @@ -27,7 +27,7 @@ //! | 1 | **折行**而不是横向滚动 | 见 [`lines`] 模块文档:424px × 200 字符的现实下,横滚与外壳竖向滚动打架,且等宽族的 CJK 回退会毁掉「按列对齐」这个横滚唯一的好处 | //! | 2 | 诊断卡片在日志列表**之上** | 崩溃时用户先要结论;把结论放在 14 行日志下面等于让人先翻页 | //! | 3 | 规则原文(中文)当**数据**直接画 | 界面文案全走 i18n key;规则表的 `cause.summary` 是诊断结论本身,翻译要等 `rules.d` 带 `cause.key` 进 DTO(已报 finding) | -//! | 4 | 严重度按**置信度折算** | `dto::Diagnosis` 没有 `severity` 字段(core 有,DTO 丢了,已报 finding),阈值写在 `Severity::of_confidence` | +//! | 4 | 分组用**置信度档**(High/Medium/Low),不借 core 的严重度词汇 | `dto::Diagnosis` 没带 core 的 `severity`;拿置信度凑严重度会让 64 条规则里 12 条进错组(两个方向都有,复审实测)。档位由 `Confidence::of` 定,DTO 补齐后应换回真严重度(已报 finding) | //! | 5 | 页头保持**两行 84** | M84 定的版式(运行状态在副标题上);计数留在两张卡片自己的标题行,避免为了拼计数把 i18n 字符串漏进页头生命期 | //! //! ## 指令(只有真业务命令才 dispatch) @@ -60,7 +60,7 @@ const DIAGNOSIS_SHARE: u16 = 4; const LOGS_SHARE: u16 = 2; pub use demo::demo; -pub use model::{Finding, Level, Line, Logs, Outcome, Severity}; +pub use model::{Confidence, Finding, Level, Line, Logs, Outcome}; use iced::widget::column; use iced::{Element, Length}; diff --git a/crates/app-iced/src/pages/logs/demo.rs b/crates/app-iced/src/pages/logs/demo.rs index 7bcc654..97b2cd6 100644 --- a/crates/app-iced/src/pages/logs/demo.rs +++ b/crates/app-iced/src/pages/logs/demo.rs @@ -26,7 +26,7 @@ use unlucky_i18n::Lang; -use super::model::{Finding, Level, Line, Logs, Outcome, Severity}; +use super::model::{Confidence, Finding, Level, Line, Logs, Outcome}; /// 挑演示状态的环境变量。 pub const STATE_ENV: &str = "ULL_LOGS_FIXTURE"; @@ -46,15 +46,21 @@ pub enum State { Live, /// 崩溃现场:日志 + 诊断结论。 Crash, + /// **正在跑**的一场(页头走「正在运行」那条文案;session 索引里还没有它)。 + Running, } impl State { /// 从环境变量取值解析(未设 / 不认识一律 `Crash`)。 + /// + /// 取值:`empty`(空态)/ `live`(正常跑完的一场)/ `running`(**正在跑**:页头走「正在运行」) + /// / 其余 = `crash`。 #[must_use] pub fn of_env_value(raw: Option<&str>) -> Self { match raw { Some("empty") => Self::Empty, Some("live") => Self::Live, + Some("running") => Self::Running, _ => Self::Crash, } } @@ -74,6 +80,14 @@ impl State { ..Logs::default() } .with_status(lang), + // 运行中:还没有结局、也不在 session 索引里(`host::logs::sessions` 只在结束时追加), + // 所以既没有实例名也没有 outcome —— 页头只能走「正在运行」那条中性文案。 + Self::Running => Logs { + live: true, + lines: parse(LIVE_SAMPLE), + ..Logs::default() + } + .with_status(lang), Self::Crash => crash().with_status(lang), } } @@ -128,8 +142,8 @@ pub(super) fn parse(text: &str) -> Vec { .collect() } -/// 三条诊断(`rule` / `title` / `hint` 抄自规则表;`severity` 走 [`Severity::of_confidence`], -/// 也就是 DTO 缺 `severity` 时产品路径用的同一条折算)。 +/// 三条诊断(`rule` / `title` / `hint` 抄自规则表;置信度档走 [`Confidence::of`], +/// 也就是 DTO 缺 `severity` 时产品路径用的同一条档位定义)。 fn findings() -> Vec { vec![ finding( @@ -159,7 +173,7 @@ fn findings() -> Vec { fn finding(rule: &str, confidence: u8, title: &str, hint: &str, action: Option<&str>) -> Finding { Finding { rule: rule.to_owned(), - severity: Severity::of_confidence(confidence), + confidence_band: Confidence::of(confidence), title: title.to_owned(), hint: hint.to_owned(), confidence, diff --git a/crates/app-iced/src/pages/logs/diagnostic.rs b/crates/app-iced/src/pages/logs/diagnostic.rs index 4f6f4ff..8466f06 100644 --- a/crates/app-iced/src/pages/logs/diagnostic.rs +++ b/crates/app-iced/src/pages/logs/diagnostic.rs @@ -1,11 +1,11 @@ -//! 诊断结果区:**按严重度分组**的结论卡片 + 可直接粘进 issue 的一行摘要。 +//! 诊断结果区:**按置信度档分组**的结论卡片 + 可直接粘进 issue 的一行摘要。 //! //! ## 一条结论要能看出什么(tower 的三问) //! //! | 问题 | 落点 | 数据来源 | //! |---|---|---| //! | 哪条规则命中 | 规则 id(等宽、`LabelMedium`) | `Finding::rule` | -//! | 严重度 | 分组标题 + 分组顺序(`CRITICAL` → `WARNING` → `NOTICE`) | `Finding::severity` | +//! | 置信度(不是严重度) | 分组标题 + 分组顺序(`HIGH` → `MEDIUM` → `LOW`) | `Finding::confidence_band` | //! | 建议动作 | `Suggested fix` 一行(规则表的 `fix_action`,技术标识原样) | `Finding::action` | //! //! ## 用户真的要拿去用的两样东西 @@ -33,7 +33,7 @@ use crate::theme::Ui; use super::super::frame; use super::super::Message; use super::lines::{mono, mono_owned}; -use super::model::{Finding, Logs, Severity, GROUPS}; +use super::model::{Confidence, Finding, Logs, GROUPS}; use super::panel; /// 诊断卡片(只在有结论时画)。高度由调用方定(见 [`panel`])。 @@ -46,18 +46,14 @@ pub fn card<'a>(ui: &'a Ui, logs: &'a Logs, lang: Lang, height: Length) -> Eleme let mut group = column![].spacing(ui.space(Space::X2)).width(Length::Fill); - for severity in GROUPS { - let findings = logs.group(severity); + for band in GROUPS { + let findings = logs.group(band); if findings.is_empty() { continue; } group = group.push(frame::divider(ui)); - group = group.push(title_row( - ui, - catalog.text(key_of(severity)), - findings.len(), - )); + group = group.push(title_row(ui, catalog.text(key_of(band)), findings.len())); for finding in findings { group = group.push(finding_block(ui, &catalog, finding)); } @@ -189,12 +185,16 @@ fn summary_block<'a>(ui: &'a Ui, catalog: &Catalog, logs: &Logs) -> Element<'a, .into() } -/// 严重度 → 分组标题的 key。 -fn key_of(severity: Severity) -> Key { - match severity { - Severity::Critical => Key::LogsSeverityCritical, - Severity::Warning => Key::LogsSeverityWarning, - Severity::Info => Key::LogsSeverityInfo, +/// 置信度档 → 分组标题的 key。 +/// +/// 刻意**不叫严重度**(`CRITICAL` / `WARNING` / `NOTICE`):DTO 没带 core 的 severity, +/// 拿置信度去凑严重度会让 64 条规则里的 12 条进错组(`mod.mixin_failure` critical/0.70 沉底、 +/// `other.debug_crash` info/0.95 冒顶)。标题只说它对「这条结论有多确定」。 +fn key_of(band: Confidence) -> Key { + match band { + Confidence::High => Key::LogsConfidenceHigh, + Confidence::Medium => Key::LogsConfidenceMedium, + Confidence::Low => Key::LogsConfidenceLow, } } diff --git a/crates/app-iced/src/pages/logs/model.rs b/crates/app-iced/src/pages/logs/model.rs index 4103f87..49d8762 100644 --- a/crates/app-iced/src/pages/logs/model.rs +++ b/crates/app-iced/src/pages/logs/model.rs @@ -18,8 +18,14 @@ //! presenter 的 `dto::Diagnosis` 只有 `rule` / `title` / `hint` / `confidence` / `fix`: //! //! 1. **core 的 `Severity`(info / warning / critical)在半路丢了**——`core::diagnose::rules` -//! 里每条规则都带它,DTO 没有。页面要「按严重度分组」就只能用 -//! [`Severity::of_confidence`] 按置信度**折算**(阈值写死在函数里,是债,不是规格)。 +//! 里每条规则都带它,`presenter::dto::Diagnosis` 没有。拿严重度当分组名就只能拿置信度硬凑, +//! 实测 **64 条规则里 12 条会被凑错档、两个方向都有**(`mod.mixin_failure` critical/0.70 沉到 +//! 第二档、`other.debug_crash` info/0.95 冒到第一档)。 +//! +//! 所以 M95 r4 起页面**不再借 core 的词汇**:分组改用**置信度档** +//! [`Confidence::High`] / [`Confidence::Medium`] / [`Confidence::Low`],标题直接写「高/中/低 +//! 置信度」——它描述的是「规则引擎有多确定」,不是「这毛病有多严重」。DTO 补上 `severity` +//! 之后这里应该换回真严重度(finding 已记给 presenter)。 //! 2. `title` / `hint` 只有中文单语,而且规则表里其实已经有结构化出口 //! (`rules.d/*.yaml` 的 `cause.key: diagnose.cause.…`)。DTO 没带 `key`,页面就把规则原文 //! 当**数据**画出来,页面的界面文案(标题 / 按钮 / 分组名 / 空态)仍然全部走 i18n key。 @@ -53,42 +59,46 @@ impl Level { } } -/// 诊断结论的严重度(`core::diagnose::rules::Severity` 的展示投影)。 +/// 诊断结论的**置信度档**(规则引擎有多确定,不是「这毛病有多严重」)。 +/// +/// 为什么不是 `Severity`:`presenter::dto::Diagnosis` 没带 core 规则表的 `severity`, +/// 页面自己按置信度折算严重度会凑错档(64 条规则里 12 条,两个方向都有)。宁可**如实** +/// 说「这条结论置信度多高」,也不要编一个「严重度」(视觉复审 r3 的 P2-2 + 塔裁决)。 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum Severity { - /// 提示:能跑但不最佳。 - Info, - /// 警告:可疑但结论不确定。 - Warning, - /// 严重:基本可以确定崩溃原因。 - Critical, +pub enum Confidence { + /// 低:规则只是「有点像」。 + Low, + /// 中:可疑。 + Medium, + /// 高:基本可以确定。 + High, } -/// 分区顺序(**主因在上**:崩溃日志换 issue 时第一段就是最该看的)。 -pub const GROUPS: [Severity; 3] = [Severity::Critical, Severity::Warning, Severity::Info]; +/// 分区顺序(**高置信度在上**:崩溃日志换 issue 时第一段就是最该看的)。 +pub const GROUPS: [Confidence; 3] = [Confidence::High, Confidence::Medium, Confidence::Low]; -impl Severity { +impl Confidence { /// 稳定 slug(摘要行用;不是文案——分组标题走 i18n key)。 #[must_use] pub const fn slug(self) -> &'static str { match self { - Self::Info => "info", - Self::Warning => "warning", - Self::Critical => "critical", + Self::Low => "low", + Self::Medium => "medium", + Self::High => "high", } } - /// 按置信度折算严重度——**过渡口径**,见模块文档的缺口 #1。 + /// 0~100 置信度 → 档位(阈值 `≥80` 高 / `≥50` 中 / 其余低)。 /// - /// 阈值:`≥80` 严重 / `≥50` 警告 / 其余提示。DTO 补上 `severity` 之后这个函数就该删掉。 + /// 阈值是**档位定义**,不是从严重度反推的代理:标题与文案都说的是「置信度」。 #[must_use] - pub const fn of_confidence(confidence: u8) -> Self { + pub const fn of(confidence: u8) -> Self { if confidence >= 80 { - Self::Critical + Self::High } else if confidence >= 50 { - Self::Warning + Self::Medium } else { - Self::Info + Self::Low } } } @@ -107,8 +117,8 @@ pub struct Line { pub struct Finding { /// 命中的规则 id(`graphics.opengl_not_supported`):技术标识,原样显示。 pub rule: String, - /// 严重度(DTO 没有,见 [`Severity::of_confidence`])。 - pub severity: Severity, + /// 置信度档(DTO 没有 core 的 severity,见 [`Confidence`])。 + pub confidence_band: Confidence, /// 结论标题(规则表的 `cause.summary`,**数据不是文案**)。 pub title: String, /// 建议说明(规则表的 `cause.detail`)。 @@ -154,6 +164,12 @@ pub struct Logs { /// 页头副标题(`实例 · 时间 · 结局`):**构造时**拼好(见 [`Logs::with_status`]), /// 页面只负责显示——i18n 的「一句话 = 一个 key」不许在渲染里拼句子。 pub status: Option, + /// 有没有正在跑的会话(`live_session` 非空)。 + /// + /// 为什么需要它:session 索引只在会话**结束**时追加(`host::logs::sessions`),所以运行中的 + /// 那一场根本不在 `log_sessions` 里 ⇒ 拿不到实例名与结局。r3 之前这里直接退回 + /// 「当前没有运行中的实例」,于是**每次实时运行页头都在说反话**(视觉复审 r3 的 P2-3)。 + pub live: bool, /// 日志行(顺序 = 文件顺序)。 pub lines: Vec, /// 诊断结论(顺序 = presenter 给的顺序,页面不重排)。 @@ -161,18 +177,28 @@ pub struct Logs { } impl Logs { - /// 补上页头副标题(M95 r2,视觉复审 P2-2)。 + /// 补上页头副标题(M95 r2,视觉复审 P2-2;r4 补「运行中」这一档,P2-3)。 /// /// 为什么在这里做而不是在页面里:`Header::subtitle` 要 `&'a str`,而拼出来的是 `String`; /// 让模型持有它,页面就永远只借不拼。 + /// + /// 三级回退:**有实例 + 结局** → `实例 · 时间 · 结局`;**正在跑但还没有结局**(session 索引 + /// 里没有它) → 中性的「正在运行」;都没有 → `None`(页面退回 [`Key::LogsSubtitleIdle`])。 #[must_use] pub fn with_status(mut self, lang: Lang) -> Self { + let catalog = Catalog::of(lang); + self.status = status_line( self.instance.as_deref(), self.when.as_deref(), self.outcome, lang, - ); + ) + .or_else(|| { + self.live + .then(|| catalog.text(Key::LogsSubtitleLive).to_owned()) + }); + self } @@ -182,12 +208,12 @@ impl Logs { self.lines.is_empty() && self.findings.is_empty() } - /// 某一严重度下的结论(顺序不变;空组返回空 `Vec`)。 + /// 某一置信度档下的结论(顺序不变;空组返回空 `Vec`)。 #[must_use] - pub fn group(&self, severity: Severity) -> Vec<&Finding> { + pub fn group(&self, band: Confidence) -> Vec<&Finding> { self.findings .iter() - .filter(|finding| finding.severity == severity) + .filter(|finding| finding.confidence_band == band) .collect() } @@ -223,7 +249,7 @@ impl Logs { format!( "{}:{}({})", finding.rule, - finding.severity.slug(), + finding.confidence_band.slug(), finding.confidence ) }) diff --git a/crates/app-iced/src/pages/logs/tests.rs b/crates/app-iced/src/pages/logs/tests.rs index 0911641..5d650e2 100644 --- a/crates/app-iced/src/pages/logs/tests.rs +++ b/crates/app-iced/src/pages/logs/tests.rs @@ -68,35 +68,32 @@ fn findings_are_grouped_critical_first_and_empty_groups_do_not_exist() { findings, ..Logs::default() }; - let counts: Vec = GROUPS - .iter() - .map(|severity| logs.group(*severity).len()) - .collect(); + let counts: Vec = GROUPS.iter().map(|band| logs.group(*band).len()).collect(); - // 抓得住:分组顺序被改成 info 在前(主因会沉底);空组也被画出一个标题(截图上出现 0 条的分区)。 + // 抓得住:分组顺序被改成低置信度在前(最像主因的沉底);空组也被画出一个标题。 // 抓不住:组内顺序(页面刻意不重排 presenter 给的顺序)。 - assert_eq!(GROUPS[0], Severity::Critical); + assert_eq!(GROUPS[0], Confidence::High); assert_eq!( counts.iter().sum::(), logs.findings.len(), "每条结论必须落进恰好一个分组" ); - assert!(counts[0] >= 1, "样例至少有主因"); - assert!(logs.group(Severity::Info).is_empty()); + assert!(counts[0] >= 1, "样例至少有高置信度结论"); + assert!(logs.group(Confidence::Low).is_empty()); } #[test] -fn summary_is_a_single_deterministic_line_carrying_rule_and_severity() { +fn summary_is_a_single_deterministic_line_carrying_rule_and_confidence() { let logs = demo::crash(); let summary = logs.summary(); - // 抓得住:摘要里丢了规则 id / 严重度 / 置信度(换 issue 时最关键的三个字段); + // 抓得住:摘要里丢了规则 id / 置信度档 / 置信度百分比(换 issue 时最关键的三个字段); // 有人把换行塞进摘要("一行"就不成立了)。 // 抓不住:粘贴到 issue 后的可读性(那是人判断的),也不守文案语言(摘要刻意不含界面文案)。 assert!(!summary.contains('\n'), "{summary}"); assert!(summary.starts_with(SUMMARY_HEADER), "{summary}"); assert!( - summary.contains("integrity.unsatisfied_link:critical(85)"), + summary.contains("integrity.unsatisfied_link:high(85)"), "{summary}" ); assert!(summary.contains("lines="), "{summary}"); @@ -107,15 +104,21 @@ fn summary_is_a_single_deterministic_line_carrying_rule_and_severity() { } #[test] -fn severity_is_derived_from_confidence_with_the_documented_thresholds() { - // 抓得住:阈值边界写成 `>` / `>=` 那种 off-by-one(80 与 50 是文档里写死的两个点)。 - // 抓不住:折算本身对不对——DTO 补上 `severity` 之前这只是**过渡口径**(已报 finding)。 - assert_eq!(Severity::of_confidence(100), Severity::Critical); - assert_eq!(Severity::of_confidence(80), Severity::Critical); - assert_eq!(Severity::of_confidence(79), Severity::Warning); - assert_eq!(Severity::of_confidence(50), Severity::Warning); - assert_eq!(Severity::of_confidence(49), Severity::Info); - assert_eq!(Severity::of_confidence(0), Severity::Info); +fn the_band_is_the_documented_confidence_band_not_a_severity_impostor() { + // 抓得住:阈值边界写成 `>` / `>=` 的 off-by-one(80 / 50 是档位的定义点); + // 以及有人把档位名字改回 `critical/warning/notice` 那种**严重度词汇** + // (那会让 64 条规则里 12 条进错组,两个方向都有——视觉复审 r3 的 P2-2)。 + // 抓不住:档位划分本身对不对(那是产品口径;DTO 补上 `severity` 后应换回真严重度,已报 finding)。 + assert_eq!(Confidence::of(100), Confidence::High); + assert_eq!(Confidence::of(80), Confidence::High); + assert_eq!(Confidence::of(79), Confidence::Medium); + assert_eq!(Confidence::of(50), Confidence::Medium); + assert_eq!(Confidence::of(49), Confidence::Low); + assert_eq!(Confidence::of(0), Confidence::Low); + + assert_eq!(Confidence::High.slug(), "high"); + assert_eq!(Confidence::Medium.slug(), "medium"); + assert_eq!(Confidence::Low.slug(), "low"); } #[test] @@ -255,9 +258,10 @@ fn every_key_the_page_uses_is_translated_in_both_languages() { Key::LogsClear, Key::LogsOutputTitle, Key::LogsDiagnosisTitle, - Key::LogsSeverityCritical, - Key::LogsSeverityWarning, - Key::LogsSeverityInfo, + Key::LogsConfidenceHigh, + Key::LogsConfidenceMedium, + Key::LogsConfidenceLow, + Key::LogsSubtitleLive, Key::LogsConfidence, Key::LogsSuggestedFix, Key::LogsSummaryLabel, diff --git a/crates/app-iced/src/pages/mod.rs b/crates/app-iced/src/pages/mod.rs index afb60af..c28420b 100644 --- a/crates/app-iced/src/pages/mod.rs +++ b/crates/app-iced/src/pages/mod.rs @@ -108,7 +108,7 @@ mod tests; pub use download::{Category, Download}; pub use layout::{FrameMetrics, PageLayout, PaneKit, WidthClass}; -pub use logs::{Finding, Level, Line, Logs, Outcome, Severity}; +pub use logs::{Confidence, Finding, Level, Line, Logs, Outcome}; pub use model::{Detail, Instances, Meta, Model, Row}; use iced::widget::{column, container, row, space}; diff --git a/crates/i18n/src/catalog.rs b/crates/i18n/src/catalog.rs index d299995..469c2e8 100644 --- a/crates/i18n/src/catalog.rs +++ b/crates/i18n/src/catalog.rs @@ -178,9 +178,10 @@ catalog! { LogsClear => ("Clear", "清空"), LogsOutputTitle => ("OUTPUT", "输出"), LogsDiagnosisTitle => ("DIAGNOSIS", "诊断"), - LogsSeverityCritical => ("CRITICAL", "严重"), - LogsSeverityWarning => ("WARNING", "警告"), - LogsSeverityInfo => ("NOTICE", "提示"), + LogsConfidenceHigh => ("HIGH CONFIDENCE", "高置信度"), + LogsConfidenceMedium => ("MEDIUM CONFIDENCE", "中置信度"), + LogsConfidenceLow => ("LOW CONFIDENCE", "低置信度"), + LogsSubtitleLive => ("Running now — logs stream as they arrive", "正在运行 — 日志实时滚动"), LogsConfidence => ("Confidence {percent}%", "置信度 {percent}%"), LogsSuggestedFix => ("Suggested fix", "建议动作"), LogsSummaryLabel => ("One-line summary", "一行摘要"),