#!/usr/bin/env python3 """ WFDash PB data cleanup — removes junk placeholder/smoke-test records, fixes two quote status anomalies, and clears {0,0} (null-island) coordinates so the map doesn't render ocean pins. Safe: checks for references before deleting. DRY-RUN by default; pass --apply to actually mutate. """ import sys, json, urllib.request, urllib.error PB = "https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io" ADMIN_EMAIL = "david.els1@outlook.com" ADMIN_PASS = "1234567890" APPLY = "--apply" in sys.argv def req(method, path, body=None, token=None): url = f"{PB}{path}" data = json.dumps(body).encode() if body is not None else None r = urllib.request.Request(url, data=data, method=method) r.add_header("Content-Type", "application/json") if token: r.add_header("Authorization", token) try: with urllib.request.urlopen(r) as resp: return json.loads(resp.read()) if resp.status != 204 else {} except urllib.error.HTTPError as e: return {"__error": e.code, "__body": e.read().decode()[:300]} # 1. Auth auth = req("POST", "/api/collections/_superusers/auth-with-password", {"identity": ADMIN_EMAIL, "password": ADMIN_PASS}) token = auth.get("token", "") if not token: print("AUTH FAILED:", auth); sys.exit(1) print(f"[auth] admin token ok ({len(token)} chars)") def list_all(collection, filter_str="", per_page=500): out = [] page = 1 while True: f = f"&filter={urllib.parse.quote(filter_str)}" if filter_str else "" r = req("GET", f"/api/collections/{collection}/records?page={page}&perPage={per_page}{f}", token=token) if "__error" in r: print(f" [warn] list {collection} failed: {r['__error']}"); break items = r.get("items", []) out.extend(items) if page >= r.get("totalPages", 1): break page += 1 return out import urllib.parse # 2. Find junk records print("\n=== Finding junk records ===") companies = list_all("companies") customers = list_all("customers") junk_company_ids = [c["id"] for c in companies if c.get("company","").startswith("SELECT/CREATE") or c.get("company","").startswith("SMOKE-CO")] junk_customer_ids = [c["id"] for c in customers if c.get("customer","").startswith("SELECT/CREATE")] print(f" junk companies: {junk_company_ids}") print(f" junk customers: {junk_customer_ids}") # 3. Check references before deleting print("\n=== Checking references ===") # Quotes that reference junk customers or companies quotes = list_all("quotes") ref_customers = set(junk_customer_ids) ref_companies = set(junk_company_ids) quotes_blocked = [q for q in quotes if q.get("customer") in ref_customers or q.get("company") in ref_companies] print(f" quotes referencing junk customer/company: {len(quotes_blocked)}") if quotes_blocked: for q in quotes_blocked[:5]: print(f" {q.get('quote_number')} (customer={q.get('customer')}, company={q.get('company')})") # Orders referencing junk orders = list_all("orders") orders_blocked = [o for o in orders if o.get("customer") in ref_customers or o.get("company") in ref_companies] print(f" orders referencing junk customer/company: {len(orders_blocked)}") # Customers referencing junk company cust_blocked = [c for c in customers if c.get("company") in ref_companies and c["id"] not in junk_customer_ids] print(f" non-junk customers referencing junk company: {len(cust_blocked)}") # 4. Delete junk (customers first, then companies — avoids dangling customer→company ref) print("\n=== Deleting junk ===") deleted = 0 for cid in junk_customer_ids: if quotes_blocked or orders_blocked or cust_blocked: # only block if THIS customer is referenced this_ref = any(q.get("customer")==cid for q in quotes_blocked) or any(o.get("customer")==cid for o in orders_blocked) if this_ref: print(f" SKIP customer {cid} — referenced by quotes/orders"); continue if APPLY: r = req("DELETE", f"/api/collections/customers/records/{cid}", token=token) if "__error" in r: print(f" FAIL customer {cid}: {r['__error']} {r['__body']}") else: print(f" DELETED customer {cid}"); deleted += 1 else: print(f" [dry-run] would delete customer {cid}"); deleted += 1 for cid in junk_company_ids: # check no remaining references still_ref = any(q.get("company")==cid for q in quotes) or any(o.get("company")==cid for o in orders) or any(c.get("company")==cid for c in customers if c["id"] not in junk_customer_ids) if still_ref: print(f" SKIP company {cid} — still referenced"); continue if APPLY: r = req("DELETE", f"/api/collections/companies/records/{cid}", token=token) if "__error" in r: print(f" FAIL company {cid}: {r['__error']} {r['__body']}") else: print(f" DELETED company {cid}"); deleted += 1 else: print(f" [dry-run] would delete company {cid}"); deleted += 1 # 5. Fix quote status anomalies print("\n=== Fixing quote status anomalies ===") fixed = 0 for q in quotes: qid = q["id"]; qnum = q.get("quote_number",""); st = q.get("status",""); cb = q.get("claimed_by","") if st == "": print(f" {qnum}: empty status -> 'new'") if APPLY: req("PATCH", f"/api/collections/quotes/records/{qid}", {"status":"new"}, token=token) fixed += 1 elif st == "claimed" and not cb: print(f" {qnum}: claimed w/o claimer -> 'new'") if APPLY: req("PATCH", f"/api/collections/quotes/records/{qid}", {"status":"new","claimed_by":""}, token=token) fixed += 1 # 6. Clear {0,0} coordinates on companies (null-island) print("\n=== Clearing {0,0} coordinates ===") cleared = 0 for c in companies: co = c.get("coordinates") if isinstance(co, dict) and co.get("lon")==0 and co.get("lat")==0: if APPLY: req("PATCH", f"/api/collections/companies/records/{c['id']}", {"coordinates":None}, token=token) print(f" {c.get('company','?')}: cleared {{0,0}}") cleared += 1 print(f"\n=== Summary ({'APPLIED' if APPLY else 'DRY-RUN'}) ===") print(f" deleted: {deleted}") print(f" quotes fixed: {fixed}") print(f" coordinates cleared: {cleared}") if not APPLY: print("\n Re-run with --apply to make changes.")