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:
2026-08-23 09:34:11 +08:00
parent 29ff66de88
commit 4693c9d684
12 changed files with 1257 additions and 27 deletions
+62
View File
@@ -0,0 +1,62 @@
// Builds n8n/W4-daily-reconcile.json — the daily full reconciliation workflow.
// Single source of truth: shared/effective-grants.js (inlined) + w4-body.js.
// Run: node n8n/gen_w4.js
const fs = require('fs');
const path = require('path');
const { sharedSource } = require('./inline_shared');
let body = fs.readFileSync(path.join(__dirname, 'w4-body.js'), 'utf8');
body = body.replace('// __SHARED_INLINE__ (replaced by shared/effective-grants.js source at build)', sharedSource());
// n8n API import (POST /api/v1/workflows) strictly validates: node ids must be
// UUIDs and top-level must NOT carry pinData (class-validator whitelist).
const uuid = () => require('crypto').randomUUID();
const node = (o) => Object.assign({ id: uuid() }, o);
const workflow = {
name: 'W4 - Daily Full Reconciliation (AList Drift Repair)',
nodes: [
node({
parameters: {
rule: {
interval: [{ field: 'cronExpression', expression: '0 3 * * *' }]
}
},
name: 'Daily 03:00',
type: 'n8n-nodes-base.scheduleTrigger',
typeVersion: 1.2,
position: [-420, 0]
}),
node({
parameters: {
httpMethod: 'POST',
path: 'alist-w4-reconcile',
options: {}
},
name: 'Manual / Webhook',
type: 'n8n-nodes-base.webhook',
typeVersion: 2,
webhookId: 'alist-w4-reconcile', // required: without it the production URL never registers
position: [-420, 240]
}),
node({
parameters: { jsCode: body },
name: 'Reconcile & Repair (Daily Full)',
type: 'n8n-nodes-base.code',
typeVersion: 2,
position: [0, 120]
})
],
connections: {
'Daily 03:00': {
main: [[{ node: 'Reconcile & Repair (Daily Full)', type: 'main', index: 0 }]]
},
'Manual / Webhook': {
main: [[{ node: 'Reconcile & Repair (Daily Full)', type: 'main', index: 0 }]]
}
},
settings: { executionOrder: 'v1' }
};
fs.writeFileSync(path.join(__dirname, 'W4-daily-reconcile.json'), JSON.stringify(workflow, null, 2) + '\n');
console.log('wrote W4-daily-reconcile.json (' + body.length + ' chars body)');