feat(W4): daily full reconciliation — user/role drift repair + ReconcileLogs audit
- w4-body.js: ONE-DIRECTIONAL sync (NocoDB status = SOT, never reverse);
drift matrix: missing user/role (provision w/ username lookup to avoid
duplicates), dangling refs (adopt/recreate), disabled<->status, canonical
base_path '/' + permission 16384, scopes vs calculateEffectiveGrants,
username drift detect+log_only; verify-after-repair on every change;
password never pushed on repair (方案 C); dry-run via ?dryRun=true
- gen_w4.js: daily 03:00 cron + webhook alist-w4-reconcile; API-import-safe
(UUID node ids, no pinData, webhookId)
- ReconcileLogs table mktsb7le6fqr2u1 (add_reconcilelogs_table.py, idempotent,
v2 API); FK column nc_jitu___Customers_id for bt-link writes (v2 ignores
link column itself)
- test_w4_reconcile.js: 34 assertions / 12 mock worlds, ALL PASSED
- live-verified against production: dry-run (3 drifts + 1 error found),
real repair (3 repaired, verify all passed, 4 log rows), idempotent rerun
- NocoDB 2026.08.x pitfalls documented (v1 col endpoints gone, row delete
via body [{Id}]); n8n API import/activation pitfalls (activeVersionId)
- docs: W4-DESIGN.md new; DATABASE_SCHEMA 8 tables; N8N_WORKFLOWS, SETUP,
PROJECT_STATE updated
This commit is contained in:
@@ -76,16 +76,27 @@ def main():
|
||||
body.update(extra)
|
||||
return curl("POST", f"/api/v1/db/meta/tables/{tid}/columns", body)
|
||||
|
||||
def set_options(colid, opts):
|
||||
return curl("PATCH", f"/api/v1/db/meta/columns/{colid}",
|
||||
def col_id(tid, title):
|
||||
# v1 GET /columns is removed on NocoDB 2026.08.x — read table meta instead
|
||||
t = curl("GET", f"/api/v2/meta/tables/{tid}")
|
||||
for c in jget(t, "columns", d=[]) or []:
|
||||
if c.get("title") == title:
|
||||
return c.get("id")
|
||||
return None
|
||||
|
||||
def set_options(tid, title, opts):
|
||||
cid = col_id(tid, title)
|
||||
if not cid:
|
||||
raise RuntimeError(f"column {title} not found on {tid}")
|
||||
return curl("PATCH", f"/api/v2/meta/columns/{cid}",
|
||||
{"colOptions": {"options": [{"title": o, "order": i + 1, "color": None}
|
||||
for i, o in enumerate(opts)]},
|
||||
"dtxp": ",".join(f"'{o}'" for o in opts)})
|
||||
|
||||
def add_ss(tid, title, opts):
|
||||
r = add_col(tid, title, "SingleSelect")
|
||||
cid = jget(r, "id")
|
||||
set_options(cid, opts)
|
||||
add_col(tid, title, "SingleSelect")
|
||||
cid = col_id(tid, title)
|
||||
set_options(tid, title, opts)
|
||||
return cid
|
||||
|
||||
def add_link(child_tid, title, parent_tid, type_="bt"):
|
||||
@@ -166,6 +177,19 @@ def main():
|
||||
{"title": "notes", "uidt": "LongText"},
|
||||
]); pur = jget(pur, "id")
|
||||
|
||||
rlog = create_table("ReconcileLogs", [
|
||||
{"title": "run_id", "uidt": "SingleLineText"},
|
||||
{"title": "details", "uidt": "LongText"},
|
||||
{"title": "action", "uidt": "SingleLineText"},
|
||||
{"title": "timestamp", "uidt": "DateTime"},
|
||||
]); rlog = jget(rlog, "id")
|
||||
add_ss(rlog, "drift_type",
|
||||
["user_missing", "role_missing", "dangling_user_ref", "dangling_role_ref",
|
||||
"user_disabled_drift", "scopes_drift", "base_path_drift",
|
||||
"permission_drift", "role_binding_drift", "username_drift",
|
||||
"verify_failed", "reconcile_error"])
|
||||
add_ss(rlog, "result", ["repaired", "log_only", "failed", "skipped"])
|
||||
|
||||
# ---- links ----
|
||||
add_link(cp, "customer", cust)
|
||||
add_link(cp, "product", prod)
|
||||
@@ -175,9 +199,11 @@ def main():
|
||||
add_link(al, "customer", cust)
|
||||
add_link(pur, "customer", cust)
|
||||
add_link(pur, "product", prod)
|
||||
add_link(rlog, "customer", cust)
|
||||
|
||||
ids = {"base": base_id, "Customers": cust, "Products": prod, "Resources": res,
|
||||
"CustomerProducts": cp, "AccessGrants": ag, "AccessLogs": al, "Purchases": pur}
|
||||
"CustomerProducts": cp, "AccessGrants": ag, "AccessLogs": al,
|
||||
"Purchases": pur, "ReconcileLogs": rlog}
|
||||
print("DONE. IDs:", json.dumps(ids, indent=2))
|
||||
# persist for later steps
|
||||
open("_nocodb_ids.json", "w").write(json.dumps(ids, indent=2))
|
||||
|
||||
Reference in New Issue
Block a user