Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions docs/admin-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -227,17 +229,31 @@ Sign-in flow:
http://<mac-lan-ip>: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://<mac-lan-ip>: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:
Expand Down Expand Up @@ -1083,7 +1099,11 @@ Stop it, or change the port mapping in `stacklets/<name>/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

Expand Down
2 changes: 1 addition & 1 deletion docs/agent/ops.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <url>` output, "Voice messages" line | The AI server lists no speech-to-text model. Pass `--whisper <url>` 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://<ip>: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. |
Expand Down
1 change: 1 addition & 0 deletions docs/stack-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ Available template variables:
| `{ai_tts_voice}` | Derived from `[ai].language` |
| `{messages_server_name}` | `stack.toml` → `[messages].server_name` |
| `{url}`, `{<id>_url}`, `{home_url}` | Public URLs: `http://<ip>:<port>` in port mode; `https://<id>.<domain>` 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:<port>` in port mode, because a web app that needs WebCrypto (Element) only runs over HTTPS or on localhost |
| `{url_host}` | The host of `{url}`: `<id>.<domain>` in domain mode, `{ip}` in port mode. For a protocol the stacklet serves under the same name besides HTTP, such as Forgejo's SSH |
| `{<id>__<NAME>}` | 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 |
Expand Down
3 changes: 3 additions & 0 deletions docs/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
21 changes: 17 additions & 4 deletions lib/stack/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand All @@ -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")
Expand Down
10 changes: 9 additions & 1 deletion lib/stack/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,10 +521,18 @@ def _own_template_vars(self, stacklet_id: str, port: int) -> dict:
`{url_host}` is the host of `{url}`: `<id>.<domain>` 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.
Expand Down
5 changes: 3 additions & 2 deletions stacklets/messages/stacklet.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 17 additions & 2 deletions tests/e2e/install/test_first_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 20 additions & 0 deletions tests/unit/framework/test_stack_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]}})
Expand Down
Loading