The watcher scripts now write live status and append-only run history onto the shared volume; the api exposes them via /shopdbbackups/status and /history. The restore modal shows an indeterminate spinner with phase labels, and two cards list backup and restore run records. Timestamps render in UTC to match the Zulu-stamped set ids, the history directory is excluded from the set list, and a stale-status guard stops a previous run's done phase from collapsing the spinner on the next restore.
101 lines
3.4 KiB
Bash
101 lines
3.4 KiB
Bash
# ShopDB backup/restore status + history helper.
|
|
#
|
|
# WHY this exists: the restore and backup runs happen in the watcher container,
|
|
# but the operator watches the UI, which only talks to the api. This helper is
|
|
# the bridge: it writes two kinds of state onto the shared /backups volume that
|
|
# the api can read.
|
|
#
|
|
# 1. A LIVE status file (.restore-status.json / .backup-status.json) rewritten
|
|
# at each phase so the UI can show an indeterminate spinner with a phase
|
|
# label (queued, restoring files, loading database, done, failed).
|
|
# 2. An append-only HISTORY file (history/restores.json / history/backups.json)
|
|
# that keeps the record of past runs so the UI can list them in a card.
|
|
#
|
|
# Both writes are atomic (write temp, rename) so the api never reads a half
|
|
# written file. History is bounded to the most recent HISTORY_MAX entries.
|
|
#
|
|
# This file is SOURCED, not executed. It exposes:
|
|
# status_write <kind> <phase> <setname> [message]
|
|
# history_append <kind> <outcome> <setname> [message]
|
|
#
|
|
# kind is "restore" or "backup". ASCII only, no emojis - matches the style gate.
|
|
|
|
STATUS_BACKUPS_ROOT="${SHOPDB_BACKUPS_ROOT:-/backups}"
|
|
HISTORY_MAX="${BACKUP_HISTORY_MAX:-50}"
|
|
|
|
_status_now() { date -u +%Y-%m-%dT%H%M%SZ; }
|
|
|
|
_status_json_escape() {
|
|
# Escape backslashes and double quotes for embedding in a JSON string.
|
|
printf '%s' "${1:-}" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
|
}
|
|
|
|
# status_write <kind> <phase> <setname> [message]
|
|
# Rewrites the live status file for this run kind.
|
|
status_write() {
|
|
kind="$1"
|
|
phase="$2"
|
|
setname="${3:-}"
|
|
message="${4:-}"
|
|
statusfile="${STATUS_BACKUPS_ROOT}/.${kind}-status.json"
|
|
tmpfile="${statusfile}.tmp.$$"
|
|
{
|
|
printf '{'
|
|
printf '"kind":"%s",' "$(_status_json_escape "${kind}")"
|
|
printf '"phase":"%s",' "$(_status_json_escape "${phase}")"
|
|
printf '"setname":"%s",' "$(_status_json_escape "${setname}")"
|
|
printf '"message":"%s",' "$(_status_json_escape "${message}")"
|
|
printf '"updatedat":"%s"' "$(_status_now)"
|
|
printf '}\n'
|
|
} > "${tmpfile}" 2>/dev/null || return 0
|
|
mv -f "${tmpfile}" "${statusfile}" 2>/dev/null || rm -f "${tmpfile}" 2>/dev/null
|
|
return 0
|
|
}
|
|
|
|
# history_append <kind> <outcome> <setname> [message]
|
|
# Appends a completed run record and trims to HISTORY_MAX newest entries.
|
|
history_append() {
|
|
kind="$1"
|
|
outcome="$2"
|
|
setname="${3:-}"
|
|
message="${4:-}"
|
|
historydir="${STATUS_BACKUPS_ROOT}/history"
|
|
historyfile="${historydir}/${kind}s.json"
|
|
tmpfile="${historyfile}.tmp.$$"
|
|
mkdir -p "${historydir}" 2>/dev/null || return 0
|
|
|
|
record="$(printf '{"setname":"%s","outcome":"%s","message":"%s","finishedat":"%s"}' \
|
|
"$(_status_json_escape "${setname}")" \
|
|
"$(_status_json_escape "${outcome}")" \
|
|
"$(_status_json_escape "${message}")" \
|
|
"$(_status_now)")"
|
|
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
HISTORY_FILE="${historyfile}" HISTORY_RECORD="${record}" \
|
|
HISTORY_MAX="${HISTORY_MAX}" python3 - "${tmpfile}" <<'PY' 2>/dev/null || return 0
|
|
import json, os, sys
|
|
|
|
path = os.environ["HISTORY_FILE"]
|
|
record = json.loads(os.environ["HISTORY_RECORD"])
|
|
limit = int(os.environ.get("HISTORY_MAX", "50"))
|
|
tmp = sys.argv[1]
|
|
|
|
try:
|
|
with open(path, "r", encoding="utf-8") as handle:
|
|
entries = json.load(handle)
|
|
if not isinstance(entries, list):
|
|
entries = []
|
|
except (OSError, ValueError):
|
|
entries = []
|
|
|
|
entries.append(record)
|
|
entries = entries[-limit:]
|
|
|
|
with open(tmp, "w", encoding="utf-8") as handle:
|
|
json.dump(entries, handle)
|
|
os.replace(tmp, path)
|
|
PY
|
|
fi
|
|
return 0
|
|
}
|