#!/usr/bin/env bash
# =====================================================================
# sync-from-prod.sh — reflect production drift onto the golden template VM
# WITHOUT re-cloning (the full re-clone is REFRESH-QUICKSTART.md).
#
#   sync-from-prod.sh [--since YYYY-MM-DD] [--dry-run] [--prod IP] [--tpl IP]
#
# Runs on a workstation that can ssh as root to BOTH hosts. Transfers
# prod -> template go directly (rsync over ssh with agent forwarding), so
# run `ssh-add` first. Production is only ever READ.
#
# What it syncs (template keeps its own .env values, data/, DB data):
#   - apt packages where prod is newer (same repos) + packages only prod has
#   - Node version of prod's nvm default + the same pm2 version; pm2-root
#     unit + override from prod; erpfrontend restarted on the new Node
#   - /opt/erp-nuxt-frontend (build + node_modules) — .env and data/ kept;
#     env keys that only prod has are appended BLANK (review the list it
#     prints: non-secret ones like ANTHROPIC_MODEL need a value);
#     GITHASH/BUILDTIME taken from prod
#   - iDempiere: new tables (pg_dump -s), new physical columns (+ CHECK
#     constraints), Application-Dictionary rows created since --since
#     (ad_table/element/reference/ref_list/column/field + _trl), AD sequences
#   - Jasper report files, jetty.xml/jetty-template.xml, postfix main.cf +
#     sender_relay + tls_policy (never sasl_passwd; postfix stays disabled),
#     the data.logship.de hosts line, git pull of /opt/logship-scripts
# Reported only (template data is intentionally different): config-row
# updates in keep tables / price lists / shippers / order sources, new
# AD windows/tabs/menus/processes/print formats, ad_sysconfig changes.
#
# --since defaults to the template's BUILDTIME (.env) minus one day.
# Idempotent: a second run finds nothing to do. Every DB change runs in one
# transaction; the previous frontend tree stays in /opt/erp-nuxt-frontend-rollback.
# =====================================================================
set -euo pipefail

PROD=192.168.12.72
TPL=192.168.12.67
SINCE=""
DRY=0
while [ $# -gt 0 ]; do
  case "$1" in
    --since) SINCE="$2"; shift 2;;
    --dry-run) DRY=1; shift;;
    --prod) PROD="$2"; shift 2;;
    --tpl) TPL="$2"; shift 2;;
    *) echo "unknown arg $1" >&2; exit 2;;
  esac
done

SSH="ssh -o BatchMode=yes -o ConnectTimeout=20"
P() { $SSH "root@$PROD" "$@"; }
T() { $SSH "root@$TPL" "$@"; }
WORK="$(mktemp -d /tmp/tpl-sync.XXXXXX)"
STAMP="$(date +%Y%m%d-%H%M%S)"
RDIR="/root/sync-$STAMP"
say() { printf '\n== %s\n' "$*"; }

# --- guards --------------------------------------------------------------
TPL_HOST="$(T hostname)"
case "$TPL_HOST" in logship-production) echo "FATAL: target is production" >&2; exit 1;; esac
P "hostname | grep -q '^logship-production$'" || { echo "FATAL: $PROD is not logship-production" >&2; exit 1; }
if ! $SSH -A "root@$PROD" "ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new root@$TPL true" 2>/dev/null; then
  echo "FATAL: prod cannot reach the template over ssh — run 'ssh-add' (agent forwarding is used for rsync)" >&2; exit 1
fi
if [ -z "$SINCE" ]; then
  bt="$(T "grep -oE '^BUILDTIME=[0-9.]+' /opt/erp-nuxt-frontend/.env | cut -d= -f2")"   # dd.mm.yyyy
  SINCE="$(python3 -c "import datetime as d,sys; x=d.datetime.strptime('$bt','%d.%m.%Y')-d.timedelta(days=1); print(x.date())")"
fi
echo "prod $PROD -> template $TPL ($TPL_HOST), AD rows since $SINCE, dry-run=$DRY, work dir $WORK"

# --- 1. collect ------------------------------------------------------------
say "1/7 collecting state"
NODE_PROD="$(P 'cat /root/.nvm/alias/default')"
NODE_TPL="$(T 'cat /root/.nvm/alias/default')"
PM2_PROD="$(P "grep -o '\"version\": *\"[^\"]*\"' /root/.nvm/versions/node/$NODE_PROD/lib/node_modules/pm2/package.json | grep -oE '[0-9][0-9.]*'")"
PM2_TPL="$(T "grep -o '\"version\": *\"[^\"]*\"' /root/.nvm/versions/node/$NODE_TPL/lib/node_modules/pm2/package.json 2>/dev/null | grep -oE '[0-9][0-9.]*' || echo none")"
HASH_PROD="$(P "grep -oE '^GITHASH=.*' /opt/erp-nuxt-frontend/.env | cut -d= -f2")"
HASH_TPL="$(T "grep -oE '^GITHASH=.*' /opt/erp-nuxt-frontend/.env | cut -d= -f2")"
BUILD_PROD="$(P "grep -oE '^BUILDTIME=.*' /opt/erp-nuxt-frontend/.env | cut -d= -f2-")"
echo "node     prod $NODE_PROD / tpl $NODE_TPL"
echo "pm2      prod $PM2_PROD / tpl $PM2_TPL"
echo "frontend prod $HASH_PROD ($BUILD_PROD) / tpl $HASH_TPL"

# packages: prod newer or prod-only
P "dpkg-query -W -f='\${Package} \${Version}\n'" | sort > "$WORK/pkg-prod.txt"
T "dpkg-query -W -f='\${Package} \${Version}\n'" | sort > "$WORK/pkg-tpl.txt"
scp -q "$WORK/pkg-prod.txt" "root@$TPL:/tmp/pkg-prod.txt"
T 'while read p v; do tv=$(dpkg-query -W -f=\${Version} "$p" 2>/dev/null || true); if [ -z "$tv" ]; then echo "$p (missing, prod $v)"; elif dpkg --compare-versions "$v" gt "$tv"; then echo "$p ($tv -> $v)"; fi; done < /tmp/pkg-prod.txt; rm -f /tmp/pkg-prod.txt' > "$WORK/pkg-diff.txt" || true
PKGS="$(awk '{print $1}' "$WORK/pkg-diff.txt" | tr '\n' ' ')"
echo "packages to upgrade/install: ${PKGS:-none}"
[ -s "$WORK/pkg-diff.txt" ] && sed 's/^/   /' "$WORK/pkg-diff.txt"

# iDempiere schema: template column + constraint + table lists -> prod generates DDL
T "sudo -u postgres psql -d idempiere -Atc \"select c.relname||'.'||a.attname from pg_attribute a join pg_class c on c.oid=a.attrelid join pg_namespace n on n.oid=c.relnamespace where n.nspname='adempiere' and c.relkind='r' and a.attnum>0 and not a.attisdropped order by 1\" 2>/dev/null" > "$WORK/tpl-cols.txt"
T "sudo -u postgres psql -d idempiere -Atc \"select conname from pg_constraint k join pg_class c on c.oid=k.conrelid join pg_namespace n on n.oid=c.relnamespace where n.nspname='adempiere'\" 2>/dev/null" > "$WORK/tpl-cons.txt"
T "sudo -u postgres psql -d idempiere -Atc \"select relname from pg_class c join pg_namespace n on n.oid=c.relnamespace where n.nspname='adempiere' and relkind='r'\" 2>/dev/null" > "$WORK/tpl-tables.txt"
P "mkdir -p /tmp/tpl-sync && chmod 777 /tmp/tpl-sync"
scp -q "$WORK/tpl-cols.txt" "$WORK/tpl-cons.txt" "$WORK/tpl-tables.txt" "root@$PROD:/tmp/tpl-sync/"
cat > "$WORK/gen-ddl.sql" <<'SQL'
\set ON_ERROR_STOP on
create temp table tpl_cols(tc text); create temp table tpl_cons(cn text); create temp table tpl_tables(tn text);
\copy tpl_cols from '/tmp/tpl-sync/tpl-cols.txt'
\copy tpl_cons from '/tmp/tpl-sync/tpl-cons.txt'
\copy tpl_tables from '/tmp/tpl-sync/tpl-tables.txt'
\o /tmp/tpl-sync/new-tables.txt
select relname from pg_class c join pg_namespace n on n.oid=c.relnamespace where n.nspname='adempiere' and relkind='r' and relname not in (select tn from tpl_tables) order by 1;
\o /tmp/tpl-sync/ddl-columns.sql
select format('ALTER TABLE adempiere.%I ADD COLUMN %I %s%s%s;', c.relname, a.attname, format_type(a.atttypid,a.atttypmod),
       coalesce(' DEFAULT '||pg_get_expr(d.adbin,d.adrelid),''), case when a.attnotnull then ' NOT NULL' else '' end)
from pg_attribute a join pg_class c on c.oid=a.attrelid join pg_namespace n on n.oid=c.relnamespace
left join pg_attrdef d on d.adrelid=a.attrelid and d.adnum=a.attnum
where n.nspname='adempiere' and c.relkind='r' and a.attnum>0 and not a.attisdropped
  and c.relname in (select tn from tpl_tables) and (c.relname||'.'||a.attname) not in (select tc from tpl_cols)
order by c.relname, a.attnum;
\o /tmp/tpl-sync/ddl-constraints.sql
select format('ALTER TABLE adempiere.%I ADD CONSTRAINT %I %s;', c.relname, k.conname, pg_get_constraintdef(k.oid))
from pg_constraint k join pg_class c on c.oid=k.conrelid join pg_namespace n on n.oid=c.relnamespace
where n.nspname='adempiere' and k.contype='c' and c.relname in (select tn from tpl_tables)
  and k.conname not in (select cn from tpl_cons)
  and exists (select 1 from unnest(k.conkey) ck join pg_attribute a on a.attrelid=k.conrelid and a.attnum=ck
              where (c.relname||'.'||a.attname) not in (select tc from tpl_cols))
order by 1;
\o
SQL
scp -q "$WORK/gen-ddl.sql" "root@$PROD:/tmp/tpl-sync/gen-ddl.sql"
P "cd /tmp/tpl-sync && sudo -u postgres psql -d idempiere -Atf gen-ddl.sql >/dev/null 2>gen.err || { grep -v perl gen.err; exit 1; }"
# AD rows + sequences + report-only items, exported on prod
P "cd /tmp/tpl-sync && export LANG=C.UTF-8 && Q='sudo -u postgres psql -d idempiere -At' && \
   for t in ad_table ad_element ad_reference ad_ref_list ad_column ad_field; do \
     \$Q -c \"COPY (select * from adempiere.\$t where created > '$SINCE') TO STDOUT\" > \$t.copy 2>/dev/null; \
     \$Q -c \"COPY (select x.* from adempiere.\${t}_trl x join adempiere.\$t b on b.\${t}_id = x.\${t}_id where b.created > '$SINCE') TO STDOUT\" > \${t}_trl.copy 2>/dev/null; \
   done; \
   \$Q -c \"COPY (select name, currentnext from adempiere.ad_sequence where name in ('AD_Table','AD_Element','AD_Reference','AD_Ref_List','AD_Column','AD_Field')) TO STDOUT\" > sequences.copy 2>/dev/null; \
   \$Q -F' ' -c \"select 'ad_window',count(*) from adempiere.ad_window where created > '$SINCE' union all select 'ad_tab',count(*) from adempiere.ad_tab where created > '$SINCE' union all select 'ad_menu',count(*) from adempiere.ad_menu where created > '$SINCE' union all select 'ad_process',count(*) from adempiere.ad_process where created > '$SINCE' union all select 'ad_printformat',count(*) from adempiere.ad_printformat where created > '$SINCE' union all select 'ad_sysconfig updated',count(*) from adempiere.ad_sysconfig where updated > '$SINCE' union all select 'ad_val_rule',count(*) from adempiere.ad_val_rule where created > '$SINCE'\" 2>/dev/null > manual.txt; \
   \$Q -F' ' -c \"select 'm_pricelist',name,updated::date from adempiere.m_pricelist where updated > '$SINCE' union all select 'm_shipper',name,updated::date from adempiere.m_shipper where updated > '$SINCE' and ad_org_id in (0,1000000) union all select 'c_ordersource',name,updated::date from adempiere.c_ordersource where updated > '$SINCE' and ad_org_id in (0,1000000) union all select 'cust_printer',name,updated::date from adempiere.cust_printer where updated > '$SINCE' union all select 'cust_labeltemplate',name,updated::date from adempiere.cust_labeltemplate where updated > '$SINCE' union all select 'cust_commissiontable',name,updated::date from adempiere.cust_commissiontable where updated > '$SINCE' union all select 'c_doctype',name,updated::date from adempiere.c_doctype where updated > '$SINCE' union all select 'ad_role',name,updated::date from adempiere.ad_role where updated > '$SINCE'\" 2>/dev/null >> manual.txt; \
   wc -l *.copy ddl-columns.sql ddl-constraints.sql new-tables.txt | grep -v total"
scp -q "root@$PROD:/tmp/tpl-sync/*" "$WORK/" 2>/dev/null || true
if [ -s "$WORK/new-tables.txt" ]; then
  P "cd /tmp/tpl-sync && sudo -u postgres pg_dump -s -n adempiere $(sed 's/^/-t adempiere./' "$WORK/new-tables.txt" | tr '\n' ' ') idempiere" > "$WORK/ddl-tables.sql" 2>/dev/null
fi
P "rm -rf /tmp/tpl-sync"
echo "new tables:      $(wc -l < "$WORK/new-tables.txt" | tr -d ' ')  $(tr '\n' ' ' < "$WORK/new-tables.txt")"
echo "new columns:     $(wc -l < "$WORK/ddl-columns.sql" | tr -d ' ')"; sed 's/^/   /' "$WORK/ddl-columns.sql"
echo "new constraints: $(wc -l < "$WORK/ddl-constraints.sql" | tr -d ' ')"
for t in ad_table ad_element ad_reference ad_ref_list ad_column ad_field; do printf '%-16s %s rows (+%s trl)\n' "$t" "$(wc -l < "$WORK/$t.copy" | tr -d ' ')" "$(wc -l < "$WORK/${t}_trl.copy" | tr -d ' ')"; done
echo "report-only (handle by hand if non-zero / listed):"; sed 's/^/   /' "$WORK/manual.txt"

# files
P "cd /opt/idempiere-server/data/reports && find . -type f -exec md5sum {} + | sort -k2" > "$WORK/jasper-prod.txt"
T "cd /opt/idempiere-server/data/reports && find . -type f -exec md5sum {} + | sort -k2" > "$WORK/jasper-tpl.txt"
JASPER_DIFF="$(diff "$WORK/jasper-prod.txt" "$WORK/jasper-tpl.txt" | grep -c '^[<>]' || true)"
echo "jasper files differing: $JASPER_DIFF"
for f in /opt/idempiere-server/jettyhome/etc/jetty.xml /opt/idempiere-server/jettyhome/etc/jetty-template.xml /etc/postfix/main.cf /etc/postfix/sender_relay /etc/postfix/tls_policy /etc/systemd/system/pm2-root.service /etc/systemd/system/pm2-root.service.d/override.conf; do
  a="$(P "md5sum $f 2>/dev/null | cut -c1-8")"; b="$(T "md5sum $f 2>/dev/null | cut -c1-8")"
  [ "$a" = "$b" ] || echo "config differs: $f (prod ${a:-none} / tpl ${b:-none})"
done
HOSTS_LINE="$(P "grep -E 'data\.logship\.de' /etc/hosts || true")"
SCRIPTS_PROD="$(P 'cd /opt/logship-scripts && git rev-parse --short HEAD')"; SCRIPTS_TPL="$(T 'cd /opt/logship-scripts && git rev-parse --short HEAD')"
echo "logship-scripts prod $SCRIPTS_PROD / tpl $SCRIPTS_TPL"
# env keys only prod has
P "grep -oE '^[A-Z_0-9]+=' /opt/erp-nuxt-frontend/.env | tr -d = | sort -u" > "$WORK/env-prod.txt"
T "grep -oE '^[A-Z_0-9]+=' /opt/erp-nuxt-frontend/.env | tr -d = | sort -u" > "$WORK/env-tpl.txt"
NEWKEYS="$(comm -23 "$WORK/env-prod.txt" "$WORK/env-tpl.txt" | tr '\n' ' ')"
echo "env keys only on prod (appended blank): ${NEWKEYS:-none}"

if [ "$DRY" = 1 ]; then echo; echo "dry run — nothing changed. work dir kept: $WORK"; exit 0; fi

# --- 2. packages -------------------------------------------------------------
say "2/7 packages"
if [ -n "$PKGS" ]; then
  T "export DEBIAN_FRONTEND=noninteractive; apt-get update -qq >/dev/null 2>&1 || true; apt-get install -y -q $PKGS 2>&1 | tail -3"
else echo "nothing to do"; fi

# --- 3. node + pm2 -----------------------------------------------------------
say "3/7 node $NODE_PROD + pm2 $PM2_PROD"
if [ "$NODE_PROD" != "$NODE_TPL" ] || [ "$PM2_PROD" != "$PM2_TPL" ]; then
  T "export NVM_DIR=/root/.nvm; . \$NVM_DIR/nvm.sh >/dev/null; nvm install $NODE_PROD >/dev/null 2>&1; nvm alias default $NODE_PROD >/dev/null; nvm use $NODE_PROD >/dev/null; npm install -g pm2@$PM2_PROD >/dev/null 2>&1; node -v; pm2 -v"
else echo "already equal"; fi

# --- 4. frontend ---------------------------------------------------------------
say "4/7 frontend $HASH_PROD"
T "mkdir -p $RDIR"
if [ "$HASH_PROD" != "$HASH_TPL" ] || ! T "test -d /opt/erp-nuxt-frontend/.output"; then
  $SSH -A "root@$PROD" "rsync -a --delete --exclude .env --exclude data/ --exclude 'logship__*.db' -e 'ssh -o BatchMode=yes' /opt/erp-nuxt-frontend/ root@$TPL:/opt/erp-nuxt-frontend-sync/"
  T "set -e; export NVM_DIR=/root/.nvm; . \$NVM_DIR/nvm.sh >/dev/null; export PATH=/root/.nvm/versions/node/$NODE_PROD/bin:\$PATH
     pm2 delete erpfrontend >/dev/null 2>&1 || true
     rm -rf /opt/erp-nuxt-frontend-rollback; mv /opt/erp-nuxt-frontend /opt/erp-nuxt-frontend-rollback; mv /opt/erp-nuxt-frontend-sync /opt/erp-nuxt-frontend
     cp -p /opt/erp-nuxt-frontend-rollback/.env /opt/erp-nuxt-frontend/.env; cp -p /opt/erp-nuxt-frontend-rollback/.env $RDIR/env.before
     mkdir -p /opt/erp-nuxt-frontend/data; printf '' > /opt/erp-nuxt-frontend/data/.gitkeep; chmod 755 /opt/erp-nuxt-frontend/data
     cd /opt/erp-nuxt-frontend/.output/server && node -e 'require(\"./node_modules/sqlite3\"); require(\"./node_modules/better-sqlite3\"); console.log(\"native modules load OK\")'"
else echo "build already equal — skipping rsync"; fi
# env: build id + new keys (blank)
T "sed -i -E 's|^GITHASH=.*|GITHASH=$HASH_PROD|; s|^BUILDTIME=.*|BUILDTIME=$BUILD_PROD|' /opt/erp-nuxt-frontend/.env
   for k in $NEWKEYS; do grep -qE \"^\$k=\" /opt/erp-nuxt-frontend/.env || printf '%s=\n' \"\$k\" >> /opt/erp-nuxt-frontend/.env; done"
# pm2 unit files from prod, restart under systemd
P "cat /etc/systemd/system/pm2-root.service" > "$WORK/pm2-root.service"
P "cat /etc/systemd/system/pm2-root.service.d/override.conf" > "$WORK/override.conf"
scp -q "$WORK/pm2-root.service" "root@$TPL:/etc/systemd/system/pm2-root.service"
scp -q "$WORK/override.conf" "root@$TPL:/etc/systemd/system/pm2-root.service.d/override.conf"
T "set -e; export NVM_DIR=/root/.nvm; . \$NVM_DIR/nvm.sh >/dev/null; export PATH=/root/.nvm/versions/node/$NODE_PROD/bin:\$PATH
   systemctl daemon-reload
   cd /opt/erp-nuxt-frontend; pm2 delete erpfrontend >/dev/null 2>&1 || true
   pm2 start ecosystem.config.cjs --only erpfrontend --update-env --interpreter /root/.nvm/versions/node/$NODE_PROD/bin/node >/dev/null
   pm2 describe media-strapi-prod >/dev/null 2>&1 || (cd /opt/mediaserver && pm2 start ecosystem.config.js --update-env >/dev/null)
   pm2 save --force >/dev/null; systemctl restart pm2-root; sleep 10
   # pm2 update replaces the daemon and may drop the apps: re-add + save if so
   pm2 describe erpfrontend >/dev/null 2>&1 || { pm2 start ecosystem.config.cjs --only erpfrontend --update-env --interpreter /root/.nvm/versions/node/$NODE_PROD/bin/node >/dev/null; pm2 save --force >/dev/null; }
   for i in \$(seq 1 30); do code=\$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3001/signin); [ \"\$code\" = 200 ] && break; sleep 3; done; echo \"frontend /signin http \$code\"
   pm2 list | grep -E 'erpfrontend|media-strapi' | cut -c1-100"

# --- 5. iDempiere ----------------------------------------------------------------
say "5/7 iDempiere schema + AD rows"
cat > "$WORK/apply-db.sql" <<SQL
\set ON_ERROR_STOP on
BEGIN;
\i ddl-tables.sql
\i ddl-columns.sql
\i ddl-constraints.sql
SQL
for t in ad_table ad_element ad_reference ad_ref_list ad_column ad_field; do
  cat >> "$WORK/apply-db.sql" <<SQL
CREATE TEMP TABLE tmp_$t (LIKE adempiere.$t);
\copy tmp_$t from '$t.copy'
INSERT INTO adempiere.$t SELECT * FROM tmp_$t x WHERE NOT EXISTS (SELECT 1 FROM adempiere.$t y WHERE y.${t}_id = x.${t}_id);
CREATE TEMP TABLE tmp_${t}_trl (LIKE adempiere.${t}_trl);
\copy tmp_${t}_trl from '${t}_trl.copy'
INSERT INTO adempiere.${t}_trl SELECT * FROM tmp_${t}_trl x WHERE NOT EXISTS (SELECT 1 FROM adempiere.${t}_trl y WHERE y.${t}_id = x.${t}_id AND y.ad_language = x.ad_language);
SQL
done
cat >> "$WORK/apply-db.sql" <<'SQL'
CREATE TEMP TABLE tmp_seq (name varchar(60), currentnext numeric);
\copy tmp_seq from 'sequences.copy'
UPDATE adempiere.ad_sequence s SET currentnext = t.currentnext FROM tmp_seq t WHERE s.name = t.name AND s.currentnext < t.currentnext;
SELECT 'columns' AS chk, count(*) FROM information_schema.columns WHERE table_schema = 'adempiere'
UNION ALL SELECT 'ad_column', count(*) FROM adempiere.ad_column
UNION ALL SELECT 'ad_element', count(*) FROM adempiere.ad_element;
COMMIT;
SQL
touch "$WORK/ddl-tables.sql"
T "mkdir -p $RDIR/db && chmod 755 $RDIR $RDIR/db"
scp -q "$WORK"/*.copy "$WORK"/ddl-*.sql "$WORK/apply-db.sql" "root@$TPL:$RDIR/db/"
T "chmod 644 $RDIR/db/*; cd $RDIR/db && export LANG=C.UTF-8; sudo -u postgres psql -d idempiere -v ON_ERROR_STOP=1 -f apply-db.sql > apply.log 2>&1 || { grep -v perl apply.log; echo DB_APPLY_FAILED; exit 1; }; grep -vE '^(perl|LANGUAGE|LC_|LANG|\s+are supported|CREATE TABLE|COPY|INSERT 0 0|BEGIN|COMMIT|SET|SELECT)' apply.log | grep -vE '^\s*$'"

# --- 6. files ----------------------------------------------------------------------
say "6/7 report files, jetty, postfix, hosts, scripts"
[ "$JASPER_DIFF" = 0 ] || $SSH -A "root@$PROD" "rsync -a -e 'ssh -o BatchMode=yes' /opt/idempiere-server/data/reports/ root@$TPL:/opt/idempiere-server/data/reports/"
for f in /opt/idempiere-server/jettyhome/etc/jetty.xml /opt/idempiere-server/jettyhome/etc/jetty-template.xml /etc/postfix/main.cf /etc/postfix/sender_relay /etc/postfix/tls_policy; do
  P "cat $f" > "$WORK/$(basename "$f")" && scp -q "$WORK/$(basename "$f")" "root@$TPL:$f"
done
T "cd /etc/postfix && { [ -f sasl_passwd ] || { printf '# SASL credentials for the sender-dependent relay (see sender_relay); fill in per instance:\n# [host]:587 user@example.com:password\n' > sasl_passwd; chmod 600 sasl_passwd; }; }; postmap sender_relay tls_policy sasl_passwd 2>/dev/null || true; systemctl is-enabled postfix >/dev/null 2>&1 && echo 'WARNING: postfix enabled on the template' || true"
[ -z "$HOSTS_LINE" ] || T "grep -qF '$HOSTS_LINE' /etc/hosts || echo '$HOSTS_LINE' >> /etc/hosts"
[ "$SCRIPTS_PROD" = "$SCRIPTS_TPL" ] || T "cd /opt/logship-scripts && git pull --ff-only -q && git rev-parse --short HEAD"
say "restarting idempiere"
T "systemctl restart idempiere; for i in \$(seq 1 90); do curl -ks -o /dev/null --connect-timeout 2 https://127.0.0.1:8443/ && break; sleep 3; done; curl -ks -o /dev/null -w 'idempiere http %{http_code}\n' https://127.0.0.1:8443/"

# --- 7. validate ---------------------------------------------------------------------
say "7/7 validate"
T "export LANG=C.UTF-8; bash /root/template-cleanup/validate-template.sh 2>/dev/null | grep -E 'active|signin|GITHASH|zero|c_order |ad_user_client|disk|/dev'"
echo
echo "done. Review above; new env keys appended blank: ${NEWKEYS:-none}. Reboot the template if the kernel changed (prod $(P uname -r) / tpl $(T uname -r))."
echo "Then update TEMPLATE-README.md (build/date) and the memory note. Work dir: $WORK"
