M12 — the Android client, end to end #2

Merged
bvandeusen merged 86 commits from dev into main 2026-08-21 08:53:58 -04:00
6 changed files with 187 additions and 17 deletions
Showing only changes of commit 02c932260e - Show all commits
+54
View File
@@ -0,0 +1,54 @@
# ThoughtSync production settings. Copy to `.env` and edit:
#
# cp .env.example .env
#
# Only POSTGRES_PASSWORD has no default — compose refuses to start without it.
# Everything else here is optional. Anything NOT in this file (site name, signups,
# attachment limits, trash retention, link previews) is configured in the admin
# Settings UI and stored in the database, not here.
# --- required ---------------------------------------------------------------
# Generate one and keep it: changing it later means also changing it inside the
# database, or Postgres will reject the app's connection.
#
# openssl rand -base64 24 | tr -d '/+=' | head -c 32
#
# Stick to letters and digits. This value goes into a connection URL, so a `@`,
# `/`, `:` or `#` in it will be misparsed as URL structure rather than password.
POSTGRES_PASSWORD=
# --- optional ---------------------------------------------------------------
# Which build to run.
#
# latest tracks the `main` branch — the production line (default)
# dev tracks the `dev` branch — newer, less settled
# <commit sha> pins one exact build; every push publishes one, and this is
# the rollback lever when an upgrade misbehaves
#
# NOTE: `main` can sit well behind `dev`. If a feature you expect is missing,
# check which branch it actually landed on before assuming a bug.
#THOUGHTSYNC_TAG=latest
# The host port the app is published on.
#THOUGHTSYNC_PORT=5000
# Which interface to bind. The default (all interfaces) is what lets desktop
# clients on your network reach the server. Behind a reverse proxy, set this to
# 127.0.0.1 so only the proxy can talk to it.
#THOUGHTSYNC_BIND=0.0.0.0
# Database identity. Changing these AFTER the first start does not rename anything
# that already exists — the volume keeps whatever the first run created.
#POSTGRES_USER=thoughtsync
#POSTGRES_DB=thoughtsync
# --- a note on HTTPS --------------------------------------------------------
#
# The app marks its session cookie Secure automatically when a request arrives over
# HTTPS, directly or via a proxy setting X-Forwarded-Proto — no setting needed.
#
# Worth knowing if you use the desktop app: typing a bare hostname there defaults to
# https://, deliberately, so a device token never crosses the wire in cleartext by
# accident. Serving over plain HTTP means typing the `http://` yourself.
+19 -3
View File
@@ -91,8 +91,11 @@ jobs:
else
echo "No TAURI_SIGNING_PRIVATE_KEY — building unsigned, no updater artifacts."
fi
version="$(sh ../packaging/build-version.sh)"
echo "Building version $version"
cargo tauri build \
--config '{"build":{"beforeBuildCommand":""}}' \
--config "{\"version\":\"$version\"}" \
--config "$updater"
working-directory: desktop/src-tauri
@@ -217,12 +220,23 @@ jobs:
# the MSVC CRT/SDK, pre-warmed into the image, and links with lld-link).
# Frontend already built above; skip the beforeBuildCommand rebuild.
- name: Tauri build (NSIS installer)
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
version="$(sh ../packaging/build-version.sh)"
echo "Building version $version"
updater='{}'
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
updater='{"bundle":{"createUpdaterArtifacts":true}}'
fi
cargo tauri build \
--runner cargo-xwin \
--target x86_64-pc-windows-msvc \
--bundles nsis \
--config '{"build":{"beforeBuildCommand":""}}'
--config '{"build":{"beforeBuildCommand":""}}' \
--config "{\"version\":\"$version\"}" \
--config "$updater"
working-directory: desktop/src-tauri
- name: Upload installer
@@ -292,8 +306,10 @@ jobs:
echo "manifest to write. Add the secret to enable in-app updates."
exit 0
fi
# The version the bundles carry, read from the crate rather than guessed.
version="$(grep -m1 '^version' desktop/src-tauri/Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')"
# The SAME helper the bundles were built with — a second derivation here
# could drift, and a manifest whose version doesn't match the binary it
# points at is an updater that never settles.
version="$(sh desktop/packaging/build-version.sh)"
if [ "${GITHUB_REF_NAME}" = "dev" ]; then
export RELEASE_TAG=dev
export RELEASE_NOTES="Development build from ${GITHUB_SHA}"
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env sh
#
# Echo the version this build should carry. One definition, used in three places
# (both bundle jobs and the manifest writer) — if they ever disagreed, the app would
# compare its own version against a manifest describing a different build, and the
# updater would either offer nothing or loop forever offering the same thing.
#
# WHY DEV BUILDS NEED THEIR OWN VERSION AT ALL:
# an updater decides by comparing semver. Every dev build carries the version in
# Cargo.toml, so without this they'd all be `0.1.0` — an installed build would see a
# manifest advertising the version it already has, conclude it was current, and never
# update. The rolling channel needs a number that actually rises.
#
# The CI run number is that number: monotonic, already unique per build, and it needs
# no state carried between runs. `0.1.0` + run 2932 becomes `0.1.2932`.
#
# Plain semver on purpose, NOT a `-dev.N` prerelease tag: prerelease versions sort
# BELOW the release they qualify (`0.1.0-dev.5` < `0.1.0`), so a tagged build would
# never update to a newer dev one, and Windows installer metadata wants a numeric
# X.Y.Z anyway. Bumping the minor in Cargo.toml still wins over any dev build on the
# old line, which is the ordering you want: 0.2.0 > 0.1.2932.
set -eu
CARGO_TOML="$(dirname "$0")/../src-tauri/Cargo.toml"
base="$(grep -m1 '^version' "$CARGO_TOML" | sed -E 's/.*"([^"]+)".*/\1/')"
# Dev builds only. Anything else (a v* tag, main) ships the version as written.
if [ "${GITHUB_REF_NAME:-}" = "dev" ] && [ -n "${GITHUB_RUN_NUMBER:-}" ]; then
printf '%s.%s\n' "${base%.*}" "$GITHUB_RUN_NUMBER"
else
printf '%s\n' "$base"
fi
+1 -1
View File
@@ -32,7 +32,7 @@
"endpoints": [
"https://git.fabledsword.com/bvandeusen/thoughtsync/releases/download/stable/latest.json"
],
"pubkey": "",
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDkwRTk2RkVBMkY2RDlCNkEKUldScW0yMHY2bS9wa0VBdWFpM3c1d2trQnlNVUJXUUtwZXBzQjduM3FRVzdGa3dXNGxObkZFV28K",
"windows": {
"installMode": "passive"
}
+5 -1
View File
@@ -5,7 +5,11 @@
# docker compose -f docker-compose.dev.yml up
#
# Then open http://localhost:5173 (the Vite dev server proxies /api to the backend).
# (docker-compose.yml, by contrast, builds + runs the production image on :5000.)
#
# docker-compose.yml — the DEFAULT file — is the production stack instead: it pulls
# the published image, keeps Postgres off the host network, and expects a .env. This
# one builds nothing and is deliberately insecure-by-convenience (weak password,
# Postgres published on 5432) because it is meant for a laptop, not a deployment.
services:
db:
image: postgres:16-alpine
+76 -12
View File
@@ -1,34 +1,98 @@
# Local two-service stack (app + Postgres). Provided for convenience — per family
# rule 12 the agent does NOT start this; run it yourself with `docker compose up`.
# ThoughtSync — PRODUCTION stack (app + Postgres).
#
# This is the default compose file: `docker compose up -d` runs a real deployment
# from the published image. Development lives in docker-compose.dev.yml (hot-reload,
# builds from source).
#
# cp .env.example .env # then set POSTGRES_PASSWORD
# docker compose up -d
#
# Per family rule 12 the agent does NOT start this — run it yourself.
#
# Upgrades: docker compose pull && docker compose up -d
# Rollback: set THOUGHTSYNC_TAG to a commit sha in .env, then the same two commands.
# Every push publishes an immutable :<sha> image for exactly this.
#
# Schema migrations run automatically at container start (see the Dockerfile CMD),
# so an upgrade is just a pull and a restart. Take a backup first anyway:
#
# docker compose exec -T db pg_dump -U thoughtsync thoughtsync > backup.sql
#
# Attachments are files, not rows — they live in the `thoughtsync-data` volume and
# a pg_dump does NOT contain them. Back up both or you'll restore notes whose images
# are gone.
services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: thoughtsync
POSTGRES_PASSWORD: thoughtsync
POSTGRES_DB: thoughtsync
POSTGRES_USER: ${POSTGRES_USER:-thoughtsync}
# No default on purpose. A production compose that ships a known password is
# how self-hosted databases end up in search engines; compose fails fast here
# instead, with the message below.
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env — see .env.example}
POSTGRES_DB: ${POSTGRES_DB:-thoughtsync}
volumes:
# Volume names kept from the previous compose file so an existing deployment
# upgrades in place. Renaming them would silently start against an empty
# database while the old one sat there, orphaned and looking like data loss.
- thoughtsync-db:/var/lib/postgresql/data
ports:
- "5432:5432"
# Deliberately NOT published to the host. The app reaches Postgres over the
# compose network; exposing 5432 only widens the attack surface. If you need
# psql, `docker compose exec db psql -U thoughtsync` gets you there without it.
healthcheck:
test: ["CMD-SHELL", "pg_isready -U thoughtsync"]
interval: 5s
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-thoughtsync} -d ${POSTGRES_DB:-thoughtsync}"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
logging: &logging
driver: json-file
options:
# Unbounded container logs are a slow-motion disk-full outage on a
# long-running self-hosted box.
max-size: "10m"
max-file: "3"
app:
build: .
# :latest tracks `main`. Set THOUGHTSYNC_TAG=dev in .env to follow the
# development line instead, or a commit sha to pin exactly.
image: git.fabledsword.com/bvandeusen/thoughtsync:${THOUGHTSYNC_TAG:-latest}
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
THOUGHTSYNC_DATABASE_URL: postgresql+asyncpg://thoughtsync:thoughtsync@db:5432/thoughtsync
# The only required application setting. Everything else a person might want
# to tune lives in the admin Settings UI, backed by the database (rule 25).
THOUGHTSYNC_DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-thoughtsync}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-thoughtsync}
volumes:
# Uploaded attachments. /var/thoughtsync is fixed in the app (Config.DATA_DIR),
# not configurable — mount it or lose every image on container recreation.
- thoughtsync-data:/var/thoughtsync
ports:
- "5000:5000"
# Default binds every interface, which is what lets desktop clients on the LAN
# reach it. Behind a reverse proxy, set THOUGHTSYNC_BIND=127.0.0.1 so only the
# proxy can talk to it.
- "${THOUGHTSYNC_BIND:-0.0.0.0}:${THOUGHTSYNC_PORT:-5000}:5000"
healthcheck:
# python rather than curl: the runtime image is python:3.12-slim and carries no
# HTTP client binary. Hits the app's own /api/health.
test:
- CMD
- python
- -c
- |
import sys, urllib.request
sys.exit(0 if urllib.request.urlopen("http://127.0.0.1:5000/api/health", timeout=5).status == 200 else 1)
interval: 30s
timeout: 10s
retries: 5
# Generous: the container waits for Postgres and runs migrations before it
# serves anything, and a first boot builds the whole schema.
start_period: 60s
logging: *logging
volumes:
thoughtsync-db: