From 5b4efc87b38d99770fa794672947c03d93602dfe Mon Sep 17 00:00:00 2001 From: saphid <4596216+saphid@users.noreply.github.com> Date: Thu, 1 Oct 2026 23:03:08 +1000 Subject: [PATCH] Show toasts above open dialogs, and harden the superseded-test test From the review of the Reconnect change: - A toast raised while a dialog was open (Reconnect refused during an install, say) sat under the dialog's backdrop, dimmed. The toast is now a manual popover, raised again when a dialog has opened since, so it shows on top. - The new test could wait for ever if the first test never reached its probe, and didn't check its threads finished. It now waits on events with time limits, releases everything in finally, and also checks the first test finishing doesn't mark the second, still running, done. - docs/devices.md: Reconnect applies your changes by reconnecting; which address wins still depends on ranking and timing. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/devices.md | 7 +++---- tests/test_link.py | 42 ++++++++++++++++++++++++++++-------------- ui/index.html | 10 ++++++++-- 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/docs/devices.md b/docs/devices.md index 6dc4d09..4cfd823 100644 --- a/docs/devices.md +++ b/docs/devices.md @@ -79,10 +79,9 @@ the headset reports a LAN IP on the same network as this computer and that IP isn't saved, it offers to add it. The offer puts the address first in the list, so on that network it wins over the Tailscale name; away from home the Tailscale name still leads. A new or edited address is tested straight away. While -connected, **Reconnect** tries the addresses again in their current order, so an -address just added or moved up is used now rather than at the next connection. -It doesn't pick a particular address: the first to answer in order wins, and a -slow one loses to a later one. +connected, **Reconnect** applies your changes now rather than at the next +connection: it tries the addresses again, ranked as above, and the best-ranked +one that answers promptly wins. It doesn't pick a particular address. ## Networks diff --git a/tests/test_link.py b/tests/test_link.py index d098a85..2ceb1e1 100644 --- a/tests/test_link.py +++ b/tests/test_link.py @@ -459,27 +459,41 @@ class Connecting(unittest.TestCase): def test_a_test_started_earlier_cant_overwrite_a_newer_one(self): d = self.device("nothing.invalid") - release, calls = threading.Event(), [] + entered = {1: threading.Event(), 2: threading.Event()} + release = {1: threading.Event(), 2: threading.Event()} + calls = [] def probe(host, port, update=None): calls.append(host) - if len(calls) == 1: - release.wait(10) # the first test is still probing when the second finishes - return {"state": "refused", "detail": "first test", "ip": None, "rtt_ms": None} - return {"state": "refused", "detail": "second test", "ip": None, "rtt_ms": None} + n = len(calls) + entered[n].set() + release[n].wait(10) + return {"state": "refused", "detail": f"test {n}", "ip": None, "rtt_ms": None} + tests = [threading.Thread(target=self.link.test, args=(d["id"],), daemon=True) for _ in range(2)] with mock.patch.object(fl, "probe", probe): - first = threading.Thread(target=self.link.test, args=(d["id"],)) - first.start() - while not calls: - time.sleep(0.01) - self.link.test(d["id"]) - self.assertEqual(self.link.snapshot()["tests"][d["id"]]["rows"][0]["detail"], "second test") - release.set() - first.join(10) + try: + tests[0].start() + self.assertTrue(entered[1].wait(5), "the first test never probed") + tests[1].start() + self.assertTrue(entered[2].wait(5), "the second test never probed") + # The first finishes while the second is still probing: it mustn't show its + # rows or mark the second done. + release[1].set() + tests[0].join(10) + self.assertFalse(tests[0].is_alive()) + running = self.link.snapshot()["tests"][d["id"]] + self.assertFalse(running["done"]) + self.assertEqual(running["rows"][0]["detail"], "Waiting") + release[2].set() + tests[1].join(10) + self.assertFalse(tests[1].is_alive()) + finally: + for e in release.values(): + e.set() result = self.link.snapshot()["tests"][d["id"]] self.assertTrue(result["done"]) - self.assertEqual(result["rows"][0]["detail"], "second test") + self.assertEqual(result["rows"][0]["detail"], "test 2") def test_switching_to_a_headset_that_never_answers_stops_using_the_last_one(self): self.device("localhost") diff --git a/ui/index.html b/ui/index.html index 3246e28..9c02111 100644 --- a/ui/index.html +++ b/ui/index.html @@ -368,7 +368,7 @@ white-space: pre-wrap; padding: 12px calc(28px + env(safe-area-inset-right)) 12px calc(28px + env(safe-area-inset-left)); } .log:empty::before { content: "Actions you take here are logged with their output."; color: var(--dim); } .log .e { color: #ff8a73; } .log .ok { color: #a4d007; } .log time { color: var(--dim); } - .toast { position: fixed; bottom: calc(58px + env(safe-area-inset-bottom)); right: calc(28px + env(safe-area-inset-right)); z-index: 30; max-width: 460px; padding: 12px 16px; border-radius: 3px; + .toast { position: fixed; inset: auto; margin: 0; border: 0; overflow: visible; width: auto; height: auto; bottom: calc(58px + env(safe-area-inset-bottom)); right: calc(28px + env(safe-area-inset-right)); z-index: 30; max-width: 460px; padding: 12px 16px; border-radius: 3px; background: #3d4450; color: #fff; box-shadow: 0 10px 30px rgba(0,0,0,.5); border-left: 3px solid var(--blue); opacity: 0; transform: translateY(10px); transition: opacity .2s, transform .2s; pointer-events: none; } .toast.show { opacity: 1; transform: none; } @@ -1374,7 +1374,7 @@ -
+