From c40c871a4b0fec75bf58633020c793b576a45475 Mon Sep 17 00:00:00 2001 From: Rene Arumetsa Date: Sun, 26 Jul 2026 21:29:35 +0300 Subject: [PATCH] Sync pb --- README.md | 9 +-- docs/POCKETBASE_SETUP.md | 11 ++++ scripts/sync_pb_schema.py | 131 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 scripts/sync_pb_schema.py diff --git a/README.md b/README.md index fa10ef3..5b882fe 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ The economy system stores all player data in [PocketBase](https://pocketbase.io/ 1. Download `pocketbase.exe` (Windows) from https://pocketbase.io/docs/ and place it in the project root. 2. Start PocketBase: `.\pocketbase.exe serve` 3. Open the admin UI at http://127.0.0.1:8090/_/ and create a superuser account. -4. Create two collections for profile separation: `economy_users_dev` and `economy_users_prod` - see `docs/POCKETBASE_SETUP.md` for schema notes. +4. Create two collections for profile separation: `economy_users_dev` and `economy_users_prod` - see `docs/POCKETBASE_SETUP.md` for schema notes, then run `python scripts/sync_pb_schema.py` to add every field the bot persists. Re-run it after any update that introduces new fields (PocketBase silently drops writes to fields missing from the schema). 5. Set `PB_URL`, `PB_ADMIN_EMAIL`, `PB_ADMIN_PASSWORD` in `.env`. 6. **One-time data migration** (only if you have an existing `data/economy.json`): `python scripts/migrate_to_pb.py` @@ -390,7 +390,7 @@ Spend PP in `/prestigeshop`: - The daily set resets at **UTC midnight**, the weekly set on the **ISO week** rollover. Each player gets their own rotation. - Rewards are TipiCOINs (prestige coin multiplier applies) plus EXP through the normal level-up path. -> Schema note: the quest fields live in PocketBase - after pulling this feature run `python scripts/add_quest_fields.py` once (it patches both the dev and prod collections). +> Schema note: the quest fields live in PocketBase - after pulling this feature run `python scripts/sync_pb_schema.py` once (it reconciles both the dev and prod collections with everything the bot persists). --- @@ -511,8 +511,9 @@ Every slash command invocation is logged with the user ID, display name, and all │ └── POCKETBASE_SETUP.md # PocketBase collection schema + setup instructions ├── scripts/ │ ├── migrate_to_pb.py # One-time legacy migration: economy.json → PocketBase -│ ├── add_stats_fields.py # Schema migration: add stats fields to economy_users collection -│ ├── add_quest_fields.py # Schema migration: add quest fields (patches dev + prod) +│ ├── sync_pb_schema.py # Reconcile PB collections with the full user schema (dev + prod) +│ ├── add_stats_fields.py # Older partial schema migration (superseded by sync_pb_schema.py) +│ ├── add_quest_fields.py # Older partial schema migration (superseded by sync_pb_schema.py) │ └── reset_pb_collections.py # Destructive: deletes & recreates economy collections (--confirm required) ├── requirements.txt # Python dependencies ├── .env.example # Template for secrets diff --git a/docs/POCKETBASE_SETUP.md b/docs/POCKETBASE_SETUP.md index f4ed270..0fa3563 100644 --- a/docs/POCKETBASE_SETUP.md +++ b/docs/POCKETBASE_SETUP.md @@ -44,6 +44,17 @@ Add the following fields: > **Tip:** Set `user_id` as a unique index under **Indexes** tab. +The table above is only the base schema. After creating the collections, run: + +```bash +python scripts/sync_pb_schema.py +``` + +It derives the complete field list from the bot's user model and adds whatever +is missing to **both** collections (stats, fishing, prestige, quests, ...). +Re-run it after every update - PocketBase silently drops writes to fields that +are not in the collection schema, which breaks features without any error. + Set **API rules** (all four: list, view, create, update) to admin-only (leave blank / locked). --- diff --git a/scripts/sync_pb_schema.py b/scripts/sync_pb_schema.py new file mode 100644 index 0000000..39e7d29 --- /dev/null +++ b/scripts/sync_pb_schema.py @@ -0,0 +1,131 @@ +"""Reconcile the PocketBase economy collections with the bot's user schema. + +The expected field list is derived from core.economy._default_user() - the +single source of truth for what the bot persists - so this supersedes +add_stats_fields.py / add_quest_fields.py and also covers the fields those +scripts never added (fishing, prestige, quests, last_heist, ...). + +PocketBase silently drops record fields that are missing from the collection +schema, so any drift here breaks features without a single error message. + +Patches BOTH profile collections regardless of BOT_PROFILE. Only ever adds +fields; existing fields are never removed or retyped (type mismatches are +reported for manual review). + + python scripts/sync_pb_schema.py # add whatever is missing + python scripts/sync_pb_schema.py --check # report only; exit 1 on drift +""" + +from __future__ import annotations + +import asyncio +import sys +from pathlib import Path + +import aiohttp +from dotenv import load_dotenv + +sys.path.insert(0, str(Path(__file__).parent.parent)) +load_dotenv() + +import config # noqa: E402 +from core import economy # noqa: E402 + +PB_URL = config.PB_URL +COLLECTIONS = sorted({config.PB_ECONOMY_COLLECTION_DEV, config.PB_ECONOMY_COLLECTION_ECONOMY}) + +# _default_user() keys whose default is None, all ISO date/datetime strings +_TEXT_FIELDS = { + "last_daily", "last_work", "last_beg", "last_crime", "last_rob", + "last_heist", "last_fish", "last_streak_date", "jailed_until", +} + + +def _expected_fields() -> dict[str, str]: + """Map every persisted field name to its PocketBase field type.""" + expected = {"user_id": "text"} + for key, default in economy._default_user().items(): + if key in _TEXT_FIELDS: + expected[key] = "text" + elif isinstance(default, bool): + expected[key] = "bool" + elif isinstance(default, (int, float)): + expected[key] = "number" + elif isinstance(default, (list, dict)): + expected[key] = "json" + else: + raise SystemExit( + f"Cannot infer a PocketBase type for {key!r} (default {default!r}). " + "Add it to _TEXT_FIELDS or extend the mapping." + ) + return expected + + +async def main() -> int: + check_only = "--check" in sys.argv + expected = _expected_fields() + drift = False + + timeout = aiohttp.ClientTimeout(total=15) + async with aiohttp.ClientSession(timeout=timeout) as session: + async with session.post( + f"{PB_URL}/api/collections/_superusers/auth-with-password", + json={"identity": config.PB_ADMIN_EMAIL, "password": config.PB_ADMIN_PASSWORD}, + ) as resp: + if resp.status != 200: + print(f"Auth failed ({resp.status}): {await resp.text()}") + return 1 + hdrs = {"Authorization": (await resp.json())["token"]} + + for collection in COLLECTIONS: + print(f"── {collection} ──") + async with session.get( + f"{PB_URL}/api/collections/{collection}", headers=hdrs + ) as resp: + if resp.status != 200: + print(f"Could not fetch collection ({resp.status}): {await resp.text()}\n") + drift = True + continue + col = await resp.json() + + existing = {f["name"]: f.get("type", "?") for f in col.get("fields", [])} + + missing = [name for name in expected if name not in existing] + mismatched = [ + (name, existing[name], ftype) + for name, ftype in expected.items() + if name in existing and existing[name] != ftype + ] + + for name, have, want in mismatched: + drift = True + print(f" ! {name}: schema has type '{have}', bot expects '{want}' - fix manually") + + if not missing: + print(" ✓ no missing fields\n" if not mismatched else "") + continue + + drift = True + for name in missing: + print(f" + {name} ({expected[name]})") + + if check_only: + print() + continue + + new_fields = [{"name": name, "type": expected[name]} for name in missing] + async with session.patch( + f"{PB_URL}/api/collections/{collection}", + json={"fields": col.get("fields", []) + new_fields}, + headers=hdrs, + ) as resp: + if resp.status != 200: + print(f" Schema update failed ({resp.status}): {await resp.text()}\n") + continue + print(f" ✅ added {len(new_fields)} field(s)\n") + + return 1 if (check_only and drift) else 0 + + +if __name__ == "__main__": + raise SystemExit(asyncio.run(main()))