# Migration Plan — EZShop + Kiranhart AH → ExcellentShop unified

**Date:** 2026-05-08
**Author:** s8n
**Supersedes:** AUDIT-2026-05-07 F-11; SHOP-SYSTEM-DECISION sec. 6 (now operational)
**Staging:** `staging/excellentshop/`
**Status:** Ready for operator-scheduled maintenance window.

---

## 1. Summary

Replace two plugins (EZShop 1.0-SNAPSHOT bespoke + Kiranhart AuctionHouse
1.4.6 ARR-no-LICENSE) with one GPL-3 stack:
ExcellentShop 5.0.1 + CoinsEngine 2.7.0, both built by NightExpress against
nightcore 2.15.3. All three jars are sourced from the upstream Reposilite
(`repo.nightexpressdev.com`) — source-of-truth identical to a local
`mvn package` from `github.com/nulli0n/ExcellentShop-spigot`, just reproducible
without the brittle `alex9849` integration that fails on TLS handshake.

Both modules share one theme, one perm tree, one update cadence. Chest Shop
module is disabled — not a future-of-server commit (QuickShop-Hikari path
already documented in SHOP-SYSTEM-DECISION).

## 2. Pre-flight (not in `swap.sh`, do these first)

1. **Heads-up window:** post in-game notice via `/pm news` minus 7d, 1d, 1h.
   "Auction listings will be drained on YYYY-MM-DD HH:MM."
2. **Drain Kiranhart AH:** instruct players to claim/cancel listings.
   Bid-in-progress items revert at the close of the bid window (max 2h,
   per `bid-auction-duration: 7200`). Manual refund the residue to the
   listing owner via `/eco give` after the window — record the audit
   trail in `live-server/migration-2026-05-08-residue.log`.
3. **Snapshot live data:** invoke
   `scripts/restic-backup-playerdata.sh` (already wired for nullstone).
   Verify the snapshot via `restic snapshots --tag pre-shop-migration`.
4. **Verify staged bundle:**
   ```
   cd staging/excellentshop/build
   sha256sum *.jar          # match the values committed alongside this doc
   ```
5. **Confirm operator window:** compose stop → up takes ~3 minutes;
   ExcellentShop generates ~11 default shops + 400 items on first start
   (~30s extra). Total expected downtime: 4-6 minutes best case,
   12 minutes worst case (if itzg downloads stall).

## 3. Swap procedure

`scripts/swap.sh` runs as `user@nullstone` and performs:

| Step | Action |
|------|--------|
| 1 | `docker compose stop mc` |
| 2 | tar legacy `EZShop` + `AuctionHouse` plugin trees + jars to `/opt/docker/minecraft/plugins.tar.bak-<ts>` |
| 3 | privileged `rm -rf` of legacy trees and jars (uses `docker run --privileged --userns=host` per `feedback_docker_sudo_bypass`) |
| 4 | populate `/opt/docker/minecraft-custom-plugins/` with the three staged jars, chown 1000:1000 |
| 5 | replace `docker-compose.yml` with `docker-compose.patch.yml` (adds `COPY_PLUGINS_SRC=/plugins-custom`, the bind mount, and the new `REMOVE_OLD_MODS_EXCLUDE` glob list) — keeps a `.bak-<ts>` |
| 6 | `docker compose up -d mc` (first-start config generation runs here) |
| 7 | poll `mc-health` for up to 4 minutes |
| 8 | `docker cp` the staged config overrides into the now-populated plugin tree, run `excellentshop reload` + `coinsengine reload`, then run `lp-shop-migration.sh` over RCON to grant default-tier perms |

The compose patch is identical to the existing live compose **except** for
the three lines: `PLUGINS` (drops EZShop+AH URLs), `REMOVE_OLD_MODS_EXCLUDE`
(adds three new globs), and `COPY_PLUGINS_SRC` + matching volume mount.

## 4. Smoke test (post-swap, manual)

Run as a non-staff player and as a staff player:

- `/shop` → main category menu, 11 shops, GUI renders within 1s.
- `/sellall` → sells inventory, `+N coins` chat feedback, `/balance` updates.
- `/sellhand` → sells hand stack, hand cleared.
- `/ah` → AH GUI opens, list a test item via `/ah list 100`, see it in browse.
- `/ah` admin: `/ah admin remove <listing>` works, `/ah admin expire` works.
- `/balance`, `/pay`, `/eco` resolve through CoinsEngine via Vault bridge.
- Tail logs `docker logs -f minecraft-mc | grep -E 'nightcore|ExcellentShop|CoinsEngine|ERROR|WARN'` for 5 minutes after first player joins.

If any of the above fail, run `scripts/rollback.sh`.

## 5. Rollback

`scripts/rollback.sh` reverses everything: stops the container, removes
ExcellentShop+CoinsEngine+nightcore plugin trees and jars, clears the
custom-plugins bind dir, restores legacy `EZShop`+`AuctionHouse` from the
tar snapshot, restores the previous `docker-compose.yml`, restarts, and
runs `lp-shop-rollback.sh` over RCON to restore the legacy perm nodes.

Rollback total time: ~5 minutes.

## 6. Risk register

| # | Risk | Likelihood | Impact | Mitigation |
|---|------|-----------|--------|-----------|
| R1 | nightcore 2.15.3 incompatible with Purpur 1.21.11 (api-version 1.21 should match) | Low | High | Smoke-test on a throwaway container first; rollback path < 5 min if breakage |
| R2 | CoinsEngine 2.7.0 fails Vault bridge (EssentialsX expects Vault) | Medium | High | `Vault_Compatibility.Default: true` set in staged currency YAML; verify `/balance` in smoke step 4 |
| R3 | NightExpress reposilite goes dark before next maintenance | Low | Low | Jars are committed to the repo at `staging/excellentshop/build/` with sha256 receipts — never re-fetched at swap time |
| R4 | Players keep typing `/auctionhouse` (legacy alias) | Certain | Low | `commands.yml` aliases `[ah, auctionhouse, market]` keep muscle memory |
| R5 | EZShop balance migration loss (if EZShop tracked any) | Very Low | Low | EZShop was sell-only with Vault → balances live in EssentialsX, untouched by this swap |
| R6 | Live AH listings lost (residual stuck items) | Medium | Medium | Pre-flight step 2 drains, residue refund is documented |
| R7 | First-start delay > 4 min healthcheck window (large default catalog generation) | Low | Medium | `start_period: 240s` already set; if exceeded, manual `docker exec mc mc-health` poll |
| R8 | Compose typo locks server out of plugins | Low | High | `swap.sh` keeps `docker-compose.yml.bak-<ts>` for trivial revert; rollback restores it |
| R9 | `excellentshop.shop.use` not granted to default group → no players can `/shop` | Medium | High | `lp-shop-migration.sh` step 8 grants the full perm tree; verify with `/lp user <name> permission check excellentshop.shop.use` |
| R10 | Compose `cap_drop: ALL` strips a cap CoinsEngine needs | Low | Low | If `/data/plugins/CoinsEngine/` write fails, `cap_add: DAC_OVERRIDE` rescue documented in swap.sh comments |

## 7. Top 3 swap-window risks (operator brief)

1. **R9 (perm regression)** — if the LP migration RCON call drops mid-script,
   players see `/shop` work but `/sellall` fail. Mitigation: re-run
   `lp-shop-migration.sh` idempotently — every line is `permission set <node> true`.
2. **R7 (slow first-start)** — first start of a fresh ExcellentShop with the
   bundled catalog generation takes ~30s extra and might trip the 240s
   healthcheck on a cold pool. Mitigation: `swap.sh` step 7 polls manually
   before declaring success.
3. **R2 (Vault bridge)** — if `/balance` returns 0 instead of the player's
   actual balance, the Vault bridge from CoinsEngine is mis-wired. Mitigation:
   `Vault_Compatibility.Default: true` is in the staged YAML; if still broken,
   `/eco give <player> <prev-balance>` from the EssentialsX-side dump that
   `restic-backup-playerdata.sh` already takes.

## 8. Estimated downtime

- **Best case:** 4 minutes (compose stop → up + health check, no log churn).
- **Typical:** 6 minutes (one extra stall on jar copy or LP RCON timeout).
- **Worst case:** 12 minutes (first-start catalog generation slow + manual smoke).

If the worst case is exceeded, abort and run `rollback.sh` — the data
snapshot from pre-flight step 3 is the source of truth.

## 9. Post-migration

- Update `AUDIT-2026-05-07.md` F-11: status → Resolved (date 2026-05-08).
- Update `docs/COMMANDS.md`: `/shop`, `/ah`, `/sellall`, `/sellhand`,
  `/balance` all now route through ExcellentShop+CoinsEngine.
- Update `SYSTEM.md` plugin table: drop EZShop, drop AuctionHouse 1.4.6,
  add ExcellentShop 5.0.1, CoinsEngine 2.7.0, nightcore 2.15.3.
- Schedule a follow-up audit in 7d to confirm no log spam, no economy
  drift, no listing exploits.
