Follow the sibling project's rename to dev-updater

It is no longer "local" -- it serves over WireGuard rather than the LAN --
and it is specifically for developing new apps. Renaming the references
here at the same time keeps one name to search for across both repos.

Also drops the last references to gen-dev-cert.sh, which the in-process
certificate generation replaced: the build script and the Gradle task now
say to start the server once, and test-wg-tunnel.sh reads the
certificates from the XDG directory rather than the repo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
This commit is contained in:
irisandClaude Fable 5 committed 2026-08-25 05:10:20 -04:00
1 parent 65743b899d
commit 56491f84b0
12 files changed
+42 -41

No files matched your search

+7 -7
View File
@@ -22,11 +22,11 @@ session-type branch in shared code (routes, transcript, app screens).
## Layout ## Layout
Mirrors `../local-updater` deliberately — same stack (axum 0.8 + Mirrors `../dev-updater` deliberately — same stack (axum 0.8 +
axum-server/rustls, tokio, clap; Kotlin 2.4.x + Compose Multiplatform, axum-server/rustls, tokio, clap; Kotlin 2.4.x + Compose Multiplatform,
single `:androidApp` module), same cert scheme, same registry pattern (every single `:androidApp` module), same cert scheme, same registry pattern (every
session mutation funnels through the manager so in-memory and on-disk state session mutation funnels through the manager so in-memory and on-disk state
can't come apart). Read local-updater's `README.md` and `AGENTS.md` for the can't come apart). Read dev-updater's `README.md` and `AGENTS.md` for the
conventions before diverging from them; module-by-module intent for this conventions before diverging from them; module-by-module intent for this
repo is in PLAN.md's "Backend layout" section. repo is in PLAN.md's "Backend layout" section.
@@ -62,7 +62,7 @@ real-phone/WireGuard bring-up, which is operational rather than code.
from `server/` — the build stays warning-clean, keep it that way. from `server/` — the build stays warning-clean, keep it that way.
- App: from `app/`, `. ./android-env.sh && ./gradlew :androidApp:compileDebugKotlin` - App: from `app/`, `. ./android-env.sh && ./gradlew :androidApp:compileDebugKotlin`
to typecheck; `./build-apk.sh` to produce the APK to install on a phone to typecheck; `./build-apk.sh` to produce the APK to install on a phone
(through Local Updater); `./run-android.sh` to build, install, and launch (through Dev Updater); `./run-android.sh` to build, install, and launch
on the emulator. on the emulator.
- **The APK pins the CA of the machine that builds it**, read at build time - **The APK pins the CA of the machine that builds it**, read at build time
from `$XDG_CONFIG_HOME/ai-app/certs/ca.pem` (`AI_APP_CA` overrides) and from `$XDG_CONFIG_HOME/ai-app/certs/ca.pem` (`AI_APP_CA` overrides) and
@@ -83,7 +83,7 @@ real-phone/WireGuard bring-up, which is operational rather than code.
Established 2026-08-25, and it decides more than it looks like: Established 2026-08-25, and it decides more than it looks like:
- **The host (192.168.1.168) is the backend machine.** It runs - **The host (192.168.1.168) is the backend machine.** It runs
local-updater's server today and is where `ai-server` belongs in dev-updater's server today and is where `ai-server` belongs in
production: it has the LAN address the phone can reach, and it's where production: it has the LAN address the phone can reach, and it's where
WireGuard terminates. `wg-setup-host.sh` sets that up (keys, `wg0.conf`, WireGuard terminates. `wg-setup-host.sh` sets that up (keys, `wg0.conf`,
the phone's QR); run it there with `sudo WG_ENDPOINT=<ddns name>`. the phone's QR); run it there with `sudo WG_ENDPOINT=<ddns name>`.
@@ -157,10 +157,10 @@ Established 2026-08-25, and it decides more than it looks like:
SourceDirectory(task, Task::outputDir) }`, which also carries the task SourceDirectory(task, Task::outputDir) }`, which also carries the task
dependency. dependency.
## Environment notes (this machine, learned in local-updater) ## Environment notes (this machine, learned in dev-updater)
- Android SDK is at `~/Android/Sdk`, not the root-owned `/opt/android-sdk` - Android SDK is at `~/Android/Sdk`, not the root-owned `/opt/android-sdk`
the ambient `$ANDROID_HOME` may point at; copy local-updater's the ambient `$ANDROID_HOME` may point at; copy dev-updater's
`android-env.sh` override pattern. `android-env.sh` override pattern.
- Each agent command runs in a fresh shell — exported environment does not - Each agent command runs in a fresh shell — exported environment does not
carry over. Chain: `cd app && . ./android-env.sh && ./gradlew …`. Never carry over. Chain: `cd app && . ./android-env.sh && ./gradlew …`. Never
@@ -171,5 +171,5 @@ Established 2026-08-25, and it decides more than it looks like:
(`setsid nohup … & disown -h`, verify `PPID 1`), and every (`setsid nohup … & disown -h`, verify `PPID 1`), and every
`pgrep -f`/`pkill -f` pattern needs its first character bracketed `pgrep -f`/`pkill -f` pattern needs its first character bracketed
(`[a]i-server`) in **every** occurrence in the command, or the pattern (`[a]i-server`) in **every** occurrence in the command, or the pattern
matches the shell running it. Full explanation in local-updater's matches the shell running it. Full explanation in dev-updater's
`AGENTS.md` — it bites exactly the same way here. `AGENTS.md` — it bites exactly the same way here.
+10 -10
View File
@@ -7,7 +7,7 @@ the official app gets wrong (e.g. it won't deliver a typed message until the
session fully finishes its turn, where the TUI injects it at the next tool session fully finishes its turn, where the TUI injects it at the next tool
boundary). boundary).
Same shape as `../local-updater`: a Rust (Axum) backend on the desktop, a Same shape as `../dev-updater`: a Rust (Axum) backend on the desktop, a
Kotlin/Compose Android app, pinned self-signed TLS between them. Kotlin/Compose Android app, pinned self-signed TLS between them.
## The one idea everything hangs off ## The one idea everything hangs off
@@ -81,12 +81,12 @@ backend (Rust/Axum, desktop)
### Backend layout (`server/`) ### Backend layout (`server/`)
Mirroring local-updater's stack: axum 0.8, axum-server + rustls, tokio, serde, Mirroring dev-updater's stack: axum 0.8, axum-server + rustls, tokio, serde,
clap, tracing. Rust edition 2024, warning-clean, clippy in CI habit. clap, tracing. Rust edition 2024, warning-clean, clippy in CI habit.
- `main.rs` — bootstrap, TLS listener. - `main.rs` — bootstrap, TLS listener.
- `routes.rs` — the whole HTTP table in one module doc comment (as in - `routes.rs` — the whole HTTP table in one module doc comment (as in
local-updater). dev-updater).
- `session/mod.rs``SessionManager`: the live session registry, every - `session/mod.rs``SessionManager`: the live session registry, every
mutation funnels through it (the `registry.rs` pattern: in-memory and mutation funnels through it (the `registry.rs` pattern: in-memory and
on-disk state can't come apart). on-disk state can't come apart).
@@ -260,14 +260,14 @@ complete path out of everything spawning one created.
### Security ### Security
- TLS with a self-signed CA, pinned in the app — same - TLS with a self-signed CA, pinned in the app — same
idempotent-CA/reissued-leaf scheme as local-updater, same one-way-door idempotent-CA/reissued-leaf scheme as dev-updater, same one-way-door
caveat about regenerating the CA, but generated **in process on first caveat about regenerating the CA, but generated **in process on first
start** (`certs.rs`) rather than by a shell script calling openssl start** (`certs.rs`) rather than by a shell script calling openssl
(2026-08-25). One place then decides the extensions, the file modes, and (2026-08-25). One place then decides the extensions, the file modes, and
which addresses the leaf covers — every local IPv4 plus loopback and the which addresses the leaf covers — every local IPv4 plus loopback and the
emulator's host alias, so nobody maintains a hardcoded IP — and there is emulator's host alias, so nobody maintains a hardcoded IP — and there is
no setup step to forget. no setup step to forget.
- Unlike local-updater, the pinned CA is **not a constant in the source**: - Unlike dev-updater, the pinned CA is **not a constant in the source**:
the build reads `$XDG_CONFIG_HOME/ai-app/certs/ca.pem` from the machine the build reads `$XDG_CONFIG_HOME/ai-app/certs/ca.pem` from the machine
doing the build and generates the constant (`generatePinnedCert` in doing the build and generates the constant (`generatePinnedCert` in
`app/androidApp/build.gradle.kts`; `AI_APP_CA` overrides). Decided `app/androidApp/build.gradle.kts`; `AI_APP_CA` overrides). Decided
@@ -392,12 +392,12 @@ complete path out of everything spawning one created.
a deliberate flag, never a fallback, so the fail-closed default is a deliberate flag, never a fallback, so the fail-closed default is
untouched (2026-08-24). untouched (2026-08-24).
- The bootstrap-over-HTTP trick from the updater is unnecessary here — the - The bootstrap-over-HTTP trick from the updater is unnecessary here — the
app installs via Local Updater. app installs via Dev Updater.
## App (`app/`) ## App (`app/`)
Kotlin + Compose Multiplatform, single `:androidApp` module, same versions as Kotlin + Compose Multiplatform, single `:androidApp` module, same versions as
local-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21). Screens: dev-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21). Screens:
1. **Session list** — cards: kind icon, title, host, model, status 1. **Session list** — cards: kind icon, title, host, model, status
(running / awaiting answer / idle / exited), last activity. Spawn FAB; (running / awaiting answer / idle / exited), last activity. Spawn FAB;
@@ -420,7 +420,7 @@ local-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21). Screens:
5. **Settings** — server address + token, hosts editor, llama model list 5. **Settings** — server address + token, hosts editor, llama model list
editor. editor.
Networking mirrors local-updater's app layer (`AppsApi.kt` style thin client + Networking mirrors dev-updater's app layer (`AppsApi.kt` style thin client +
pinned transport), plus an SSE client with `after=` resume driven by pinned transport), plus an SSE client with `after=` resume driven by
connectivity/lifecycle. The app keeps no persistent transcript store — the connectivity/lifecycle. The app keeps no persistent transcript store — the
backend's transcript is the source of truth; the app caches only for the backend's transcript is the source of truth; the app caches only for the
@@ -501,7 +501,7 @@ window just fills.
Each phase ends runnable and verified against the real thing (rule 22); the Each phase ends runnable and verified against the real thing (rule 22); the
backend gets tests where logic is pure (event normalization, transcript backend gets tests where logic is pure (event normalization, transcript
cursors, config persistence, refcounting) — the app is UI over the API and is cursors, config persistence, refcounting) — the app is UI over the API and is
verified by running it, matching local-updater's posture. verified by running it, matching dev-updater's posture.
## Open questions / risks ## Open questions / risks
@@ -539,6 +539,6 @@ installed versions when each phase starts):
≥180 s polling; wrong User-Agent → aggressive 429 bucket): ≥180 s polling; wrong User-Agent → aggressive 429 bucket):
https://github.com/anthropics/claude-code/issues/31637 and https://github.com/anthropics/claude-code/issues/31637 and
https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/issues/202 https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/issues/202
- Sibling project this repo's conventions mirror: `../local-updater` - Sibling project this repo's conventions mirror: `../dev-updater`
(README.md + AGENTS.md — server/registry/routes layout, cert scheme, (README.md + AGENTS.md — server/registry/routes layout, cert scheme,
testing posture, Android env notes). testing posture, Android env notes).
+3 -3
View File
@@ -6,7 +6,7 @@ plugins {
// The CA this app pins is baked in at build time from the certificates on // The CA this app pins is baked in at build time from the certificates on
// the machine doing the build -- `$XDG_CONFIG_HOME/ai-app/certs/ca.pem`, // the machine doing the build -- `$XDG_CONFIG_HOME/ai-app/certs/ca.pem`,
// as written by ../gen-dev-cert.sh. AI_APP_CA overrides the path. // which the server generates on first start. AI_APP_CA overrides the path.
// //
// Reading it rather than keeping a pasted copy in the source is what makes // Reading it rather than keeping a pasted copy in the source is what makes
// the trust boundary follow the build: an APK built on the backend host // the trust boundary follow the build: an APK built on the backend host
@@ -45,8 +45,8 @@ abstract class GeneratePinnedCert : DefaultTask() {
if (!ca.isFile) { if (!ca.isFile) {
throw GradleException( throw GradleException(
"No CA certificate at $path.\n" + "No CA certificate at $path.\n" +
"Run ./gen-dev-cert.sh on this machine first -- the app pins the CA it " + "Start ai-server once on this machine first -- it generates the CA the " +
"generates, and the certificate has to exist before an APK can embed it.\n" + "app pins, and the certificate has to exist before an APK can embed it.\n" +
"Set AI_APP_CA=/path/to/ca.pem to build against a different one.", "Set AI_APP_CA=/path/to/ca.pem to build against a different one.",
) )
} }
+1 -1
View File
@@ -6,7 +6,7 @@
network address, including a plain socket to a LAN IP literal. network address, including a plain socket to a LAN IP literal.
Without it the traffic is silently dropped, surfacing only as a Without it the traffic is silently dropped, surfacing only as a
connect timeout. See MainActivity.kt's runtime request, and connect timeout. See MainActivity.kt's runtime request, and
local-updater's manifest for the full story. --> dev-updater's manifest for the full story. -->
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" /> <uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
<application <application
@@ -36,7 +36,7 @@ class MainActivity : ComponentActivity() {
// Transparent status bar on every version; the Surface below paints // Transparent status bar on every version; the Surface below paints
// through underneath it and content insets itself. Same reasoning // through underneath it and content insets itself. Same reasoning
// as local-updater's MainActivity. // as dev-updater's MainActivity.
enableEdgeToEdge() enableEdgeToEdge()
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = true WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = true
@@ -36,7 +36,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
// Status colors, keyed by the wire strings in Events.kt. Light theme only, // Status colors, keyed by the wire strings in Events.kt. Light theme only,
// as in local-updater. // as in dev-updater.
private val AWAITING_COLOR = Color(0xFFB26A00) private val AWAITING_COLOR = Color(0xFFB26A00)
private val RUNNING_COLOR = Color(0xFF2E7D32) private val RUNNING_COLOR = Color(0xFF2E7D32)
+9 -8
View File
@@ -1,16 +1,17 @@
#!/bin/sh #!/bin/sh
# Builds the app's APK, ready to install on a phone through Local Updater. # Builds the app's APK, ready to install on a phone through Dev Updater.
# #
# ./build-apk.sh # ./build-apk.sh
# #
# The APK pins the CA on *this* machine ($XDG_CONFIG_HOME/ai-app/certs/ca.pem, # The APK pins the CA on *this* machine ($XDG_CONFIG_HOME/ai-app/certs/ca.pem,
# or AI_APP_CA), so build it on the machine that runs the backend: an app # or AI_APP_CA), so build it on the machine that runs the backend: an app
# built somewhere else trusts a CA that backend can't present, and simply # built somewhere else trusts a CA that backend can't present, and simply
# won't connect. Run ../gen-dev-cert.sh first if there are no certificates # won't connect. Start ai-server once first if there are no certificates
# yet; the build stops with that instruction if it can't find one. # yet -- it generates them; the build stops with that instruction if it
# can't find one.
# #
# Unlike ./run-android.sh, this touches no emulator: it only produces the # Unlike ./run-android.sh, this touches no emulator: it only produces the
# file. Installing on a real phone goes through Local Updater, which serves # file. Installing on a real phone goes through Dev Updater, which serves
# whatever is under this project's build directory. # whatever is under this project's build directory.
set -eu set -eu
@@ -41,7 +42,7 @@ CA="${AI_APP_CA:-${XDG_CONFIG_HOME:-$HOME/.config}/ai-app/certs/ca.pem}"
if [ -f "$CA" ]; then if [ -f "$CA" ]; then
# Printed so a wrong or stale certificate is visible here rather than as # Printed so a wrong or stale certificate is visible here rather than as
# a handshake failure on the phone. Compare with the server's own # a handshake failure on the phone. Compare with the server's own
# ca-sha256.txt, which gen-dev-cert.sh writes beside it. # the CA the server is actually presenting.
FINGERPRINT=$(openssl x509 -in "$CA" -pubkey -noout 2>/dev/null \ FINGERPRINT=$(openssl x509 -in "$CA" -pubkey -noout 2>/dev/null \
| openssl pkey -pubin -outform der 2>/dev/null \ | openssl pkey -pubin -outform der 2>/dev/null \
| openssl dgst -sha256 -binary 2>/dev/null \ | openssl dgst -sha256 -binary 2>/dev/null \
@@ -49,8 +50,8 @@ if [ -f "$CA" ]; then
echo "==> Pinning the CA at $CA" echo "==> Pinning the CA at $CA"
echo " fingerprint: $FINGERPRINT" echo " fingerprint: $FINGERPRINT"
else else
echo "No CA certificate at $CA -- run ../gen-dev-cert.sh on this machine" >&2 echo "No CA certificate at $CA -- start ai-server once on this machine" >&2
echo "first, or set AI_APP_CA to one. The APK has to embed it at build time." >&2 echo "(it generates them), or set AI_APP_CA. The APK embeds it at build time." >&2
exit 1 exit 1
fi fi
@@ -62,7 +63,7 @@ echo
echo "==> Built $APK" echo "==> Built $APK"
[ -f "$APK" ] && ls -lh "$APK" | awk '{print " " $5}' [ -f "$APK" ] && ls -lh "$APK" | awk '{print " " $5}'
echo echo
echo "To get it onto the phone: add this project to Local Updater (or hit" echo "To get it onto the phone: add this project to Dev Updater (or hit"
echo "Update on it if it's already there) and install from there." echo "Update on it if it's already there) and install from there."
echo "Then start the backend and scan the enrollment QR it prints:" echo "Then start the backend and scan the enrollment QR it prints:"
echo " ./server/target/release/ai-server --rotate-token" echo " ./server/target/release/ai-server --rotate-token"
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
# Builds and runs this app on an emulator, creating/booting the AVD first if # Builds and runs this app on an emulator, creating/booting the AVD first if
# it isn't already up. Same flow as local-updater's run-android.sh; see that # it isn't already up. Same flow as dev-updater's run-android.sh; see that
# script for the reasoning behind the avd handling. # script for the reasoning behind the avd handling.
# #
# Environment setup (SDK location, PATH, ...) lives in ./android-env.sh, # Environment setup (SDK location, PATH, ...) lives in ./android-env.sh,
+1 -1
View File
@@ -111,7 +111,7 @@ fn generate_leaf(
params.distinguished_name.push(DnType::OrganizationName, "ai-app dev"); params.distinguished_name.push(DnType::OrganizationName, "ai-app dev");
params.distinguished_name.push( params.distinguished_name.push(
DnType::CommonName, DnType::CommonName,
addresses.first().map(|a| a.to_string()).unwrap_or_else(|| "local-updater".to_string()), addresses.first().map(|a| a.to_string()).unwrap_or_else(|| "dev-updater".to_string()),
); );
params.subject_alt_names = addresses.iter().map(|a| SanType::IpAddress(*a)).collect(); params.subject_alt_names = addresses.iter().map(|a| SanType::IpAddress(*a)).collect();
params.is_ca = IsCa::ExplicitNoCa; params.is_ca = IsCa::ExplicitNoCa;
+1 -1
View File
@@ -193,7 +193,7 @@ async fn main() -> Result<()> {
tracing::warn!( tracing::warn!(
"a new CA was generated in {} -- any installed app pins the previous one and can no \ "a new CA was generated in {} -- any installed app pins the previous one and can no \
longer reach this server. Rebuild it with app/build-apk.sh, which embeds this CA, \ longer reach this server. Rebuild it with app/build-apk.sh, which embeds this CA, \
and reinstall through Local Updater.", and reinstall through Dev Updater.",
certs_dir.display(), certs_dir.display(),
); );
} }
+1 -1
View File
@@ -1,7 +1,7 @@
//! The live session registry. Every session mutation -- spawn, delete, //! The live session registry. Every session mutation -- spawn, delete,
//! token changes -- funnels through [`SessionManager`] under one lock, so //! token changes -- funnels through [`SessionManager`] under one lock, so
//! in-memory state and `config.json` can't come apart (the same pattern as //! in-memory state and `config.json` can't come apart (the same pattern as
//! local-updater's `registry.rs`). //! dev-updater's `registry.rs`).
//! //!
//! A live session is a driver plus one event pump: the driver reports //! A live session is a driver plus one event pump: the driver reports
//! [`Event`]s into an mpsc channel; the pump assigns each a sequence //! [`Event`]s into an mpsc channel; the pump assigns each a sequence
+6 -6
View File
@@ -14,8 +14,8 @@
# The veth pair stands in for "the internet" carrying WireGuard's UDP; the # The veth pair stands in for "the internet" carrying WireGuard's UDP; the
# wg interfaces are real, with a real handshake and real keys. 10.66.0.1 is # wg interfaces are real, with a real handshake and real keys. 10.66.0.1 is
# deliberately the same address the leaf certificate carries a SAN for # deliberately the same address the leaf certificate carries a SAN for
# (gen-dev-cert.sh), so a client inside the tunnel completes the same # (certs.rs covers every local address), so a client inside the tunnel
# pinned-TLS handshake a phone will. # completes the same pinned-TLS handshake a phone will.
# #
# ./test-wg-tunnel.sh up create the tunnel (needs sudo) # ./test-wg-tunnel.sh up create the tunnel (needs sudo)
# ./test-wg-tunnel.sh test run the server on wg0 and reach it from "phone" # ./test-wg-tunnel.sh test run the server on wg0 and reach it from "phone"
@@ -80,8 +80,9 @@ up() {
} }
test_tunnel() { test_tunnel() {
if [ ! -f "$REPO/certs/leaf.pem" ]; then CERTS="${XDG_CONFIG_HOME:-$HOME/.config}/ai-app/certs"
echo "No certs/ -- run ./gen-dev-cert.sh first." >&2 if [ ! -f "$CERTS/leaf.pem" ]; then
echo "No certificates in $CERTS -- start ai-server once; it makes them." >&2
exit 1 exit 1
fi fi
if [ ! -x "$REPO/server/target/debug/ai-server" ]; then if [ ! -x "$REPO/server/target/debug/ai-server" ]; then
@@ -98,13 +99,12 @@ test_tunnel() {
ss -tlnp 2>/dev/null | grep 8443 | sed 's/^/ /' || echo " (nothing on 8443)" ss -tlnp 2>/dev/null | grep 8443 | sed 's/^/ /' || echo " (nothing on 8443)"
echo "==> From inside the tunnel: GET /sessions through wg1 -> wg0" echo "==> From inside the tunnel: GET /sessions through wg1 -> wg0"
TOKEN=$(sudo cat "$REPO/config.json" 2>/dev/null | sed -n 's/.*"sha256": "\(.*\)".*/\1/p' | head -1)
if [ -z "${AI_TOKEN:-}" ]; then if [ -z "${AI_TOKEN:-}" ]; then
echo " (set AI_TOKEN=<the enrollment token> to test an authorized call;" echo " (set AI_TOKEN=<the enrollment token> to test an authorized call;"
echo " without it this only proves reachability + TLS, via a 401)" echo " without it this only proves reachability + TLS, via a 401)"
fi fi
sudo ip netns exec "$NS" curl -s -o /dev/null -w " HTTP %{http_code} (TLS ok, pinned CA)\n" \ sudo ip netns exec "$NS" curl -s -o /dev/null -w " HTTP %{http_code} (TLS ok, pinned CA)\n" \
--cacert "$REPO/certs/ca.pem" \ --cacert "$CERTS/ca.pem" \
${AI_TOKEN:+-H "Authorization: Bearer $AI_TOKEN"} \ ${AI_TOKEN:+-H "Authorization: Bearer $AI_TOKEN"} \
"https://$SERVER_WG_IP:8443/sessions" || echo " UNREACHABLE" "https://$SERVER_WG_IP:8443/sessions" || echo " UNREACHABLE"