From 1bd346ce203c2335f301fd02694aa09a11aa1d74 Mon Sep 17 00:00:00 2001 From: TZGyn Date: Thu, 8 Oct 2026 10:38:54 +0800 Subject: [PATCH] Explain the SSL failure instead of just reporting it A Postgres URL without an sslmode made lib/pq ask for TLS and die with "SSL is not enabled on the server", which reads like a server fault when it is really a connection-string default. The error now says what to append, and the server retries briefly so a deploy is not marked unhealthy just because Postgres was still starting. The sync server URL also defaults to the hosted instance now, so a fresh install needs no setup. --- AGENTS.md | 5 +++- README.md | 13 +++++----- Server/README.md | 26 +++++++++++++++++++- Server/main.go | 41 ++++++++++++++++++++++++++++---- Sources/Prompt/SyncService.swift | 5 +++- 5 files changed, 76 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 890b4d2..dd4fa72 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,7 +40,10 @@ clicking an item copies it back to the clipboard. a global queue); only the store append hops back to main. - `Sources/Prompt/SyncService.swift` — optional sync client. Holds the group code and server URL in `UserDefaults` (`sync.code`, `sync.serverURL`, - `sync.version`), pushes the whole store as JSON, and polls every 5s. + `sync.version`). The URL defaults to `SyncService.defaultServerURL` + (`https://prompt.tzgyn.dev`); nothing is sent anywhere until a code exists, + because polling only runs when one is set. Pushes the whole store as JSON and + polls every 5s. - `Server/main.go` — the Go sync backend (single file, `net/http` + `lib/pq`). Also ships `Server/Dockerfile` (static binary on distroless) and `Server/README.md` (self-hosting). Logs one line per request, creates its diff --git a/README.md b/README.md index e04d9b7..de3e0a9 100644 --- a/README.md +++ b/README.md @@ -62,15 +62,16 @@ Or build the Docker image (`docker build -t prompt-sync Server/`) and point it at your own Postgres — see [Server/README.md](Server/README.md) for that and for deploying to a host. -Then in the panel: click the sync icon, set the server URL (e.g. -`http://192.168.1.10:8080` or an HTTPS host), and either **Generate Code** on -the first Mac or paste the code into **Join** on the other. +Then in the panel: click the sync icon (or Cmd+,), confirm the server URL, and +either **Generate Code** on the first Mac or paste the code into **Join** on the +other. Notes: -- Both Macs must be able to reach the server. A LAN address or a Tailscale - hostname works; `localhost` only works if the server runs on the same Mac. - See [Server/README.md](Server/README.md) to deploy it on a VPS or home server. +- Sync defaults to `https://prompt.tzgyn.dev`; change it in the sync panel to + point at your own server. Both Macs must be able to reach it — a LAN address + or a Tailscale hostname works, and `localhost` only works if the server runs + on the same Mac. See [Server/README.md](Server/README.md) to deploy it. - Joining a group that already has notes replaces what's on that Mac. - The app polls every 5 seconds (a tiny version check) and pushes 1.5s after a change; last write wins, and changes that came from the server are not echoed diff --git a/Server/README.md b/Server/README.md index 288ea46..75fb7c0 100644 --- a/Server/README.md +++ b/Server/README.md @@ -26,7 +26,31 @@ PROMPT_SYNC_DATABASE_URL="postgres://localhost:5432/prompt_sync?sslmode=disable" The schema is created on startup. Every request is logged as `METHOD PATH STATUS DURATION`; SIGTERM/SIGINT shut down gracefully so an -in-flight push is not cut off mid-write. +in-flight push is not cut off mid-write. If the database is not reachable yet +it retries five times, 2s apart, before giving up. + +> **`sslmode` matters.** lib/pq defaults to `sslmode=require`, so a connection +> string copied from a host that runs Postgres *without* TLS (Coolify's +> internal Postgres, a plain `docker run postgres`, Homebrew) fails with +> `pq: SSL is not enabled on the server`. Append `?sslmode=disable` in that +> case — the server prints exactly that hint when it hits this. + +## Deploying with Coolify + +Use a Dockerfile application pointed at this repo with base directory `Server/` +(so Coolify builds `Server/Dockerfile`), expose port `8080`, and set: + +| Variable | Value | +| --- | --- | +| `PROMPT_SYNC_DATABASE_URL` | your Postgres URL **plus `?sslmode=disable`** | +| `PROMPT_SYNC_ADDR` | `:8080` (this is already the default) | + +Coolify detects the `HEALTHCHECK` in the Dockerfile and runs +`prompt-sync -healthcheck` with its 5s start period, so the container only goes +live once `/healthz` answers. It also logs a generic warning that a healthcheck +needs `curl` or `wget` — that does not apply here, the binary does its own +check, so there is no reason to add either to the image (it is distroless and +has no shell). ## Run with Docker diff --git a/Server/main.go b/Server/main.go index 4eba7ef..04ab4c1 100644 --- a/Server/main.go +++ b/Server/main.go @@ -65,14 +65,11 @@ func main() { addr = ":8080" } - db, err := sql.Open("postgres", dsn) + db, err := openDatabase(dsn) if err != nil { - log.Fatalf("open database: %v", err) + log.Fatalf("connect to database: %v%s", err, hintFor(err)) } defer db.Close() - if err := db.Ping(); err != nil { - log.Fatalf("connect to database: %v", err) - } if _, err := db.Exec(schema); err != nil { log.Fatalf("ensure schema: %v", err) } @@ -111,6 +108,40 @@ func main() { } } +// openDatabase connects, retrying briefly so a deploy is not marked unhealthy +// just because Postgres was still starting up. +func openDatabase(dsn string) (*sql.DB, error) { + db, err := sql.Open("postgres", dsn) + if err != nil { + return nil, err + } + var lastErr error + for attempt := 1; attempt <= 5; attempt++ { + if lastErr = db.Ping(); lastErr == nil { + return db, nil + } + if attempt < 5 { + log.Printf("database not ready (attempt %d/5): %v", attempt, lastErr) + time.Sleep(2 * time.Second) + } + } + db.Close() + return nil, lastErr +} + +// hintFor turns the most common connection failure into something actionable. +// lib/pq defaults to sslmode=require, so any DSN copied from a host that runs +// Postgres without TLS (Coolify's internal Postgres, docker-compose, Homebrew) +// fails with a bare "SSL is not enabled on the server". +func hintFor(err error) string { + if !strings.Contains(err.Error(), "SSL is not enabled") { + return "" + } + return "\n\nhint: lib/pq defaults to sslmode=require, but that Postgres has TLS off." + + "\n Append ?sslmode=disable to PROMPT_SYNC_DATABASE_URL" + + "\n (e.g. postgres://user:pass@host:5432/prompt_sync?sslmode=disable)." +} + // healthcheck returns 0 when the local /healthz answers 200. func healthcheck() int { addr := os.Getenv("PROMPT_SYNC_ADDR") diff --git a/Sources/Prompt/SyncService.swift b/Sources/Prompt/SyncService.swift index 15addd6..d06008e 100644 --- a/Sources/Prompt/SyncService.swift +++ b/Sources/Prompt/SyncService.swift @@ -39,6 +39,9 @@ final class SyncService: ObservableObject { private static let urlKey = "sync.serverURL" private static let versionKey = "sync.version" + /// Where sync points unless the user overrides it in the sync panel. + static let defaultServerURL = "https://prompt.tzgyn.dev" + private var lastVersion: Int64 { get { Int64(defaults.integer(forKey: Self.versionKey)) } set { defaults.set(newValue, forKey: Self.versionKey) } @@ -54,7 +57,7 @@ final class SyncService: ObservableObject { private init() { code = defaults.string(forKey: Self.codeKey) - serverURL = defaults.string(forKey: Self.urlKey) ?? "http://localhost:8080" + serverURL = defaults.string(forKey: Self.urlKey) ?? Self.defaultServerURL } func start() {