it is risen - add restore progress and backup/restore history
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.
This commit is contained in:
100
scripts/backup/status.sh
Normal file
100
scripts/backup/status.sh
Normal file
@@ -0,0 +1,100 @@
|
||||
# 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
|
||||
}
|
||||
Reference in New Issue
Block a user