Files
EpicNext-Cms/scripts/catalog-sync.sh
T
openhands f56b345598
CI / check (push) Successful in 37s
CI / tests-unit (push) Successful in 2m3s
CI / tests-ui (push) Successful in 2m52s
CI / tests-integration (push) Successful in 2m54s
CI / preflight (push) Skipped
CI / tests-imaging-browsers (push) Failing after 3m23s
CI / deploy (push) Successful in 3m3s
CI / tests-quality (push) Successful in 6m8s
feat(catalog-sync): add import, reconcile, sprite-id and offer steps
The CLI could audit and repair what already existed, but adding furniture
present at a source was still only possible from the admin UI item by item. The
repo already ships every piece of that pipeline, so wire them together as
further steps of the same run:

- `--import-new` imports furniture the compared source has and this hotel lacks.
  It reuses `getClonableClassnames` for the diff and `cloneSingleFurni` per item,
  which downloads the icon and the bundle (sources serve `.nitro`, stored as
  `.hab`), appends the furnidata entry with `deferFurniData` and creates the
  catalog page plus the offer itself. Deferred entries are written in one
  `appendFurniEntriesBulk` call, so 26000 items cost one JSON write, not one per
  item. `--only`, `--limit` and `--concurrency` narrow/tune it, failures are
  isolated per item and never abort the run.
- `--reconcile`, `--fix-sprite-ids` and `--rebuild-offers` run the existing
  `reconcileFurniDataWithItemsBase`, `verifyAndFixSpriteIds` and
  `rebuildCatalogOfferIds` passes — the post-import consistency work.
- `--align-ids` and `--rename-bundles` delegate to the existing standalone
  scripts; both only report until `--apply` is given, matching their own
  contracts.
- The menu grows options 6-8 plus a dry run, each with its own confirmation.

`--everything` turns on every repair + import step but deliberately leaves the
destructive ones off. A live dry run against Wibbo listed 26593 items on the
source that this hotel does not have, and Habbo (COM) reported 2871.
2026-10-15 20:12:13 +02:00

193 lines
5.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# Update the hotel catalog from a clone source (Habbo, retros, …).
#
# Two ways to use it:
# ./scripts/catalog-sync.sh interactive menu
# ./scripts/catalog-sync.sh --all --sql same flags as the CLI, no menu
#
# The menu asks which sites to compare (Habbo is the preset) and what may be
# written. An audit is read-only; only a repair option touches the catalog, and
# applying the generated catalog SQL always needs its own choice.
cd "$(dirname "$0")/.." || exit 1
CLI="scripts/catalog-sync.ts"
# The react-server export condition resolves `server-only` to its no-op build,
# exactly like `pnpm jobs:worker` — the audit engine imports server modules.
RUN=(node --conditions=react-server --import tsx "$CLI")
# Any flag passed straight through means "no menu", so the same entry point
# works for cron/CI where only the flag path is safe.
if [ $# -gt 0 ]; then
exec "${RUN[@]}" "$@"
fi
if [ ! -t 0 ]; then
echo "Not a terminal. Use the non-interactive form, e.g.:" >&2
echo " $0 --all --sources=default-habbo-com" >&2
exit 1
fi
echo "=== Catalog sync ==="
echo
# ── Step 1: which sites to compare ───────────────────────────────────────────
# Ask the CLI for the configured sources so the menu always matches the live
# `clone_sources` setting (and its defaults) instead of a hardcoded list.
mapfile -t SOURCE_ROWS < <("${RUN[@]}" --list-sources --tsv)
if [ "${#SOURCE_ROWS[@]}" -eq 0 ]; then
echo "No clone sources are configured." >&2
exit 1
fi
IDS=()
NAMES=()
echo "Compare this catalog against:"
for i in "${!SOURCE_ROWS[@]}"; do
IFS=$'\t' read -r SOURCE_ID SOURCE_NAME _SOURCE_URL <<<"${SOURCE_ROWS[$i]}"
IDS+=("$SOURCE_ID")
NAMES+=("$SOURCE_NAME")
printf " %2d) %-24s %s\n" "$((i + 1))" "$SOURCE_NAME" "$SOURCE_ID"
done
echo
echo " a) all sources above"
echo " Enter) preset: ${NAMES[0]}"
read -r -p "Sources [a, 1-${#IDS[@]}, Enter]: " SOURCE_CHOICE
SELECTED_FLAG=""
case "$SOURCE_CHOICE" in
"" | "Habbo" | "habbo" | "habbo.com")
# Habbo is the first default preset (Habbo (COM)).
SELECTED_FLAG="--sources=${IDS[0]}"
SOURCE_LABEL="${NAMES[0]}"
;;
a | A | all)
SOURCE_LABEL="all sources"
;;
*)
SELECTED_IDS=()
IFS=', ' read -r -a CHOICES <<<"$SOURCE_CHOICE"
for CHOICE in "${CHOICES[@]}"; do
[ -z "$CHOICE" ] && continue
case "$CHOICE" in
*[!0-9]* | "")
echo "Invalid choice: '$CHOICE'" >&2
exit 1
;;
*)
if [ "$CHOICE" -lt 1 ] || [ "$CHOICE" -gt "${#IDS[@]}" ]; then
echo "Out of range: $CHOICE" >&2
exit 1
fi
SELECTED_IDS+=("${IDS[$((CHOICE - 1))]}")
;;
esac
done
if [ "${#SELECTED_IDS[@]}" -eq 0 ]; then
echo "No sources selected." >&2
exit 1
fi
SELECTED_FLAG="--sources=$(IFS=,; echo "${SELECTED_IDS[*]}")"
SOURCE_LABEL="${#SELECTED_IDS[@]} source(s)"
;;
esac
# ── Step 2: what may be changed ──────────────────────────────────────────────
cat <<EOF
What should be done? (comparing with: $SOURCE_LABEL)
1) Audit only — nothing is written
2) Repair furniture icons + nitro bundles
3) 2 + FurnitureData.json + catalog structure
4) 3 + generate the catalog SQL (saved, not applied)
5) 4 + APPLY the catalog SQL to the catalog
6) 5 + import the furniture the source has and this hotel lacks
(downloads icon + bundle, adds FurnitureData.json, creates the catalog
page and the offer for every new item)
7) 6 + reconcile FurnitureData.json / sprite ids / offer ids
8) 7 + align items_base ids and rename legacy .nitro bundles (heaviest)
d) Dry run — only report what 6 would import
q) Cancel
EOF
read -r -p "Action [1]: " ACTION_CHOICE
ACTION_CHOICE="${ACTION_CHOICE:-1}"
ACTIONS=(audit)
CONFIRM=""
# Empty for the audit-only branch on purpose: no repair flags are passed.
ACTION_FLAGS=()
case "$ACTION_CHOICE" in
1)
ACTIONS=(audit "[nothing written]")
;;
2)
ACTIONS=(audit "icons + nitro bundles downloaded")
ACTION_FLAGS=(--icons --nitros)
;;
3)
ACTIONS=(audit "icons + nitro bundles + FurnitureData.json + structure")
ACTION_FLAGS=(--all)
;;
4)
ACTIONS=(audit "everything + catalog SQL generated")
ACTION_FLAGS=(--all --sql)
;;
5)
ACTIONS=(audit "everything + catalog SQL APPLIED")
ACTION_FLAGS=(--all --sql --apply-sql)
CONFIRM="This writes to the catalog. Continue? [y/N]: "
;;
6)
ACTIONS=(audit "everything + catalog SQL + import new furniture")
ACTION_FLAGS=(--all --sql --apply-sql --import-new)
CONFIRM="This writes to the catalog AND imports every missing item. Continue? [y/N]: "
;;
7)
ACTIONS=(audit "everything + import + reconcile")
ACTION_FLAGS=(
--all --sql --apply-sql --import-new --reconcile --fix-sprite-ids
--rebuild-offers
)
CONFIRM="This writes to the catalog AND imports every missing item. Continue? [y/N]: "
;;
8)
ACTIONS=(audit "everything + import + reconcile + id alignment + rename")
ACTION_FLAGS=(
--all --sql --apply-sql --import-new --reconcile --fix-sprite-ids
--rebuild-offers --align-ids --rename-bundles --apply
)
read -r -p " This rewrites items_base ids and renames bundle files. Type RUN to confirm: " REPLY
if [ "$REPLY" != "RUN" ]; then
echo "Cancelled."
exit 0
fi
;;
d | D)
ACTIONS=(audit "report only — nothing is imported")
ACTION_FLAGS=(--import-new --dry-run)
;;
q | Q)
echo "Cancelled."
exit 0
;;
*)
echo "Invalid action: '$ACTION_CHOICE'" >&2
exit 1
;;
esac
echo
printf 'Running: %s against %s\n' "${ACTIONS[1]}" "$SOURCE_LABEL"
if [ -n "$CONFIRM" ]; then
read -r -p "$CONFIRM" REPLY
if [ "$REPLY" != "y" ] && [ "$REPLY" != "Y" ]; then
echo "Cancelled."
exit 0
fi
fi
echo
# shellcheck disable=SC2086
"${RUN[@]}" $SELECTED_FLAG "${ACTION_FLAGS[@]}"