Merge branch 'ui-3-home' into ui-4-store

This commit is contained in:
saphid committed 2026-10-01 23:08:06 +10:00
commit cc9af9eba4
3 files changed
+39 -20

No files matched your search

+3 -4
View File
@@ -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, 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 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 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 connected, **Reconnect** applies your changes now rather than at the next
address just added or moved up is used now rather than at the next connection. connection: it tries the addresses again, ranked as above, and the best-ranked
It doesn't pick a particular address: the first to answer in order wins, and a one that answers promptly wins. It doesn't pick a particular address.
slow one loses to a later one.
## Networks ## Networks
+28 -14
View File
@@ -459,27 +459,41 @@ class Connecting(unittest.TestCase):
def test_a_test_started_earlier_cant_overwrite_a_newer_one(self): def test_a_test_started_earlier_cant_overwrite_a_newer_one(self):
d = self.device("nothing.invalid") 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): def probe(host, port, update=None):
calls.append(host) calls.append(host)
if len(calls) == 1: n = len(calls)
release.wait(10) # the first test is still probing when the second finishes entered[n].set()
return {"state": "refused", "detail": "first test", "ip": None, "rtt_ms": None} release[n].wait(10)
return {"state": "refused", "detail": "second test", "ip": None, "rtt_ms": None} 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): with mock.patch.object(fl, "probe", probe):
first = threading.Thread(target=self.link.test, args=(d["id"],)) try:
first.start() tests[0].start()
while not calls: self.assertTrue(entered[1].wait(5), "the first test never probed")
time.sleep(0.01) tests[1].start()
self.link.test(d["id"]) self.assertTrue(entered[2].wait(5), "the second test never probed")
self.assertEqual(self.link.snapshot()["tests"][d["id"]]["rows"][0]["detail"], "second test") # The first finishes while the second is still probing: it mustn't show its
release.set() # rows or mark the second done.
first.join(10) 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"]] result = self.link.snapshot()["tests"][d["id"]]
self.assertTrue(result["done"]) 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): def test_switching_to_a_headset_that_never_answers_stops_using_the_last_one(self):
self.device("localhost") self.device("localhost")
+8 -2
View File
@@ -394,7 +394,7 @@
white-space: pre-wrap; padding: 12px calc(28px + env(safe-area-inset-right)) 12px calc(28px + env(safe-area-inset-left)); } 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: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); } .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); 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; } opacity: 0; transform: translateY(10px); transition: opacity .2s, transform .2s; pointer-events: none; }
.toast.show { opacity: 1; transform: none; } .toast.show { opacity: 1; transform: none; }
@@ -1439,7 +1439,7 @@
<button type="submit" class="small danger" id="rmGo">Remove</button></div> <button type="submit" class="small danger" id="rmGo">Remove</button></div>
</form> </form>
</dialog> </dialog>
<div class="toast" id="toast"></div> <div class="toast" id="toast" popover="manual"></div>
<script> <script>
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
@@ -1483,6 +1483,12 @@ function toast(msg, err = false) {
const el = $("toast"); const el = $("toast");
el.textContent = msg.split("\n").slice(-1)[0] || msg; el.textContent = msg.split("\n").slice(-1)[0] || msg;
el.className = "toast show" + (err ? " err" : ""); el.className = "toast show" + (err ? " err" : "");
// A popover, so it shows above an open dialog (a modal one covers everything else): opened
// again when a dialog has opened since, which puts it back on top.
if (el.showPopover && (!el.matches(":popover-open") || document.querySelector("dialog:modal"))) {
try { el.hidePopover(); } catch {}
el.showPopover();
}
clearTimeout(toastTimer); clearTimeout(toastTimer);
toastTimer = setTimeout(() => el.className = "toast" + (err ? " err" : ""), 3500); toastTimer = setTimeout(() => el.className = "toast" + (err ? " err" : ""), 3500);
} }