Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package ttl for openSUSE:Factory checked in 
at 2026-09-04 12:38:40
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/ttl (Old)
 and      /work/SRC/openSUSE:Factory/.ttl.new.1265 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "ttl"

Fri Sep  4 12:38:40 2026 rev:12 rq:1375443 version:0.22.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/ttl/ttl.changes  2026-08-18 17:17:03.775614983 
+0200
+++ /work/SRC/openSUSE:Factory/.ttl.new.1265/ttl.changes        2026-09-04 
12:39:12.251043271 +0200
@@ -1,0 +2,9 @@
+Tue Sep  1 12:37:04 UTC 2026 - Martin Hauke <[email protected]>
+
+- Update to version 0.22.0:
+  * chore(tui): make the render fingerprint exhaustive over its
+    inputs.
+  * perf(tui): skip snapshot+draw when idle (LAN-1222 experiment).
+  * chore: exclude demo assets (gif/tape) from published crate.
+
+-------------------------------------------------------------------

Old:
----
  ttl-0.21.0.obscpio

New:
----
  ttl-0.22.0.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ ttl.spec ++++++
--- /var/tmp/diff_new_pack.c14MWX/_old  2026-09-04 12:39:16.333186671 +0200
+++ /var/tmp/diff_new_pack.c14MWX/_new  2026-09-04 12:39:16.336186777 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           ttl
-Version:        0.21.0
+Version:        0.22.0
 Release:        0
 Summary:        Modern traceroute/mtr-style TUI
 License:        Apache-2.0 OR MIT

++++++ _service ++++++
--- /var/tmp/diff_new_pack.c14MWX/_old  2026-09-04 12:39:16.419189692 +0200
+++ /var/tmp/diff_new_pack.c14MWX/_new  2026-09-04 12:39:16.430190079 +0200
@@ -2,7 +2,7 @@
   <service name="obs_scm" mode="manual">
     <param name="url">https://github.com/lance0/ttl</param>
     <param name="scm">git</param>
-    <param name="revision">v0.21.0</param>
+    <param name="revision">v0.22.0</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(\d+\.\d+\.\d+)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.c14MWX/_old  2026-09-04 12:39:16.491192222 +0200
+++ /var/tmp/diff_new_pack.c14MWX/_new  2026-09-04 12:39:16.501192573 +0200
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param name="url">https://github.com/lance0/ttl</param>
-              <param 
name="changesrevision">46f1dc2cd8c055408a764d5674afa897ca7cd30c</param></service></servicedata>
+              <param 
name="changesrevision">987b926707b3ae64de54793c49c7bae104498402</param></service></servicedata>
 (No newline at EOF)
 

++++++ ttl-0.21.0.obscpio -> ttl-0.22.0.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ttl-0.21.0/CHANGELOG.md new/ttl-0.22.0/CHANGELOG.md
--- old/ttl-0.21.0/CHANGELOG.md 2026-07-07 16:42:22.000000000 +0200
+++ new/ttl-0.22.0/CHANGELOG.md 2026-08-24 18:31:53.000000000 +0200
@@ -5,6 +5,19 @@
 The format is based on [Keep a 
Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic 
Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [0.22.0] - 2026-08-24
+
+### Changed
+- **The TUI redraws only when something changed.** The interactive view 
previously rebuilt its session snapshot and repainted the terminal on every 16 
ms tick (~60 fps) whether or not a probe had landed — at the default 1 s probe 
interval roughly 55 of every 60 frames were pure churn. Each tick now compares 
a fingerprint of everything the view renders (hop-stat generation, overlays, 
theme, status line, replay clock, settings, target list, IX cache state) 
against the previous frame and skips both the snapshot clone and the draw when 
they match. Measured on a 9-hop trace at the default interval, **1.4% → 0.4% of 
one CPU core**, and the saving grows with hop count. Output is unchanged and 
the tick rate is still 16 ms, so input, replay animation, and terminal resizes 
stay as responsive as before.
+- **The published crate no longer bundles the demo assets.** `docs/*.gif` and 
`docs/*.tape` are excluded from the package, roughly halving the crates.io 
tarball (1.14 MB → 0.56 MB compressed) for every `cargo install ttl`. The 
README still renders the demo from the repository.
+
+### Security
+- **`rkyv` 0.8.16 → 0.8.18** clears RUSTSEC-2026-0233 (use-after-free during 
deserialization) and RUSTSEC-2026-0234 / RUSTSEC-2026-0235 (out-of-bounds reads 
on crafted archives). The advisory reaches ttl transitively via `getifs` → 
`smallvec-wrapper`.
+- **`lru` 0.18.0 → 0.18.2** clears RUSTSEC-2026-0253 (potential use-after-free 
from missing panic safety in `LruCache::pop()`). The advisory reaches ttl 
transitively via `ratatui` → `ratatui-core`.
+
+### Dependencies
+- maxminddb 0.29 → 0.30 (direct), plus tokio 1.52.3 → 1.53.1, clap 4.6.1 → 
4.6.6, clap_complete 4.6.7 → 4.6.9, futures 0.3.32 → 0.3.34, libc 0.2.186 → 
0.2.189, serde 1.0.228 → 1.0.229, serde_json 1.0.150 → 1.0.151, socket2 0.6.4 → 
0.6.5, and anyhow 1.0.103 → 1.0.104.
+
 ## [0.21.0] - 2026-07-07
 
 ### Added
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ttl-0.21.0/Cargo.lock new/ttl-0.22.0/Cargo.lock
--- old/ttl-0.21.0/Cargo.lock   2026-07-07 16:42:22.000000000 +0200
+++ new/ttl-0.22.0/Cargo.lock   2026-08-24 18:31:53.000000000 +0200
@@ -84,9 +84,9 @@
 
 [[package]]
 name = "anyhow"
-version = "1.0.103"
+version = "1.0.104"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
 
 [[package]]
 name = "approx"
@@ -325,9 +325,9 @@
 
 [[package]]
 name = "clap"
-version = "4.6.1"
+version = "4.6.6"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -335,9 +335,9 @@
 
 [[package]]
 name = "clap_builder"
-version = "4.6.0"
+version = "4.6.6"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
 dependencies = [
  "anstream",
  "anstyle",
@@ -347,23 +347,23 @@
 
 [[package]]
 name = "clap_complete"
-version = "4.6.7"
+version = "4.6.9"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "db8b397918185f0161ff3d6fcaa9e4bfc09b8367caf6e1d4a2848e5477ed027b"
+checksum = "3be2ad0423bdbbb0e25bc89add796f3559706d4a95e1bc98e4d9662a957b6a19"
 dependencies = [
  "clap",
 ]
 
 [[package]]
 name = "clap_derive"
-version = "4.6.1"
+version = "4.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
 dependencies = [
  "heck",
  "proc-macro2",
  "quote",
- "syn 2.0.117",
+ "syn 3.0.3",
 ]
 
 [[package]]
@@ -872,9 +872,9 @@
 
 [[package]]
 name = "futures"
-version = "0.3.32"
+version = "0.3.34"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
+checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -887,9 +887,9 @@
 
 [[package]]
 name = "futures-channel"
-version = "0.3.32"
+version = "0.3.34"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
 dependencies = [
  "futures-core",
  "futures-sink",
@@ -897,15 +897,15 @@
 
 [[package]]
 name = "futures-core"
-version = "0.3.32"
+version = "0.3.34"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
 
 [[package]]
 name = "futures-executor"
-version = "0.3.32"
+version = "0.3.34"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
 dependencies = [
  "futures-core",
  "futures-task",
@@ -914,38 +914,38 @@
 
 [[package]]
 name = "futures-io"
-version = "0.3.32"
+version = "0.3.34"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
 
 [[package]]
 name = "futures-macro"
-version = "0.3.32"
+version = "0.3.34"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.117",
+ "syn 3.0.3",
 ]
 
 [[package]]
 name = "futures-sink"
-version = "0.3.32"
+version = "0.3.34"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
 
 [[package]]
 name = "futures-task"
-version = "0.3.32"
+version = "0.3.34"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
 
 [[package]]
 name = "futures-util"
-version = "0.3.32"
+version = "0.3.34"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -1648,9 +1648,9 @@
 
 [[package]]
 name = "libc"
-version = "0.2.186"
+version = "0.2.189"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
 
 [[package]]
 name = "libm"
@@ -1711,9 +1711,9 @@
 
 [[package]]
 name = "lru"
-version = "0.18.0"
+version = "0.18.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9"
+checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a"
 dependencies = [
  "hashbrown 0.17.0",
 ]
@@ -1736,12 +1736,11 @@
 
 [[package]]
 name = "maxminddb"
-version = "0.29.0"
+version = "0.30.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "65e84ef32bcbf18a95548989e880db4af6fafd563463753afb4b9a149fb2782c"
+checksum = "a7bd67e2f5d65937ff283e418fcad53c8e2eeda0719e455508b62dad8ffe6180"
 dependencies = [
  "ipnetwork 0.21.1",
- "log",
  "memchr",
  "serde",
  "thiserror 2.0.18",
@@ -2710,9 +2709,9 @@
 
 [[package]]
 name = "rkyv"
-version = "0.8.16"
+version = "0.8.18"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "73389e0c99e664f919275ab5b5b0471391fe9a8de61e1dff9b1eaf56a90f16e3"
+checksum = "d9776093b7ca170454ab1406954f7b7d97a57c51dc6c0642957fb2ef25c2d399"
 dependencies = [
  "bytecheck",
  "bytes",
@@ -2730,13 +2729,13 @@
 
 [[package]]
 name = "rkyv_derive"
-version = "0.8.16"
+version = "0.8.18"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "5d2ed0b54125315fb36bd021e82d314d1c126548f871634b483f46b31d13cac6"
+checksum = "1c25ef604ac7dd839d44d64648952ea23c97866f124ff671b0ed2cf3ad9bb06e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.117",
+ "syn 3.0.3",
 ]
 
 [[package]]
@@ -2923,9 +2922,9 @@
 
 [[package]]
 name = "serde"
-version = "1.0.228"
+version = "1.0.229"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
 dependencies = [
  "serde_core",
  "serde_derive",
@@ -2933,29 +2932,29 @@
 
 [[package]]
 name = "serde_core"
-version = "1.0.228"
+version = "1.0.229"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.228"
+version = "1.0.229"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.117",
+ "syn 3.0.3",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.150"
+version = "1.0.151"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
 dependencies = [
  "itoa",
  "memchr",
@@ -3098,9 +3097,9 @@
 
 [[package]]
 name = "socket2"
-version = "0.6.4"
+version = "0.6.5"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
 dependencies = [
  "libc",
  "windows-sys 0.61.2",
@@ -3174,6 +3173,17 @@
 ]
 
 [[package]]
+name = "syn"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
 name = "sync_wrapper"
 version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
@@ -3409,9 +3419,9 @@
 
 [[package]]
 name = "tokio"
-version = "1.52.3"
+version = "1.53.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
 dependencies = [
  "bytes",
  "libc",
@@ -3469,9 +3479,9 @@
 
 [[package]]
 name = "tokio-util"
-version = "0.7.18"
+version = "0.7.19"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
 dependencies = [
  "bytes",
  "futures-core",
@@ -3482,9 +3492,9 @@
 
 [[package]]
 name = "toml"
-version = "1.1.2+spec-1.1.0"
+version = "1.1.4+spec-1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
+checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5"
 dependencies = [
  "indexmap",
  "serde_core",
@@ -3506,18 +3516,18 @@
 
 [[package]]
 name = "toml_parser"
-version = "1.1.2+spec-1.1.0"
+version = "1.1.3+spec-1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
+checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
 dependencies = [
  "winnow",
 ]
 
 [[package]]
 name = "toml_writer"
-version = "1.1.1+spec-1.1.0"
+version = "1.1.2+spec-1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
+checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
 
 [[package]]
 name = "tower"
@@ -3601,7 +3611,7 @@
 
 [[package]]
 name = "ttl"
-version = "0.21.0"
+version = "0.22.0"
 dependencies = [
  "anyhow",
  "chrono",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ttl-0.21.0/Cargo.toml new/ttl-0.22.0/Cargo.toml
--- old/ttl-0.21.0/Cargo.toml   2026-07-07 16:42:22.000000000 +0200
+++ new/ttl-0.22.0/Cargo.toml   2026-08-24 18:31:53.000000000 +0200
@@ -1,6 +1,6 @@
 [package]
 name = "ttl"
-version = "0.21.0"
+version = "0.22.0"
 edition = "2024"
 rust-version = "1.88"
 description = "Modern traceroute/mtr-style TUI with hop stats and optional 
ASN/geo enrichment"
@@ -8,6 +8,9 @@
 repository = "https://github.com/lance0/ttl";
 keywords = ["traceroute", "mtr", "network", "tui", "diagnostics"]
 categories = ["command-line-utilities", "network-programming"]
+# Keep the published crate lean: the README demo assets live in the git repo
+# only (crates.io renders README images from the repo, not the tarball).
+exclude = ["docs/*.gif", "docs/*.tape"]
 
 [features]
 default = ["update-check"]
@@ -42,7 +45,7 @@
 reqwest = { version = "0.13", default-features = false, features = ["json", 
"rustls"] }
 
 # ASN/Geo databases
-maxminddb = "0.29"
+maxminddb = "0.30"
 ipnetwork = "0.21"
 
 # Serialization
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ttl-0.21.0/ROADMAP.md new/ttl-0.22.0/ROADMAP.md
--- old/ttl-0.21.0/ROADMAP.md   2026-07-07 16:42:22.000000000 +0200
+++ new/ttl-0.22.0/ROADMAP.md   2026-08-24 18:31:53.000000000 +0200
@@ -105,6 +105,14 @@
 
 ---
 
+## Completed (v0.22.0)
+
+- [x] TUI renders only on change (#129, #131): a per-tick render fingerprint 
skips the session snapshot and the redraw when nothing moved — 1.4% → 0.4% of 
one CPU core on a 9-hop trace, scaling with hop count. Session mutations mark 
the view dirty through the lock's write guard, so no writer can leave the 
display stale.
+- [x] Published crate excludes the demo GIF/tape, halving the crates.io 
tarball (1.14 MB → 0.56 MB compressed)
+- [x] Security: rkyv 0.8.18 (RUSTSEC-2026-0233/0234/0235) and lru 0.18.2 
(RUSTSEC-2026-0253)
+
+---
+
 ## Planned Features
 
 ### Before next release — TTL send-path correctness (follow-ups to #12)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ttl-0.21.0/src/main.rs new/ttl-0.22.0/src/main.rs
--- old/ttl-0.21.0/src/main.rs  2026-07-07 16:42:22.000000000 +0200
+++ new/ttl-0.22.0/src/main.rs  2026-08-24 18:31:53.000000000 +0200
@@ -36,7 +36,7 @@
     InterfaceInfo, check_permissions, create_send_socket_with_interface, 
detect_default_gateway,
     get_local_addr_with_interface, validate_interface,
 };
-use state::{Session, Target, run_ratelimit_worker};
+use state::{Session, SessionLock, Target, run_ratelimit_worker};
 use trace::engine::ProbeEngine;
 use trace::pending::{PendingMap, new_pending_map};
 use trace::receiver::{ReceiverConfig, SessionMap, spawn_receiver};
@@ -112,7 +112,7 @@
 
     // Resolve all targets
     let mut targets: Vec<IpAddr> = Vec::new();
-    let mut sessions_map: HashMap<IpAddr, Arc<RwLock<Session>>> = 
HashMap::new();
+    let mut sessions_map: HashMap<IpAddr, Arc<SessionLock>> = HashMap::new();
     let config = Config::from(&args);
     let mut effective_flows_v4 = None;
     let mut effective_flows_v6 = None;
@@ -157,7 +157,7 @@
                 detect_default_gateway(ipv6)
             };
 
-            sessions_map.insert(resolved_ip, Arc::new(RwLock::new(session)));
+            sessions_map.insert(resolved_ip, 
Arc::new(SessionLock::new(session)));
             targets.push(resolved_ip);
         }
 
@@ -212,7 +212,7 @@
                 detect_default_gateway(ipv6)
             };
 
-            sessions_map.insert(resolved_ip, Arc::new(RwLock::new(session)));
+            sessions_map.insert(resolved_ip, 
Arc::new(SessionLock::new(session)));
             targets.push(resolved_ip);
         }
 
@@ -506,11 +506,11 @@
         };
 
         // Show in TUI
-        let state = Arc::new(RwLock::new(session_to_display));
+        let state = Arc::new(SessionLock::new(session_to_display));
         let cancel = CancellationToken::new();
 
         // Create SessionMap with single session
-        let mut sessions_map: HashMap<IpAddr, Arc<RwLock<Session>>> = 
HashMap::new();
+        let mut sessions_map: HashMap<IpAddr, Arc<SessionLock>> = 
HashMap::new();
         sessions_map.insert(target_ip, state);
         let sessions: SessionMap = Arc::new(RwLock::new(sessions_map));
         let targets = vec![target_ip];
@@ -1126,7 +1126,7 @@
             detect_default_gateway(ipv6)
         };
 
-        let state = Arc::new(RwLock::new(session));
+        let state = Arc::new(SessionLock::new(session));
         ctx.sessions.write().insert(ip, state.clone());
 
         // Spawn a receiver if this IP family doesn't have one yet
@@ -1674,7 +1674,7 @@
                 if session.events.is_empty() {
                     continue;
                 }
-                session.events.drain(..).collect()
+                std::mem::take(&mut session.events)
             };
             for event in &events {
                 write_event_line(&mut out, *target_ip, event)?;
@@ -1822,7 +1822,7 @@
     }
 
     fn make_session_map(entries: &[(IpAddr, u8)]) -> SessionMap {
-        let mut map: HashMap<IpAddr, Arc<RwLock<Session>>> = HashMap::new();
+        let mut map: HashMap<IpAddr, Arc<SessionLock>> = HashMap::new();
         for (target_ip, flows) in entries {
             let mut target = Target::new(target_ip.to_string(), *target_ip);
             target.hostname = Some(format!("host-{}", target_ip));
@@ -1832,7 +1832,7 @@
             };
             map.insert(
                 *target_ip,
-                Arc::new(RwLock::new(Session::new(target, config))),
+                Arc::new(SessionLock::new(Session::new(target, config))),
             );
         }
         Arc::new(RwLock::new(map))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ttl-0.21.0/src/metrics.rs 
new/ttl-0.22.0/src/metrics.rs
--- old/ttl-0.21.0/src/metrics.rs       2026-07-07 16:42:22.000000000 +0200
+++ new/ttl-0.22.0/src/metrics.rs       2026-08-24 18:31:53.000000000 +0200
@@ -268,7 +268,7 @@
 mod tests {
     use super::*;
     use crate::config::Config;
-    use crate::state::{Session, Target};
+    use crate::state::{Session, SessionLock, Target};
     use parking_lot::RwLock;
     use std::collections::HashMap;
     use std::net::{IpAddr, Ipv4Addr};
@@ -292,7 +292,7 @@
         session.total_sent = 2;
 
         let mut map = HashMap::new();
-        map.insert(target_ip, Arc::new(RwLock::new(session)));
+        map.insert(target_ip, Arc::new(SessionLock::new(session)));
         Arc::new(RwLock::new(map))
     }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ttl-0.21.0/src/probe/correlate.rs 
new/ttl-0.22.0/src/probe/correlate.rs
--- old/ttl-0.21.0/src/probe/correlate.rs       2026-07-07 16:42:22.000000000 
+0200
+++ new/ttl-0.22.0/src/probe/correlate.rs       2026-08-24 18:31:53.000000000 
+0200
@@ -103,9 +103,8 @@
             let mut labels = Vec::new();
 
             // Each label entry is 4 bytes
-            for chunk in label_data.chunks_exact(4) {
-                // chunks_exact(4) guarantees chunk.len() == 4, so this is 
infallible
-                let bytes: [u8; 4] = [chunk[0], chunk[1], chunk[2], chunk[3]];
+            for chunk in label_data.as_chunks::<4>().0 {
+                let bytes: [u8; 4] = *chunk;
                 let label = MplsLabel::from_bytes(&bytes);
                 labels.push(label);
                 // Stop at bottom of stack
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ttl-0.21.0/src/state/session.rs 
new/ttl-0.22.0/src/state/session.rs
--- old/ttl-0.21.0/src/state/session.rs 2026-07-07 16:42:22.000000000 +0200
+++ new/ttl-0.22.0/src/state/session.rs 2026-08-24 18:31:53.000000000 +0200
@@ -2,8 +2,11 @@
 use serde::{Deserialize, Serialize};
 use std::collections::{HashMap, HashSet, VecDeque};
 use std::net::IpAddr;
+use std::ops::{Deref, DerefMut};
 use std::time::Duration;
 
+use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
+
 use crate::config::Config;
 
 /// Window size for recent RTT/result tracking (used for sparklines, jitter 
calculation)
@@ -1331,6 +1334,51 @@
     /// Recorded probe events for animated replay
     #[serde(default, skip_serializing_if = "Vec::is_empty")]
     pub events: Vec<ProbeEvent>,
+    /// Bumped every time a [`SessionLock`] write guard is released, so the TUI
+    /// can skip snapshot+draw on ticks where nothing changed. Not serialized.
+    #[serde(skip)]
+    pub render_generation: u64,
+}
+
+/// `RwLock<Session>` whose write guard bumps [`Session::render_generation`]
+/// on release, so every mutation path — hop stats, enrichment, PMTUD, pause —
+/// marks the TUI dirty without each writer having to remember to.
+pub struct SessionLock(RwLock<Session>);
+
+impl SessionLock {
+    pub fn new(session: Session) -> Self {
+        Self(RwLock::new(session))
+    }
+
+    pub fn read(&self) -> RwLockReadGuard<'_, Session> {
+        self.0.read()
+    }
+
+    pub fn write(&self) -> SessionWriteGuard<'_> {
+        SessionWriteGuard(self.0.write())
+    }
+}
+
+/// Write guard for [`SessionLock`]; bumps `render_generation` when dropped.
+pub struct SessionWriteGuard<'a>(RwLockWriteGuard<'a, Session>);
+
+impl Deref for SessionWriteGuard<'_> {
+    type Target = Session;
+    fn deref(&self) -> &Session {
+        &self.0
+    }
+}
+
+impl DerefMut for SessionWriteGuard<'_> {
+    fn deref_mut(&mut self) -> &mut Session {
+        &mut self.0
+    }
+}
+
+impl Drop for SessionWriteGuard<'_> {
+    fn drop(&mut self) {
+        self.0.render_generation = self.0.render_generation.wrapping_add(1);
+    }
 }
 
 impl Session {
@@ -1362,6 +1410,7 @@
             source_ip: None,
             gateway: None,
             events: Vec::new(),
+            render_generation: 0,
         }
     }
 
@@ -1507,6 +1556,7 @@
             source_ip: self.source_ip,
             gateway: self.gateway,
             events: Vec::new(),
+            render_generation: self.render_generation,
         }
     }
 
@@ -2607,6 +2657,28 @@
     }
 
     #[test]
+    fn test_session_lock_write_bumps_render_generation() {
+        let target = Target::new(
+            "test.com".to_string(),
+            IpAddr::V4(std::net::Ipv4Addr::new(1, 2, 3, 4)),
+        );
+        let lock = SessionLock::new(Session::new(target, Config::default()));
+        assert_eq!(lock.read().render_generation, 0);
+
+        // Any write-guard release bumps, regardless of which field changed.
+        lock.write().total_sent += 1;
+        assert_eq!(lock.read().render_generation, 1);
+        lock.write().reset_stats();
+        assert_eq!(lock.read().render_generation, 2);
+
+        // Reads never bump.
+        let snap = lock.read().snapshot_for_render();
+        assert_eq!(lock.read().render_generation, 2);
+        assert_eq!(snap.render_generation, 2);
+        assert!(snap.events.is_empty());
+    }
+
+    #[test]
     fn test_target_new() {
         let ip = IpAddr::V4(std::net::Ipv4Addr::new(8, 8, 8, 8));
         let target = Target::new("google.com".to_string(), ip);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ttl-0.21.0/src/trace/engine.rs 
new/ttl-0.22.0/src/trace/engine.rs
--- old/ttl-0.21.0/src/trace/engine.rs  2026-07-07 16:42:22.000000000 +0200
+++ new/ttl-0.22.0/src/trace/engine.rs  2026-08-24 18:31:53.000000000 +0200
@@ -1,5 +1,4 @@
 use anyhow::Result;
-use parking_lot::RwLock;
 use socket2::Socket;
 use std::net::{IpAddr, Ipv4Addr};
 use std::sync::Arc;
@@ -24,7 +23,7 @@
 use crate::probe::{enable_recv_ttl, parse_icmp_response, recv_icmp_with_ttl};
 #[cfg(target_os = "linux")]
 use crate::state::IcmpResponseType;
-use crate::state::{PmtudPhase, ProbeId, Session};
+use crate::state::{PmtudPhase, ProbeId, SessionLock};
 use crate::trace::pending::{PendingMap, PendingProbe};
 
 /// Safety cap for IPv6 echo-reply draining per tick.
@@ -37,7 +36,7 @@
     config: Config,
     target: IpAddr,
     identifier: u16,
-    state: Arc<RwLock<Session>>,
+    state: Arc<SessionLock>,
     pending: PendingMap,
     cancel: CancellationToken,
     interface: Option<InterfaceInfo>,
@@ -47,7 +46,7 @@
     pub fn new(
         config: Config,
         target: IpAddr,
-        state: Arc<RwLock<Session>>,
+        state: Arc<SessionLock>,
         pending: PendingMap,
         cancel: CancellationToken,
         interface: Option<InterfaceInfo>,
@@ -1509,6 +1508,7 @@
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::state::Session;
     use crate::state::Target;
     use crate::trace::pending::new_pending_map;
     use std::net::{IpAddr, Ipv4Addr};
@@ -1519,7 +1519,7 @@
             ..Config::default()
         };
         let target = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1));
-        let session = Arc::new(RwLock::new(Session::new(
+        let session = Arc::new(SessionLock::new(Session::new(
             Target::new("test".to_string(), target),
             config.clone(),
         )));
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ttl-0.21.0/src/trace/receiver.rs 
new/ttl-0.22.0/src/trace/receiver.rs
--- old/ttl-0.21.0/src/trace/receiver.rs        2026-07-07 16:42:22.000000000 
+0200
+++ new/ttl-0.22.0/src/trace/receiver.rs        2026-08-24 18:31:53.000000000 
+0200
@@ -11,12 +11,12 @@
     recv_icmp_with_ttl,
 };
 use crate::state::{
-    IcmpResponseType, MplsLabel, PmtudPhase, ProbeEvent, ProbeId, 
ProbeOutcome, Session,
+    IcmpResponseType, MplsLabel, PmtudPhase, ProbeEvent, ProbeId, 
ProbeOutcome, SessionLock,
 };
 use crate::trace::pending::{PendingKey, PendingMap, PendingProbe};
 
 /// Map of target IP to session, shared across multiple engines and the 
receiver
-pub type SessionMap = Arc<RwLock<HashMap<IpAddr, Arc<RwLock<Session>>>>>;
+pub type SessionMap = Arc<RwLock<HashMap<IpAddr, Arc<SessionLock>>>>;
 
 /// Configuration for the ICMP receiver
 #[derive(Clone)]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ttl-0.21.0/src/tui/app.rs 
new/ttl-0.22.0/src/tui/app.rs
--- old/ttl-0.21.0/src/tui/app.rs       2026-07-07 16:42:22.000000000 +0200
+++ new/ttl-0.22.0/src/tui/app.rs       2026-08-24 18:31:53.000000000 +0200
@@ -128,6 +128,186 @@
     }
 }
 
+/// Post-poll render fingerprint (LAN-1222). Built after status expiry,
+/// add-target, update check, IX cache status, and target-list refresh so
+/// idle skip cannot leave those chrome surfaces stale.
+#[derive(Clone, Debug, PartialEq, Eq)]
+struct RenderFingerprint {
+    session_generation: Option<u64>,
+    selected: Option<usize>,
+    paused: bool,
+    show_help: bool,
+    show_hop_detail: bool,
+    show_settings: bool,
+    show_target_list: bool,
+    show_target_input: bool,
+    theme_index: usize,
+    display_mode: crate::prefs::DisplayMode,
+    selected_target: usize,
+    num_targets: usize,
+    target_list_index: usize,
+    status: Option<String>,
+    update_available: Option<String>,
+    replay: Option<(bool, bool, usize, u64, u32)>,
+    target_input: (String, usize, Option<String>, bool),
+    settings: (
+        usize,
+        usize,
+        usize,
+        crate::prefs::DisplayMode,
+        String,
+        usize,
+        bool,
+    ),
+    ix_cache: Option<(bool, usize, Option<u64>, bool, bool)>,
+    target_list_rows: Option<Vec<(IpAddr, String, String, String)>>,
+}
+
+fn render_fingerprint(
+    ui_state: &UiState,
+    num_targets: usize,
+    session_generation: Option<u64>,
+    cache_status: Option<&crate::lookup::ix::CacheStatus>,
+) -> RenderFingerprint {
+    // Exhaustive destructures, deliberately without `..`: adding a field to
+    // any of these structs fails to compile here until it is either
+    // fingerprinted or explicitly bound to `_`, so new render-relevant state
+    // cannot silently render late.
+    let UiState {
+        selected,
+        paused,
+        show_help,
+        show_hop_detail,
+        show_settings,
+        settings,
+        status_message,
+        theme_index,
+        display_mode,
+        selected_target,
+        show_target_list,
+        target_list_index,
+        update_available,
+        update_rx: _,
+        replay_state,
+        target_list_cache,
+        target_list_tick: _,
+        show_target_input,
+        target_input,
+    } = ui_state;
+    let SettingsState {
+        selected_section,
+        theme_scroll,
+        theme_index: settings_theme_index,
+        display_mode: settings_display_mode,
+        api_key,
+        api_key_cursor,
+        update_check,
+    } = settings;
+    let TargetInputState {
+        input,
+        cursor,
+        error,
+        resolving,
+        pending: _,
+    } = target_input;
+    let replay = replay_state.as_ref().map(|r| {
+        let ReplayState {
+            events: _, // fixed after load
+            current_index,
+            replay_started_at: _, // both clock fields feed 
replay_current_ms(r)
+            speed_multiplier,
+            paused: replay_paused,
+            finished,
+            paused_at_elapsed_ms: _,
+        } = r;
+        (
+            *replay_paused,
+            *finished,
+            *current_index,
+            replay_current_ms(r),
+            speed_multiplier.to_bits(),
+        )
+    });
+    RenderFingerprint {
+        session_generation,
+        selected: *selected,
+        paused: *paused,
+        show_help: *show_help,
+        show_hop_detail: *show_hop_detail,
+        show_settings: *show_settings,
+        show_target_list: *show_target_list,
+        show_target_input: *show_target_input,
+        theme_index: *theme_index,
+        display_mode: *display_mode,
+        selected_target: *selected_target,
+        num_targets,
+        target_list_index: *target_list_index,
+        status: status_message.as_ref().map(|(m, _)| m.clone()),
+        update_available: update_available.clone(),
+        replay,
+        target_input: (input.clone(), *cursor, error.clone(), *resolving),
+        settings: (
+            *selected_section,
+            *theme_scroll,
+            *settings_theme_index,
+            *settings_display_mode,
+            api_key.clone(),
+            *api_key_cursor,
+            *update_check,
+        ),
+        ix_cache: cache_status.map(|c| {
+            let crate::lookup::ix::CacheStatus {
+                loaded,
+                prefix_count,
+                fetched_at,
+                expired,
+                refreshing,
+            } = c;
+            (*loaded, *prefix_count, *fetched_at, *expired, *refreshing)
+        }),
+        target_list_rows: target_list_cache.as_ref().map(|rows| {
+            rows.iter()
+                .map(|t| {
+                    let TargetInfo {
+                        ip,
+                        hostname,
+                        hops_str,
+                        loss_str,
+                    } = t;
+                    (*ip, hostname.clone(), hops_str.clone(), loss_str.clone())
+                })
+                .collect()
+        }),
+    }
+}
+
+fn poll_update_check(ui_state: &mut UiState) {
+    if let (None, Some(rx)) = (&ui_state.update_available, 
&ui_state.update_rx) {
+        match rx.try_recv() {
+            Ok(result) => {
+                ui_state.update_available = result;
+                ui_state.update_rx = None;
+            }
+            Err(std::sync::mpsc::TryRecvError::Disconnected) => {
+                ui_state.update_rx = None;
+            }
+            Err(std::sync::mpsc::TryRecvError::Empty) => {}
+        }
+    }
+}
+
+fn refresh_target_list_cache(ui_state: &mut UiState, sessions: &SessionMap, 
targets: &[IpAddr]) {
+    if ui_state.show_target_list {
+        ui_state.target_list_tick += 1;
+        if ui_state.target_list_cache.is_none() || 
ui_state.target_list_tick.is_multiple_of(30) {
+            ui_state.target_list_cache = Some(extract_target_infos(sessions, 
targets));
+        }
+    } else {
+        ui_state.target_list_cache = None;
+        ui_state.target_list_tick = 0;
+    }
+}
+
 /// Run the TUI application. Returns the final preferences for persistence.
 #[allow(clippy::too_many_arguments)]
 pub async fn run_tui(
@@ -527,6 +707,7 @@
 {
     let theme_names = Theme::list();
     let event_rx = spawn_event_reader(tick_rate, cancel.clone());
+    let mut last_draw: Option<RenderFingerprint> = None;
     let ix_enabled = ix_lookup.is_some();
 
     loop {
@@ -545,20 +726,7 @@
         // Compute count AFTER polling so newly added targets are reflected 
this tick.
         let num_targets = targets.len();
 
-        // Poll for update check result (non-blocking)
-        // Drop receiver once we get any result (Some or None) or sender 
disconnects
-        if let (None, Some(rx)) = (&ui_state.update_available, 
&ui_state.update_rx) {
-            match rx.try_recv() {
-                Ok(result) => {
-                    ui_state.update_available = result;
-                    ui_state.update_rx = None;
-                }
-                Err(std::sync::mpsc::TryRecvError::Disconnected) => {
-                    ui_state.update_rx = None;
-                }
-                Err(std::sync::mpsc::TryRecvError::Empty) => {}
-            }
-        }
+        poll_update_check(ui_state);
 
         // Process replay animation tick
         if let Some(ref mut replay) = ui_state.replay_state
@@ -613,50 +781,61 @@
         // Get cache status for settings modal
         let cache_status = ix_lookup.as_ref().map(|ix| ix.get_cache_status());
 
-        // Snapshot session data BEFORE draw so no locks are held during 
rendering.
-        // Uses snapshot_for_render() to skip cloning the events vec 
(unbounded, not used in render).
-        let session_snapshot = {
+        // Snapshot only when dirty so no locks are held during rendering.
+        refresh_target_list_cache(ui_state, &sessions, &targets);
+
+        // Fingerprint after polls so status expiry, add-target, update
+        // check, IX cache, and target-list rows participate (LAN-1222).
+        let session_generation = {
             let sessions_read = sessions.read();
             sessions_read
                 .get(&current_target)
-                .map(|state| state.read().snapshot_for_render())
+                .map(|state| state.read().render_generation)
         };
+        let fingerprint = render_fingerprint(
+            ui_state,
+            num_targets,
+            session_generation,
+            cache_status.as_ref(),
+        );
+        let dirty = last_draw.as_ref() != Some(&fingerprint);
 
-        // Refresh target list cache (~every 500ms while overlay is open)
-        if ui_state.show_target_list {
-            ui_state.target_list_tick += 1;
-            if ui_state.target_list_cache.is_none() || 
ui_state.target_list_tick.is_multiple_of(30)
-            {
-                ui_state.target_list_cache = 
Some(extract_target_infos(&sessions, &targets));
+        if dirty {
+            let session_snapshot = {
+                let sessions_read = sessions.read();
+                sessions_read
+                    .get(&current_target)
+                    .map(|state| state.read().snapshot_for_render())
+            };
+
+            if let Some(ref session) = session_snapshot {
+                terminal.draw(|f| {
+                    draw_ui(
+                        f,
+                        session,
+                        ui_state,
+                        &theme,
+                        num_targets,
+                        cache_status.clone(),
+                        ix_enabled,
+                    );
+                })?;
+            } else {
+                terminal.draw(|f| {
+                    draw_empty_state(f, ui_state, &theme, 
cache_status.clone(), ix_enabled);
+                })?;
             }
-        } else {
-            ui_state.target_list_cache = None;
-            ui_state.target_list_tick = 0;
+            last_draw = Some(fingerprint);
         }
-
-        // Draw (no locks held — all data is pre-extracted snapshots)
-        if let Some(ref session) = session_snapshot {
-            terminal.draw(|f| {
-                draw_ui(
-                    f,
-                    session,
-                    ui_state,
-                    &theme,
-                    num_targets,
-                    cache_status.clone(),
-                    ix_enabled,
-                );
-            })?;
-        } else {
-            // No targets yet (interactive empty mode)
-            terminal.draw(|f| {
-                draw_empty_state(f, ui_state, &theme, cache_status.clone(), 
ix_enabled);
-            })?;
-        }
-
         // Handle input — non-blocking: the event-reader thread forwards
         // crossterm events over the channel so we never stall the async 
runtime.
-        if let Ok(Event::Key(key)) = event_rx.try_recv() {
+        let event = event_rx.try_recv().ok();
+        // A resize changes the layout without touching any fingerprinted 
state,
+        // so force the next tick to draw (terminal.draw re-measures the area).
+        if matches!(event, Some(Event::Resize(..))) {
+            last_draw = None;
+        }
+        if let Some(Event::Key(key)) = event {
             if key.kind != KeyEventKind::Press {
                 continue;
             }
@@ -1454,6 +1633,7 @@
 mod tests {
     use super::*;
     use crate::config::Config;
+    use crate::state::SessionLock;
     use crate::state::Target;
     use std::collections::HashMap;
     use std::net::{IpAddr, Ipv4Addr};
@@ -1461,7 +1641,7 @@
 
     fn make_single_session_map(target: IpAddr, session: Session) -> SessionMap 
{
         let mut map = HashMap::new();
-        map.insert(target, Arc::new(parking_lot::RwLock::new(session)));
+        map.insert(target, Arc::new(SessionLock::new(session)));
         Arc::new(parking_lot::RwLock::new(map))
     }
 
@@ -1585,4 +1765,135 @@
         assert_eq!(replay.current_index, 0);
         assert!(replay.paused);
     }
+
+    #[test]
+    fn test_fingerprint_changes_when_status_expires() {
+        let mut ui_state = UiState {
+            status_message: Some((
+                "Working".to_string(),
+                std::time::Instant::now() - Duration::from_secs(4),
+            )),
+            ..UiState::default()
+        };
+        let before = render_fingerprint(&ui_state, 1, Some(0), None);
+        ui_state.clear_old_status();
+        let after = render_fingerprint(&ui_state, 1, Some(0), None);
+        assert!(ui_state.status_message.is_none());
+        assert_ne!(before, after);
+    }
+
+    #[test]
+    fn test_fingerprint_changes_on_add_target_poll() {
+        let existing = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1));
+        let added = IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8));
+        let mut targets = vec![existing];
+        let mut ui_state = UiState {
+            target_input: TargetInputState {
+                resolving: true,
+                ..TargetInputState::default()
+            },
+            show_target_input: true,
+            ..UiState::default()
+        };
+        let (tx, rx) = tokio::sync::oneshot::channel();
+        ui_state.target_input.pending = Some(rx);
+        let before = render_fingerprint(&ui_state, targets.len(), Some(0), 
None);
+        tx.send(Ok(crate::tui::views::AddedTarget {
+            ip: added,
+            name: "dns.google".to_string(),
+            existed: false,
+        }))
+        .unwrap();
+        poll_add_target_result(&mut ui_state, &mut targets);
+        let after = render_fingerprint(&ui_state, targets.len(), Some(0), 
None);
+        assert_eq!(targets, vec![existing, added]);
+        assert_eq!(ui_state.selected_target, 1);
+        assert!(!ui_state.show_target_input);
+        assert!(ui_state.status_message.is_some());
+        assert_ne!(before, after);
+    }
+
+    #[test]
+    fn test_fingerprint_changes_on_update_check_recv() {
+        let mut ui_state = UiState::default();
+        let (tx, rx) = std::sync::mpsc::channel();
+        ui_state.update_rx = Some(rx);
+        let before = render_fingerprint(&ui_state, 1, Some(0), None);
+        tx.send(Some("0.22.0".to_string())).unwrap();
+        poll_update_check(&mut ui_state);
+        let after = render_fingerprint(&ui_state, 1, Some(0), None);
+        assert_eq!(ui_state.update_available.as_deref(), Some("0.22.0"));
+        assert!(ui_state.update_rx.is_none());
+        assert_ne!(before, after);
+    }
+
+    #[test]
+    fn test_fingerprint_changes_when_open_target_list_refreshes() {
+        let current = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1));
+        let other = IpAddr::V4(Ipv4Addr::new(9, 9, 9, 9));
+        let current_session =
+            Session::new(Target::new("a".to_string(), current), 
Config::default());
+        let other_session = Session::new(Target::new("b".to_string(), other), 
Config::default());
+        let mut map = HashMap::new();
+        map.insert(current, Arc::new(SessionLock::new(current_session)));
+        map.insert(other, Arc::new(SessionLock::new(other_session)));
+        let sessions: SessionMap = Arc::new(parking_lot::RwLock::new(map));
+        let targets = vec![current, other];
+
+        let mut ui_state = UiState {
+            show_target_list: true,
+            target_list_tick: 29,
+            target_list_cache: Some(extract_target_infos(&sessions, &targets)),
+            ..UiState::default()
+        };
+        let current_gen = sessions
+            .read()
+            .get(&current)
+            .unwrap()
+            .read()
+            .render_generation;
+        let before = render_fingerprint(&ui_state, targets.len(), 
Some(current_gen), None);
+
+        {
+            let sessions_read = sessions.read();
+            let mut other_session = sessions_read.get(&other).unwrap().write();
+            other_session.dest_ttl = Some(1);
+            if let Some(hop) = other_session.hop_mut(1) {
+                hop.sent = 10;
+                hop.received = 5;
+            }
+        }
+
+        refresh_target_list_cache(&mut ui_state, &sessions, &targets);
+        let current_gen_after = sessions
+            .read()
+            .get(&current)
+            .unwrap()
+            .read()
+            .render_generation;
+        let after = render_fingerprint(&ui_state, targets.len(), 
Some(current_gen_after), None);
+
+        assert_eq!(current_gen, current_gen_after);
+        assert_eq!(ui_state.target_list_tick, 30);
+        assert_ne!(before, after);
+    }
+
+    #[test]
+    fn test_fingerprint_includes_ix_fetched_at() {
+        let stale = crate::lookup::ix::CacheStatus {
+            loaded: true,
+            prefix_count: 3,
+            fetched_at: Some(1),
+            expired: false,
+            refreshing: false,
+        };
+        let fresh = crate::lookup::ix::CacheStatus {
+            fetched_at: Some(2),
+            ..stale.clone()
+        };
+        let ui_state = UiState::default();
+        let a = render_fingerprint(&ui_state, 0, None, Some(&stale));
+        let b = render_fingerprint(&ui_state, 0, None, Some(&fresh));
+        assert_ne!(a, b);
+    }
 }

++++++ ttl.obsinfo ++++++
--- /var/tmp/diff_new_pack.c14MWX/_old  2026-09-04 12:39:16.927207539 +0200
+++ /var/tmp/diff_new_pack.c14MWX/_new  2026-09-04 12:39:16.944208136 +0200
@@ -1,5 +1,5 @@
 name: ttl
-version: 0.21.0
-mtime: 1783435342
-commit: 46f1dc2cd8c055408a764d5674afa897ca7cd30c
+version: 0.22.0
+mtime: 1787589113
+commit: 987b926707b3ae64de54793c49c7bae104498402
 

++++++ vendor.tar.zst ++++++
/work/SRC/openSUSE:Factory/ttl/vendor.tar.zst 
/work/SRC/openSUSE:Factory/.ttl.new.1265/vendor.tar.zst differ: char 7, line 1

Reply via email to