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
54 changes: 54 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,57 @@ jobs:
severity: HIGH,CRITICAL
ignore-unfixed: true
exit-code: "1"

# Prebuilt helper binaries for the no-Docker path: install.sh downloads these
# (with checksum verification) and installs a systemd service or launchd
# agent. Pure CGO_ENABLED=0 Go, so a plain cross-compile loop covers every
# target — no QEMU, no goreleaser.
release-binaries:
name: Release Binaries
runs-on: ubuntu-latest
timeout-minutes: 10
needs: notify-guard
if: startsWith(github.ref, 'refs/tags/notify-v')
permissions:
contents: write
steps:
# This job holds a contents:write token for the release upload, so keep
# the checkout credential out of the workspace and skip the shared Go
# module cache — release binaries must not be assembled from cacheable
# state another workflow run could have written.
- uses: actions/checkout@v6
with:
persist-credentials: false

- uses: actions/setup-go@v6
with:
go-version-file: notify/go.mod
cache: false

Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: Cross-compile helper binaries
working-directory: notify
run: |
set -eu
mkdir -p dist
for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64; do
export GOOS="${target%/*}" GOARCH="${target#*/}"
out="dist/vaultsync-notify_${GOOS}_${GOARCH}"
[ "$GOOS" = windows ] && out="$out.exe"
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o "$out" .
done
(cd dist && sha256sum -- * > SHA256SUMS)

# --latest=false keeps the app release (v*) as the repo's "latest";
# install.sh resolves notify releases by tag prefix, not by latest.
- name: Publish release with binaries
env:
GH_TOKEN: ${{ github.token }}
run: |
set -eu
tag="${GITHUB_REF_NAME}"
if ! gh release view "$tag" >/dev/null 2>&1; then
gh release create "$tag" --verify-tag --latest=false \
--title "vaultsync-notify ${tag#notify-v}" \
--notes "Prebuilt \`vaultsync-notify\` helper binaries for the [Cloud Relay server setup](https://github.com/${GITHUB_REPOSITORY}/blob/main/notify/README.md). The one-line installer picks these up automatically; verify manual downloads against \`SHA256SUMS\`."
fi
gh release upload "$tag" --clobber notify/dist/*
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ All notable changes to VaultSync are documented here.

### Added

- **Cloud Relay server setup is now one line** — `curl -fsSL https://vaultsync.eu/notify.sh | sh` on the server replaces the edit-this-command Docker snippet: the installer finds Syncthing's `config.xml` on its own, runs the helper as exactly the user that owns it (the #1 setup failure), starts it via Docker — or, without Docker, installs a prebuilt binary behind a systemd service (Linux) or launchd agent (macOS) — and finishes with the helper's `--doctor` preflight so problems surface immediately with the fix spelled out. Skeptics can append `--dry-run` to preview every action without changing anything. The in-app setup screen now leads with the one-liner; the manual `docker run` command remains as the alternative. Localized in English, German, Spanish, and Simplified Chinese.
- **Prebuilt `vaultsync-notify` binaries** — every `notify-v*` release now ships static helper binaries for linux/amd64, linux/arm64, macOS (Intel and Apple silicon), and Windows, with a `SHA256SUMS` file — running the helper without Docker no longer requires a Go toolchain.
- **Helper permission errors now spell out the exact fix** — when the helper cannot read `config.xml`, the error names the file's actual owner and the exact `-u uid:gid` flag to use, instead of a generic "match the owner" hint (it also resolves the owner through an unreadable config directory).
- **Missed wake-ups now catch up on their own** — A device that misses a Cloud Relay wake-up (offline too long, push expired) no longer stays stale until the next vault change: the server helper (`vaultsync-notify`) now re-sends a wake-up on a slow cadence while any of your devices still needs data (default every 6 hours; `STALE_RETRIGGER_SECONDS`, `0` disables). Fully synced devices never cause a push, and a wake-up that is already on its way is never duplicated.
- **Overnight catch-up sync** — VaultSync now also schedules a long-running background task that iOS runs while the iPhone is charging with a network connection — typically overnight. It gets a multi-minute budget instead of the ~30 seconds of a normal background refresh, so large catch-ups complete on the charger instead of timing out.
- **See what Cloud Relay actually delivers** — Relay Diagnostics now counts the wake-ups received in the last 7 days (stored only on your device, never reported anywhere), warns live when Low Power Mode is deferring silent pushes, and explains the most common silent killer: force-quitting VaultSync from the app switcher stops all wake-ups until the next manual launch. Localized in English, German, Spanish, and Simplified Chinese.
Expand Down
3 changes: 2 additions & 1 deletion docs/relay-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ In the app, the **Cloud Relay** tab → **Relay health & diagnostics** is the li
### Components

**vaultsync-notify (Homeserver Container)**
- Docker container, runs alongside the user's Syncthing instance
- Docker container or prebuilt static binary, runs alongside the user's Syncthing instance
- One-line installer (`curl -fsSL https://vaultsync.eu/notify.sh | sh`): detects `config.xml`, runs the helper as the uid:gid owning it, picks Docker or a binary-backed system service, and finishes with the `--doctor` preflight
- Subscribes to Syncthing's REST API event stream (`/rest/events`)
- Filters for relevant outgoing-change signals: `LocalIndexUpdated`, plus `FolderCompletion` only when a peer still needs data
- On file changes: sends a wake-up signal to the central relay
Expand Down
81 changes: 52 additions & 29 deletions ios/VaultSync/Views/RelayServerSetupView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,25 @@ struct RelayServerSetupView: View {
/// true the helper is confirmed running and we show a success banner.
var isDelivering: Bool = false

@State private var installerCopied = false
@State private var commandCopied = false

/// Self-contained command a user can paste into their server shell. The relay
/// URL is pre-filled and there is no API key to copy — the helper reads the
/// Syncthing key (and address) straight from the mounted config.xml. `--network
/// host` is kept so the address auto-inferred from config.xml (typically
/// 127.0.0.1:8384) reaches a same-host Syncthing. We interpolate
/// `productionRelayURL` (never the DEBUG-overridable `relayURL`) so a lab build
/// pointed at a mock can't leak that URL into the command shown to the user.
/// The primary setup path: a one-line installer that finds config.xml on the
/// server, runs the helper as the uid:gid owning it (the #1 setup failure),
/// and picks Docker or a prebuilt-binary service automatically. Nothing in
/// it is user-specific — identity comes from the server's own Syncthing at
/// runtime — so it is a stable constant, short enough to type by hand when
/// copy-paste can't reach the server shell.
private static let installerCommand = "curl -fsSL https://vaultsync.eu/notify.sh | sh"

/// Self-contained command for users who prefer to run the container
/// themselves. The relay URL is pre-filled and there is no API key to copy —
/// the helper reads the Syncthing key (and address) straight from the
/// mounted config.xml. `--network host` is kept so the address auto-inferred
/// from config.xml (typically 127.0.0.1:8384) reaches a same-host Syncthing.
/// We interpolate `productionRelayURL` (never the DEBUG-overridable
/// `relayURL`) so a lab build pointed at a mock can't leak that URL into the
/// command shown to the user.
private var dockerCommand: String {
"""
docker run -d --name vaultsync-notify --restart unless-stopped \\
Expand Down Expand Up @@ -48,25 +58,12 @@ struct RelayServerSetupView: View {
}

Section {
commandBox
Button {
UIPasteboard.general.string = dockerCommand
UINotificationFeedbackGenerator().notificationOccurred(.success)
commandCopied = true
Task {
try? await Task.sleep(for: .seconds(1.5))
commandCopied = false
}
} label: {
Label(
commandCopied ? L10n.tr("Copied") : L10n.tr("Copy Command"),
systemImage: commandCopied ? "checkmark.circle" : "doc.on.doc"
)
}
commandBox(Self.installerCommand, accessibilityLabelKey: "Installer command")
copyButton(for: Self.installerCommand, copied: $installerCopied)
} header: {
Text(L10n.tr("Step 1 — Run this on your server"))
} footer: {
Text(L10n.tr("No API key needed — the helper reads it from Syncthing’s config.xml. Replace /PATH/TO/syncthing with your Syncthing config folder (often ~/.local/state/syncthing or ~/.config/syncthing). Permission error? Add -u <uid>:<gid> for the user that owns config.xml."))
Text(L10n.tr("The installer finds your Syncthing config, sets the right permissions, and starts the helper — with Docker if available, otherwise as a system service. It’s open source; add --dry-run to preview every action without changing anything."))
}

Section {
Expand All @@ -77,25 +74,51 @@ struct RelayServerSetupView: View {
}

Section {
ExternalLinkButton(titleKey: "Full setup guide (Docker Compose or guided setup script)", url: DocURL.serverSetupGuide)
commandBox(dockerCommand, accessibilityLabelKey: "Server setup command")
copyButton(for: dockerCommand, copied: $commandCopied)
} header: {
Text(L10n.tr("Manual alternative — run the container yourself"))
} footer: {
Text(L10n.tr("No API key needed — the helper reads it from Syncthing’s config.xml. Replace /PATH/TO/syncthing with your Syncthing config folder (often ~/.local/state/syncthing or ~/.config/syncthing). Permission error? Add -u <uid>:<gid> for the user that owns config.xml."))
}

Section {
ExternalLinkButton(titleKey: "Full setup guide (Docker Compose, prebuilt binaries, NAS notes)", url: DocURL.serverSetupGuide)
.font(.subheadline)
} footer: {
Text(L10n.tr("Prefer Docker Compose or a guided setup script? The full guide covers both."))
Text(L10n.tr("Prefer Docker Compose, a NAS package, or a plain binary? The full guide covers them all."))
}
}
.navigationTitle(L10n.tr("Set Up Your Server"))
.navigationBarTitleDisplayMode(.inline)
}

private var commandBox: some View {
private func commandBox(_ command: String, accessibilityLabelKey: String) -> some View {
ScrollView(.horizontal, showsIndicators: true) {
Text(dockerCommand)
Text(command)
.font(.vaultMono(.caption))
.textSelection(.enabled)
.padding(VaultSpacing.m)
}
.background(Color(.secondarySystemBackground), in: RoundedRectangle(cornerRadius: VaultRadius.control, style: .continuous))
.accessibilityLabel(L10n.tr("Server setup command"))
.accessibilityValue(dockerCommand)
.accessibilityLabel(L10n.tr(accessibilityLabelKey))
.accessibilityValue(command)
}

private func copyButton(for command: String, copied: Binding<Bool>) -> some View {
Button {
UIPasteboard.general.string = command
UINotificationFeedbackGenerator().notificationOccurred(.success)
copied.wrappedValue = true
Task {
try? await Task.sleep(for: .seconds(1.5))
copied.wrappedValue = false
}
} label: {
Label(
copied.wrappedValue ? L10n.tr("Copied") : L10n.tr("Copy Command"),
systemImage: copied.wrappedValue ? "checkmark.circle" : "doc.on.doc"
)
}
}
}
7 changes: 5 additions & 2 deletions ios/VaultSync/de.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -525,10 +525,13 @@
"Copy Command" = "Befehl kopieren";
"Step 3 — Confirm it works" = "Schritt 3 – Bestätige, dass es funktioniert";
"Come back to VaultSync. The moment a change happens on your server, Relay Diagnostics will confirm that wake-ups are being delivered — that means everything works." = "Komm zurück zu VaultSync. Sobald sich auf deinem Server etwas ändert, bestätigt die Relay-Diagnose, dass Weck-Signale zugestellt werden – dann funktioniert alles.";
"Full setup guide (Docker Compose or guided setup script)" = "Vollständige Anleitung (Docker Compose oder geführtes Setup-Skript)";
"Prefer Docker Compose or a guided setup script? The full guide covers both." = "Lieber Docker Compose oder ein geführtes Setup-Skript? Die vollständige Anleitung deckt beides ab.";
"Full setup guide (Docker Compose, prebuilt binaries, NAS notes)" = "Vollständige Anleitung (Docker Compose, vorkompilierte Binaries, NAS-Hinweise)";
"Prefer Docker Compose, a NAS package, or a plain binary? The full guide covers them all." = "Lieber Docker Compose, ein NAS-Setup oder ein einfaches Binary? Die vollständige Anleitung deckt alles ab.";
"Set Up Your Server" = "Server einrichten";
"Server setup command" = "Server-Einrichtungsbefehl";
"Installer command" = "Installer-Befehl";
"The installer finds your Syncthing config, sets the right permissions, and starts the helper — with Docker if available, otherwise as a system service. It’s open source; add --dry-run to preview every action without changing anything." = "Der Installer findet deine Syncthing-Konfiguration, setzt die richtigen Berechtigungen und startet den Helfer – mit Docker, falls vorhanden, sonst als Systemdienst. Er ist Open Source; mit --dry-run siehst du vorab jede Aktion, ohne dass etwas verändert wird.";
"Manual alternative — run the container yourself" = "Manuelle Alternative – Container selbst starten";
"Last wake-up" = "Letztes Weck-Signal";
"Auto-renews until canceled. Cancel anytime in Settings → Subscriptions." = "Verlängert sich automatisch bis zur Kündigung. Jederzeit kündbar unter Einstellungen → Abonnements.";
"Cancel anytime in Settings → Subscriptions" = "Jederzeit kündbar unter Einstellungen → Abonnements";
Expand Down
7 changes: 5 additions & 2 deletions ios/VaultSync/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -525,10 +525,13 @@
"Copy Command" = "Copy Command";
"Step 3 — Confirm it works" = "Step 3 — Confirm it works";
"Come back to VaultSync. The moment a change happens on your server, Relay Diagnostics will confirm that wake-ups are being delivered — that means everything works." = "Come back to VaultSync. The moment a change happens on your server, Relay Diagnostics will confirm that wake-ups are being delivered — that means everything works.";
"Full setup guide (Docker Compose or guided setup script)" = "Full setup guide (Docker Compose or guided setup script)";
"Prefer Docker Compose or a guided setup script? The full guide covers both." = "Prefer Docker Compose or a guided setup script? The full guide covers both.";
"Full setup guide (Docker Compose, prebuilt binaries, NAS notes)" = "Full setup guide (Docker Compose, prebuilt binaries, NAS notes)";
"Prefer Docker Compose, a NAS package, or a plain binary? The full guide covers them all." = "Prefer Docker Compose, a NAS package, or a plain binary? The full guide covers them all.";
"Set Up Your Server" = "Set Up Your Server";
"Server setup command" = "Server setup command";
"Installer command" = "Installer command";
"The installer finds your Syncthing config, sets the right permissions, and starts the helper — with Docker if available, otherwise as a system service. It’s open source; add --dry-run to preview every action without changing anything." = "The installer finds your Syncthing config, sets the right permissions, and starts the helper — with Docker if available, otherwise as a system service. It’s open source; add --dry-run to preview every action without changing anything.";
"Manual alternative — run the container yourself" = "Manual alternative — run the container yourself";
"Last wake-up" = "Last wake-up";
"Auto-renews until canceled. Cancel anytime in Settings → Subscriptions." = "Auto-renews until canceled. Cancel anytime in Settings → Subscriptions.";
"Cancel anytime in Settings → Subscriptions" = "Cancel anytime in Settings → Subscriptions";
Expand Down
7 changes: 5 additions & 2 deletions ios/VaultSync/es.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -525,10 +525,13 @@
"Copy Command" = "Copiar comando";
"Step 3 — Confirm it works" = "Paso 3: Confirma que funciona";
"Come back to VaultSync. The moment a change happens on your server, Relay Diagnostics will confirm that wake-ups are being delivered — that means everything works." = "Vuelve a VaultSync. En cuanto ocurra un cambio en tu servidor, Diagnóstico del Relay confirmará que se están entregando las señales de activación: eso significa que todo funciona.";
"Full setup guide (Docker Compose or guided setup script)" = "Guía completa (Docker Compose o script de configuración guiado)";
"Prefer Docker Compose or a guided setup script? The full guide covers both." = "¿Prefieres Docker Compose o un script de configuración guiado? La guía completa cubre ambos.";
"Full setup guide (Docker Compose, prebuilt binaries, NAS notes)" = "Guía completa (Docker Compose, binarios precompilados, notas para NAS)";
"Prefer Docker Compose, a NAS package, or a plain binary? The full guide covers them all." = "¿Prefieres Docker Compose, un NAS o un binario sin más? La guía completa lo cubre todo.";
"Set Up Your Server" = "Configura tu servidor";
"Server setup command" = "Comando de configuración del servidor";
"Installer command" = "Comando del instalador";
"The installer finds your Syncthing config, sets the right permissions, and starts the helper — with Docker if available, otherwise as a system service. It’s open source; add --dry-run to preview every action without changing anything." = "El instalador encuentra tu configuración de Syncthing, establece los permisos correctos e inicia el asistente: con Docker si está disponible o, si no, como servicio del sistema. Es de código abierto; añade --dry-run para previsualizar cada acción sin cambiar nada.";
"Manual alternative — run the container yourself" = "Alternativa manual: ejecuta el contenedor tú mismo";
"Last wake-up" = "Última señal de activación";
"Auto-renews until canceled. Cancel anytime in Settings → Subscriptions." = "Se renueva automáticamente hasta que se cancele. Cancela cuando quieras en Ajustes → Suscripciones.";
"Cancel anytime in Settings → Subscriptions" = "Cancela cuando quieras en Ajustes → Suscripciones";
Expand Down
Loading
Loading