V6.1 continue beating on the backup
Quality Gate / gate (push) Failing after 2m34s

This commit is contained in:
Jim Lancaster
2026-09-02 10:04:38 -05:00
parent 75dc946123
commit 494f378e48
6 changed files with 124 additions and 29 deletions
+11 -7
View File
@@ -31,7 +31,14 @@ SQLITE_CHECK_SAME_THREAD=false
# --- filesystem paths ---
UPLOAD_DIR=/app/uploads
PROMPT_DIR=/app/prompts
DATABASE_BACKUP_DIR=/app/data/backups
DATABASE_BACKUP_DIR=/backup
# --- backup workflow helpers (not Runtime Settings model fields) ---
BACKUP_DIR=/backup
# APP_DATA_BACKUP_DIR=/backup/data
# UPLOADS_BACKUP_DIR=/backup/uploads
BACKUP_RETENTION_DAYS=14
# Optional mounted Synology destination path for replicated dumps
# SYNOLOGY_BACKUP_DIR=/mnt/synology/transcription-backups
# --- worker reliability ---
WORKER_MAX_RETRIES=0
@@ -53,9 +60,6 @@ POSTGRES_PASSWORD=replace-with-strong-password
# Required for token-based tunnel startup.
CLOUDFLARE_TUNNEL_TOKEN=replace-with-cloudflare-tunnel-token
# --- backup workflow helpers ---
# Used by deploy/backup/create_postgres_backup.sh
BACKUP_DIR=./data/backups
BACKUP_RETENTION_DAYS=14
# Optional mounted Synology destination path for replicated dumps
# SYNOLOGY_BACKUP_DIR=/mnt/synology/transcription-backups
# --- deployment wiring helpers ---
# Runtime Settings writes target this file path inside the app container.
RUNTIME_SETTINGS_ENV_FILE=/app/.env.production
+11 -2
View File
@@ -16,6 +16,8 @@ logs_file="logs-${timestamp}.tar.gz"
manifest_file="backup-${timestamp}.manifest"
mkdir -p "${BACKUP_DIR}"
mkdir -p "${APP_DATA_BACKUP_DIR}"
app_data_backup_abs="$(cd "${APP_DATA_BACKUP_DIR}" && pwd -P)"
# If SYNOLOGY_BACKUP_DIR wasn't exported in the shell, read it from ENV_FILE.
if [ -z "${SYNOLOGY_BACKUP_DIR}" ] && [ -f "${ENV_FILE}" ]; then
@@ -68,10 +70,17 @@ find "${BACKUP_DIR}" -type f \( \
-name 'backup-*.manifest' \
\) -mtime +"${RETENTION_DAYS}" -delete
# Always keep a rolling incremental mirror of uploaded files in BACKUP_DIR.
mkdir -p "${APP_DATA_BACKUP_DIR}"
# Always keep a rolling mirror of app data in BACKUP_DIR, unless that target
# is nested under /app/data (which would recursively copy into itself).
if [ -d /app/data ]; then
case "${app_data_backup_abs}" in
/app/data|/app/data/*)
echo "Skipping app data mirror because APP_DATA_BACKUP_DIR is nested under /app/data: ${APP_DATA_BACKUP_DIR}"
;;
*)
cp -a /app/data/. "${APP_DATA_BACKUP_DIR}/"
;;
esac
fi
mkdir -p "${UPLOADS_BACKUP_DIR}/documents" "${UPLOADS_BACKUP_DIR}/photos"
+1 -1
View File
@@ -30,7 +30,7 @@ Settings manages installation-local registries, safe runtime .env settings, and
- Runtime Settings includes an explicit "Other settings not shown here" markdown table listing:
- secrets (`OPENROUTER_API_KEY`, `DATABASE__PASSWORD`)
- high-risk database connection settings (`DATABASE__DRIVER`, `DATABASE__PATH`, `DATABASE__HOST`, `DATABASE__PORT`, `DATABASE__DATABASE`, `DATABASE__USER`)
and directs edits for those keys to `.env`.
and deployment/helper keys (`POSTGRES_*`, `CLOUDFLARE_TUNNEL_TOKEN`, `BACKUP_DIR`, `APP_DATA_BACKUP_DIR`, `UPLOADS_BACKUP_DIR`, `BACKUP_RETENTION_DAYS`, `SYNOLOGY_BACKUP_DIR`, `RUNTIME_SETTINGS_ENV_FILE`, `ENV_FILE`, `COMPOSE_FILE`), and directs edits for those keys to the resolved runtime env file path.
- Document Types, Person Roles, and Tags support Add/Edit/Delete with existing guardrails.
- Prompts exposes only `transcribe_document.md` for editing and restore-from-backup.
- Home Page Text edits the same Markdown content rendered on `/homepage`.
+1 -1
View File
@@ -711,7 +711,7 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
with ui.column().classes("w-full max-w-3xl mx-auto gap-2 mt-3"):
ui.label("Other settings not shown here").classes("text-sm font-semibold")
ui.label("Edit these directly in .env:").classes("text-xs ui-text-muted")
ui.label(f"Edit these directly in {snapshot.env_file_path}:").classes("text-xs ui-text-muted")
table_rows = [
f"| {category.title} | {', '.join(f'`{key}`' for key in category.env_keys)} |"
for category in HIDDEN_SETTINGS_CATEGORIES
@@ -84,6 +84,29 @@ HIDDEN_SETTINGS_CATEGORIES: tuple[HiddenSettingsCategory, ...] = (
"DATABASE__USER",
),
),
HiddenSettingsCategory(
title="Deployment and backup/tunnel helpers (not Runtime Settings model fields)",
reason=(
"These keys are consumed by docker-compose, backup scripts, or deployment adapters and are "
"intentionally edited outside the Runtime Settings UI."
),
env_keys=(
"POSTGRES_DB",
"POSTGRES_USER",
"POSTGRES_PASSWORD",
"POSTGRES_HOST",
"POSTGRES_PORT",
"CLOUDFLARE_TUNNEL_TOKEN",
"RUNTIME_SETTINGS_ENV_FILE",
"BACKUP_DIR",
"APP_DATA_BACKUP_DIR",
"UPLOADS_BACKUP_DIR",
"BACKUP_RETENTION_DAYS",
"SYNOLOGY_BACKUP_DIR",
"ENV_FILE",
"COMPOSE_FILE",
),
),
)
RUNTIME_SETTINGS_CATALOG: tuple[RuntimeSettingDescriptor, ...] = (
@@ -358,7 +381,15 @@ def save_runtime_settings(
env_map[descriptor.env_key] = encoded
_validate_candidate_env(env_map=env_map, env_file_path=resolved_env_path)
try:
_write_env_lines_atomic(path=resolved_env_path, lines=mutable_lines)
except OSError as exc:
raise AppError(
"Runtime settings file is not writable.",
category=ErrorCategory.INFRA_PERSISTENT,
suggestion="Verify file path and write permissions, then retry.",
detail=f"Failed writing runtime env file {resolved_env_path}: {type(exc).__name__}: {exc}",
) from exc
refreshed = Settings(_env_file=resolved_env_path, _cli_parse_args=False)
return read_runtime_settings_snapshot(settings=refreshed, env_file_path=resolved_env_path)
@@ -438,7 +469,15 @@ def _validate_candidate_env(*, env_map: dict[str, str], env_file_path: Path) ->
def _read_env_lines(path: Path) -> list[str]:
if not path.exists():
return []
try:
return path.read_text(encoding="utf-8").splitlines()
except OSError as exc:
raise AppError(
"Runtime settings file is unreadable.",
category=ErrorCategory.INFRA_PERSISTENT,
suggestion="Verify file path and read permissions, then retry.",
detail=f"Failed reading runtime env file {path}: {type(exc).__name__}: {exc}",
) from exc
def _parse_env_map(lines: list[str]) -> dict[str, str]:
@@ -485,6 +524,7 @@ def _upsert_env_key(lines: list[str], key: str, value: str) -> None:
def _write_env_lines_atomic(*, path: Path, lines: list[str]) -> None:
try:
path.parent.mkdir(parents=True, exist_ok=True)
content = "\n".join(lines).rstrip("\n")
if content:
@@ -500,6 +540,13 @@ def _write_env_lines_atomic(*, path: Path, lines: list[str]) -> None:
temp_path = Path(handle.name)
handle.write(content)
temp_path.replace(path)
except OSError as exc:
raise AppError(
"Runtime settings file is not writable.",
category=ErrorCategory.INFRA_PERSISTENT,
suggestion="Verify file path and write permissions, then retry.",
detail=f"Failed writing runtime env file {path}: {type(exc).__name__}: {exc}",
) from exc
def _escape_json_string(value: str) -> str:
+35
View File
@@ -8,6 +8,7 @@ import pytest
from transcription.config import Settings
from transcription.errors import AppError
from transcription.ui import runtime_settings_store
from transcription.ui.runtime_settings_store import HIDDEN_SETTINGS_CATEGORIES
from transcription.ui.runtime_settings_store import RUNTIME_SETTINGS_CATALOG
from transcription.ui.runtime_settings_store import SETTINGS_UI_EXCLUDED_FIELDS
@@ -35,6 +36,12 @@ def test_hidden_settings_categories_list_secret_and_high_risk_env_keys():
assert "DATABASE__PASSWORD" in hidden_keys
assert "DATABASE__DRIVER" in hidden_keys
assert "DATABASE__HOST" in hidden_keys
assert "POSTGRES_DB" in hidden_keys
assert "POSTGRES_USER" in hidden_keys
assert "POSTGRES_PASSWORD" in hidden_keys
assert "CLOUDFLARE_TUNNEL_TOKEN" in hidden_keys
assert "BACKUP_DIR" in hidden_keys
assert "SYNOLOGY_BACKUP_DIR" in hidden_keys
def test_runtime_settings_snapshot_includes_catalog_fields(tmp_path: Path):
@@ -98,3 +105,31 @@ def test_runtime_settings_uses_override_env_file(tmp_path: Path, monkeypatch):
assert snapshot.env_file_path == target
assert "PORT=9001" in target.read_text(encoding="utf-8")
def test_save_runtime_settings_surfaces_unreadable_target(tmp_path: Path, monkeypatch):
settings = _settings_for_runtime_editing(tmp_path)
env_path = tmp_path / ".env"
def _raise_read_error(_self, encoding="utf-8"):
_ = encoding
raise OSError("denied")
monkeypatch.setattr(Path, "read_text", _raise_read_error)
with pytest.raises(AppError) as exc:
save_runtime_settings(settings=settings, updates={"port": "9001"}, env_file_path=env_path)
assert exc.value.message == "Runtime settings file is unreadable."
def test_save_runtime_settings_surfaces_unwritable_target(tmp_path: Path, monkeypatch):
settings = _settings_for_runtime_editing(tmp_path)
env_path = tmp_path / ".env"
def _raise_write_error(*args, **kwargs):
_ = args, kwargs
raise OSError("denied")
monkeypatch.setattr(runtime_settings_store, "_write_env_lines_atomic", _raise_write_error)
with pytest.raises(AppError) as exc:
save_runtime_settings(settings=settings, updates={"port": "9001"}, env_file_path=env_path)
assert exc.value.message == "Runtime settings file is not writable."