# Template cleanup — turn a production clone into a clean golden VM

Scripts to reset a **clone of VM 105 (logship-production)** into a clean,
data-free template VM. Built 2026-07 for VM 113 `logship-template-2`,
rebuilt 2026-10-01 as VM 115 `logship-template-3` (192.168.12.67).
Full runbook: RECLONE-RUNBOOK.md; short version:

## Order of operations

1. **Clone from PBS backup** (prod untouched), on prox2:
   ```bash
   qmrestore pbs-logyou:backup/vm/105/<latest> 115 --storage Disk1-NVMe --unique 1
   qm set 115 --name logship-template-3 --onboot 0 --protection 0 \
     --ipconfig0 ip=192.168.12.67/24,gw=192.168.12.1
   # read MAC from `qm config 115`, then boot quarantined:
   qm set 115 --net0 virtio=<MAC>,bridge=vmbr1,link_down=1
   qm start 115
   ```
2. **`00-defuse.sh`** — via `qm guest exec` while NIC is down: wipes root
   crontab (DHL crawler! customer reports!), disables postfix, clears all
   frontend runtime data (`data/` DBs + dirs, per-user caches), wipes
   `/root/storage` PDFs + stale deploy copies, removes prod ES bind +
   data.logship.de hosts entry, drops the Check_MK registration.
3. **`03-neutralize-env.sh`** — blanks external endpoints/credentials in
   `/opt/erp-nuxt-frontend/.env` (LARAVEL, Sentry, carrier/marketplace/
   Amazon-Ads creds, FreePBX, IVR, landing/Laravel tokens, SEPA), repoints
   STRAPIPUBLIC/WEBAUTHN_ORIGIN/MOBILE_APP_PUBLIC_BASE at the clone IP.
4. Bring NIC up (`qm set 115 --net0 virtio=<MAC>,bridge=vmbr1`), then
   everything else over SSH.
5. **`run-cleanup.sh --dry-run`** first — runs the iDempiere SQL with
   ROLLBACK and prints the dynamic targets, survivors and zero checks.
   Then **`run-cleanup.sh`** — safety pg_dumps, stops services, runs
   `01-idempiere-truncate.sql` + `02-strapi-truncate.sql`, wipes uploads,
   deletes ES indices, restarts everything, log hygiene.
6. **Review together** the survivor report + row counts before finalizing.
7. Finalize: `rm -rf /root/template-cleanup/backup/`, remove
   `/root/env.production.bak`, clear shell history, `fstrim -a`,
   right-size RAM (`qm set 115 --memory 20480`), shutdown.

## What the iDempiere truncation keeps / deletes

- **Kept:** client 1000000 config (roles, doc types, price list headers,
  taxes, warehouses/locators of org 1000000, `cust_commissiontable`,
  `cust_printer`, `cust_labeltemplate`, `cust_nonworkingdays`,
  `cust_shippingcost_default`, `cust_softwareplan`, `cust_softwarecatalogitem`
  (software-offer catalog), `m_attributeset`, Jasper processes), staff
  users, GardenWorld demo client.
- **Deleted:** all transactional documents of client 1000000 (orders,
  invoices, shipments, payments, fact_acct, stock, requests, …), all
  global log tables, ALL products + business partners of client 1000000
  except rows FK-referenced by kept config, and orgs 1000001+ from every
  table with an `ad_org_id` column.
- Engine: pass-based delete list with FK-retry; rows whose DELETE hits a
  foreign-key violation after convergence are reported as survivors —
  that's the "required by config" set, by construction.
- **Dynamic catch-all** (pass 1, after the explicit list): every populated
  `cust_*` table not in `keep_tables`, every table carrying a document FK
  column (`c_order_id`, `m_inoutline_id`, …) outside dictionary/master
  data, and all `i_*` / `t_*` / `*log` tables. Each addition is announced
  as `DYNAMIC target … : N rows` in the log — review them in the dry run.
  Strapi: every non-infrastructure table is truncated the same way.

All scripts carry a **production guard** (hostname/IP check) and are
idempotent — safe to re-run after fixing an issue.

Kept in `reference/` (tracked, not built/watched) per repo convention.
