From 50d6f2e57201babd10def4e9638680356cf6d718 Mon Sep 17 00:00:00 2001 From: Arthur Date: Sat, 26 Sep 2026 17:24:51 +0200 Subject: [PATCH] fix(messages): open Element on localhost here and in the apps elsewhere In port mode the installer and `stack up messages` sent the browser to the Mac's LAN address. That address is plain HTTP, which is not a secure context, so Element in the browser showed "does not support this browser" and never finished loading, on the server Mac as on any other computer. The advice to click "Continue anyway" led to the same endless spinner. The address printed for the browser is now `http://localhost:42030` in port mode, through a new `{browser_url}` template variable (the public address in domain mode). Phones are sent to Element X, other computers to the Element desktop app, both with the server's full address, `http://` included: the apps do not need a secure context. The admin guide covers the desktop app and drops the "Continue anyway" advice, and the guides say to turn on threads in Element X, where the archivist's replies and the diary cards live. Closes: #147 --- docs/admin-guide.md | 26 +++++++++++++++++--- docs/agent/ops.md | 2 +- docs/stack-reference.md | 1 + docs/user-guide.md | 3 +++ lib/stack/installer.py | 21 +++++++++++++--- lib/stack/stack.py | 10 +++++++- stacklets/messages/stacklet.toml | 5 ++-- tests/e2e/install/test_first_install.py | 19 ++++++++++++-- tests/unit/framework/test_stack_lifecycle.py | 20 +++++++++++++++ 9 files changed, 94 insertions(+), 13 deletions(-) diff --git a/docs/admin-guide.md b/docs/admin-guide.md index e3492e3e..cf57c731 100644 --- a/docs/admin-guide.md +++ b/docs/admin-guide.md @@ -197,7 +197,9 @@ If something fails partway through, fix the underlying issue (usually Docker not ### 4. Sign in (browser) -Open the URL the installer printed. You should see Element. If your browser warns "this browser is not supported", click Continue anyway. Element works in Safari, Chrome, Firefox and Edge despite the warning. +On the Mac that runs famstack, open the address the installer printed: `http://localhost:42030` in port mode. You should see Element. + +Element runs in a browser only over HTTPS or on `localhost`. In port mode the Mac's LAN address is plain HTTP, so on any other computer the browser shows "does not support this browser" and Element never finishes loading. Other computers and phones use the Element apps instead (below), or you set up domain mode with HTTPS. - **Username**: your first name, lowercase. - **Password**: same as the username. @@ -227,17 +229,31 @@ Sign-in flow: http://:42031 ``` - That is the Synapse port (`42031`), not the Element web port (`42030`). Phones talk to Synapse directly. + Type it in full, with `http://` at the front: without it the app tries HTTPS and cannot connect. That is the Synapse port (`42031`), not the Element web port (`42030`). Phones talk to Synapse directly. 3. Element X warns the connection is not HTTPS. Correct. famstack runs unencrypted on your LAN by default, because trusting your own router is fine and Let's Encrypt does not issue certs for `192.168.x.x`. Confirm and continue. (For HTTPS, set up `domain` mode and Caddy will issue certs. See [stack-reference.md](stack-reference.md).) 4. Username: first name, lowercase. Password: same, unless changed in the browser. 5. Set up encryption. First device on the account: **Reset identity**. Subsequent devices: **Verify** with the QR-code flow from an already-signed-in session. 6. Allow notifications when prompted. Without this, you do not get pings for new messages. +7. Turn on threads: **Settings > Labs > Threads**, then restart the app. The archivist answers in threads and takes corrections there, and the diary cards sit in the thread under each memory. Without it, Element X shows those replies as a flat list. It is a beta in Element X, and works. Verify: send a message from the phone. It should appear in the browser session within a second. Reverse it. If both directions work, you are done. If only one direction works, it is almost always a notification permission issue, not a Matrix issue. Repeat for every family member. Each one logs in with their own first-name account. +#### Element (other computers) + +The browser on another computer cannot run Element in port mode (see [Sign in](#4-sign-in-browser)). Download the Element desktop app from [element.io/download](https://element.io/download) instead (the "Desktop" section; there is no Element X for computers): + +1. Open Element and click **Sign in**. +2. Next to the homeserver, click **Edit** and enter the server in full, with `http://` at the front: + + ``` + http://:42031 + ``` + +3. Sign in with your first name, lowercase, and your password. + #### Immich (photos) Once you bring up the photos stacklet (next section), install **Immich** on every phone: @@ -1083,7 +1099,11 @@ Stop it, or change the port mapping in `stacklets//docker-compose.yml`. ### Element shows "this browser is not supported" -Click Continue anyway. Element works in every modern browser. The warning is from an outdated check. +The browser is fine; the address is the problem. Element needs HTTPS or `localhost`, and in port mode the Mac's LAN address is plain HTTP. "Continue anyway" then leads to a page that never finishes loading. + +- On the Mac that runs famstack: open `http://localhost:42030`. +- On any other computer: use the Element desktop app, see [Element (other computers)](#element-other-computers). +- For Element in the browser everywhere: domain mode with HTTPS (`dns_provider`). ### Phones cannot reach the server diff --git a/docs/agent/ops.md b/docs/agent/ops.md index 45204451..16b07923 100644 --- a/docs/agent/ops.md +++ b/docs/agent/ops.md @@ -112,7 +112,7 @@ Port collisions: do not silently rebind. Surface them. The user's fix is "stop t | `stack list` shows ai as `remote` | `[ai] provider` in `stack.toml` | The stack uses an AI server elsewhere (`stack ai connect`). `stack up ai` asks before switching back to this Mac. | | Voice messages fail on a remote AI setup | `./stack ai connect ` output, "Voice messages" line | The AI server lists no speech-to-text model. Pass `--whisper ` for a speech server. | | Disk full | `./stack host` | Likely the photo library. Move `data_dir` to external SSD. | -| Element warns "browser not supported" | n/a | Click Continue. The check is outdated; Element works in every modern browser. | +| Element warns "browser not supported", then never loads | Which address is open? | Port mode over the LAN IP: plain HTTP is not a secure context. On the server Mac open `http://localhost:42030`; other computers use the Element desktop app with `http://:42031`. | | Wiki pages stale after filings | `docker logs stack-memory-curator` | Curator debounces (~3 min quiet) before rebuilding; topic pages wait for the nightly sweep. Manual override: `./stack memory wiki`. | | Wiki shows old content entirely | `docker logs stack-memory-curator` | The curator owns the vault `git pull` (the wiki container is a read-only view). Curator down/stuck → vault and wiki go stale together. | | Curator logs "waiting for vault" | `./stack status` → memory, code | Vault not cloned yet — the memory install hooks own the initial clone. | diff --git a/docs/stack-reference.md b/docs/stack-reference.md index ea96cbb2..c89fbec9 100644 --- a/docs/stack-reference.md +++ b/docs/stack-reference.md @@ -203,6 +203,7 @@ Available template variables: | `{ai_tts_voice}` | Derived from `[ai].language` | | `{messages_server_name}` | `stack.toml` → `[messages].server_name` | | `{url}`, `{_url}`, `{home_url}` | Public URLs: `http://:` in port mode; `https://.` in domain mode once `[core].dns_provider` is set, `http://` before that | +| `{browser_url}` | The address to open in a browser on this Mac: `{url}` in domain mode, `http://localhost:` in port mode, because a web app that needs WebCrypto (Element) only runs over HTTPS or on localhost | | `{url_host}` | The host of `{url}`: `.` in domain mode, `{ip}` in port mode. For a protocol the stacklet serves under the same name besides HTTP, such as Forgejo's SSH | | `{__}` | A secret from `secrets.toml`, e.g. `{docs__API_TOKEN}`, `{infra__DNS_API_TOKEN}` | | `{oidc_issuer}`, `{oidc_client_id}`, `{oidc_client_secret}` | Single sign-on for a stacklet with an `[oidc]` table, see [Single Sign-On](#single-sign-on-oidc). All three empty until the provider has registered the stacklet | diff --git a/docs/user-guide.md b/docs/user-guide.md index ec9ae75d..6623589a 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -18,6 +18,9 @@ Setting the server up, or keeping it running? That is the [Admin Guide](admin-gu > [!TIP] > **Try the Memories Room.** Record a voice message for your kids tonight, and tomorrow morning it is in your family diary. After four months of recording memos and our kids' voices, it has already become one of our most valuable artifacts. [How it works](#the-memories-room) +> [!TIP] +> **On your phone, turn on threads.** In Element X: **Settings > Labs > Threads**, then close and reopen the app. The archivist answers in threads, you correct it there, and each diary card sits in the thread under its memory. Without threads those replies land in one flat list. + ## How do I...? | You want to | Go to | diff --git a/lib/stack/installer.py b/lib/stack/installer.py index 0fe6fc97..9b9f6cd4 100644 --- a/lib/stack/installer.py +++ b/lib/stack/installer.py @@ -653,13 +653,18 @@ def wizard(): admin_id = uid2(admin) # Resolve URLs from the Stack instance - messages_url = stck._public_url("messages", 42030) + # The browser address is localhost in port mode: Element only runs in + # a secure context, and a LAN address over plain HTTP is not one. + # Phones and other computers use the Element apps with the server's + # address instead. + messages_url = stck._own_template_vars("messages", 42030)["browser_url"] + server_url = stck._public_url("messages", 42031) # ── Step-by-step guide ──────────────────────────────────────────── rule() nl() - bold("1. Open your browser") + bold("1. Open your browser on this Mac") nl() if messages_url: out(f" {BOLD}{TEAL}{messages_url}{RESET}") @@ -668,14 +673,22 @@ def wizard(): bold("2. Sign in") nl() out(f" Welcome to Element. Press {BOLD}Sign in{RESET}.") - out(" If you see 'does not support this browser', click Continue anyway.") nl() out(f" Username {BOLD}{TEAL}{admin_id}{RESET}") out(f" Password {BOLD}{TEAL}{admin_id}{RESET}") dim(" (your first name, lowercase — change it after login)") nl() - bold("3. Explore your rooms") + bold("3. Phones and other computers") + nl() + out(f" On phones: install {BOLD}Element X{RESET} (App Store / Play Store).") + out(f" On other computers: the {BOLD}Element{RESET} desktop app, {TEAL}element.io/download{RESET}") + out(" In both, enter as the server") + out(f" {BOLD}{TEAL}{server_url}{RESET} " + f"{DIM}with {server_url.split('://')[0]}:// at the front{RESET}") + nl() + + bold("4. Explore your rooms") nl() out(f" {TEAL}#famchat{RESET} Your private family conversations") out(f" {TEAL}#famstack{RESET} Notifications about your server") diff --git a/lib/stack/stack.py b/lib/stack/stack.py index c9e90251..c313c69f 100644 --- a/lib/stack/stack.py +++ b/lib/stack/stack.py @@ -521,10 +521,18 @@ def _own_template_vars(self, stacklet_id: str, port: int) -> dict: `{url_host}` is the host of `{url}`: `.` in domain mode, the address `{ip}` names in port mode. It is for a stacklet that serves another protocol under the same name, such as SSH. + + `{browser_url}` is the address to open in a browser on this Mac. + A browser runs a web app that needs WebCrypto, such as Element, + only in a secure context: HTTPS, or localhost. The LAN address in + port mode is neither, so it is localhost there; other devices use + an app and `{url}`. """ from urllib.parse import urlsplit url = self._public_url(stacklet_id, port) - return {"url": url, "url_host": urlsplit(url).hostname or "", "ip": self._lan_ip()} + browser_url = url if self._cfg("core", "domain") else f"http://localhost:{port}" + return {"url": url, "url_host": urlsplit(url).hostname or "", "ip": self._lan_ip(), + "browser_url": browser_url} def _domain_scheme(self) -> str: """https once the proxy serves certificates, http until then. diff --git a/stacklets/messages/stacklet.toml b/stacklets/messages/stacklet.toml index 3f105ede..91469adf 100644 --- a/stacklets/messages/stacklet.toml +++ b/stacklets/messages/stacklet.toml @@ -28,8 +28,9 @@ port = 42030 login_field = "username" hints = [ - "Open {url} in your browser", - "On phones: install Element, use {messages_synapse_url} as the server", + "On this Mac: open {browser_url} in your browser", + "On phones: install Element X (App Store / Play Store). On other computers: the Element desktop app (element.io/download)", + "In both apps, enter {messages_synapse_url} as the server: the full address, starting with http", ] # Secondary ports — the main 'port' is Element (web client), but Synapse diff --git a/tests/e2e/install/test_first_install.py b/tests/e2e/install/test_first_install.py index 52f59822..8e0c865f 100644 --- a/tests/e2e/install/test_first_install.py +++ b/tests/e2e/install/test_first_install.py @@ -336,15 +336,30 @@ def test_messages_and_core_are_healthy(install): assert not bad, f"not healthy: {bad}" -def test_the_printed_address_serves_element_on_the_lan(install): +def test_the_browser_address_is_one_element_can_start_on(install): + # A browser runs Element only in a secure context: HTTPS or localhost. + # The LAN address over plain HTTP serves the page, but Element never + # finishes loading there, so the address printed for the browser on + # this Mac is localhost. url = printed_url(install) parts = urllib.parse.urlsplit(url) - assert parts.hostname == lan_ip(), f"{url} is not this Mac's LAN address {lan_ip()}" + assert parts.hostname == "localhost", f"{url} is not a secure context for Element in a browser" assert parts.port == 42030, f"{url} is not Element's port 42030" status, body = _http(url) assert status == 200 and b"Element" in body, f"{url} answered {status} without Element Web" +def test_the_app_address_is_the_server_on_the_lan(install): + # Phones and other computers use the Element apps. They need the full + # address, http:// included, and reach Synapse on the LAN directly. + urls = re.findall(r"https?://\S+", install.between("Phones and other computers", "Explore your rooms")) + assert len(urls) == 1, f"expected one server address for the apps, got {urls}" + parts = urllib.parse.urlsplit(urls[0]) + assert (parts.scheme, parts.hostname, parts.port) == ("http", lan_ip(), 42031), urls[0] + status, body = _http(f"{urls[0]}/_matrix/client/versions") + assert status == 200 and b"versions" in body, f"{urls[0]} is not answering as a Matrix server" + + def test_the_printed_login_works(install): user, password = printed_login(install) login(homeserver(printed_url(install)), user, password) diff --git a/tests/unit/framework/test_stack_lifecycle.py b/tests/unit/framework/test_stack_lifecycle.py index f4586e6b..d4e26c41 100644 --- a/tests/unit/framework/test_stack_lifecycle.py +++ b/tests/unit/framework/test_stack_lifecycle.py @@ -102,6 +102,26 @@ def test_renders_env(self, tmp_path): assert result["ok"] assert result["env"]["TZ"] == "Europe/Berlin" + def test_the_browser_address_is_localhost_in_port_mode(self, tmp_path): + """A browser runs Element only over HTTPS or on localhost; the LAN + address in port mode is plain HTTP, so the browser gets localhost.""" + manifest = ('id = "web"\nname = "Web"\nversion = "0.1.0"\ncategory = "test"\n' + 'port = 42030\n\n[env.defaults]\nOPEN = "{browser_url}"\nLAN = "{url}"\n') + s = _make_stack(tmp_path, {"web": {"manifest": manifest}}) + env = s.up("web")["env"] + assert env["OPEN"] == "http://localhost:42030" + assert env["LAN"] != env["OPEN"] + + def test_the_browser_address_is_the_public_one_in_domain_mode(self, tmp_path): + manifest = ('id = "web"\nname = "Web"\nversion = "0.1.0"\ncategory = "test"\n' + 'port = 42030\n\n[env.defaults]\nOPEN = "{browser_url}"\nLAN = "{url}"\n') + s = _make_stack(tmp_path, {"web": {"manifest": manifest}}) + toml = tmp_path / "stack.toml" + toml.write_text(toml.read_text().replace('domain = ""', 'domain = "home.example.family"')) + env = s.up("web")["env"] + assert env["OPEN"] == env["LAN"] + assert "home.example.family" in env["OPEN"] + def test_generates_secrets(self, tmp_path): """up() generates declared secrets on first run.""" s = _make_stack(tmp_path, {"myapp": {"generate": ["DB_PASSWORD"]}})