mcp-scraper 0.49.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +1 -1
  2. package/dist/{analytics-repository-WWXVU3VW.js → analytics-repository-IKE53S3E.js} +10 -2
  3. package/dist/bin/api-server.cjs +573 -139
  4. package/dist/bin/api-server.cjs.map +1 -1
  5. package/dist/bin/api-server.js +4 -4
  6. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  7. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  8. package/dist/bin/mcp-scraper-cli.js +1 -1
  9. package/dist/bin/mcp-scraper-install.cjs +1 -1
  10. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  11. package/dist/bin/mcp-scraper-install.js +1 -1
  12. package/dist/bin/mcp-stdio-server.cjs +30 -6
  13. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  14. package/dist/bin/mcp-stdio-server.js +4 -4
  15. package/dist/bin/paa-harvest.cjs.map +1 -1
  16. package/dist/bin/paa-harvest.js +3 -3
  17. package/dist/{chunk-MFNGUM4L.js → chunk-2XTYLJQK.js} +15 -1
  18. package/dist/chunk-2XTYLJQK.js.map +1 -0
  19. package/dist/{chunk-64WBUDPC.js → chunk-44LNKZTE.js} +32 -8
  20. package/dist/chunk-44LNKZTE.js.map +1 -0
  21. package/dist/{chunk-YW2LXLDU.js → chunk-5BEPOFKG.js} +160 -15
  22. package/dist/chunk-5BEPOFKG.js.map +1 -0
  23. package/dist/chunk-C7NN57WH.js +7 -0
  24. package/dist/chunk-C7NN57WH.js.map +1 -0
  25. package/dist/{chunk-MFE6RMIA.js → chunk-GOZIG6HD.js} +2 -2
  26. package/dist/{chunk-R66PJOZW.js → chunk-NNZXZTJ2.js} +2 -2
  27. package/dist/{chunk-6DPIE262.js → chunk-QHVFKFME.js} +2 -2
  28. package/dist/{chunk-PODASLGT.js → chunk-QXAY44SA.js} +2 -2
  29. package/dist/{chunk-ZEMHL36J.js → chunk-YMSZO62N.js} +2 -2
  30. package/dist/{db-A4YUPV4Z.js → db-UPSS5BGM.js} +2 -2
  31. package/dist/{extract-bundle-2HONVC5T.js → extract-bundle-RCTNANCH.js} +3 -3
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.js +3 -3
  34. package/dist/{location-data-repository-YWFJGNIP.js → location-data-repository-UUSHH5M2.js} +3 -3
  35. package/dist/{server-JE7SLZLJ.js → server-3ND2EXYO.js} +364 -128
  36. package/dist/server-3ND2EXYO.js.map +1 -0
  37. package/dist/{site-extract-repository-STNOI5OH.js → site-extract-repository-3VQAALNR.js} +3 -3
  38. package/dist/{worker-4MHZS3OB.js → worker-KQN673JF.js} +5 -5
  39. package/package.json +1 -1
  40. package/dist/chunk-64WBUDPC.js.map +0 -1
  41. package/dist/chunk-MFNGUM4L.js.map +0 -1
  42. package/dist/chunk-XLEDHV3J.js +0 -7
  43. package/dist/chunk-XLEDHV3J.js.map +0 -1
  44. package/dist/chunk-YW2LXLDU.js.map +0 -1
  45. package/dist/server-JE7SLZLJ.js.map +0 -1
  46. /package/dist/{analytics-repository-WWXVU3VW.js.map → analytics-repository-IKE53S3E.js.map} +0 -0
  47. /package/dist/{chunk-MFE6RMIA.js.map → chunk-GOZIG6HD.js.map} +0 -0
  48. /package/dist/{chunk-R66PJOZW.js.map → chunk-NNZXZTJ2.js.map} +0 -0
  49. /package/dist/{chunk-6DPIE262.js.map → chunk-QHVFKFME.js.map} +0 -0
  50. /package/dist/{chunk-PODASLGT.js.map → chunk-QXAY44SA.js.map} +0 -0
  51. /package/dist/{chunk-ZEMHL36J.js.map → chunk-YMSZO62N.js.map} +0 -0
  52. /package/dist/{db-A4YUPV4Z.js.map → db-UPSS5BGM.js.map} +0 -0
  53. /package/dist/{extract-bundle-2HONVC5T.js.map → extract-bundle-RCTNANCH.js.map} +0 -0
  54. /package/dist/{location-data-repository-YWFJGNIP.js.map → location-data-repository-UUSHH5M2.js.map} +0 -0
  55. /package/dist/{site-extract-repository-STNOI5OH.js.map → site-extract-repository-3VQAALNR.js.map} +0 -0
  56. /package/dist/{worker-4MHZS3OB.js.map → worker-KQN673JF.js.map} +0 -0
@@ -41,11 +41,12 @@ function harvestTimeoutBudget(maxQuestions, serpOnly = false) {
41
41
  const clientMs = Math.min(serverMs + CLIENT_OVER_SERVER_MARGIN_MS, VERCEL_FUNCTION_MAX_MS - 5e3);
42
42
  return { serverMs, clientMs };
43
43
  }
44
- var VERCEL_FUNCTION_MAX_MS, CLIENT_OVER_SERVER_MARGIN_MS;
44
+ var VERCEL_FUNCTION_MAX_MS, DEFAULT_TOOL_CLIENT_TIMEOUT_MS, CLIENT_OVER_SERVER_MARGIN_MS;
45
45
  var init_harvest_timeout = __esm({
46
46
  "src/harvest-timeout.ts"() {
47
47
  "use strict";
48
48
  VERCEL_FUNCTION_MAX_MS = 3e5;
49
+ DEFAULT_TOOL_CLIENT_TIMEOUT_MS = 3e5;
49
50
  CLIENT_OVER_SERVER_MARGIN_MS = 15e3;
50
51
  }
51
52
  });
@@ -4067,6 +4068,7 @@ async function migrate(options = {}) {
4067
4068
  `);
4068
4069
  await db.execute(`CREATE INDEX IF NOT EXISTS concurrency_locks_user_status ON concurrency_locks(user_id, status, expires_at)`);
4069
4070
  await db.execute(`CREATE INDEX IF NOT EXISTS concurrency_locks_operation ON concurrency_locks(operation, status, expires_at)`);
4071
+ await db.execute(`CREATE INDEX IF NOT EXISTS concurrency_locks_active_user ON concurrency_locks(user_id, expires_at) WHERE status = 'active'`);
4070
4072
  await db.execute(`
4071
4073
  CREATE TABLE IF NOT EXISTS ledger (
4072
4074
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -4253,6 +4255,16 @@ async function migrate(options = {}) {
4253
4255
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
4254
4256
  )
4255
4257
  `);
4258
+ await db.execute(`
4259
+ CREATE TABLE IF NOT EXISTS connected_account_charges (
4260
+ user_id INTEGER NOT NULL,
4261
+ connection_id TEXT NOT NULL,
4262
+ period_start TEXT NOT NULL,
4263
+ amount_mc INTEGER NOT NULL,
4264
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
4265
+ PRIMARY KEY (user_id, connection_id, period_start)
4266
+ )
4267
+ `);
4256
4268
  await db.execute(`
4257
4269
  CREATE TABLE IF NOT EXISTS billing_credits (
4258
4270
  idempotency_key TEXT PRIMARY KEY,
@@ -4441,6 +4453,7 @@ async function migrate(options = {}) {
4441
4453
  )
4442
4454
  `);
4443
4455
  await db.execute(`CREATE INDEX IF NOT EXISTS request_events_user_created_at ON request_events(user_id, created_at DESC)`);
4456
+ await db.execute(`CREATE INDEX IF NOT EXISTS request_events_created_at ON request_events(created_at)`);
4444
4457
  await db.execute(`
4445
4458
  CREATE TABLE IF NOT EXISTS workflow_schedules (
4446
4459
  id TEXT PRIMARY KEY,
@@ -6746,6 +6759,7 @@ var init_db = __esm({
6746
6759
  "billing_debits_user_status",
6747
6760
  "billing_events_authorization",
6748
6761
  "billing_events_user_created_at",
6762
+ "concurrency_locks_active_user",
6749
6763
  "concurrency_locks_operation",
6750
6764
  "concurrency_locks_user_status",
6751
6765
  "credit_lots_user_active",
@@ -6765,6 +6779,7 @@ var init_db = __esm({
6765
6779
  "oauth_tokens_identity",
6766
6780
  "page_snapshots_user_checked_at",
6767
6781
  "prt_user_id",
6782
+ "request_events_created_at",
6768
6783
  "request_events_user_created_at",
6769
6784
  "scheduled_run_view_links_expiry",
6770
6785
  "scheduled_run_view_links_owner_run",
@@ -23288,40 +23303,15 @@ function safeMetadata(metadata) {
23288
23303
  return null;
23289
23304
  }
23290
23305
  }
23291
- async function ensureConcurrencyGateSchema() {
23292
- if (!schemaReady) {
23293
- schemaReady = (async () => {
23294
- const db = getDb();
23295
- await db.execute(`
23296
- CREATE TABLE IF NOT EXISTS concurrency_locks (
23297
- id TEXT PRIMARY KEY,
23298
- user_id INTEGER NOT NULL REFERENCES users(id),
23299
- operation TEXT NOT NULL,
23300
- status TEXT NOT NULL DEFAULT 'active',
23301
- acquired_at TEXT NOT NULL DEFAULT (datetime('now')),
23302
- expires_at TEXT NOT NULL,
23303
- released_at TEXT,
23304
- metadata TEXT
23305
- )
23306
- `);
23307
- await db.execute(`CREATE INDEX IF NOT EXISTS concurrency_locks_user_status ON concurrency_locks(user_id, status, expires_at)`);
23308
- await db.execute(`CREATE INDEX IF NOT EXISTS concurrency_locks_operation ON concurrency_locks(operation, status, expires_at)`);
23309
- })();
23310
- }
23311
- return schemaReady;
23312
- }
23313
23306
  async function expireConcurrencyLocksForUser(userId) {
23314
- await ensureConcurrencyGateSchema();
23315
23307
  await getDb().execute({
23316
- sql: `UPDATE concurrency_locks
23317
- SET status = 'expired', released_at = datetime('now')
23308
+ sql: `DELETE FROM concurrency_locks
23318
23309
  WHERE user_id = ? AND status = 'active' AND expires_at <= datetime('now')`,
23319
23310
  args: [userId]
23320
23311
  });
23321
23312
  }
23322
- async function countActiveUsageForUser(userId) {
23323
- await ensureConcurrencyGateSchema();
23324
- await expireConcurrencyLocksForUser(userId);
23313
+ async function countActiveUsageForUser(userId, alreadyExpired = false) {
23314
+ if (!alreadyExpired) await expireConcurrencyLocksForUser(userId);
23325
23315
  const res = await getDb().execute({
23326
23316
  sql: `SELECT
23327
23317
  (
@@ -23341,7 +23331,6 @@ async function countActiveUsageForUser(userId) {
23341
23331
  }
23342
23332
  async function reuseExistingConcurrencyGate(userId, lockId, ttlSeconds) {
23343
23333
  if (!/^cl_[a-f0-9]{24,32}$/.test(lockId)) return false;
23344
- await ensureConcurrencyGateSchema();
23345
23334
  await expireConcurrencyLocksForUser(userId);
23346
23335
  const res = await getDb().execute({
23347
23336
  sql: `UPDATE concurrency_locks
@@ -23355,13 +23344,12 @@ async function reuseExistingConcurrencyGate(userId, lockId, ttlSeconds) {
23355
23344
  return Number(res.rowsAffected ?? 0) > 0;
23356
23345
  }
23357
23346
  async function acquireConcurrencyGate(user, operation, options = {}) {
23358
- await ensureConcurrencyGateSchema();
23359
23347
  const limit = concurrencyLimitForUser(user);
23360
23348
  if (isConcurrencyLimitBypassed(user)) {
23361
23349
  return { ok: true, lockId: null, active: 0, limit, operation, bypassed: true };
23362
23350
  }
23363
23351
  if (options.reuseLockId && await reuseExistingConcurrencyGate(user.id, options.reuseLockId, options.ttlSeconds)) {
23364
- return { ok: true, lockId: null, active: await countActiveUsageForUser(user.id), limit, operation, reused: true };
23352
+ return { ok: true, lockId: null, active: await countActiveUsageForUser(user.id, true), limit, operation, reused: true };
23365
23353
  }
23366
23354
  await expireConcurrencyLocksForUser(user.id);
23367
23355
  const lockId = `cl_${(0, import_node_crypto10.randomUUID)().replace(/-/g, "").slice(0, 24)}`;
@@ -23392,7 +23380,7 @@ async function acquireConcurrencyGate(user, operation, options = {}) {
23392
23380
  limit
23393
23381
  ]
23394
23382
  });
23395
- const active = await countActiveUsageForUser(user.id);
23383
+ const active = await countActiveUsageForUser(user.id, true);
23396
23384
  if (Number(res.rowsAffected ?? 0) > 0) {
23397
23385
  return { ok: true, lockId, active, limit, operation };
23398
23386
  }
@@ -23400,17 +23388,13 @@ async function acquireConcurrencyGate(user, operation, options = {}) {
23400
23388
  }
23401
23389
  async function releaseConcurrencyGate(lockId) {
23402
23390
  if (!lockId) return;
23403
- await ensureConcurrencyGateSchema();
23404
23391
  await getDb().execute({
23405
- sql: `UPDATE concurrency_locks
23406
- SET status = 'released', released_at = datetime('now')
23407
- WHERE id = ? AND status = 'active'`,
23392
+ sql: `DELETE FROM concurrency_locks WHERE id = ?`,
23408
23393
  args: [lockId]
23409
23394
  });
23410
23395
  }
23411
23396
  async function extendConcurrencyGate(lockId, ttlSeconds = DEFAULT_LOCK_TTL_SECONDS) {
23412
23397
  if (!lockId) return;
23413
- await ensureConcurrencyGateSchema();
23414
23398
  await getDb().execute({
23415
23399
  sql: `UPDATE concurrency_locks
23416
23400
  SET expires_at = datetime('now', ?)
@@ -23418,7 +23402,7 @@ async function extendConcurrencyGate(lockId, ttlSeconds = DEFAULT_LOCK_TTL_SECON
23418
23402
  args: [lockTtlModifier(ttlSeconds), lockId]
23419
23403
  });
23420
23404
  }
23421
- var import_node_crypto10, DEFAULT_LOCK_TTL_SECONDS, DEFAULT_RETRY_AFTER_SECONDS, MAX_LOCK_TTL_SECONDS, schemaReady;
23405
+ var import_node_crypto10, DEFAULT_LOCK_TTL_SECONDS, DEFAULT_RETRY_AFTER_SECONDS, MAX_LOCK_TTL_SECONDS;
23422
23406
  var init_concurrency_gates = __esm({
23423
23407
  "src/api/concurrency-gates.ts"() {
23424
23408
  "use strict";
@@ -23428,7 +23412,6 @@ var init_concurrency_gates = __esm({
23428
23412
  DEFAULT_LOCK_TTL_SECONDS = 15 * 60;
23429
23413
  DEFAULT_RETRY_AFTER_SECONDS = 30;
23430
23414
  MAX_LOCK_TTL_SECONDS = 24 * 60 * 60;
23431
- schemaReady = null;
23432
23415
  }
23433
23416
  });
23434
23417
 
@@ -27530,7 +27513,7 @@ async function recoverSettlement(row, user, authorization, duplicate) {
27530
27513
  }
27531
27514
  }
27532
27515
  await getDb().execute({
27533
- sql: "UPDATE billing_events SET status = 'settled', settled_at = datetime('now') WHERE id = ? AND status = 'settling'",
27516
+ sql: "UPDATE billing_events SET status = 'settled', settled_at = datetime('now') WHERE id = ? AND status IN ('settling', 'cost_pending')",
27534
27517
  args: [row.id]
27535
27518
  });
27536
27519
  await getDb().execute({
@@ -27575,11 +27558,13 @@ async function settleScheduledRun(args) {
27575
27558
  const rawCostNanos = rawCostUsd == null ? null : Math.round(rawCostUsd * USD_NANOS);
27576
27559
  const modelMc = rawCostUsd == null ? 0 : vendorCostUsdToMc(rawCostUsd, SCHEDULED_AGENT_LLM_MARKUP_BPS);
27577
27560
  const maximumModelMc = Math.max(0, Number(authorization.maximum_amount_mc) - SCHEDULED_RUN_BASE_MC);
27578
- const pendingReason = args.modelCostStatus === "missing" ? "openrouter_cost_missing" : modelMc > maximumModelMc ? "max_credits_per_run_exceeded" : null;
27561
+ const modelCostUnreported = args.modelCostStatus === "missing";
27562
+ const pendingReason = modelMc > maximumModelMc ? "max_credits_per_run_exceeded" : null;
27579
27563
  const metadata = safeMetadata2({
27580
27564
  ...args.metadata,
27581
27565
  billingPhase: "settlement",
27582
27566
  chargeMode: "model_delta",
27567
+ ...modelCostUnreported ? { modelCostUnreported: true } : {},
27583
27568
  ...pendingReason ? { reason: pendingReason } : {}
27584
27569
  });
27585
27570
  const eventId = existing?.id ?? (0, import_node_crypto14.randomUUID)();
@@ -27623,6 +27608,72 @@ async function settleScheduledRun(args) {
27623
27608
  }
27624
27609
  return recoverSettlement(existing, user, authorization, duplicateClaim);
27625
27610
  }
27611
+ async function reconcileScheduledRunSettlements(limit = 25, minimumAgeHours = 24) {
27612
+ const stale = await getDb().execute({
27613
+ sql: `SELECT id, authorization_id, status, amount_mc, raw_cost_usd_nanos, multiplier_bps, metadata
27614
+ FROM billing_events
27615
+ WHERE billing_class = ? AND status = 'cost_pending'
27616
+ AND created_at < datetime('now', ?)
27617
+ ORDER BY created_at
27618
+ LIMIT ?`,
27619
+ args: [SCHEDULED_RUN_BILLING_CLASS, `-${Math.max(1, Math.trunc(minimumAgeHours))} hours`, Math.max(1, Math.trunc(limit))]
27620
+ });
27621
+ const result = { checked: stale.rows.length, settled: 0, chargedMc: 0, failed: 0 };
27622
+ for (const raw of stale.rows) {
27623
+ const row = raw;
27624
+ try {
27625
+ const authorization = await authorizationById(row.authorization_id);
27626
+ if (!authorization) {
27627
+ result.failed += 1;
27628
+ continue;
27629
+ }
27630
+ const cap = Math.max(0, Number(authorization.maximum_amount_mc) - SCHEDULED_RUN_BASE_MC);
27631
+ const chargeMc = Math.min(Math.max(0, Math.round(Number(row.amount_mc))), cap);
27632
+ if (chargeMc > 0) {
27633
+ const debit = await debitMcIdempotent(
27634
+ Number(authorization.user_id),
27635
+ chargeMc,
27636
+ LedgerOperation.SCHEDULED_RUN,
27637
+ "scheduled-run:" + authorization.id + ":model:reconciled",
27638
+ authorization.id + ":model"
27639
+ );
27640
+ if (!debit.ok) {
27641
+ result.failed += 1;
27642
+ continue;
27643
+ }
27644
+ }
27645
+ await getDb().execute({
27646
+ sql: `UPDATE billing_events
27647
+ SET status = 'settled', amount_mc = ?, settled_at = datetime('now'), metadata = ?
27648
+ WHERE id = ? AND status = 'cost_pending'`,
27649
+ args: [
27650
+ chargeMc,
27651
+ safeMetadata2({ ...metadataReason(row) ? { reason: metadataReason(row) } : {}, autoReconciled: true, cappedAtAuthorizedMaximum: chargeMc < Number(row.amount_mc) }),
27652
+ row.id
27653
+ ]
27654
+ });
27655
+ await getDb().execute({
27656
+ sql: "UPDATE billing_authorizations SET status = 'settled', settled_at = datetime('now') WHERE id = ? AND status IN ('started', 'cost_pending')",
27657
+ args: [authorization.id]
27658
+ });
27659
+ result.settled += 1;
27660
+ result.chargedMc += chargeMc;
27661
+ console.error(JSON.stringify({
27662
+ event: "scheduled_run_settlement_reconciled",
27663
+ authorization_id: authorization.id,
27664
+ charged_mc: chargeMc,
27665
+ reason: metadataReason(row) ?? "unreported_cost"
27666
+ }));
27667
+ } catch (err) {
27668
+ result.failed += 1;
27669
+ console.error(JSON.stringify({
27670
+ event: "scheduled_run_reconciliation_failed",
27671
+ message: err instanceof Error ? err.message : String(err)
27672
+ }));
27673
+ }
27674
+ }
27675
+ return result;
27676
+ }
27626
27677
  async function voidScheduledRunAuthorization(args) {
27627
27678
  const user = await resolveSchedulingUser(args.identity);
27628
27679
  const authorization = await authorizationById(args.authorizationId);
@@ -28609,28 +28660,6 @@ var init_inbox_routes = __esm({
28609
28660
  });
28610
28661
 
28611
28662
  // src/api/connected-account-billing.ts
28612
- async function ensureConnectedAccountBillingSchema() {
28613
- await getDb().execute(`CREATE TABLE IF NOT EXISTS connected_account_billing (
28614
- user_id INTEGER PRIMARY KEY,
28615
- stripe_subscription_id TEXT,
28616
- stripe_subscription_item_id TEXT,
28617
- price_id TEXT,
28618
- quantity INTEGER NOT NULL DEFAULT 0,
28619
- status TEXT NOT NULL DEFAULT 'pending',
28620
- last_error_code TEXT,
28621
- checked_at TEXT NOT NULL DEFAULT (datetime('now')),
28622
- synced_at TEXT,
28623
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
28624
- )`);
28625
- await getDb().execute(`CREATE TABLE IF NOT EXISTS connected_account_charges (
28626
- user_id INTEGER NOT NULL,
28627
- connection_id TEXT NOT NULL,
28628
- period_start TEXT NOT NULL,
28629
- amount_mc INTEGER NOT NULL,
28630
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
28631
- PRIMARY KEY (user_id, connection_id, period_start)
28632
- )`);
28633
- }
28634
28663
  function connectedBillingPeriodStart(now = /* @__PURE__ */ new Date()) {
28635
28664
  return `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
28636
28665
  }
@@ -28638,7 +28667,6 @@ function isBillableConnection(connection) {
28638
28667
  return BILLABLE_LIFECYCLE.has(String(connection.lifecycleStatus ?? ""));
28639
28668
  }
28640
28669
  async function chargedConnectionIds(userId, periodStart) {
28641
- await ensureConnectedAccountBillingSchema();
28642
28670
  const result = await getDb().execute({
28643
28671
  sql: "SELECT connection_id FROM connected_account_charges WHERE user_id = ? AND period_start = ?",
28644
28672
  args: [Number(userId), periodStart]
@@ -28646,7 +28674,6 @@ async function chargedConnectionIds(userId, periodStart) {
28646
28674
  return new Set(result.rows.map((row) => String(row.connection_id)));
28647
28675
  }
28648
28676
  async function getConnectedAccountBillingState2(userId) {
28649
- await ensureConnectedAccountBillingSchema();
28650
28677
  const result = await getDb().execute({
28651
28678
  sql: "SELECT * FROM connected_account_billing WHERE user_id = ? LIMIT 1",
28652
28679
  args: [Number(userId)]
@@ -28654,7 +28681,6 @@ async function getConnectedAccountBillingState2(userId) {
28654
28681
  return result.rows[0] ? result.rows[0] : null;
28655
28682
  }
28656
28683
  async function setConnectedAccountBillingState2(input) {
28657
- await ensureConnectedAccountBillingSchema();
28658
28684
  await getDb().execute({
28659
28685
  sql: `INSERT INTO connected_account_billing
28660
28686
  (user_id, stripe_subscription_id, stripe_subscription_item_id, price_id, quantity, status, last_error_code, checked_at, synced_at, updated_at)
@@ -37670,6 +37696,89 @@ var init_paa_harvest_reconciliation = __esm({
37670
37696
  }
37671
37697
  });
37672
37698
 
37699
+ // src/api/retention-sweeps.ts
37700
+ function cutoffFor(rule) {
37701
+ const cutoff = new Date(Date.now() - rule.retentionDays * 24 * 60 * 60 * 1e3);
37702
+ if (rule.timestampFormat === "iso8601") return cutoff.toISOString();
37703
+ return cutoff.toISOString().replace("T", " ").slice(0, 19);
37704
+ }
37705
+ async function tableExists(table) {
37706
+ const res = await getDb().execute({
37707
+ sql: "SELECT 1 FROM sqlite_master WHERE type='table' AND name = ? LIMIT 1",
37708
+ args: [table]
37709
+ });
37710
+ return res.rows.length > 0;
37711
+ }
37712
+ async function sweepRule(rule, maxDeletes) {
37713
+ if (!await tableExists(rule.table)) return 0;
37714
+ const filter = rule.where ? ` AND ${rule.where}` : "";
37715
+ const cutoff = cutoffFor(rule);
37716
+ let deleted = 0;
37717
+ while (deleted < maxDeletes) {
37718
+ const res = await getDb().execute({
37719
+ sql: `DELETE FROM ${rule.table} WHERE rowid IN (
37720
+ SELECT rowid FROM ${rule.table}
37721
+ WHERE ${rule.timestampColumn} < ?${filter}
37722
+ LIMIT ?)`,
37723
+ args: [cutoff, Math.min(DEFAULT_BATCH, maxDeletes - deleted)]
37724
+ });
37725
+ const n = Number(res.rowsAffected ?? 0);
37726
+ deleted += n;
37727
+ if (n === 0) break;
37728
+ }
37729
+ return deleted;
37730
+ }
37731
+ async function sweepRetention(options = {}) {
37732
+ const maxDeletes = options.maxDeletesPerRule ?? DEFAULT_MAX_DELETES_PER_RULE;
37733
+ const deadline = Date.now() + (options.deadlineMs ?? DEFAULT_SWEEP_DEADLINE_MS);
37734
+ const perTable = {};
37735
+ let deleted = 0;
37736
+ let truncated = false;
37737
+ for (const rule of RETENTION_RULES) {
37738
+ const key = rule.where ? `${rule.table}:${rule.where}` : rule.table;
37739
+ if (Date.now() >= deadline) {
37740
+ truncated = true;
37741
+ break;
37742
+ }
37743
+ try {
37744
+ const n = await sweepRule(rule, maxDeletes);
37745
+ perTable[key] = n;
37746
+ deleted += n;
37747
+ if (n >= maxDeletes) truncated = true;
37748
+ } catch (err) {
37749
+ perTable[key] = -1;
37750
+ console.error(JSON.stringify({
37751
+ event: "retention_sweep_failed",
37752
+ table: rule.table,
37753
+ message: err instanceof Error ? err.message : String(err)
37754
+ }));
37755
+ }
37756
+ }
37757
+ return { deleted, perTable, truncated };
37758
+ }
37759
+ var REPORTING_WINDOW_DAYS, TELEMETRY_RETENTION_DAYS, FAILURE_RETENTION_DAYS, RETENTION_RULES, DEFAULT_BATCH, DEFAULT_MAX_DELETES_PER_RULE, DEFAULT_SWEEP_DEADLINE_MS;
37760
+ var init_retention_sweeps = __esm({
37761
+ "src/api/retention-sweeps.ts"() {
37762
+ "use strict";
37763
+ init_db();
37764
+ REPORTING_WINDOW_DAYS = 30;
37765
+ TELEMETRY_RETENTION_DAYS = 45;
37766
+ FAILURE_RETENTION_DAYS = 90;
37767
+ RETENTION_RULES = [
37768
+ { table: "request_events", timestampColumn: "created_at", retentionDays: TELEMETRY_RETENTION_DAYS },
37769
+ { table: "kernel_session_log", timestampColumn: "created_at", retentionDays: TELEMETRY_RETENTION_DAYS },
37770
+ { table: "vendor_usage_log", timestampColumn: "created_at", retentionDays: TELEMETRY_RETENTION_DAYS },
37771
+ { table: "harvest_attempts", timestampColumn: "created_at", retentionDays: TELEMETRY_RETENTION_DAYS, timestampFormat: "iso8601" },
37772
+ { table: "browser_agent_actions", timestampColumn: "created_at", retentionDays: REPORTING_WINDOW_DAYS },
37773
+ { table: "jobs", timestampColumn: "created_at", retentionDays: TELEMETRY_RETENTION_DAYS, where: "status = 'done'" },
37774
+ { table: "jobs", timestampColumn: "created_at", retentionDays: FAILURE_RETENTION_DAYS, where: "status = 'failed'" }
37775
+ ];
37776
+ DEFAULT_BATCH = 500;
37777
+ DEFAULT_MAX_DELETES_PER_RULE = 1e3;
37778
+ DEFAULT_SWEEP_DEADLINE_MS = 15e3;
37779
+ }
37780
+ });
37781
+
37673
37782
  // src/api/directory-workflow-reconciliation.ts
37674
37783
  async function reconcileDirectoryWorkflowSettlements(limit = 25) {
37675
37784
  const expiredStarts = await listExpiredDirectoryWorkflowStarts(limit);
@@ -41371,7 +41480,7 @@ var PACKAGE_VERSION;
41371
41480
  var init_version = __esm({
41372
41481
  "src/version.ts"() {
41373
41482
  "use strict";
41374
- PACKAGE_VERSION = "0.49.0";
41483
+ PACKAGE_VERSION = "0.50.0";
41375
41484
  }
41376
41485
  });
41377
41486
 
@@ -46762,6 +46871,29 @@ var init_paa_mcp_server = __esm({
46762
46871
  });
46763
46872
 
46764
46873
  // src/mcp/http-mcp-tool-executor.ts
46874
+ function unclassifiedTransportFailure(path6, err) {
46875
+ const name = err instanceof Error ? err.name : typeof err;
46876
+ const detail = err instanceof Error ? err.message : String(err);
46877
+ console.error(JSON.stringify({
46878
+ event: "mcp_executor_transport_failed",
46879
+ path: path6,
46880
+ error_name: name,
46881
+ error_message: sanitizeVendorName(detail).slice(0, 400)
46882
+ }));
46883
+ return {
46884
+ content: [{
46885
+ type: "text",
46886
+ text: JSON.stringify({
46887
+ error: "service_unavailable",
46888
+ error_type: name,
46889
+ retryable: true,
46890
+ path: path6,
46891
+ message: publicErrorMessage("service_unavailable")
46892
+ })
46893
+ }],
46894
+ isError: true
46895
+ };
46896
+ }
46765
46897
  function youtubeVideoIdFromUrl(url) {
46766
46898
  if (!url) return null;
46767
46899
  try {
@@ -46871,7 +47003,7 @@ var init_http_mcp_tool_executor = __esm({
46871
47003
  const rawOverride = process.env.MCP_SCRAPER_HTTP_TIMEOUT_MS;
46872
47004
  const parsedOverride = rawOverride === void 0 ? NaN : Number(rawOverride);
46873
47005
  this.httpTimeoutOverrideMs = Number.isFinite(parsedOverride) && parsedOverride > 0 ? parsedOverride : null;
46874
- this.timeoutMs = this.httpTimeoutOverrideMs ?? 11e4;
47006
+ this.timeoutMs = this.httpTimeoutOverrideMs ?? DEFAULT_TOOL_CLIENT_TIMEOUT_MS;
46875
47007
  const configuredSerpIntelligenceTimeoutMs = Number(process.env.MCP_SCRAPER_SERP_INTELLIGENCE_HTTP_TIMEOUT_MS ?? this.timeoutMs);
46876
47008
  this.serpIntelligenceTimeoutMs = Number.isFinite(configuredSerpIntelligenceTimeoutMs) && configuredSerpIntelligenceTimeoutMs > 0 ? configuredSerpIntelligenceTimeoutMs : this.timeoutMs;
46877
47009
  }
@@ -46910,7 +47042,7 @@ var init_http_mcp_tool_executor = __esm({
46910
47042
  isError: true
46911
47043
  };
46912
47044
  }
46913
- return { content: [{ type: "text", text: publicErrorMessage("service_unavailable") }], isError: true };
47045
+ return unclassifiedTransportFailure(path6, err);
46914
47046
  }
46915
47047
  }
46916
47048
  async callConnectedMutation(path6, body, idempotencyKey3, timeoutMs = this.timeoutMs) {
@@ -46944,8 +47076,8 @@ var init_http_mcp_tool_executor = __esm({
46944
47076
  return { content: [{ type: "text", text: JSON.stringify(httpErrorPayload(path6, res, data)) }], isError: true };
46945
47077
  }
46946
47078
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
46947
- } catch {
46948
- return { content: [{ type: "text", text: publicErrorMessage("service_unavailable") }], isError: true };
47079
+ } catch (err) {
47080
+ return unclassifiedTransportFailure(path6, err);
46949
47081
  }
46950
47082
  }
46951
47083
  async getTextArtifact(path6, maxBytes, timeoutMs = this.timeoutMs) {
@@ -46982,7 +47114,7 @@ var init_http_mcp_tool_executor = __esm({
46982
47114
  }]
46983
47115
  };
46984
47116
  } catch (err) {
46985
- return { content: [{ type: "text", text: publicErrorMessage("service_unavailable") }], isError: true };
47117
+ return unclassifiedTransportFailure(path6, err);
46986
47118
  }
46987
47119
  }
46988
47120
  harvestPaa(input) {
@@ -53305,13 +53437,21 @@ async function setBilledMc(id, billedMc) {
53305
53437
  args: [Math.round(billedMc), id]
53306
53438
  });
53307
53439
  }
53308
- async function markSessionClosed(id) {
53440
+ async function markSessionClosed(id, runtimeClosed = true) {
53309
53441
  const db = getDb();
53310
53442
  await db.execute({
53311
- sql: `UPDATE browser_agent_sessions SET status = 'closed', closed_at = datetime('now') WHERE id = ?`,
53312
- args: [id]
53443
+ sql: `UPDATE browser_agent_sessions SET status = ?, closed_at = datetime('now') WHERE id = ?`,
53444
+ args: [runtimeClosed ? "closed" : ORPHANED_SESSION_STATUS, id]
53313
53445
  });
53314
53446
  }
53447
+ async function listOrphanedSessionRows(limit = 50) {
53448
+ const db = getDb();
53449
+ const res = await db.execute({
53450
+ sql: `SELECT * FROM browser_agent_sessions WHERE status = ? ORDER BY closed_at LIMIT ?`,
53451
+ args: [ORPHANED_SESSION_STATUS, Math.max(1, Math.min(200, Math.round(limit)))]
53452
+ });
53453
+ return res.rows;
53454
+ }
53315
53455
  async function recordAction(input) {
53316
53456
  const db = getDb();
53317
53457
  await db.execute({
@@ -53358,7 +53498,7 @@ async function listReplayRows(sessionId) {
53358
53498
  });
53359
53499
  return res.rows;
53360
53500
  }
53361
- var import_node_crypto28, _ready2, _migrationPromise2;
53501
+ var import_node_crypto28, _ready2, _migrationPromise2, ORPHANED_SESSION_STATUS;
53362
53502
  var init_browser_agent_db = __esm({
53363
53503
  "src/api/browser-agent-db.ts"() {
53364
53504
  "use strict";
@@ -53366,6 +53506,7 @@ var init_browser_agent_db = __esm({
53366
53506
  init_db();
53367
53507
  _ready2 = false;
53368
53508
  _migrationPromise2 = null;
53509
+ ORPHANED_SESSION_STATUS = "runtime_close_pending";
53369
53510
  }
53370
53511
  });
53371
53512
 
@@ -54522,6 +54663,21 @@ async function charge(sessionId, userId, _startedAtMs) {
54522
54663
  if (res.ok) await setBilledMc(sessionId, owed);
54523
54664
  }
54524
54665
  }
54666
+ async function closeRuntimeSession(runtimeSessionId, sessionId) {
54667
+ try {
54668
+ await closeSession(runtimeSessionId);
54669
+ return true;
54670
+ } catch (err) {
54671
+ console.error(JSON.stringify({
54672
+ event: "browser_runtime_close_failed",
54673
+ session_id: sessionId,
54674
+ runtime_session_id: runtimeSessionId,
54675
+ error_name: err instanceof Error ? err.name : typeof err,
54676
+ message: sanitizeVendorName(err instanceof Error ? err.message : String(err)).slice(0, 300)
54677
+ }));
54678
+ return false;
54679
+ }
54680
+ }
54525
54681
  async function closeAndSettleSession(row) {
54526
54682
  if (row.user_id != null) {
54527
54683
  try {
@@ -54529,15 +54685,24 @@ async function closeAndSettleSession(row) {
54529
54685
  } catch {
54530
54686
  }
54531
54687
  }
54532
- try {
54533
- await closeSession(row.runtime_session_id);
54534
- } catch {
54535
- }
54536
- await markSessionClosed(row.id).catch(() => {
54688
+ const runtimeClosed = await closeRuntimeSession(row.runtime_session_id, row.id);
54689
+ await markSessionClosed(row.id, runtimeClosed).catch(() => {
54537
54690
  });
54538
54691
  await releaseConcurrencyGate(row.concurrency_lock_id).catch(() => {
54539
54692
  });
54540
54693
  }
54694
+ async function retryOrphanedBrowserSessions(limit = 50) {
54695
+ const rows = await listOrphanedSessionRows(limit);
54696
+ let recovered = 0;
54697
+ for (const row of rows) {
54698
+ if (await closeRuntimeSession(row.runtime_session_id, row.id)) {
54699
+ await markSessionClosed(row.id, true).catch(() => {
54700
+ });
54701
+ recovered++;
54702
+ }
54703
+ }
54704
+ return { recovered, stillOpen: rows.length - recovered };
54705
+ }
54541
54706
  async function completeAuthConnection(connectionId) {
54542
54707
  const connection = await getAuthConnectionRow(connectionId);
54543
54708
  if (!connection) return { ok: false, error: "not found" };
@@ -54562,7 +54727,7 @@ async function reapIdleBrowserSessions(idleSeconds = 120) {
54562
54727
  function publicSession(row) {
54563
54728
  return {
54564
54729
  session_id: row.id,
54565
- status: row.status,
54730
+ status: row.status === ORPHANED_SESSION_STATUS ? "closed" : row.status,
54566
54731
  label: row.label,
54567
54732
  watch_url: `/console/${row.id}`,
54568
54733
  created_at: row.created_at,
@@ -59447,6 +59612,38 @@ var init_editorial_reading_room_routes = __esm({
59447
59612
  }
59448
59613
  });
59449
59614
 
59615
+ // src/api/schema-presence.ts
59616
+ async function loadSchemaObjects() {
59617
+ const res = await getDb().execute(
59618
+ "SELECT name FROM sqlite_master WHERE type IN ('table', 'index')"
59619
+ );
59620
+ return new Set(res.rows.map((row) => String(row.name)));
59621
+ }
59622
+ function invalidateSchemaPresence() {
59623
+ cache = null;
59624
+ }
59625
+ async function schemaObjectsPresent(names) {
59626
+ if (names.length === 0) return true;
59627
+ try {
59628
+ if (!cache) cache = loadSchemaObjects();
59629
+ const present = await cache;
59630
+ if (names.every((name) => present.has(name))) return true;
59631
+ invalidateSchemaPresence();
59632
+ return false;
59633
+ } catch {
59634
+ invalidateSchemaPresence();
59635
+ return false;
59636
+ }
59637
+ }
59638
+ var cache;
59639
+ var init_schema_presence = __esm({
59640
+ "src/api/schema-presence.ts"() {
59641
+ "use strict";
59642
+ init_db();
59643
+ cache = null;
59644
+ }
59645
+ });
59646
+
59450
59647
  // src/api/commons-linksets.ts
59451
59648
  function normalizeCommonsClaims(input) {
59452
59649
  if (input === void 0) return void 0;
@@ -59632,7 +59829,11 @@ var init_commons_linksets = __esm({
59632
59829
 
59633
59830
  // src/api/commons-repository.ts
59634
59831
  async function ensureCommonsSchema() {
59635
- if (schemaReady2) return;
59832
+ if (schemaReady) return;
59833
+ if (await schemaObjectsPresent(COMMONS_SCHEMA_OBJECTS)) {
59834
+ schemaReady = true;
59835
+ return;
59836
+ }
59636
59837
  const db = getDb();
59637
59838
  await db.batch([
59638
59839
  {
@@ -59853,7 +60054,7 @@ async function ensureCommonsSchema() {
59853
60054
  args: [COMMONS_SCHEMA_VERSION]
59854
60055
  }
59855
60056
  ], "write");
59856
- schemaReady2 = true;
60057
+ schemaReady = true;
59857
60058
  }
59858
60059
  async function searchCommonsEntities(filters = {}, userId) {
59859
60060
  await ensureCommonsSchema();
@@ -61568,12 +61769,13 @@ function jsonLikeValue(value) {
61568
61769
  function escapeLike(value) {
61569
61770
  return value.replace(/[%_]/g, "");
61570
61771
  }
61571
- var import_node_crypto35, COMMONS_SCHEMA_VERSION, DEFAULT_COMMONS_BASE_URL, DEFAULT_ENTITY_TYPE, COMMONS_ENTITY_PROFILES, schemaReady2, CommonsRepositoryError;
61772
+ var import_node_crypto35, COMMONS_SCHEMA_VERSION, DEFAULT_COMMONS_BASE_URL, DEFAULT_ENTITY_TYPE, COMMONS_ENTITY_PROFILES, schemaReady, COMMONS_SCHEMA_OBJECTS, CommonsRepositoryError;
61572
61773
  var init_commons_repository = __esm({
61573
61774
  "src/api/commons-repository.ts"() {
61574
61775
  "use strict";
61575
61776
  import_node_crypto35 = require("crypto");
61576
61777
  init_db();
61778
+ init_schema_presence();
61577
61779
  init_rates();
61578
61780
  init_commons_linksets();
61579
61781
  COMMONS_SCHEMA_VERSION = "2026-08-05.1";
@@ -61724,7 +61926,32 @@ var init_commons_repository = __esm({
61724
61926
  ]
61725
61927
  }
61726
61928
  };
61727
- schemaReady2 = false;
61929
+ schemaReady = false;
61930
+ COMMONS_SCHEMA_OBJECTS = [
61931
+ "schema_migrations",
61932
+ "commons_entities",
61933
+ "commons_claims",
61934
+ "commons_proposals",
61935
+ "commons_ledger",
61936
+ "commons_saved_filters",
61937
+ "commons_index_documents",
61938
+ "commons_publications",
61939
+ "commons_publication_editions",
61940
+ "commons_entities_status_updated",
61941
+ "commons_entities_type_updated",
61942
+ "commons_entities_source_domain",
61943
+ "commons_claims_subject_predicate",
61944
+ "commons_claims_object_entity",
61945
+ "commons_proposals_user_created",
61946
+ "commons_proposals_entity_created",
61947
+ "commons_ledger_entity_created",
61948
+ "commons_ledger_user_created",
61949
+ "commons_saved_filters_user_updated",
61950
+ "commons_index_documents_status_updated",
61951
+ "commons_index_documents_entity_type",
61952
+ "commons_publications_updated",
61953
+ "commons_publication_editions_publication_published"
61954
+ ];
61728
61955
  CommonsRepositoryError = class extends Error {
61729
61956
  constructor(code, message, httpStatus = 400) {
61730
61957
  super(message);
@@ -62681,7 +62908,10 @@ var init_analytics_attribution = __esm({
62681
62908
  // src/api/analytics-repository.ts
62682
62909
  var analytics_repository_exports = {};
62683
62910
  __export(analytics_repository_exports, {
62911
+ ANALYTICS_CONTENT_SORTS: () => ANALYTICS_CONTENT_SORTS,
62684
62912
  AnalyticsRepositoryError: () => AnalyticsRepositoryError,
62913
+ ENGAGED_SESSION_MS: () => ENGAGED_SESSION_MS,
62914
+ MAX_ENGAGED_MS: () => MAX_ENGAGED_MS,
62685
62915
  analyticsAcquisition: () => analyticsAcquisition,
62686
62916
  analyticsBusinessMetrics: () => analyticsBusinessMetrics,
62687
62917
  analyticsChannelBreakdown: () => analyticsChannelBreakdown,
@@ -62725,6 +62955,7 @@ __export(analytics_repository_exports, {
62725
62955
  migrateAnalytics: () => migrateAnalytics,
62726
62956
  normalizeAnalyticsPath: () => normalizeAnalyticsPath,
62727
62957
  normalizeAnalyticsUrl: () => normalizeAnalyticsUrl,
62958
+ normalizeContentOptions: () => normalizeContentOptions,
62728
62959
  normalizeObservedHostname: () => normalizeObservedHostname,
62729
62960
  queueAnalyticsActivation: () => queueAnalyticsActivation,
62730
62961
  queueAnalyticsFormDelivery: () => queueAnalyticsFormDelivery,
@@ -63075,6 +63306,12 @@ async function migrateAnalytics() {
63075
63306
  await client2.query(
63076
63307
  `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS utm_content text`
63077
63308
  );
63309
+ await client2.query(
63310
+ `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS engaged_ms integer`
63311
+ );
63312
+ await client2.query(
63313
+ `ALTER TABLE analytics_events ADD COLUMN IF NOT EXISTS scroll_depth smallint`
63314
+ );
63078
63315
  await client2.query(
63079
63316
  `ALTER TABLE analytics_conversions ADD COLUMN IF NOT EXISTS person_id uuid REFERENCES analytics_people(id) ON DELETE SET NULL`
63080
63317
  );
@@ -63600,6 +63837,12 @@ async function setAnalyticsPixelDomainState(input) {
63600
63837
  404
63601
63838
  );
63602
63839
  }
63840
+ function boundedEngagedMs(value) {
63841
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.min(Math.trunc(value), MAX_ENGAGED_MS) : null;
63842
+ }
63843
+ function boundedScrollDepth(value) {
63844
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.min(Math.trunc(value), 100) : null;
63845
+ }
63603
63846
  function normalizeAnalyticsPath(value) {
63604
63847
  if (!value) return "/";
63605
63848
  try {
@@ -63787,10 +64030,11 @@ async function ingestAnalyticsEvents(input) {
63787
64030
  id, event_id, site_id, pixel_id, hostname, visitor_id, session_id, event_name,
63788
64031
  occurred_at, path, canonical_url, title, referrer, source, medium, campaign,
63789
64032
  device_class, properties, country_code, region_code, click_ids, person_id,
63790
- channel_family, platform, campaign_id, ad_set_id, ad_id, creative_id, placement, utm_term, utm_content
64033
+ channel_family, platform, campaign_id, ad_set_id, ad_id, creative_id, placement, utm_term, utm_content,
64034
+ engaged_ms, scroll_depth
63791
64035
  ) VALUES (
63792
64036
  $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18::jsonb, $19, $20, $21::jsonb, $22,
63793
- $23, $24, $25, $26, $27, $28, $29, $30, $31
64037
+ $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33
63794
64038
  ) ON CONFLICT(site_id, event_id) DO NOTHING`,
63795
64039
  [
63796
64040
  (0, import_node_crypto37.randomUUID)(),
@@ -63823,7 +64067,9 @@ async function ingestAnalyticsEvents(input) {
63823
64067
  attribution.creativeId,
63824
64068
  attribution.placement,
63825
64069
  attribution.term,
63826
- attribution.content
64070
+ attribution.content,
64071
+ boundedEngagedMs(event2.engagedMs),
64072
+ boundedScrollDepth(event2.scrollDepth)
63827
64073
  ]
63828
64074
  );
63829
64075
  if (inserted.rowCount) accepted += 1;
@@ -64334,10 +64580,21 @@ async function analyticsChannelBreakdown(siteId, userId, filters) {
64334
64580
  dataFreshThrough: (/* @__PURE__ */ new Date()).toISOString()
64335
64581
  };
64336
64582
  }
64337
- async function analyticsContent(siteId, userId, filters, page) {
64583
+ function normalizeContentOptions(options) {
64584
+ const sort = ANALYTICS_CONTENT_SORTS.includes(options?.sort) ? options.sort : "pageviews";
64585
+ return {
64586
+ sort,
64587
+ direction: options?.direction === "asc" ? "asc" : "desc",
64588
+ search: options?.search?.trim().slice(0, 300) || null
64589
+ };
64590
+ }
64591
+ async function analyticsContent(siteId, userId, filters, page, options) {
64338
64592
  const db = getAnalyticsPool();
64339
64593
  await accessFor(db, siteId, userId);
64340
64594
  const query = eventFilterSql(siteId, filters);
64595
+ const table = normalizeContentOptions(options);
64596
+ const searchParam = `$${query.values.length + 1}`;
64597
+ const order = table.sort === "path" ? `metrics.path ${table.direction === "asc" ? "ASC" : "DESC"}` : `${table.sort} ${table.direction === "asc" ? "ASC" : "DESC"} NULLS LAST, metrics.path ASC`;
64341
64598
  const result = await db.query(
64342
64599
  `WITH filtered AS (
64343
64600
  SELECT e.* FROM analytics_events e WHERE ${query.where}
@@ -64345,7 +64602,7 @@ async function analyticsContent(siteId, userId, filters, page) {
64345
64602
  SELECT path, count(DISTINCT visitor_id)::int AS visitors,
64346
64603
  count(DISTINCT session_id)::int AS sessions,
64347
64604
  count(*) FILTER (WHERE event_name = 'page_view')::int AS pageviews,
64348
- count(*) FILTER (WHERE event_name <> 'page_view')::int AS engagement_events,
64605
+ count(*) FILTER (WHERE event_name NOT IN ('page_view', 'page_engagement'))::int AS engagement_events,
64349
64606
  count(*)::int AS events
64350
64607
  FROM filtered GROUP BY path
64351
64608
  ), session_bounds AS (
@@ -64353,35 +64610,98 @@ async function analyticsContent(siteId, userId, filters, page) {
64353
64610
  (array_agg(path ORDER BY occurred_at ASC, id ASC))[1] AS entrance_path,
64354
64611
  (array_agg(path ORDER BY occurred_at DESC, id DESC))[1] AS exit_path
64355
64612
  FROM filtered WHERE session_id IS NOT NULL GROUP BY session_id
64613
+ ), session_pageviews AS (
64614
+ SELECT session_id, count(*) FILTER (WHERE event_name = 'page_view')::int AS pageviews
64615
+ FROM filtered WHERE session_id IS NOT NULL GROUP BY session_id
64616
+ ), page_dwell AS (
64617
+ SELECT session_id, path,
64618
+ sum(engaged_ms)::bigint AS engaged_ms,
64619
+ max(scroll_depth)::int AS scroll_depth
64620
+ FROM filtered
64621
+ WHERE event_name = 'page_engagement' AND session_id IS NOT NULL
64622
+ GROUP BY session_id, path
64623
+ ), dwell_metrics AS (
64624
+ SELECT path,
64625
+ avg(engaged_ms) FILTER (WHERE engaged_ms IS NOT NULL) AS avg_engaged_ms,
64626
+ avg(scroll_depth) FILTER (WHERE scroll_depth IS NOT NULL) AS avg_scroll,
64627
+ count(*) FILTER (WHERE engaged_ms IS NOT NULL)::int AS measured_pageviews
64628
+ FROM page_dwell GROUP BY path
64356
64629
  ), entrances AS (
64357
64630
  SELECT entrance_path AS path, count(*)::int AS entrances FROM session_bounds GROUP BY entrance_path
64358
64631
  ), exits AS (
64359
64632
  SELECT exit_path AS path, count(*)::int AS exits FROM session_bounds GROUP BY exit_path
64633
+ ), bounces AS (
64634
+ SELECT b.entrance_path AS path, count(*)::int AS bounces
64635
+ FROM session_bounds b JOIN session_pageviews p USING(session_id)
64636
+ WHERE p.pageviews <= 1
64637
+ GROUP BY b.entrance_path
64638
+ ), converted_sessions AS (
64639
+ SELECT DISTINCT c.session_id
64640
+ FROM analytics_conversions c
64641
+ WHERE c.site_id = $1 AND c.occurred_at >= $2::timestamptz AND c.occurred_at < $3::timestamptz
64642
+ AND c.session_id IS NOT NULL
64643
+ ), path_sessions AS (
64644
+ SELECT DISTINCT path, session_id FROM filtered WHERE session_id IS NOT NULL
64645
+ ), engaged AS (
64646
+ SELECT ps.path,
64647
+ count(*) FILTER (
64648
+ WHERE sp.pageviews >= 2
64649
+ OR COALESCE(pd.engaged_ms, 0) >= ${ENGAGED_SESSION_MS}
64650
+ OR cs.session_id IS NOT NULL
64651
+ )::int AS engaged_sessions
64652
+ FROM path_sessions ps
64653
+ JOIN session_pageviews sp USING(session_id)
64654
+ LEFT JOIN page_dwell pd ON pd.session_id = ps.session_id AND pd.path = ps.path
64655
+ LEFT JOIN converted_sessions cs ON cs.session_id = ps.session_id
64656
+ GROUP BY ps.path
64360
64657
  ), assisted AS (
64361
64658
  SELECT f.path, count(DISTINCT c.id)::int AS conversions
64362
64659
  FROM analytics_conversions c JOIN filtered f ON f.session_id = c.session_id
64363
64660
  WHERE c.site_id = $1 AND c.occurred_at >= $2::timestamptz AND c.occurred_at < $3::timestamptz
64364
64661
  GROUP BY f.path
64365
64662
  )
64366
- SELECT metrics.*, COALESCE(entrances.entrances, 0)::int AS entrances,
64663
+ SELECT metrics.*,
64664
+ COALESCE(entrances.entrances, 0)::int AS entrances,
64367
64665
  COALESCE(exits.exits, 0)::int AS exits,
64368
- COALESCE(assisted.conversions, 0)::int AS conversions
64666
+ COALESCE(assisted.conversions, 0)::int AS conversions,
64667
+ COALESCE(dwell.measured_pageviews, 0)::int AS measured_pageviews,
64668
+ CASE WHEN dwell.avg_engaged_ms IS NULL THEN NULL
64669
+ ELSE round((dwell.avg_engaged_ms / 1000.0)::numeric, 1) END AS avg_seconds_on_page,
64670
+ CASE WHEN dwell.avg_scroll IS NULL THEN NULL
64671
+ ELSE round(dwell.avg_scroll::numeric, 1) END AS avg_scroll_depth,
64672
+ CASE WHEN COALESCE(entrances.entrances, 0) = 0 THEN NULL
64673
+ ELSE round(COALESCE(bounces.bounces, 0)::numeric * 100 / entrances.entrances, 1) END AS bounce_rate,
64674
+ CASE WHEN metrics.pageviews = 0 THEN NULL
64675
+ ELSE round(COALESCE(exits.exits, 0)::numeric * 100 / metrics.pageviews, 1) END AS exit_rate,
64676
+ CASE WHEN metrics.sessions = 0 THEN NULL
64677
+ ELSE round(COALESCE(engaged.engaged_sessions, 0)::numeric * 100 / metrics.sessions, 1) END AS engagement_rate,
64678
+ CASE WHEN metrics.sessions = 0 THEN NULL
64679
+ ELSE round(metrics.pageviews::numeric / metrics.sessions, 2) END AS pageviews_per_session
64369
64680
  FROM page_metrics metrics
64370
64681
  LEFT JOIN entrances USING(path)
64371
64682
  LEFT JOIN exits USING(path)
64372
64683
  LEFT JOIN assisted USING(path)
64373
- ORDER BY metrics.pageviews DESC, metrics.events DESC, metrics.path ASC
64684
+ LEFT JOIN bounces USING(path)
64685
+ LEFT JOIN engaged USING(path)
64686
+ LEFT JOIN dwell_metrics dwell USING(path)
64687
+ WHERE ${searchParam}::text IS NULL OR metrics.path ILIKE '%' || ${searchParam} || '%'
64688
+ ORDER BY ${order}
64374
64689
  LIMIT 1000`,
64375
- query.values
64690
+ [...query.values, table.search]
64376
64691
  );
64377
64692
  const paged = paginateRows(
64378
64693
  result.rows,
64379
64694
  page,
64380
- pageFingerprint("content", siteId, filters)
64695
+ pageFingerprint(
64696
+ `content:${table.sort}:${table.direction}:${table.search ?? ""}`,
64697
+ siteId,
64698
+ filters
64699
+ )
64381
64700
  );
64382
64701
  return {
64383
64702
  content: paged.items,
64384
64703
  pageInfo: paged.pageInfo,
64704
+ table,
64385
64705
  dataFreshThrough: (/* @__PURE__ */ new Date()).toISOString()
64386
64706
  };
64387
64707
  }
@@ -65211,7 +65531,7 @@ async function refreshAnalyticsDailyRollups(input) {
65211
65531
  try {
65212
65532
  await client2.query("BEGIN");
65213
65533
  await client2.query(
65214
- `DELETE FROM analytics_rollups_daily WHERE bucket >= $1::date AND bucket < $2::date`,
65534
+ `DELETE FROM analytics_rollups_daily WHERE bucket >= $1::date AND bucket <= $2::date`,
65215
65535
  [input.start, input.end]
65216
65536
  );
65217
65537
  const inserted = await client2.query(
@@ -65228,7 +65548,14 @@ async function refreshAnalyticsDailyRollups(input) {
65228
65548
  now()
65229
65549
  FROM analytics_events
65230
65550
  WHERE occurred_at >= $1::timestamptz AND occurred_at < $2::timestamptz
65231
- GROUP BY 1, 2, 3, 4, 5, 6, 7`,
65551
+ GROUP BY 1, 2, 3, 4, 5, 6, 7
65552
+ ON CONFLICT (site_id, pixel_id, hostname, bucket, source, medium, campaign)
65553
+ DO UPDATE SET
65554
+ pageviews = EXCLUDED.pageviews,
65555
+ events = EXCLUDED.events,
65556
+ visitors = EXCLUDED.visitors,
65557
+ sessions = EXCLUDED.sessions,
65558
+ refreshed_at = EXCLUDED.refreshed_at`,
65232
65559
  [input.start, input.end]
65233
65560
  );
65234
65561
  await client2.query("COMMIT");
@@ -65240,9 +65567,17 @@ async function refreshAnalyticsDailyRollups(input) {
65240
65567
  return { runId, rows };
65241
65568
  } catch (error) {
65242
65569
  await client2.query("ROLLBACK");
65570
+ const pgCode = error?.code;
65571
+ const detail = error instanceof Error ? error.message : String(error);
65572
+ console.error(JSON.stringify({
65573
+ event: "analytics_rollup_failed",
65574
+ runId,
65575
+ pgCode: pgCode ?? null,
65576
+ message: detail.slice(0, 400)
65577
+ }));
65243
65578
  await db.query(
65244
- `UPDATE analytics_rollup_runs SET status = 'failed', completed_at = now(), error_code = 'rollup_failed' WHERE id = $1`,
65245
- [runId]
65579
+ `UPDATE analytics_rollup_runs SET status = 'failed', completed_at = now(), error_code = $2 WHERE id = $1`,
65580
+ [runId, (pgCode ? `pg_${pgCode}` : "rollup_failed").slice(0, 60)]
65246
65581
  );
65247
65582
  throw error;
65248
65583
  } finally {
@@ -65295,6 +65630,24 @@ function rowsToCsv2(rows) {
65295
65630
  )
65296
65631
  ].join("\n");
65297
65632
  }
65633
+ function markdownHeading(column) {
65634
+ return column.split("_").map((word, index) => index ? word : word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
65635
+ }
65636
+ function markdownCell(value) {
65637
+ if (value === null || value === void 0) return "\u2014";
65638
+ return String(value).replaceAll("|", "\\|").replaceAll("\n", " ");
65639
+ }
65640
+ function rowsToMarkdown(rows) {
65641
+ const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))];
65642
+ if (!columns.length) return ["No rows matched this window and filter set."];
65643
+ return [
65644
+ `| ${columns.map(markdownHeading).join(" | ")} |`,
65645
+ `| ${columns.map(() => "---").join(" | ")} |`,
65646
+ ...rows.map(
65647
+ (row) => `| ${columns.map((column) => markdownCell(row[column])).join(" | ")} |`
65648
+ )
65649
+ ];
65650
+ }
65298
65651
  async function createAnalyticsExport(input) {
65299
65652
  const site = (await listAnalyticsSites(input.userId)).find(
65300
65653
  (candidate) => candidate.id === input.siteId
@@ -65362,6 +65715,7 @@ async function createAnalyticsExport(input) {
65362
65715
  `- Verified conversions: ${overview.conversions}`,
65363
65716
  `- Verified revenue: ${(overview.revenueMinor / 100).toFixed(2)}`,
65364
65717
  "",
65718
+ ...input.report === "overview" ? [] : [`## ${markdownHeading(input.report)} rows`, "", ...rowsToMarkdown(rows), ""],
65365
65719
  `Generated ${(/* @__PURE__ */ new Date()).toISOString()} from the governed ${input.report} report contract.`
65366
65720
  ].join("\n");
65367
65721
  }
@@ -65398,7 +65752,7 @@ async function createAnalyticsExport(input) {
65398
65752
  }
65399
65753
  return describeArtifact(replay.rows[0]);
65400
65754
  }
65401
- var import_node_crypto37, import_pg, AnalyticsRepositoryError, pool2, blockedPropertyName, inferredFamilySql, clickIdKeys;
65755
+ var import_node_crypto37, import_pg, AnalyticsRepositoryError, pool2, MAX_ENGAGED_MS, ENGAGED_SESSION_MS, blockedPropertyName, inferredFamilySql, ANALYTICS_CONTENT_SORTS, clickIdKeys;
65402
65756
  var init_analytics_repository = __esm({
65403
65757
  "src/api/analytics-repository.ts"() {
65404
65758
  "use strict";
@@ -65417,6 +65771,8 @@ var init_analytics_repository = __esm({
65417
65771
  status;
65418
65772
  };
65419
65773
  pool2 = null;
65774
+ MAX_ENGAGED_MS = 30 * 60 * 1e3;
65775
+ ENGAGED_SESSION_MS = 1e4;
65420
65776
  blockedPropertyName = /(email|phone|password|token|secret|authorization|cookie|name|address|form|body)/i;
65421
65777
  inferredFamilySql = `COALESCE(e.channel_family, CASE
65422
65778
  WHEN lower(COALESCE(e.source,'') || ' ' || COALESCE(e.referrer,'')) ~ '(perplexity|chatgpt|openai|claude|anthropic|grok|xai|ai[_ -]?overview|google[_ -]?ai|gemini)' THEN 'llm'
@@ -65425,6 +65781,22 @@ var init_analytics_repository = __esm({
65425
65781
  WHEN COALESCE(e.source,'') = '' THEN 'direct'
65426
65782
  WHEN lower(COALESCE(e.medium,'')) ~ 'email|newsletter' THEN 'email'
65427
65783
  ELSE 'other' END)`;
65784
+ ANALYTICS_CONTENT_SORTS = [
65785
+ "path",
65786
+ "visitors",
65787
+ "sessions",
65788
+ "pageviews",
65789
+ "entrances",
65790
+ "exits",
65791
+ "engagement_events",
65792
+ "conversions",
65793
+ "avg_seconds_on_page",
65794
+ "avg_scroll_depth",
65795
+ "bounce_rate",
65796
+ "exit_rate",
65797
+ "engagement_rate",
65798
+ "pageviews_per_session"
65799
+ ];
65428
65800
  clickIdKeys = /* @__PURE__ */ new Set([
65429
65801
  "fbclid",
65430
65802
  "gclid",
@@ -65744,7 +66116,10 @@ function dashboardOriginAllowed(origin) {
65744
66116
  return (process.env.ALLOWED_ORIGINS ?? process.env.APP_ORIGIN ?? "").split(",").map((value) => value.trim()).includes(origin);
65745
66117
  }
65746
66118
  function analyticsPixelSdk() {
65747
- return `(()=>{const s=document.currentScript;if(!s)return;const pixel=s.dataset.pixel;if(!pixel)return;const endpoint=new URL('/analytics/v1/events',s.src).toString(),ttl=18e5;const key='mcp_analytics_visitor',sessionKey='mcp_analytics_session',touchKey='mcp_analytics_touch';const id=()=>crypto.randomUUID?crypto.randomUUID():'e_'+Date.now().toString(36)+Math.random().toString(36).slice(2);let consent=s.dataset.consent==='granted'&&!navigator.globalPrivacyControl,visitor=null,session=null;const read=k=>{try{return localStorage.getItem(k)}catch{return null}},write=(k,v)=>{try{localStorage.setItem(k,v)}catch{}};const ensure=()=>{if(!consent)return;visitor=read(key)||id();const now=Date.now();let saved={};try{saved=JSON.parse(read(sessionKey)||'{}')}catch{}session=saved.id&&now-Number(saved.seen||0)<ttl?saved.id:id();write(key,visitor);write(sessionKey,JSON.stringify({id:session,seen:now}))};const attribution=()=>{const q=new URLSearchParams(location.search),clickIds={},properties={},map={utm_term:'utm_term',utm_content:'utm_content',utm_id:'campaign_id',campaign_id:'campaign_id',adset_id:'ad_set_id',ad_set_id:'ad_set_id',adgroup_id:'ad_group_id',ad_group_id:'ad_group_id',ad_id:'ad_id',creative_id:'creative_id',placement:'placement'};for(const k of ['fbclid','gclid','gbraid','wbraid','ttclid','rdt_cid','msclkid']){const v=q.get(k);if(v)clickIds[k]=v}for(const [from,to] of Object.entries(map)){const v=q.get(from);if(v)properties[to]=v}const current={source:q.get('utm_source')||undefined,medium:q.get('utm_medium')||undefined,campaign:q.get('utm_campaign')||undefined,clickIds,properties};if(current.source||Object.keys(clickIds).length)write(touchKey,JSON.stringify(current));else{try{return JSON.parse(read(touchKey)||'null')||current}catch{}}return current};const send=(name,properties={})=>{if(!consent||navigator.globalPrivacyControl)return;ensure();const a=attribution();const body=JSON.stringify({pixelId:pixel,events:[{eventId:id(),eventName:name,occurredAt:new Date().toISOString(),visitorId:visitor,sessionId:session,path:location.pathname,canonicalUrl:location.origin+location.pathname,title:document.title,referrer:document.referrer||undefined,...a,deviceClass:innerWidth<640?'mobile':innerWidth<1024?'tablet':'desktop',properties:{...a.properties,...properties}}]});if(navigator.sendBeacon&&navigator.sendBeacon(endpoint,new Blob([body],{type:'application/json'})))return;fetch(endpoint,{method:'POST',headers:{'content-type':'application/json'},body,keepalive:true,credentials:'omit'}).catch(()=>{})};const api={page:()=>send('page_view'),track:send,consent:state=>{consent=(state==='granted'||state===true)&&!navigator.globalPrivacyControl;if(consent){ensure();api.page()}},debug:()=>({pixel,endpoint,consent,gpc:Boolean(navigator.globalPrivacyControl),visitorId:visitor,sessionId:session,hostname:location.hostname,path:location.pathname,...attribution()})};window.mcpAnalytics=api;ensure();if(consent)api.page();let current=location.href;const changed=()=>{if(location.href!==current){current=location.href;api.page()}};for(const method of ['pushState','replaceState']){const original=history[method];history[method]=function(...args){const result=original.apply(this,args);queueMicrotask(changed);return result}}addEventListener('popstate',changed)})();`;
66119
+ return `(()=>{const s=document.currentScript;if(!s)return;const pixel=s.dataset.pixel;if(!pixel)return;const endpoint=new URL('/analytics/v1/events',s.src).toString(),ttl=18e5;const key='mcp_analytics_visitor',sessionKey='mcp_analytics_session',touchKey='mcp_analytics_touch';const id=()=>crypto.randomUUID?crypto.randomUUID():'e_'+Date.now().toString(36)+Math.random().toString(36).slice(2);let consent=s.dataset.consent==='granted'&&!navigator.globalPrivacyControl,visitor=null,session=null;const read=k=>{try{return localStorage.getItem(k)}catch{return null}},write=(k,v)=>{try{localStorage.setItem(k,v)}catch{}};const ensure=()=>{if(!consent)return;visitor=read(key)||id();const now=Date.now();let saved={};try{saved=JSON.parse(read(sessionKey)||'{}')}catch{}session=saved.id&&now-Number(saved.seen||0)<ttl?saved.id:id();write(key,visitor);write(sessionKey,JSON.stringify({id:session,seen:now}))};const attribution=()=>{const q=new URLSearchParams(location.search),clickIds={},properties={},map={utm_term:'utm_term',utm_content:'utm_content',utm_id:'campaign_id',campaign_id:'campaign_id',adset_id:'ad_set_id',ad_set_id:'ad_set_id',adgroup_id:'ad_group_id',ad_group_id:'ad_group_id',ad_id:'ad_id',creative_id:'creative_id',placement:'placement'};for(const k of ['fbclid','gclid','gbraid','wbraid','ttclid','rdt_cid','msclkid']){const v=q.get(k);if(v)clickIds[k]=v}for(const [from,to] of Object.entries(map)){const v=q.get(from);if(v)properties[to]=v}const current={source:q.get('utm_source')||undefined,medium:q.get('utm_medium')||undefined,campaign:q.get('utm_campaign')||undefined,clickIds,properties};if(current.source||Object.keys(clickIds).length)write(touchKey,JSON.stringify(current));else{try{return JSON.parse(read(touchKey)||'null')||current}catch{}}return current};const send=(name,properties={},over={})=>{if(!consent||navigator.globalPrivacyControl)return;ensure();const a=attribution();const body=JSON.stringify({pixelId:pixel,events:[{eventId:id(),eventName:name,occurredAt:new Date().toISOString(),visitorId:visitor,sessionId:session,path:location.pathname,canonicalUrl:location.origin+location.pathname,title:document.title,referrer:document.referrer||undefined,...a,deviceClass:innerWidth<640?'mobile':innerWidth<1024?'tablet':'desktop',properties:{...a.properties,...properties},...over}]});if(navigator.sendBeacon&&navigator.sendBeacon(endpoint,new Blob([body],{type:'application/json'})))return;fetch(endpoint,{method:'POST',headers:{'content-type':'application/json'},body,keepalive:true,credentials:'omit'}).catch(()=>{})};let visibleSince=document.visibilityState==='visible'?Date.now():0,dwellMs=0,maxScroll=0,dwellPath=location.pathname;const depth=()=>{const d=document.documentElement,h=d.scrollHeight||1;return Math.max(0,Math.min(100,Math.round(((d.scrollTop||document.body.scrollTop||0)+innerHeight)/h*100)))};const accrue=()=>{if(visibleSince){dwellMs+=Date.now()-visibleSince;visibleSince=0}};const resume=()=>{if(!visibleSince)visibleSince=Date.now()};const flush=()=>{accrue();const ms=Math.min(dwellMs,18e5),sd=Math.max(maxScroll,depth());dwellMs=0;maxScroll=0;if(ms<250)return;send('page_engagement',{},{path:dwellPath,canonicalUrl:location.origin+dwellPath,engagedMs:ms,scrollDepth:sd})};addEventListener('scroll',()=>{const d=depth();if(d>maxScroll)maxScroll=d},{passive:true});document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='hidden')flush();else resume()});addEventListener('pagehide',flush);const api={page:()=>send('page_view'),track:send,consent:state=>{consent=(state==='granted'||state===true)&&!navigator.globalPrivacyControl;if(consent){ensure();resume();api.page()}},debug:()=>({pixel,endpoint,consent,gpc:Boolean(navigator.globalPrivacyControl),visitorId:visitor,sessionId:session,hostname:location.hostname,path:location.pathname,engagedMs:dwellMs+(visibleSince?Date.now()-visibleSince:0),scrollDepth:Math.max(maxScroll,depth()),...attribution()})};window.mcpAnalytics=api;ensure();if(consent)api.page();let current=location.href;const changed=()=>{if(location.href!==current){flush();current=location.href;dwellPath=location.pathname;resume();api.page()}};for(const method of ['pushState','replaceState']){const original=history[method];history[method]=function(...args){const result=original.apply(this,args);queueMicrotask(changed);return result}}addEventListener('popstate',changed)})();`;
66120
+ }
66121
+ function pagePathFilter(value) {
66122
+ return value && value.startsWith("/") ? value : void 0;
65748
66123
  }
65749
66124
  function reportFilters(url) {
65750
66125
  const end = url.searchParams.get("end") || (/* @__PURE__ */ new Date()).toISOString();
@@ -65766,7 +66141,7 @@ function reportFilters(url) {
65766
66141
  channelFamily: url.searchParams.get("channelFamily") || void 0,
65767
66142
  platform: url.searchParams.get("platform") || void 0,
65768
66143
  referrer: url.searchParams.get("referrer") || void 0,
65769
- path: url.searchParams.get("path") || void 0,
66144
+ path: pagePathFilter(url.searchParams.get("path")),
65770
66145
  deviceClass: url.searchParams.get("deviceClass") || void 0,
65771
66146
  countryCode: url.searchParams.get("countryCode") || void 0,
65772
66147
  regionCode: url.searchParams.get("regionCode") || void 0,
@@ -65902,7 +66277,9 @@ var init_analytics_routes = __esm({
65902
66277
  campaign: import_zod53.z.string().trim().max(240).optional(),
65903
66278
  clickIds: import_zod53.z.record(import_zod53.z.string(), import_zod53.z.string().trim().max(500)).optional(),
65904
66279
  deviceClass: import_zod53.z.enum(["desktop", "tablet", "mobile", "unknown"]).optional(),
65905
- properties: import_zod53.z.record(import_zod53.z.string(), import_zod53.z.unknown()).optional()
66280
+ properties: import_zod53.z.record(import_zod53.z.string(), import_zod53.z.unknown()).optional(),
66281
+ engagedMs: import_zod53.z.number().int().min(0).max(MAX_ENGAGED_MS).optional(),
66282
+ scrollDepth: import_zod53.z.number().int().min(0).max(100).optional()
65906
66283
  }).strict();
65907
66284
  IngestionSchema = import_zod53.z.object({
65908
66285
  pixelId: import_zod53.z.string().trim().min(8).max(160),
@@ -66902,14 +67279,13 @@ Imported through the Site Analytics CRM workflow.
66902
67279
  ["/sites/:siteId/overview", analyticsOverview],
66903
67280
  ["/sites/:siteId/timeseries", analyticsTimeseries],
66904
67281
  ["/sites/:siteId/acquisition", analyticsAcquisition],
66905
- ["/sites/:siteId/content", analyticsContent],
66906
67282
  ["/sites/:siteId/events", analyticsEventCounts]
66907
67283
  ]) {
66908
67284
  analyticsApp.get(path6, auth3, async (c) => {
66909
67285
  const url = new URL(c.req.url);
66910
67286
  const filters = reportFilters(url);
66911
67287
  validateReportWindow(filters);
66912
- const paginated = path6.endsWith("/acquisition") || path6.endsWith("/content") || path6.endsWith("/events");
67288
+ const paginated = path6.endsWith("/acquisition") || path6.endsWith("/events");
66913
67289
  const data = await load(
66914
67290
  c.req.param("siteId"),
66915
67291
  Number(c.get("user").id),
@@ -66919,6 +67295,24 @@ Imported through the Site Analytics CRM workflow.
66919
67295
  return c.json({ ok: true, data, filters });
66920
67296
  });
66921
67297
  }
67298
+ analyticsApp.get("/sites/:siteId/content", auth3, async (c) => {
67299
+ const url = new URL(c.req.url);
67300
+ const filters = reportFilters(url);
67301
+ validateReportWindow(filters);
67302
+ const requestedSort = url.searchParams.get("sort");
67303
+ const data = await analyticsContent(
67304
+ c.req.param("siteId"),
67305
+ Number(c.get("user").id),
67306
+ filters,
67307
+ reportPage(url),
67308
+ {
67309
+ sort: ANALYTICS_CONTENT_SORTS.includes(requestedSort) ? requestedSort : void 0,
67310
+ direction: url.searchParams.get("direction") === "asc" ? "asc" : "desc",
67311
+ search: url.searchParams.get("search") || void 0
67312
+ }
67313
+ );
67314
+ return c.json({ ok: true, data, filters });
67315
+ });
66922
67316
  analyticsApp.get("/sites/:siteId/dashboard", auth3, async (c) => {
66923
67317
  const url = new URL(c.req.url);
66924
67318
  const filters = reportFilters(url);
@@ -67152,7 +67546,7 @@ var init_scheduled_artifact_owner = __esm({
67152
67546
 
67153
67547
  // src/api/scheduled-run-view-links.ts
67154
67548
  async function ensureScheduledRunViewLinksSchema() {
67155
- if (schemaReady3) return;
67549
+ if (schemaReady2) return;
67156
67550
  const db = getDb();
67157
67551
  await db.execute(`
67158
67552
  CREATE TABLE IF NOT EXISTS scheduled_run_view_links (
@@ -67200,7 +67594,7 @@ async function ensureScheduledRunViewLinksSchema() {
67200
67594
  CREATE INDEX IF NOT EXISTS scheduled_run_view_links_expiry
67201
67595
  ON scheduled_run_view_links(expires_at, revoked_at)
67202
67596
  `);
67203
- schemaReady3 = true;
67597
+ schemaReady2 = true;
67204
67598
  }
67205
67599
  function tokenHash2(token4) {
67206
67600
  return (0, import_node_crypto41.createHash)("sha256").update(token4).digest("hex");
@@ -67288,13 +67682,13 @@ async function revokeScheduledRunViewLink(ownerId, runId, shareId, now = /* @__P
67288
67682
  });
67289
67683
  return result.rowsAffected > 0;
67290
67684
  }
67291
- var import_node_crypto41, schemaReady3;
67685
+ var import_node_crypto41, schemaReady2;
67292
67686
  var init_scheduled_run_view_links = __esm({
67293
67687
  "src/api/scheduled-run-view-links.ts"() {
67294
67688
  "use strict";
67295
67689
  import_node_crypto41 = require("crypto");
67296
67690
  init_db();
67297
- schemaReady3 = false;
67691
+ schemaReady2 = false;
67298
67692
  }
67299
67693
  });
67300
67694
 
@@ -67904,9 +68298,9 @@ var init_public_scheduled_result_routes = __esm({
67904
68298
  // src/api/service-connections.ts
67905
68299
  function ensureServiceConnectionsSchema() {
67906
68300
  const currentDb = getDb();
67907
- if (schemaReady4 && schemaDb4 === currentDb) return schemaReady4;
68301
+ if (schemaReady3 && schemaDb4 === currentDb) return schemaReady3;
67908
68302
  schemaDb4 = currentDb;
67909
- schemaReady4 = (async () => {
68303
+ schemaReady3 = (async () => {
67910
68304
  const db = currentDb;
67911
68305
  await db.execute(`
67912
68306
  CREATE TABLE IF NOT EXISTS service_connections (
@@ -67974,11 +68368,11 @@ function ensureServiceConnectionsSchema() {
67974
68368
  } catch {
67975
68369
  }
67976
68370
  })().catch((error) => {
67977
- schemaReady4 = null;
68371
+ schemaReady3 = null;
67978
68372
  schemaDb4 = null;
67979
68373
  throw error;
67980
68374
  });
67981
- return schemaReady4;
68375
+ return schemaReady3;
67982
68376
  }
67983
68377
  function parseTools(value) {
67984
68378
  if (typeof value !== "string") return [];
@@ -68177,13 +68571,13 @@ async function claimServiceConnectionAction(args) {
68177
68571
  ...result !== void 0 ? { result } : {}
68178
68572
  };
68179
68573
  }
68180
- var import_node_crypto45, schemaReady4, schemaDb4;
68574
+ var import_node_crypto45, schemaReady3, schemaDb4;
68181
68575
  var init_service_connections = __esm({
68182
68576
  "src/api/service-connections.ts"() {
68183
68577
  "use strict";
68184
68578
  import_node_crypto45 = require("crypto");
68185
68579
  init_db();
68186
- schemaReady4 = null;
68580
+ schemaReady3 = null;
68187
68581
  schemaDb4 = null;
68188
68582
  }
68189
68583
  });
@@ -73113,7 +73507,31 @@ async function chargeTierChangeNow(stripeClient, subscriptionId, customerId) {
73113
73507
  return { ok: false, amountDue: 0, error: err instanceof Error ? err.message : "Unable to charge the plan change immediately." };
73114
73508
  }
73115
73509
  }
73116
- var import_resend3, import_node_crypto55, import_hono36, import_hono37, import_factory8, import_cookie2, import_stripe2, secureCookies2, isProduction2, sessionCookieOptions2, requireAllowedOrigin, auth4, sessionAuth, requireIntegrationsTier, requirePaidSchedulingTier, app, deploymentProfile, STRIPE_API_VERSION, SYNC_HARVEST_TIMEOUT_OVERRIDE_MS;
73510
+ function settleWithinTickBudget(label, unfinished, work, onDeadlineOrError) {
73511
+ return new Promise((resolve2) => {
73512
+ const timer = setTimeout(() => {
73513
+ unfinished.push(`${label}:deadline`);
73514
+ resolve2(onDeadlineOrError);
73515
+ }, CRON_TICK_BUDGET_MS);
73516
+ work.then(
73517
+ (value) => {
73518
+ clearTimeout(timer);
73519
+ resolve2(value);
73520
+ },
73521
+ (err) => {
73522
+ clearTimeout(timer);
73523
+ unfinished.push(`${label}:error`);
73524
+ console.error(JSON.stringify({
73525
+ event: "cron_tick_job_failed",
73526
+ job: label,
73527
+ message: err instanceof Error ? err.message : String(err)
73528
+ }));
73529
+ resolve2(onDeadlineOrError);
73530
+ }
73531
+ );
73532
+ });
73533
+ }
73534
+ var import_resend3, import_node_crypto55, import_hono36, import_hono37, import_factory8, import_cookie2, import_stripe2, secureCookies2, isProduction2, sessionCookieOptions2, requireAllowedOrigin, auth4, sessionAuth, requireIntegrationsTier, requirePaidSchedulingTier, app, deploymentProfile, STRIPE_API_VERSION, SYNC_HARVEST_TIMEOUT_OVERRIDE_MS, CRON_TICK_BUDGET_MS, CRON_TICK_DRAIN_BUDGET_MS;
73117
73535
  var init_server = __esm({
73118
73536
  "src/api/server.ts"() {
73119
73537
  "use strict";
@@ -73167,6 +73585,8 @@ var init_server = __esm({
73167
73585
  init_directory_workflow_dispatch();
73168
73586
  init_paa_harvest_dispatch();
73169
73587
  init_paa_harvest_reconciliation();
73588
+ init_unified_billing();
73589
+ init_retention_sweeps();
73170
73590
  init_directory_workflow_reconciliation();
73171
73591
  init_directory_artifacts();
73172
73592
  init_workflow_routes();
@@ -75858,35 +76278,49 @@ var init_server = __esm({
75858
76278
  ledger
75859
76279
  });
75860
76280
  });
76281
+ CRON_TICK_BUDGET_MS = 5e4;
76282
+ CRON_TICK_DRAIN_BUDGET_MS = 4e4;
75861
76283
  app.get("/cron/tick", async (c) => {
75862
76284
  const secret2 = c.req.header("authorization");
75863
76285
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
75864
76286
  return c.json({ error: "Unauthorized" }, 401);
75865
76287
  }
76288
+ const startedAt = Date.now();
75866
76289
  const { drainQueue: drainQueue2 } = await Promise.resolve().then(() => (init_worker(), worker_exports));
75867
- const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
75868
- const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
75869
- const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup, connectedAccountBilling, directoryWorkflowDispatch, directoryWorkflowReconciliation, directoryArtifactCleanup, siteExtractArtifactCleanup, siteExtractRedispatch, siteExtractReconciliation, paaHarvestRedispatch, paaHarvestReconciliation, localSourcebookRedispatch, analyticsRollup, analyticsFormDelivery] = await Promise.all([
75870
- drainQueue2(budget),
75871
- runMonthlyRefreshSweep(),
75872
- reapIdleBrowserSessions(120).catch(() => ({ reaped: 0 })),
75873
- expireOldLots().catch(() => ({ expired_lots: 0, expired_mc: 0 })),
75874
- cleanupExpiredScrapeBlobs().catch(() => ({ deleted: 0, store: "none" })),
75875
- cleanupExpiredConnectedDataArtifacts().catch(() => ({ deleted: 0, store: "none" })),
75876
- reconcileConnectedAccountBillingSweep().catch(() => ({ checked: 0, synced: 0, failed: 0 })),
75877
- dispatchPendingDirectoryWorkflows(25).catch(() => ({ claimed: 0, dispatched: 0, failed: 0 })),
75878
- reconcileDirectoryWorkflowSettlements(25).catch(() => ({ checked: 0, settled: 0, failed: 0 })),
75879
- cleanupExpiredDirectoryArtifacts().catch(() => ({ deleted: 0, store: "none" })),
75880
- cleanupExpiredSiteExtractArtifacts().catch(() => ({ deleted: 0, store: "none" })),
75881
- redispatchFundedSiteExtractJobs(25).catch(() => ({ checked: 0, dispatched: 0, failed: 0 })),
75882
- reconcileSiteExtractSettlements(25).catch(() => ({ checked: 0, settled: 0, failed: 0 })),
75883
- redispatchPendingPaaHarvests(25).catch(() => ({ checked: 0, dispatched: 0, failed: 0 })),
75884
- reconcilePaaHarvestSettlements(25).catch(() => ({ checked: 0, staleFailed: 0, settled: 0, failed: 0 })),
75885
- redispatchQueuedLocalSourcebookAcquisitions(25).catch(() => ({ checked: 0, dispatched: 0, failed: 0 })),
75886
- refreshAnalyticsDailyRollupsIfDue().catch(() => ({ status: "failed", rows: 0 })),
75887
- deliverPendingAnalyticsForms(10).catch(() => ({ claimed: 0, delivered: 0, deferred: 0 }))
76290
+ const budget = { maxJobs: 10, deadlineMs: startedAt + CRON_TICK_DRAIN_BUDGET_MS };
76291
+ const origin = `${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`;
76292
+ const unfinished = [];
76293
+ const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup, connectedAccountBilling, directoryWorkflowDispatch, directoryWorkflowReconciliation, directoryArtifactCleanup, siteExtractArtifactCleanup, siteExtractRedispatch, siteExtractReconciliation, paaHarvestRedispatch, paaHarvestReconciliation, localSourcebookRedispatch, analyticsRollup, analyticsFormDelivery, workflowDispatchResult, scheduledRunReconciliation, retentionSweep] = await Promise.all([
76294
+ settleWithinTickBudget("drainQueue", unfinished, drainQueue2(budget), []),
76295
+ settleWithinTickBudget("monthlyRefreshSweep", unfinished, runMonthlyRefreshSweep(), { status: "skipped_deadline" }),
76296
+ settleWithinTickBudget("reapBrowserSessions", unfinished, reapIdleBrowserSessions(120), { reaped: 0 }),
76297
+ settleWithinTickBudget("retryOrphanedBrowserSessions", unfinished, retryOrphanedBrowserSessions(50), { recovered: 0, stillOpen: 0 }),
76298
+ settleWithinTickBudget("expireLots", unfinished, expireOldLots(), { expired_lots: 0, expired_mc: 0 }),
76299
+ settleWithinTickBudget("scrapeBlobCleanup", unfinished, cleanupExpiredScrapeBlobs(), { deleted: 0, store: "none" }),
76300
+ settleWithinTickBudget("connectedDataCleanup", unfinished, cleanupExpiredConnectedDataArtifacts(), { deleted: 0, store: "none" }),
76301
+ settleWithinTickBudget("connectedBillingSweep", unfinished, reconcileConnectedAccountBillingSweep(), { checked: 0, synced: 0, failed: 0 }),
76302
+ settleWithinTickBudget("directoryDispatch", unfinished, dispatchPendingDirectoryWorkflows(5), { claimed: 0, dispatched: 0, failed: 0 }),
76303
+ settleWithinTickBudget("directoryReconcile", unfinished, reconcileDirectoryWorkflowSettlements(5), { checked: 0, settled: 0, failed: 0 }),
76304
+ settleWithinTickBudget("directoryArtifactCleanup", unfinished, cleanupExpiredDirectoryArtifacts(), { deleted: 0, store: "none" }),
76305
+ settleWithinTickBudget("siteExtractArtifactCleanup", unfinished, cleanupExpiredSiteExtractArtifacts(), { deleted: 0, store: "none" }),
76306
+ settleWithinTickBudget("siteExtractRedispatch", unfinished, redispatchFundedSiteExtractJobs(10), { checked: 0, dispatched: 0, failed: 0 }),
76307
+ settleWithinTickBudget("siteExtractReconcile", unfinished, reconcileSiteExtractSettlements(10), { checked: 0, settled: 0, failed: 0 }),
76308
+ settleWithinTickBudget("paaRedispatch", unfinished, redispatchPendingPaaHarvests(10), { checked: 0, dispatched: 0, failed: 0 }),
76309
+ settleWithinTickBudget("paaReconcile", unfinished, reconcilePaaHarvestSettlements(10), { checked: 0, staleFailed: 0, settled: 0, failed: 0 }),
76310
+ settleWithinTickBudget("localSourcebookRedispatch", unfinished, redispatchQueuedLocalSourcebookAcquisitions(5), { checked: 0, dispatched: 0, failed: 0 }),
76311
+ settleWithinTickBudget("analyticsRollup", unfinished, refreshAnalyticsDailyRollupsIfDue(), { status: "failed", rows: 0 }),
76312
+ settleWithinTickBudget("analyticsForms", unfinished, deliverPendingAnalyticsForms(10), { claimed: 0, delivered: 0, deferred: 0 }),
76313
+ settleWithinTickBudget("workflowDispatch", unfinished, dispatchDueWorkflowSchedules(origin), { dispatched: 0, status: "skipped_deadline" }),
76314
+ settleWithinTickBudget("scheduledRunReconcile", unfinished, reconcileScheduledRunSettlements(25), { checked: 0, settled: 0, chargedMc: 0, failed: 0 }),
76315
+ settleWithinTickBudget("retentionSweep", unfinished, sweepRetention(), { deleted: 0, perTable: {}, truncated: false })
75888
76316
  ]);
75889
76317
  return c.json({
76318
+ durationMs: Date.now() - startedAt,
76319
+ tickBudgetMs: CRON_TICK_BUDGET_MS,
76320
+ unfinished,
76321
+ complete: unfinished.length === 0,
76322
+ scheduledRunReconciliation,
76323
+ retentionSweep,
75890
76324
  drained: results.length,
75891
76325
  results,
75892
76326
  sweepResult,