From daf2344d9916aab19c7436560d94b31efb74a8ea Mon Sep 17 00:00:00 2001 From: DEls Date: Fri, 11 Sep 2026 22:24:03 +0200 Subject: [PATCH] feat(sites): add work_month_start_day field --- .../2026-09-11-add-site-work-month-field.py | 113 ++++++++++++++++++ scripts/pb-bootstrap-schema.py | 5 + 2 files changed, 118 insertions(+) create mode 100644 scripts/migrations/2026-09-11-add-site-work-month-field.py diff --git a/scripts/migrations/2026-09-11-add-site-work-month-field.py b/scripts/migrations/2026-09-11-add-site-work-month-field.py new file mode 100644 index 0000000..a752b21 --- /dev/null +++ b/scripts/migrations/2026-09-11-add-site-work-month-field.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +""" +One-shot PocketBase migration applied via: + + PB_URL=https://pb.mega.infra.apphub.co.za \\ + PB_ADMIN_EMAIL=admin@mega.co.za \\ + PB_ADMIN_PASSWORD=... \\ + python3 scripts/migrations/2026-09-11-add-site-work-month-field.py + +Adds sites.work_month_start_day (see +docs/superpowers/specs/2026-09-11-project-month-week-design.md for why: +some sites' "work months" don't run 1st-30th, they run e.g. 21st-21st). + +Strictly additive, and the field is not required β€” existing sites keep +whatever value they have (empty means "ordinary calendar months"). + +Idempotent: re-running skips the field if it already exists. +""" +import json +import os +import sys +import urllib.error +import urllib.request + +NEW_FIELD = { + "name": "work_month_start_day", + "type": "number", + "required": False, + "min": 1, + "max": 31, +} + + +def admin_auth(base_url: str) -> str: + body = json.dumps( + { + "identity": os.environ["PB_ADMIN_EMAIL"], + "password": os.environ["PB_ADMIN_PASSWORD"], + } + ).encode() + req = urllib.request.Request( + f"{base_url}/api/collections/_superusers/auth-with-password", + data=body, + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read())["token"] + + +def get_collection(base_url: str, token: str, name: str): + req = urllib.request.Request( + f"{base_url}/api/collections/{name}", headers={"Authorization": token} + ) + try: + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read()) + except urllib.error.HTTPError as e: + if e.code == 404: + return None + raise + + +def patch_collection(base_url: str, token: str, name: str, body: dict): + req = urllib.request.Request( + f"{base_url}/api/collections/{name}", + data=json.dumps(body).encode(), + headers={"Authorization": token, "Content-Type": "application/json"}, + method="PATCH", + ) + try: + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read()) + except urllib.error.HTTPError as e: + raise RuntimeError( + f"PATCH {name} failed: {e.code} {e.read().decode() if e.fp else ''}" + ) from e + + +def main() -> int: + base_url = os.environ.get("PB_URL", "http://127.0.0.1:8090").rstrip("/") + token = admin_auth(base_url) + + coll = get_collection(base_url, token, "sites") + if not coll: + print(f"ERROR: collection 'sites' not found on {base_url}") + return 1 + + existing = coll.get("fields", []) + have = {f["name"] for f in existing} + if NEW_FIELD["name"] in have: + print("sites: no-op, work_month_start_day already present") + else: + print("sites: adding work_month_start_day") + patch_collection(base_url, token, "sites", {"fields": existing + [NEW_FIELD]}) + + final = get_collection(base_url, token, "sites") + print("\nFinal sites field list:") + print(f" {sorted(f['name'] for f in final.get('fields', []))}") + print("\nMigration complete.") + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyError as e: + print( + f"ERROR: missing environment variable {e}. " + f"Set PB_ADMIN_EMAIL and PB_ADMIN_PASSWORD.", + file=sys.stderr, + ) + sys.exit(2) \ No newline at end of file diff --git a/scripts/pb-bootstrap-schema.py b/scripts/pb-bootstrap-schema.py index 90183a3..510da48 100644 --- a/scripts/pb-bootstrap-schema.py +++ b/scripts/pb-bootstrap-schema.py @@ -115,6 +115,11 @@ DEFS["sites"] = { field("latitude", "number", required=False), field("longitude", "number", required=False), field("job_start_date", "date", required=False), + # How the site counts its own "work months" β€” e.g. 21 means each + # work-month runs the 21st through the 20th of the next calendar + # month, not the 1st–30th. Unset (or 1) means ordinary calendar + # months. See src/lib/project-calendar.ts. + field("work_month_start_day", "number", required=False, min=1, max=31), ], "indexes": [ "CREATE UNIQUE INDEX `idx_sites_project_code` ON `sites` (`project_code`)",