Merge pull request 'Sync Obsidian both ways so notes Simba writes reach your devices' (#86) from feat/obsidian-write-sync into main
CI / backend (push) Successful in 42s
CI / frontend (push) Failing after 7s

This commit was merged in pull request #86.
This commit is contained in:
2026-10-13 08:11:46 -04:00
6 changed files with 44 additions and 20 deletions
+4
View File
@@ -195,6 +195,10 @@ OBSIDIAN_E2E_PASSWORD=
OBSIDIAN_DEVICE_NAME=simbarag
# Set to true to run continuous sync in the background
OBSIDIAN_CONTINUOUS_SYNC=false
# bidirectional: notes and journal tasks Simba writes sync back to your devices.
# pull-only: download only, Simba's writes stay in the container's copy.
# mirror-remote: download only and revert local changes.
OBSIDIAN_SYNC_MODE=bidirectional
OBSIDIAN_SYNC_INTERVAL=60
# Local path to Obsidian vault (where files are synced)
OBSIDIAN_VAULT_PATH=/app/data/obsidian
+8
View File
@@ -225,6 +225,14 @@ def validate_configuration(env: Mapping[str, str], *, production: bool = True) -
problem(flag, "signature validation must remain enabled in production")
for name in ("METRICS_TOKEN", "CRON_SECRET", "EMAIL_HMAC_SECRET"):
secret(name, minimum=32)
if env.get("OBSIDIAN_SYNC_MODE", "bidirectional") not in {
"bidirectional",
"pull-only",
"mirror-remote",
}:
problem(
"OBSIDIAN_SYNC_MODE", "must be bidirectional, pull-only or mirror-remote"
)
if env.get("OBSIDIAN_CONTINUOUS_SYNC") == "true":
for name in ("OBSIDIAN_EMAIL", "OBSIDIAN_PASSWORD", "OBSIDIAN_VAULT_ID"):
if not value(name):
+1
View File
@@ -61,6 +61,7 @@ x-raggr-common: &raggr-common
- OBSIDIAN_E2E_PASSWORD=${OBSIDIAN_E2E_PASSWORD}
- OBSIDIAN_DEVICE_NAME=${OBSIDIAN_DEVICE_NAME}
- OBSIDIAN_CONTINUOUS_SYNC=${OBSIDIAN_CONTINUOUS_SYNC:-false}
- OBSIDIAN_SYNC_MODE=${OBSIDIAN_SYNC_MODE:-bidirectional}
- OBSIDIAN_VAULT_PATH=${OBSIDIAN_VAULT_PATH:-/app/data/obsidian}
- OBSIDIAN_VAULT_NAME=${OBSIDIAN_VAULT_NAME:-}
- S3_ENDPOINT_URL=${S3_ENDPOINT_URL}
+1 -1
View File
@@ -39,7 +39,7 @@ make logs
make migrate-history
```
Startup validates configuration, runs `aerich upgrade`, optionally sets up Obsidian pull-only sync, and replaces the shell with the Python process. **A migration failure stops startup; the application is never launched.** Obsidian login/setup failures still log a warning and allow the application to continue.
Startup validates configuration, runs `aerich upgrade`, optionally starts Obsidian sync in the background (`OBSIDIAN_SYNC_MODE`, default `bidirectional` so notes and tasks Simba writes reach your devices; containers take turns via a lock file in the shared vault), and replaces the shell with the Python process. **A migration failure stops startup; the application is never launched.** Obsidian login/setup failures still log a warning and allow the application to continue.
An empty database without Aerich metadata may need explicit initialization. Inspect its state first. Run the migration tool in a one-off container because the app cannot start until migrations pass:
+28 -19
View File
@@ -19,27 +19,36 @@ if [ "${OBSIDIAN_CONTINUOUS_SYNC:-false}" = "true" ]; then
if [ -z "${OBSIDIAN_EMAIL:-}" ] || [ -z "${OBSIDIAN_PASSWORD:-}" ] || [ -z "${OBSIDIAN_VAULT_ID:-}" ]; then
echo "WARNING: OBSIDIAN_EMAIL, OBSIDIAN_PASSWORD, or OBSIDIAN_VAULT_ID not set. Skipping sync."
else
echo "Setting up Obsidian sync..."
VAULT_PATH="${OBSIDIAN_VAULT_PATH:-/app/data/obsidian}"
SYNC_MODE="${OBSIDIAN_SYNC_MODE:-bidirectional}"
mkdir -p "${VAULT_PATH}"
# Login and setup sync (foreground, must complete before sync starts)
if ob login --email "${OBSIDIAN_EMAIL}" --password "${OBSIDIAN_PASSWORD}" && \
ob sync-setup \
--vault "${OBSIDIAN_VAULT_ID}" \
--path "${VAULT_PATH}" \
--password "${OBSIDIAN_E2E_PASSWORD:-}" \
--device-name "${OBSIDIAN_DEVICE_NAME:-simbarag}"; then
# Remove stale lock from previous container run
rm -rf "${VAULT_PATH}/.obsidian/.sync.lock"
# Set sync to pull-only (read-only) mode
ob sync-config --mode pull-only --path "${VAULT_PATH}"
# Start continuous sync in background
echo "Starting Obsidian continuous sync (pull-only)..."
ob sync --continuous --path "${VAULT_PATH}" &
else
echo "WARNING: Obsidian sync setup failed. Continuing without sync."
fi
# Blue and green share the vault volume. flock on a file in it works
# across containers, so this container only sets up and runs sync once
# the previous container's sync process has exited. Two clients writing
# one vault and its sync state would otherwise race. Setup runs in the
# background so the app starts while it waits.
(
flock 9
echo "Setting up Obsidian sync (${SYNC_MODE})..."
if ob login --email "${OBSIDIAN_EMAIL}" --password "${OBSIDIAN_PASSWORD}" && \
ob sync-setup \
--vault "${OBSIDIAN_VAULT_ID}" \
--path "${VAULT_PATH}" \
--password "${OBSIDIAN_E2E_PASSWORD:-}" \
--device-name "${OBSIDIAN_DEVICE_NAME:-simbarag}"; then
# Safe now: the lock means no other container's sync is running.
rm -rf "${VAULT_PATH}/.obsidian/.sync.lock"
if ob sync-config --mode "${SYNC_MODE}" --conflict-strategy merge \
--path "${VAULT_PATH}"; then
echo "Starting Obsidian continuous sync (${SYNC_MODE})..."
exec ob sync --continuous --path "${VAULT_PATH}"
fi
echo "WARNING: Obsidian sync-config failed. Continuing without sync."
else
echo "WARNING: Obsidian sync setup failed. Continuing without sync."
fi
) 9>"${VAULT_PATH}/.raggr-sync.lock" &
fi
fi
+2
View File
@@ -63,6 +63,7 @@ def test_missing_required_settings(production_env, name):
("CHECKPOINT_DB_PATH", "/data/invalid\x00.db"),
("CONVERSATION_SUMMARY_DB_PATH", ""),
("OBSIDIAN_CONTINUOUS_SYNC", "yes"),
("OBSIDIAN_SYNC_MODE", "push-only"),
],
)
def test_malformed_and_unsafe_settings(production_env, name, value):
@@ -202,6 +203,7 @@ def test_compose_forwards_runtime_settings():
"CHECKPOINT_DB_PATH",
"CONVERSATION_SUMMARY_DB_PATH",
"OBSIDIAN_SYNC_INTERVAL",
"OBSIDIAN_SYNC_MODE",
"OIDC_AUTHORIZATION_ENDPOINT",
"OIDC_TOKEN_ENDPOINT",
"OIDC_USERINFO_ENDPOINT",