CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
CI / runtime smoke (ubuntu x64, xvfb) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
首个 iced + Material 3 Expressive 版本的源码快照。 完整历史与日常提交在 dev 分支;main 只放阶段性快照。
145 lines
7.9 KiB
Rust
145 lines
7.9 KiB
Rust
//! 日志页的展示转换:presenter 快照 → [`pages::Logs`](从 `app/view.rs` 拆出来,M95 r3)。
|
||
//!
|
||
//! ## 跨会话串场(M95 r3 的 p2,代码复审抓的)
|
||
//!
|
||
//! r2 的写法先按 `state.diagnosis` 定「这是哪一场」:
|
||
//!
|
||
//! ```ignore
|
||
//! let session = report.and_then(|r| log_sessions.find(|s| s.id == r.session)).or_else(|| …selected…);
|
||
//! ```
|
||
//!
|
||
//! 而 `state.diagnosis` **全仓只写不清**(`presenter::state::apply_core` 里
|
||
//! `CoreEvent::DiagnosisReady` 是唯一赋值点,没有任何清零点),host 也只在崩溃 / 打开会话 /
|
||
//! 导入崩溃文件时发它。于是「崩溃一场 → 再启动一场正常跑」这个最普通的路径下:
|
||
//! 页头把**新**会话标成上一场的 `The game crashed`、诊断卡挂着上一场的结论,而用户复制去贴
|
||
//! issue 的摘要里 `lines=` 是新的那一场的行数——**结论与行数不同源**,用户会把错诊断递出去。
|
||
//!
|
||
//! 现在的口径(不动 presenter 的写入口,只在页面侧判别):
|
||
//!
|
||
//! | 概念 | 取值 |
|
||
//! |---|---|
|
||
//! | **身份**(这一场是谁) | `live_session`(正在跑)→ `selected_session`(打开/刚结束的),**不从 `diagnosis` 反推** |
|
||
//! | 诊断结论 | 见下表的三个分支(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` 不清,
|
||
//! 身份会继续指向那一场;导入崩溃文件只发 `DiagnosisReady`(不带日志行),所以页面上会出现
|
||
//! 「结论 + 上一场的日志正文」——行由 `open_session` 负责,那是 presenter 的缺口。
|
||
|
||
use unlucky_presenter::dto::{Diagnosis, LogLevel, LogOutcome};
|
||
use unlucky_presenter::state::AppState;
|
||
|
||
use crate::pages;
|
||
|
||
/// 日志页视图模型:presenter 快照 → 要画的东西。
|
||
///
|
||
/// 两处 DTO 侧的**信息丢失**(已报 finding):`dto::Diagnosis` 没有 core 的 `severity`
|
||
/// (所以页面按置信度档分组,不冒称严重度)与 `cause.key`(`title`/`hint` 只能当数据画)。
|
||
pub(super) fn logs_model(state: &AppState) -> pages::Logs {
|
||
let live = state.live_session.as_deref();
|
||
let selected = state.selected_session.as_deref();
|
||
let identity = live.or(selected);
|
||
|
||
// 归属判据(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,
|
||
}
|
||
});
|
||
|
||
// 身份为空时,描述的来源只能退到报告(见模块文档的表格)。
|
||
let described = identity.or_else(|| report.map(|report| report.session.as_str()));
|
||
let session =
|
||
described.and_then(|id| state.log_sessions.iter().find(|session| session.id == id));
|
||
|
||
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()
|
||
.map(|line| pages::Line {
|
||
text: line.text.clone(),
|
||
level: log_level(line.level),
|
||
})
|
||
.collect(),
|
||
findings: report
|
||
.filter(|report| !report.diagnosis.rule.is_empty())
|
||
.map(|report| diagnosis_of(&report.diagnosis))
|
||
.into_iter()
|
||
.collect(),
|
||
..pages::Logs::default()
|
||
}
|
||
}
|
||
|
||
/// 日志等级:presenter 枚举 → 页面等级(**结构化出口**,不是认字符串)。
|
||
fn log_level(level: LogLevel) -> pages::Level {
|
||
match level {
|
||
LogLevel::Debug => pages::Level::Debug,
|
||
LogLevel::Info => pages::Level::Info,
|
||
LogLevel::Warn => pages::Level::Warn,
|
||
LogLevel::Error => pages::Level::Error,
|
||
}
|
||
}
|
||
|
||
/// 会话结局:presenter 枚举 → 页面结局(文案走 `LaunchOutcome*` key,页面不画 core 的中文串)。
|
||
fn log_outcome(outcome: LogOutcome) -> pages::Outcome {
|
||
match outcome {
|
||
LogOutcome::Ok => pages::Outcome::Ok,
|
||
LogOutcome::Crashed => pages::Outcome::Crashed,
|
||
LogOutcome::SilentFail => pages::Outcome::SilentFail,
|
||
}
|
||
}
|
||
|
||
/// 一条诊断结论 → 页面结论(置信度档见 [`pages::Confidence`])。
|
||
fn diagnosis_of(diagnosis: &Diagnosis) -> pages::Finding {
|
||
pages::Finding {
|
||
rule: diagnosis.rule.clone(),
|
||
confidence_band: pages::Confidence::of(diagnosis.confidence),
|
||
title: diagnosis.title.clone(),
|
||
hint: diagnosis.hint.clone(),
|
||
confidence: diagnosis.confidence,
|
||
action: diagnosis.fix.clone(),
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests;
|