diff --git a/app/main.py b/app/main.py index 81af2cb..80d0325 100644 --- a/app/main.py +++ b/app/main.py @@ -646,26 +646,25 @@ def compute_max_budget(allowance: float, credit_balance: float, team_spend: floa async def fetch_all_member_nodes() -> list[dict]: - """Every member node of the collective, following OC's cursor pagination. + """Every member node of the collective, via OC's offset pagination. - The members connection caps at 100 per page: an unpaginated query silently - hides everyone past the first page — the same failure class as Cloudron's - 25-per-page user list. Raises on ANY page failure so callers can apply - their own fail-safes; never returns a partial list (a partial member list - would make the sweep deactivate everyone missing from it). + The members connection has NO cursor/pageInfo pagination (older-style API) + and caps at 100 per page: an unpaginated query silently hides everyone + past the first page — the same failure class as Cloudron's 25-per-page + user list. Raises on ANY page failure so callers can apply their own + fail-safes; never returns a partial list (a partial member list would make + the sweep deactivate everyone missing from it). """ if not OC_PERSONAL_TOKEN: raise RuntimeError("OC_PERSONAL_TOKEN not set; cannot fetch members") nodes: list[dict] = [] - after: str | None = None + offset = 0 async with httpx.AsyncClient() as client: - for _ in range(20): # hard cap: 20 pages × 100 = 1000 members - after_q = f', after: "{after}"' if after else "" + for _ in range(20): # hard cap: 20 pages × 100 = 2000 members query = ( - '{ collective(slug: "%s") { members(limit: 100%s) { nodes ' - '{ role tier { name } account { name slug type ... on Individual { email } } } ' - 'pageInfo { hasNextPage endCursor } } } }' - % (OC_COLLECTIVE_SLUG, after_q) + '{ collective(slug: "%s") { members(limit: 100, offset: %d) { ' + "totalCount nodes { role tier { name } account { name slug type ... on Individual { email } } } } } }" + % (OC_COLLECTIVE_SLUG, offset) ) r = await client.post( OC_GRAPHQL_URL, @@ -674,12 +673,13 @@ async def fetch_all_member_nodes() -> list[dict]: ) if r.status_code != 200: raise RuntimeError(f"OC member fetch failed: HTTP {r.status_code}") - data = ((r.json().get("data") or {}).get("collective") or {}).get("members") or {} - nodes.extend(data.get("nodes") or []) - info = data.get("pageInfo") or {} - if not info.get("hasNextPage") or not info.get("endCursor"): + members = ((r.json().get("data") or {}).get("collective") or {}).get("members") or {} + total = members.get("totalCount") or 0 + page = members.get("nodes") or [] + nodes.extend(page) + offset += len(page) + if not page or offset >= total: return nodes - after = info["endCursor"] raise RuntimeError("OC member pagination exceeded 20 pages")