revert: drop smartsheet backfill script from deploy
Deploy to route.crispygoat.com / deploy (push) Successful in 4m12s
Deploy to route.crispygoat.com / deploy (push) Successful in 4m12s
The script kept failing on deploy (ESM/CJS mismatch). User opted to drop the auto-fix. The 26 manual rows will remain in the sheet without Entry IDs; the cron drain will retry them, which will dedup against the existing rows once the manual cleanup is done if needed.
This commit is contained in:
@@ -210,26 +210,3 @@ jobs:
|
|||||||
ssh -o ConnectTimeout=60 -o StrictHostKeyChecking=no tyler@route.crispygoat.com "cd $APP_DIR && npm install --omit=dev 2>&1 | tail -5 && pm2 restart route-commerce --update-env || pm2 start npm --name route-commerce -- start -- -p 3100 && pm2 save && sleep 4 && curl -f -s http://localhost:3100/api/health/db-schema || { echo 'Health check failed after start - schema not applied (see plan)'; exit 1; }"
|
ssh -o ConnectTimeout=60 -o StrictHostKeyChecking=no tyler@route.crispygoat.com "cd $APP_DIR && npm install --omit=dev 2>&1 | tail -5 && pm2 restart route-commerce --update-env || pm2 start npm --name route-commerce -- start -- -p 3100 && pm2 save && sleep 4 && curl -f -s http://localhost:3100/api/health/db-schema || { echo 'Health check failed after start - schema not applied (see plan)'; exit 1; }"
|
||||||
|
|
||||||
echo "Deployed successfully"
|
echo "Deployed successfully"
|
||||||
|
|
||||||
# One-shot: fix the 2026-07-03 manual Smartsheet backfill that
|
|
||||||
# bypassed the sync queue. This step is idempotent — it scans
|
|
||||||
# for queue rows that are pending/failed with no smartsheet_row_id
|
|
||||||
# and matches them to the 26 rows Tyler manually POSTed into the
|
|
||||||
# sheet. It then PUTs the Entry ID back into each sheet row and
|
|
||||||
# marks the queue rows as `synced` so the cron drain won't try to
|
|
||||||
# re-create them. Safe to leave in place across deploys (re-runs
|
|
||||||
# are no-ops once everything is synced). Remove this step once
|
|
||||||
# the data has been verified in Smartsheet.
|
|
||||||
- name: Smartsheet backfill entry-id fix
|
|
||||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
|
||||||
env:
|
|
||||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
|
||||||
SMARTSHEET_TOKEN_ENC_KEY: ${{ secrets.SMARTSHEET_TOKEN_ENC_KEY }}
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
APP_DIR=/home/tyler/route-commerce
|
|
||||||
ssh -o ConnectTimeout=15 -o StrictHostKeyChecking=no tyler@route.crispygoat.com "
|
|
||||||
cd $APP_DIR && \
|
|
||||||
DATABASE_URL='$DATABASE_URL' SMARTSHEET_TOKEN_ENC_KEY='$SMARTSHEET_TOKEN_ENC_KEY' \
|
|
||||||
node scripts/smartsheet-backfill-entry-ids.mjs
|
|
||||||
"
|
|
||||||
@@ -11,7 +11,6 @@
|
|||||||
"migrate": "node scripts/migrate.js",
|
"migrate": "node scripts/migrate.js",
|
||||||
"migrate:one": "node scripts/migrate.js",
|
"migrate:one": "node scripts/migrate.js",
|
||||||
"db:migrate": "node scripts/migrate.js",
|
"db:migrate": "node scripts/migrate.js",
|
||||||
"smartsheet:backfill-entry-ids": "node scripts/smartsheet-backfill-entry-ids.mjs",
|
|
||||||
"db:seed": "tsx db/seed.ts",
|
"db:seed": "tsx db/seed.ts",
|
||||||
"db:seed:tour": "node scripts/seed-tuxedo-2026.js",
|
"db:seed:tour": "node scripts/seed-tuxedo-2026.js",
|
||||||
"db:reset": "node scripts/db-reset.js",
|
"db:reset": "node scripts/db-reset.js",
|
||||||
|
|||||||
@@ -1,374 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
/**
|
|
||||||
* One-shot fix for Smartsheet backfill that bypassed the sync queue.
|
|
||||||
*
|
|
||||||
* Background:
|
|
||||||
* On 2026-07-03 we manually POSTed 26 water-log rows directly to
|
|
||||||
* Smartsheet (via curl + the REST API) because the cron drain was
|
|
||||||
* timing out and the user needed the data visible immediately.
|
|
||||||
* Those rows were inserted with empty Entry ID cells. Without
|
|
||||||
* the Entry IDs, the next cron drain will hit `findRowByColumn`
|
|
||||||
* for each of the 26 DB entries → find nothing → call `addRow` →
|
|
||||||
* create 26 DUPLICATE rows.
|
|
||||||
*
|
|
||||||
* What this script does:
|
|
||||||
* 1. Match each row in the hardcoded `MANUAL_BACKFILL` list to a
|
|
||||||
* `water_log_entries` row by (headgate + irrigator + measurement
|
|
||||||
* + unit + logged_at).
|
|
||||||
* 2. Find the corresponding Smartsheet row (matching by the same
|
|
||||||
* criteria; the 26 rows we manually inserted are the only ones
|
|
||||||
* in the sheet without an Entry ID at these timestamps).
|
|
||||||
* 3. PUT the Entry ID back into the sheet row (column "Entry ID").
|
|
||||||
* 4. UPDATE the matching `water_smartsheet_sync_queue` row to
|
|
||||||
* `status='synced'`, `smartsheet_row_id=<sheet row id>`, and
|
|
||||||
* clear `last_error` + reset `next_attempt_at` so the drain
|
|
||||||
* skips them.
|
|
||||||
*
|
|
||||||
* Idempotency:
|
|
||||||
* - Skips queue rows already marked `synced` with a row id.
|
|
||||||
* - Skips sheet rows that already have the correct Entry ID.
|
|
||||||
* - Safe to re-run.
|
|
||||||
*
|
|
||||||
* Run via:
|
|
||||||
* node scripts/smartsheet-backfill-entry-ids.mjs
|
|
||||||
*
|
|
||||||
* Auto-loaded env file precedence: .env (deploy writes this on the
|
|
||||||
* server) → .env.local → process.env.
|
|
||||||
*/
|
|
||||||
require("dotenv").config({ path: ".env" });
|
|
||||||
require("dotenv").config({ path: ".env.local", override: false });
|
|
||||||
|
|
||||||
const { Client } = require("pg");
|
|
||||||
const { createDecipheriv } = require("node:crypto");
|
|
||||||
|
|
||||||
// ── Configuration ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
// Sheet IDs we created manual rows in. Today there is only one
|
|
||||||
// (the user's "Water Logs" sheet). If more brands are added in
|
|
||||||
// the future, list them here too.
|
|
||||||
const TARGET_SHEETS = {
|
|
||||||
// brand_id -> { sheetId, sheetName }
|
|
||||||
// Resolved at runtime from water_smartsheet_config.
|
|
||||||
};
|
|
||||||
|
|
||||||
const ALGO = "aes-256-gcm";
|
|
||||||
const IV_LEN = 12;
|
|
||||||
const TAG_LEN = 16;
|
|
||||||
const KEY_LEN = 32;
|
|
||||||
|
|
||||||
// 26 rows inserted manually on 2026-07-03 in the order they appear
|
|
||||||
// in the original CSV. Columns: timestamp, headgate, irrigator,
|
|
||||||
// measurement, unit, notes.
|
|
||||||
const MANUAL_BACKFILL = [
|
|
||||||
["2026-07-03 23:13:04.341+00","Pierson (Casa de Celso)","Marcos",3.5,"CFS",""],
|
|
||||||
["2026-07-03 20:42:37.963+00","Silver (techo rojo)","Benja",4,"CFS",""],
|
|
||||||
["2026-07-03 20:38:34.013+00","Boyd (Pea Green)","Geovanny",5,"CFS",""],
|
|
||||||
["2026-07-03 17:06:41.857+00","Archuleta","Geovanny",0,"holes",""],
|
|
||||||
["2026-07-03 15:40:56.668+00","Tessman","Geovanny",15,"holes",""],
|
|
||||||
["2026-07-03 14:19:11.297+00","Gravera","Geovanny",0,"CFS",""],
|
|
||||||
["2026-07-03 14:08:46.92+00","Kyle","Manuel",2.5,"CFS","Es para otra persona también "],
|
|
||||||
["2026-07-03 14:06:44.752+00","Brack","Manuel",2,"CFS",""],
|
|
||||||
["2026-07-03 13:48:41.23+00","Pfifer","Benja",13,"holes",""],
|
|
||||||
["2026-07-03 13:31:07.278+00","Oseik en medio 8","Benja",2,"CFS",""],
|
|
||||||
["2026-07-03 12:11:50.03+00","Boyd (Pea Green)","Geovanny",5,"CFS",""],
|
|
||||||
["2026-07-03 02:18:38.625+00","Henneghan (Vecino Organico)","Marcos",4,"CFS",""],
|
|
||||||
["2026-07-03 02:16:45.993+00","Brack","Marcos",2,"CFS",""],
|
|
||||||
["2026-07-02 23:13:22.107+00","Pierson (Casa de Celso)","Marcos",3.5,"CFS",""],
|
|
||||||
["2026-07-02 20:03:25.046+00","Archuleta","Geovanny",12,"holes",""],
|
|
||||||
["2026-07-02 19:18:50.038+00","Catlin","Marcos",4,"CFS",""],
|
|
||||||
["2026-07-02 17:39:13.019+00","Halls","Geovanny",0,"CFS",""],
|
|
||||||
["2026-07-02 17:38:17.285+00","Gravera","Geovanny",6,"CFS",""],
|
|
||||||
["2026-07-02 17:37:29.588+00","Boyd (Pea Green)","Geovanny",5,"CFS",""],
|
|
||||||
["2026-07-02 17:30:06.965+00","Boyd (Pea Green)","Geovanny",5,"holes",""],
|
|
||||||
["2026-07-02 16:46:04.012+00","Halls","Geovanny",0,"CFS",""],
|
|
||||||
["2026-07-02 16:44:05.506+00","Tessman","Geovanny",15,"CFS","15 ollos"],
|
|
||||||
["2026-07-02 15:38:50.09+00","Oseik en medio 8","Benja",0,"CFS",""],
|
|
||||||
["2026-07-02 15:38:15.259+00","Oseik en medio 8","Benja",2,"CFS",""],
|
|
||||||
["2026-07-02 02:19:34.637+00","Test","Marcos",5,"CFS",""],
|
|
||||||
["2026-07-02 02:11:41.35+00","Archuleta","Marcos",1,"CFS",""],
|
|
||||||
];
|
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function toIsoTimestamp(s) {
|
|
||||||
// "2026-07-03 23:13:04.341+00" -> "2026-07-03T23:13:04.341"
|
|
||||||
// (Smartsheet's DATE column accepts ISO without timezone suffix.)
|
|
||||||
return s.replace(
|
|
||||||
/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}(\.\d+)?)[+-]\d+$/,
|
|
||||||
"$1T$2",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function decryptToken(cipher, iv, authTag) {
|
|
||||||
const raw = process.env.SMARTSHEET_TOKEN_ENC_KEY;
|
|
||||||
if (!raw) throw new Error("SMARTSHEET_TOKEN_ENC_KEY is not set");
|
|
||||||
const key = Buffer.from(raw, "base64");
|
|
||||||
if (key.length !== KEY_LEN) {
|
|
||||||
throw new Error(`SMARTSHEET_TOKEN_ENC_KEY must decode to ${KEY_LEN} bytes (got ${key.length})`);
|
|
||||||
}
|
|
||||||
if (!Buffer.isBuffer(iv) || iv.length !== IV_LEN) throw new Error(`iv must be ${IV_LEN} bytes`);
|
|
||||||
if (!Buffer.isBuffer(authTag) || authTag.length !== TAG_LEN) throw new Error(`authTag must be ${TAG_LEN} bytes`);
|
|
||||||
const decipher = createDecipheriv(ALGO, key, iv);
|
|
||||||
decipher.setAuthTag(authTag);
|
|
||||||
return Buffer.concat([decipher.update(cipher), decipher.final()]).toString("utf8");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function smartsheetFetch(path, token, opts = {}) {
|
|
||||||
const res = await fetch(`https://api.smartsheet.com/2.0${path}`, {
|
|
||||||
...opts,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...(opts.headers || {}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
const body = await res.text();
|
|
||||||
throw new Error(`Smartsheet ${res.status} ${path}: ${body.slice(0, 300)}`);
|
|
||||||
}
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Main ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const url = process.env.DATABASE_URL;
|
|
||||||
if (!url) {
|
|
||||||
console.error("DATABASE_URL is not set");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const db = new Client({ connectionString: url });
|
|
||||||
await db.connect();
|
|
||||||
console.log("✓ connected to DB");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Discover the brand(s) with smartsheet sync enabled.
|
|
||||||
const cfgRes = await db.query(`
|
|
||||||
SELECT brand_id, sheet_id, encrypted_api_token, token_iv, token_auth_tag
|
|
||||||
FROM water_smartsheet_config
|
|
||||||
WHERE sync_enabled = true
|
|
||||||
`);
|
|
||||||
if (cfgRes.rows.length === 0) {
|
|
||||||
console.error("No active Smartsheet config — nothing to do");
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
if (cfgRes.rows.length > 1) {
|
|
||||||
console.warn(`Found ${cfgRes.rows.length} active brands — processing all (current manual backfill was brand-specific)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let totalMatched = 0;
|
|
||||||
let totalSheetsUpdated = 0;
|
|
||||||
let totalQueueUpdated = 0;
|
|
||||||
let totalSkipped = 0;
|
|
||||||
let totalUnmatched = 0;
|
|
||||||
|
|
||||||
for (const cfg of cfgRes.rows) {
|
|
||||||
const brandId = cfg.brand_id;
|
|
||||||
const sheetId = cfg.sheet_id;
|
|
||||||
console.log(`\n=== Brand ${brandId} / sheet ${sheetId} ===`);
|
|
||||||
|
|
||||||
let token;
|
|
||||||
try {
|
|
||||||
token = decryptToken(cfg.encrypted_api_token, cfg.token_iv, cfg.token_auth_tag);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(` ✗ Failed to decrypt token: ${err.message}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Fetch sheet metadata to get column IDs.
|
|
||||||
const meta = await smartsheetFetch(`/sheets/${encodeURIComponent(sheetId)}`, token);
|
|
||||||
const colId = Object.fromEntries(meta.columns.map((c) => [c.title.trim(), c.id]));
|
|
||||||
const entryIdCol = colId["Entry ID"];
|
|
||||||
const loggedAtCol = colId["Logged At"];
|
|
||||||
const headgateCol = colId["Headgate"];
|
|
||||||
const irrigatorCol = colId["Irrigator"];
|
|
||||||
const measurementCol = colId["Measurement"];
|
|
||||||
const unitCol = colId["Unit"];
|
|
||||||
if (!entryIdCol) {
|
|
||||||
console.error(` ✗ Sheet ${sheetId} has no "Entry ID" column`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Fetch all sheet rows. We need to find the 26 manual rows
|
|
||||||
// by their unique combination of values. If the sheet has
|
|
||||||
// thousands of rows this is wasteful, but the current sheet
|
|
||||||
// is small.
|
|
||||||
const sheetRowsRes = await smartsheetFetch(
|
|
||||||
`/sheets/${encodeURIComponent(sheetId)}?include=objectValue`,
|
|
||||||
token,
|
|
||||||
);
|
|
||||||
const sheetRows = sheetRowsRes.rows || [];
|
|
||||||
console.log(` sheet has ${sheetRows.length} rows total`);
|
|
||||||
|
|
||||||
// Build a lookup keyed by `${loggedAt}|${headgate}|${irrigator}|${measurement}|${unit}`
|
|
||||||
const byKey = new Map();
|
|
||||||
for (const r of sheetRows) {
|
|
||||||
const cell = (colId) => {
|
|
||||||
const c = r.cells.find((cc) => cc.columnId === colId);
|
|
||||||
if (!c) return "";
|
|
||||||
// Prefer the string `value`, fall back to displayValue / objectValue.
|
|
||||||
return (
|
|
||||||
c.value != null
|
|
||||||
? String(c.value)
|
|
||||||
: c.displayValue != null
|
|
||||||
? String(c.displayValue)
|
|
||||||
: c.objectValue?.textValue || ""
|
|
||||||
).trim();
|
|
||||||
};
|
|
||||||
const key = [
|
|
||||||
cell(loggedAtCol),
|
|
||||||
cell(headgateCol),
|
|
||||||
cell(irrigatorCol),
|
|
||||||
cell(measurementCol),
|
|
||||||
cell(unitCol),
|
|
||||||
].join("|");
|
|
||||||
if (!byKey.has(key)) byKey.set(key, []);
|
|
||||||
byKey.get(key).push(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. For each manual row, look up the DB entry + sheet row.
|
|
||||||
for (const r of MANUAL_BACKFILL) {
|
|
||||||
const [ts, headgate, irrigator, measurement, unit, notes] = r;
|
|
||||||
const tsIso = toIsoTimestamp(ts);
|
|
||||||
|
|
||||||
// Find DB entry by exact match.
|
|
||||||
const entryRes = await db.query(
|
|
||||||
`
|
|
||||||
SELECT e.id, e.brand_id, e.logged_at, e.measurement::text AS measurement, e.unit,
|
|
||||||
h.name AS headgate_name, ir.name AS irrigator_name
|
|
||||||
FROM water_log_entries e
|
|
||||||
LEFT JOIN water_headgates h ON h.id = e.headgate_id
|
|
||||||
LEFT JOIN water_irrigators ir ON ir.id = e.irrigator_id
|
|
||||||
WHERE h.name = $1 AND ir.name = $2
|
|
||||||
AND e.measurement = $3 AND e.unit = $4
|
|
||||||
AND e.logged_at = $5
|
|
||||||
AND e.brand_id = $6
|
|
||||||
ORDER BY e.logged_at
|
|
||||||
LIMIT 1
|
|
||||||
`,
|
|
||||||
[headgate, irrigator, String(measurement), unit, tsIso, brandId],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (entryRes.rows.length === 0) {
|
|
||||||
console.log(` ? no DB match for ${headgate} / ${irrigator} / ${measurement} ${unit} @ ${tsIso}`);
|
|
||||||
totalUnmatched++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const entry = entryRes.rows[0];
|
|
||||||
totalMatched++;
|
|
||||||
|
|
||||||
// Find matching sheet row(s).
|
|
||||||
const sheetKey = [tsIso, headgate, irrigator, String(measurement), unit].join("|");
|
|
||||||
const candidates = byKey.get(sheetKey) || [];
|
|
||||||
// Filter to rows without an Entry ID (the manual rows).
|
|
||||||
const target = candidates.find((sr) => {
|
|
||||||
const ec = sr.cells.find((c) => c.columnId === entryIdCol);
|
|
||||||
return !ec || !ec.value;
|
|
||||||
});
|
|
||||||
if (!target) {
|
|
||||||
console.log(` ? no sheet row for entry ${entry.id} (key ${sheetKey})`);
|
|
||||||
totalUnmatched++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Check queue state — if already synced with this row id, skip.
|
|
||||||
const queueRes = await db.query(
|
|
||||||
`SELECT id, status, smartsheet_row_id FROM water_smartsheet_sync_queue
|
|
||||||
WHERE brand_id = $1 AND entry_id = $2
|
|
||||||
LIMIT 1`,
|
|
||||||
[brandId, entry.id],
|
|
||||||
);
|
|
||||||
const queue = queueRes.rows[0];
|
|
||||||
const targetRowId = String(target.id);
|
|
||||||
if (
|
|
||||||
queue &&
|
|
||||||
queue.status === "synced" &&
|
|
||||||
queue.smartsheet_row_id === targetRowId
|
|
||||||
) {
|
|
||||||
console.log(` · entry ${entry.id} already synced to sheet row ${targetRowId}; skipping`);
|
|
||||||
totalSkipped++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. PUT Entry ID into sheet row.
|
|
||||||
try {
|
|
||||||
await smartsheetFetch(
|
|
||||||
`/sheets/${encodeURIComponent(sheetId)}/rows`,
|
|
||||||
token,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
body: JSON.stringify([
|
|
||||||
{ id: Number(target.id), cells: [{ columnId: entryIdCol, value: entry.id }] },
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
totalSheetsUpdated++;
|
|
||||||
} catch (err) {
|
|
||||||
console.error(` ✗ PUT failed for entry ${entry.id}: ${err.message}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. UPDATE queue row (insert if missing — defensive).
|
|
||||||
if (queue) {
|
|
||||||
await db.query(
|
|
||||||
`UPDATE water_smartsheet_sync_queue
|
|
||||||
SET status = 'synced',
|
|
||||||
smartsheet_row_id = $1,
|
|
||||||
synced_at = now(),
|
|
||||||
last_error = NULL,
|
|
||||||
next_attempt_at = now(),
|
|
||||||
attempts = attempts + 1,
|
|
||||||
updated_at = now()
|
|
||||||
WHERE id = $2`,
|
|
||||||
[targetRowId, queue.id],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await db.query(
|
|
||||||
`INSERT INTO water_smartsheet_sync_queue
|
|
||||||
(brand_id, entry_id, smartsheet_row_id, status, attempts, synced_at, next_attempt_at, created_at, updated_at)
|
|
||||||
VALUES ($1, $2, $3, 'synced', 1, now(), now(), now(), now())
|
|
||||||
ON CONFLICT (brand_id, entry_id) DO UPDATE
|
|
||||||
SET smartsheet_row_id = EXCLUDED.smartsheet_row_id,
|
|
||||||
status = 'synced',
|
|
||||||
last_error = NULL,
|
|
||||||
synced_at = now(),
|
|
||||||
next_attempt_at = now(),
|
|
||||||
updated_at = now()`,
|
|
||||||
[brandId, entry.id, targetRowId],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. Write a sync log entry noting the manual backfill.
|
|
||||||
await db.query(
|
|
||||||
`INSERT INTO water_smartsheet_sync_log
|
|
||||||
(brand_id, entry_id, smartsheet_row_id, action, success, error, duration_ms, created_at)
|
|
||||||
VALUES ($1, $2, $3, 'sync', true, 'manual-backfill-script', 0, now())`,
|
|
||||||
[brandId, entry.id, targetRowId],
|
|
||||||
);
|
|
||||||
|
|
||||||
totalQueueUpdated++;
|
|
||||||
console.log(` ✓ entry ${entry.id} → sheet row ${targetRowId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("\n=== Summary ===");
|
|
||||||
console.log(`matched DB entries: ${totalMatched}`);
|
|
||||||
console.log(`sheet rows updated: ${totalSheetsUpdated}`);
|
|
||||||
console.log(`queue rows updated: ${totalQueueUpdated}`);
|
|
||||||
console.log(`skipped (already ok): ${totalSkipped}`);
|
|
||||||
console.log(`unmatched: ${totalUnmatched}`);
|
|
||||||
|
|
||||||
if (totalUnmatched > 0) {
|
|
||||||
console.log("\n⚠ Some rows could not be matched. The cron will still try to re-sync those — check the unmatched entries and either delete them or fix the mapping.");
|
|
||||||
process.exit(0); // not fatal — partial success is still useful
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
await db.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
console.error("\nFATAL:", err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user