From 41f2ba73912b6010c3675dfad5fea3562981a11a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:03:50 +0000 Subject: [PATCH 01/10] Bump actions/setup-node from 7.0.0 to 7.1.0 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 7.0.0 to 7.1.0. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/820762786026740c76f36085b0efc47a31fe5020...949feb2413d6458794dcd2491c4babbbce0c15c1) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: 7.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cc3f90f..eb6fe57a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 with: # Contract tests stub modules with module.registerHooks (Node 22.15+). node-version: "24" @@ -106,7 +106,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 with: node-version: "24" cache: npm @@ -124,7 +124,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7 with: node-version: "24" cache: npm From 61c268bbf250fc649421ffb9db31a74d3fb1efd6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:04:03 +0000 Subject: [PATCH 02/10] Bump actions/upload-artifact from 7.0.1 to 7.0.2 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 7.0.1 to 7.0.2. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/043fb46d1a93c77aae656e7c1c64a875d1fc6a0a...cf430e030ddbb5b0abf93d22962f4752f3646cd9) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: 7.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- .github/workflows/scrape.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cc3f90f..3813a6e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -144,7 +144,7 @@ jobs: # failures can be opened with `npx playwright show-trace`. - name: Upload Playwright traces if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: playwright-traces-${{ github.run_id }}-${{ github.run_attempt }} path: test-results/ diff --git a/.github/workflows/scrape.yml b/.github/workflows/scrape.yml index 7f4677af..a3f79b3e 100644 --- a/.github/workflows/scrape.yml +++ b/.github/workflows/scrape.yml @@ -195,7 +195,7 @@ jobs: - name: Upload encrypted run diagnostics if: always() && steps.encrypted.outcome == 'success' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: pipeline-report-${{ github.run_id }}-${{ github.run_attempt }} path: .pipeline-cache/state.enc From abf8aa7e5ef6bc9fc4388a79cb4f903b63b47a55 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:04:45 +0000 Subject: [PATCH 03/10] Update google-genai requirement from >=2.28.0 to >=2.29.0 in /pipeline Updates the requirements on [google-genai](https://github.com/googleapis/python-genai) to permit the latest version. - [Release notes](https://github.com/googleapis/python-genai/releases) - [Changelog](https://github.com/googleapis/python-genai/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/python-genai/compare/v2.28.0...v2.29.0) --- updated-dependencies: - dependency-name: google-genai dependency-version: 2.29.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pipeline/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipeline/requirements.txt b/pipeline/requirements.txt index f987f5b9..088ebd59 100644 --- a/pipeline/requirements.txt +++ b/pipeline/requirements.txt @@ -4,6 +4,6 @@ requests>=2.34.2 supabase>=2.32.0 python-dotenv>=1.2.4 tzdata>=2026.5 -google-genai>=2.28.0 +google-genai>=2.29.0 Pillow>=12.3.0,<13 zxing-cpp>=3.1.1,<4 # decode flyer QR codes without another API call From 4ba34d582f374a754732ffd5a87fa199ac7c4324 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:04:55 +0000 Subject: [PATCH 04/10] Bump eslint-config-next from 16.3.8 to 16.4.0 Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 16.3.8 to 16.4.0. - [Release notes](https://github.com/vercel/next.js/releases) - [Commits](https://github.com/vercel/next.js/commits/v16.4.0/packages/eslint-config-next) --- updated-dependencies: - dependency-name: eslint-config-next dependency-version: 16.4.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 20 ++++++++++---------- package.json | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index b36baae5..727fbfb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "@types/react-dom": "^19.3.0", "autoprefixer": "^10.6.1", "eslint": "9.39.4", - "eslint-config-next": "16.3.8", + "eslint-config-next": "16.4.0", "postcss": "8.5.29", "tailwindcss": "^3.4.6", "typescript": "^5.5.3" @@ -1191,9 +1191,9 @@ "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.3.8.tgz", - "integrity": "sha512-eCR9RcLTZrVS+K3+VOhi0xMdUUGNuriCHHhU1ZoABC/QQ/Gp0OWbLKQxV2/9DkmihYdYRcQWFmZXW8M1AMUXYw==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.4.0.tgz", + "integrity": "sha512-ffeflB/dL8RrvWejQximw01pOpKHN692Y3FjDB82DCd0YVvXvBD6vY7mcZp27yvSVFVM6wA73xVodEKF4spizA==", "dev": true, "license": "MIT", "dependencies": { @@ -3359,21 +3359,21 @@ } }, "node_modules/eslint-config-next": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.3.8.tgz", - "integrity": "sha512-81vovwMe6NGnoFsl0KUJWzlS+y239i3fdsxs49gSEX3pDQW2cuKy+fxUPXNkQqqx6OCPheYPtBlSufc9aZ0k+w==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.4.0.tgz", + "integrity": "sha512-2RHLYswzc+hNRktWAjIL6XAvwb1cKcjLawJ5KWywAxn/JivNHtG42ZigL1yobVXDqsimuuL2F9jcEcnHFzbppQ==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.3.8", + "@next/eslint-plugin-next": "16.4.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", - "eslint-plugin-react-hooks": "^7.0.0", + "eslint-plugin-react-hooks": "^7.1.0", "globals": "16.4.0", - "typescript-eslint": "^8.46.0" + "typescript-eslint": "^8.56.0" }, "peerDependencies": { "eslint": ">=9.0.0", diff --git a/package.json b/package.json index 11c07100..0358f061 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@types/react-dom": "^19.3.0", "autoprefixer": "^10.6.1", "eslint": "9.39.4", - "eslint-config-next": "16.3.8", + "eslint-config-next": "16.4.0", "postcss": "8.5.29", "tailwindcss": "^3.4.6", "typescript": "^5.5.3" From 03b15e24ca169b3f4221273b2197908cdcb49011 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:05:02 +0000 Subject: [PATCH 05/10] Bump @supabase/supabase-js from 2.117.2 to 2.117.3 Bumps [@supabase/supabase-js](https://github.com/supabase/supabase-js/tree/HEAD/packages/core/supabase-js) from 2.117.2 to 2.117.3. - [Release notes](https://github.com/supabase/supabase-js/releases) - [Changelog](https://github.com/supabase/supabase-js/blob/master/packages/core/supabase-js/CHANGELOG.md) - [Commits](https://github.com/supabase/supabase-js/commits/v2.117.3/packages/core/supabase-js) --- updated-dependencies: - dependency-name: "@supabase/supabase-js" dependency-version: 2.117.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 48 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index b36baae5..9b19657d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "highlander-hub", "version": "0.1.0", "dependencies": { - "@supabase/supabase-js": "^2.117.2", + "@supabase/supabase-js": "^2.117.3", "@vercel/analytics": "^2.0.1", "clsx": "^2.1.1", "motion": "^14.0.0", @@ -1431,9 +1431,9 @@ "license": "MIT" }, "node_modules/@supabase/auth-js": { - "version": "2.117.2", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.117.2.tgz", - "integrity": "sha512-Z3WnGwrphYZubrLbxp5Iv0tLA9A5GhvKzJ/ZXglxqwd2QEH2R4dXRltjVJ8sIn4xEW2BSFIGT0yALVJIgZeDYw==", + "version": "2.117.3", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.117.3.tgz", + "integrity": "sha512-XOZV+jllcU2Hzij8SwgM78zl0lOEr6BTjzi2MYkBb39nOAQzb5AelGpMUvVj7lwGSB11JmpAs/FEQroAyFzWEA==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -1443,9 +1443,9 @@ } }, "node_modules/@supabase/functions-js": { - "version": "2.117.2", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.117.2.tgz", - "integrity": "sha512-6DT4ZIjmZxa9ANKaBIrjc86AyKrX8M376ynvNeOBBVad3eP7x3UHqWfDR2ynFkHGzCuOpr1A+kObmnahxuwuog==", + "version": "2.117.3", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.117.3.tgz", + "integrity": "sha512-6GuizbP4N5xWaxEVUsI+UQpJdmMmn0t3sYNpObo9gCAldvM22gFjnYzRVmkHxp5ylU21azEYmiWVwk9iDgCMkQ==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -1461,9 +1461,9 @@ "license": "MIT" }, "node_modules/@supabase/postgrest-js": { - "version": "2.117.2", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.117.2.tgz", - "integrity": "sha512-V1Qhn+M8xzJqCasOxHZ2KG19Fj39PxU7weEAmOK65/KTsWCRXboB4hcQwFvxRvM7ZikeP9IZWNvQkfjvSxzYFw==", + "version": "2.117.3", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.117.3.tgz", + "integrity": "sha512-NpyTgullu6tw11ojYKTdZg9HannZ4mu4FUlhc9V5zDXOVllUJzEmuyLM+WWoYF0BurfrRvarimpm3SR8P0Gzfw==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -1473,9 +1473,9 @@ } }, "node_modules/@supabase/realtime-js": { - "version": "2.117.2", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.117.2.tgz", - "integrity": "sha512-lYXSAIg3eAKA58riUED6Vb+TCzF8jtx18uOoiCaVJ7ZMre6FWBpwB1MPeW+B6vykJT/hPdiuqwcSB7ILMS21ew==", + "version": "2.117.3", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.117.3.tgz", + "integrity": "sha512-4W4ISbreRygNXbhwqxeqyuRul8RPx+SF+Hp2IT8wctebQEHSVCULfKatiRiEhmasfoXvYm2AHV4XFsOxhoousA==", "license": "MIT", "dependencies": { "@supabase/phoenix": "0.4.5", @@ -1486,9 +1486,9 @@ } }, "node_modules/@supabase/storage-js": { - "version": "2.117.2", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.117.2.tgz", - "integrity": "sha512-8gAJoVaxZa/War2kFRfJMxGk4M190Q7lJ70BofwGlRxU/u9pIodd6XyTvmXkxOKXWuIYtkDaYPZrIAUt1T/7FQ==", + "version": "2.117.3", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.117.3.tgz", + "integrity": "sha512-QhUxFC33Crlw6WDeA4hLADdi1PzjI3c4UJs7ivWHHEnU7ulmCWEZSDaRCuIfmsjUwbHYaIfRNFQNsm+IsGYS6w==", "license": "MIT", "dependencies": { "iceberg-js": "^0.8.1", @@ -1499,16 +1499,16 @@ } }, "node_modules/@supabase/supabase-js": { - "version": "2.117.2", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.117.2.tgz", - "integrity": "sha512-eSG2VKnHR+Clp1PmidZ1/weJ8PJwoybjva3L2GgKqFG4YDS1Iqmc61psKGZP5xw6OMT2O7ZorPR42PY6q1BOXg==", + "version": "2.117.3", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.117.3.tgz", + "integrity": "sha512-K+f8PimXDunPWQ63CKRXaF5pbVPJxmfw5ryAnh0s9S9PkwcuqBjiSpIaqpF7KLDljeYwXIL85ST8QxaO0+XuvA==", "license": "MIT", "dependencies": { - "@supabase/auth-js": "2.117.2", - "@supabase/functions-js": "2.117.2", - "@supabase/postgrest-js": "2.117.2", - "@supabase/realtime-js": "2.117.2", - "@supabase/storage-js": "2.117.2" + "@supabase/auth-js": "2.117.3", + "@supabase/functions-js": "2.117.3", + "@supabase/postgrest-js": "2.117.3", + "@supabase/realtime-js": "2.117.3", + "@supabase/storage-js": "2.117.3" }, "engines": { "node": ">=22.0.0" diff --git a/package.json b/package.json index 11c07100..9c9b093a 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "pretest:unit": "node scripts/generate-public-clubs.mjs" }, "dependencies": { - "@supabase/supabase-js": "^2.117.2", + "@supabase/supabase-js": "^2.117.3", "@vercel/analytics": "^2.0.1", "clsx": "^2.1.1", "motion": "^14.0.0", From 8599b35c0a5b3e0a74774551d4dad7c0264282cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:05:07 +0000 Subject: [PATCH 06/10] Bump @types/node from 26.6.4 to 26.6.5 Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 26.6.4 to 26.6.5. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 26.6.5 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index b36baae5..bc118d58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "devDependencies": { "@electric-sql/pglite": "^0.5.8", "@playwright/test": "^1.63.0", - "@types/node": "^26.6.4", + "@types/node": "^26.6.5", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", "autoprefixer": "^10.6.1", @@ -1564,9 +1564,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.6.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", - "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 11c07100..1e2e227b 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "devDependencies": { "@electric-sql/pglite": "^0.5.8", "@playwright/test": "^1.63.0", - "@types/node": "^26.6.4", + "@types/node": "^26.6.5", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", "autoprefixer": "^10.6.1", From 146da4da7c18d38a551c3015099e938299e0595b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:05:15 +0000 Subject: [PATCH 07/10] Bump @playwright/test from 1.63.0 to 1.64.0 Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.63.0 to 1.64.0. - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.63.0...v1.64.0) --- updated-dependencies: - dependency-name: "@playwright/test" dependency-version: 1.64.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 24 ++++++++++++------------ package.json | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index b36baae5..bc898fb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ }, "devDependencies": { "@electric-sql/pglite": "^0.5.8", - "@playwright/test": "^1.63.0", + "@playwright/test": "^1.64.0", "@types/node": "^26.6.4", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", @@ -1408,13 +1408,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", - "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.0.tgz", + "integrity": "sha512-oTN9FNYuN6bqQMwiho6jJKLCG2/QOYCixGXuCjRmzOq23eODeukuY2XiAVoVpAECbuezMt1MHQjAiQxK8F+Y+g==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.63.0" + "playwright": "1.64.0" }, "bin": { "playwright": "cli.js" @@ -5489,13 +5489,13 @@ } }, "node_modules/playwright": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", - "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.0.tgz", + "integrity": "sha512-kVzTnYFYEyQ9RGzBjNCd2+yEqWmVbxl3AHPynv3qQ6JTjv4a+kwRcnqQwIpuP3NnrnIrE/Q6wWAIAvMfIE2/Yg==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.63.0" + "playwright-core": "1.64.0" }, "bin": { "playwright": "cli.js" @@ -5505,9 +5505,9 @@ } }, "node_modules/playwright-core": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", - "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.0.tgz", + "integrity": "sha512-T9r+MZkTECl2+oUcZ26YgZzTrYidTsIhYYkJix+6iDOymOb1FB2RLLSOxRGnZGv5DStt+aduIVbt+E8WygXYpg==", "devOptional": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 11c07100..855961f9 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ }, "devDependencies": { "@electric-sql/pglite": "^0.5.8", - "@playwright/test": "^1.63.0", + "@playwright/test": "^1.64.0", "@types/node": "^26.6.4", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", From 58a0ab9da47dc368b276b749860316dad516c790 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:05:22 +0000 Subject: [PATCH 08/10] Bump motion from 14.0.0 to 14.1.0 Bumps [motion](https://github.com/motiondivision/motion) from 14.0.0 to 14.1.0. - [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md) - [Commits](https://github.com/motiondivision/motion/compare/v14.0.0...v14.1.0) --- updated-dependencies: - dependency-name: motion dependency-version: 14.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 34 +++++++++++++++++----------------- package.json | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index b36baae5..2df9d715 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@supabase/supabase-js": "^2.117.2", "@vercel/analytics": "^2.0.1", "clsx": "^2.1.1", - "motion": "^14.0.0", + "motion": "^14.1.0", "next": "16.3.8", "react": "^19.3.0", "react-dom": "^19.3.0", @@ -3896,13 +3896,13 @@ } }, "node_modules/framer-motion": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-14.0.0.tgz", - "integrity": "sha512-QAtHmybrDFSliyW1sexMtSQTQd+1jw2ajibEmzpuI4XdPVzQkn+dAzJMm2sW3pl9MJuSS4xN51djQ7huhlP/VA==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-14.1.0.tgz", + "integrity": "sha512-QsdZSdWoyQfUmgi+GZfow8Du5COG6VRYEo9FlgKUPgH87/Le/fYekHCAKwLdYKHdCZcZfWHg4iOxlD6RoqXciQ==", "license": "MIT", "dependencies": { - "motion-dom": "14.0.0", - "motion-utils": "14.0.0", + "motion-dom": "14.1.0", + "motion-utils": "14.1.0", "tslib": "^2.4.0" }, "peerDependencies": { @@ -5010,12 +5010,12 @@ } }, "node_modules/motion": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/motion/-/motion-14.0.0.tgz", - "integrity": "sha512-oGc2lkge2lkRZCP+J8/JsnYu4NFjqP1MtXirNdetn3QbvK2JqwM9A200EdT4C598KfzDhjLT12zF21xDM4Oilw==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-14.1.0.tgz", + "integrity": "sha512-lsCsW9uJ+iMJj8MSiLccpUC+ZUpoSspaX6Pm78KrnZKMioCPAPG6WeG1d1d/Ywg+z1mny+STP6VyEYW+3nAqNw==", "license": "MIT", "dependencies": { - "framer-motion": "14.0.0", + "framer-motion": "14.1.0", "tslib": "^2.4.0" }, "peerDependencies": { @@ -5032,18 +5032,18 @@ } }, "node_modules/motion-dom": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-14.0.0.tgz", - "integrity": "sha512-aU3ApXo1yTpeUi3JU+jbDk2TELjxPE3MbBMUP7nZHlARb6RweJK+ojmLjpuB0YsY6zhbBl1YWGo8LMsI1/Llqw==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-14.1.0.tgz", + "integrity": "sha512-FyFLkNUAAETzCvf851mZcl+v+kaZrKq+UM+XmkY4G8n+ny/WYfiWh9j0fhSQ1KOKVQrM9/cLO11THO2k71HJlg==", "license": "MIT", "dependencies": { - "motion-utils": "14.0.0" + "motion-utils": "14.1.0" } }, "node_modules/motion-utils": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-14.0.0.tgz", - "integrity": "sha512-jBsyjVxTTRZNeuvd/v+btVXr90LQkYl4HhRu9T2ARc+PMnvkmjsjRVeAg4Qm10b6TlTw+kykWI8gu/6LuvpLSQ==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-14.1.0.tgz", + "integrity": "sha512-xd4FeWOnnXsJHmAaRCLNbOS7mEaNOangjMt3Pl241H07f1kxW48fADQ/8O5hLLNhf1qdlozgY3PPAHBdOoU7AA==", "license": "MIT" }, "node_modules/ms": { diff --git a/package.json b/package.json index 11c07100..024ea156 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@supabase/supabase-js": "^2.117.2", "@vercel/analytics": "^2.0.1", "clsx": "^2.1.1", - "motion": "^14.0.0", + "motion": "^14.1.0", "next": "16.3.8", "react": "^19.3.0", "react-dom": "^19.3.0", From b7e62f3792fe82cac4d68dcd8fbc896064c9ebed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:05:39 +0000 Subject: [PATCH 09/10] Bump next from 16.3.8 to 16.4.0 Bumps [next](https://github.com/vercel/next.js) from 16.3.8 to 16.4.0. - [Release notes](https://github.com/vercel/next.js/releases) - [Commits](https://github.com/vercel/next.js/compare/v16.3.8...v16.4.0) --- updated-dependencies: - dependency-name: next dependency-version: 16.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 92 ++++++++++++++++++++++++++--------------------- package.json | 2 +- 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index b36baae5..5dbc0dcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@vercel/analytics": "^2.0.1", "clsx": "^2.1.1", "motion": "^14.0.0", - "next": "16.3.8", + "next": "16.4.0", "react": "^19.3.0", "react-dom": "^19.3.0", "react-icons": "^5.7.0", @@ -1185,9 +1185,9 @@ } }, "node_modules/@next/env": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.8.tgz", - "integrity": "sha512-Al9zqHVV7TJv0eFuOU4U7Lvv74PTih4Ch63sk2xCIpSTkE3udFnaOcnzP2lQVymiL7yS9Cj2iClUXlR3EQ5sEw==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.4.0.tgz", + "integrity": "sha512-pyLlT5oafGlX8X7xjgGNXz5dNIDSxREm+qD6i9VLSZiZFopA/XWl8NM/KPmLx/8EG/++c49iid3gNWSQW1+2dQ==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1232,9 +1232,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.8.tgz", - "integrity": "sha512-2JPRMh2nmQG5CiL7cXGL9AGwnPWJQ//cTtAUCT+w511QHk79SYz3LGv/pc5X643B/WEO0rvu3Yww0hqwt3kgeA==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.4.0.tgz", + "integrity": "sha512-yyZ+9MgViAKLLb5l1gPBGjRwnq3teNLb5Zx6JEw8LzNSTjlsnp+baUHFxTgl7PpjCmKf48VQfyipG1eBflb48Q==", "cpu": [ "arm64" ], @@ -1248,9 +1248,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.8.tgz", - "integrity": "sha512-GZtCCOBKJ4leVIT/Th0llWKhD1ca92lzbQiS5R5ON9QkoiFnilFsebDae1JU2a3HWoKMEmEZWGs1AGLavVM72Q==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.4.0.tgz", + "integrity": "sha512-RePEEejEj1Auf5s9qEcG0JbcDP3ceNUS/ofNX8AhxzgIjTVlZIHMatlp16YplcSMpsVo/ZMQHJTjbMYJznjGUg==", "cpu": [ "x64" ], @@ -1264,12 +1264,15 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.8.tgz", - "integrity": "sha512-O659ygeQYqneJ1fBKMpFxIFqYkYswu8IAS1OCKK/4f3ZgJJm1dRz4fVJZRi/kLLWjnBKnebOePA4WNv+sV1pVA==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.4.0.tgz", + "integrity": "sha512-HhsyCqd+NpmvDDVY/LqkAN24f+QVvXh3ie+PfcwKinWL2Mu826JymQxJXGjAIfmJ4VlHjp4Pe/nZtXtUoEMAqw==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1280,12 +1283,15 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.8.tgz", - "integrity": "sha512-dSjKSyWpzxoO1d3DIZZcP4XJcNaKeLmxQMFOiYl5vuBRMmweIqnAhty8tAmRsvTss779cK1FtYnDMj40e4TQlg==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.4.0.tgz", + "integrity": "sha512-sN9Pf+Zh+RpZAOmbR7d8KAA8T61CVhawtbEJ2Bk6QrRzx6Z/6haaF4UiwBzytM96GRRRWZh9/WALIEO3npMamA==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1296,12 +1302,15 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.8.tgz", - "integrity": "sha512-lbqOuz3RPRcv+o9msNsJw5x4+Y1ZwPTs6vmL6DCf7i0fZfvng/F59wyeDwqHIvV0mK//RBy/jJkZ+nCKsSMXjQ==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.4.0.tgz", + "integrity": "sha512-D5M5xLWQVd1CvR+L7BickM1weRfmM41x3L1NWcnKY/oyJJqr1qwaCgmu3dMAhLBOMdZwSuDCRV6DJRRpiUkbDQ==", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1312,12 +1321,15 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.8.tgz", - "integrity": "sha512-+316WswI8ScVgZeUd+1KGaXkHhaYQzCjvH/05TZSpJ8zBizb1a4G7DtO7F12jcBIqMOtsz9ji1t48fmKtzqsGA==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.4.0.tgz", + "integrity": "sha512-8d5WLS6a9YalqpOgXdlbvonFu+sIgjA+CO6tGzetrpPVcUL56foa7B7664h41rAlbLi5fJwK4vWji3zf4QDy+w==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1328,9 +1340,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.8.tgz", - "integrity": "sha512-ji0gd4kMYUxO+1fJBIbiBVRCjzG/lloiyCccnlebvb1ZJ5qXCPZqYg4Jl1DrrixnWNMKylzgpmMWx0yNDYXlzw==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.4.0.tgz", + "integrity": "sha512-sQuOIiW3snb48UwrBcDMvFA/u03qtw2LeuSIxFIQ9AJuW9UG/4Ddsfo+eV73PnZwBhyncMtsKVh7PrzMol2/fA==", "cpu": [ "arm64" ], @@ -1344,9 +1356,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.8.tgz", - "integrity": "sha512-WcTlaKt/TWkh5kUjdJcUmB1XgZ+1c6fz4Y9fDHL73YNSdGaUWjceeWrrlwF0nv19iABYWC4iAq1oX1w4Bn0vfg==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.4.0.tgz", + "integrity": "sha512-6/33lCuxpdHUy27RegyGM/5uTpF7YZd3UR4D0gpkOA/tWY5YR7msbQjqPgCZmSmxrrTLGCXYA4A8vT2H5DPGTQ==", "cpu": [ "x64" ], @@ -5107,12 +5119,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/next/-/next-16.3.8.tgz", - "integrity": "sha512-U7QEZaTini6wKrb8A8hqLLqYQyCetegKjCpJOyxk642vWoMoU1x5PyZCJFvgYgiptA8xc5j/9xYlZFO7w9Sjmw==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/next/-/next-16.4.0.tgz", + "integrity": "sha512-N2C+9q89Z/oLrsepSRS4jRMtwmUZAoZuAHU0e9T6+sAB5DdhADmrnOW+zjYijlrYFmVfmQhGVGy9eMFoB7UgKg==", "license": "MIT", "dependencies": { - "@next/env": "16.3.8", + "@next/env": "16.4.0", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -5126,14 +5138,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.3.8", - "@next/swc-darwin-x64": "16.3.8", - "@next/swc-linux-arm64-gnu": "16.3.8", - "@next/swc-linux-arm64-musl": "16.3.8", - "@next/swc-linux-x64-gnu": "16.3.8", - "@next/swc-linux-x64-musl": "16.3.8", - "@next/swc-win32-arm64-msvc": "16.3.8", - "@next/swc-win32-x64-msvc": "16.3.8", + "@next/swc-darwin-arm64": "16.4.0", + "@next/swc-darwin-x64": "16.4.0", + "@next/swc-linux-arm64-gnu": "16.4.0", + "@next/swc-linux-arm64-musl": "16.4.0", + "@next/swc-linux-x64-gnu": "16.4.0", + "@next/swc-linux-x64-musl": "16.4.0", + "@next/swc-win32-arm64-msvc": "16.4.0", + "@next/swc-win32-x64-msvc": "16.4.0", "sharp": "^0.35.4" }, "peerDependencies": { diff --git a/package.json b/package.json index 11c07100..f4a099a2 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@vercel/analytics": "^2.0.1", "clsx": "^2.1.1", "motion": "^14.0.0", - "next": "16.3.8", + "next": "16.4.0", "react": "^19.3.0", "react-dom": "^19.3.0", "react-icons": "^5.7.0", From 7a9eba6e72969a9b13e8a0f51fe93510cf5b8c9a Mon Sep 17 00:00:00 2001 From: kevinjylin <146326281+kevinjylin@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:50:44 -0700 Subject: [PATCH 10/10] Stop rebilling completed Instagram recheck batches (#91) --- pipeline/README.md | 5 + pipeline/apify_posts.py | 5 +- pipeline/post_recheck.py | 78 ++++++---- pipeline/tests/test_post_recheck.py | 217 ++++++++++++++++++++++++++++ 4 files changed, 276 insertions(+), 29 deletions(-) diff --git a/pipeline/README.md b/pipeline/README.md index f024f579..5cdab8d2 100644 --- a/pipeline/README.md +++ b/pipeline/README.md @@ -413,3 +413,8 @@ over half of a batch reports deleted is treated as an outage and changes nothing Withdrawn sources carry `assessment.deleted`, which publication skips so the archived copy is not republished. Progress is kept in `data/post_rechecks.json`. A post is checked once, so a deletion after the check is not seen. +Each batch saves its check results before marking the paid run consumed. +An interrupted batch reuses its original run and post list; completed batches +use the next due list. Replaying a run cannot count as a second deletion +confirmation. Empty or unrelated output stops further batches and retains the +paid run for investigation rather than silently buying the same lookup again. diff --git a/pipeline/apify_posts.py b/pipeline/apify_posts.py index ea611d4d..841242f2 100644 --- a/pipeline/apify_posts.py +++ b/pipeline/apify_posts.py @@ -154,7 +154,10 @@ def details(self, codes: list[str], *, max_charge: float, timeout: int) -> tuple # This intent stays unconsumed even for FAILED runs: replacing it within # a batch could spend its reserved detail ceiling twice. A fresh plan's # cycle-specific filename permits a new attempt for incomplete profiles. - codes = saved.get("usernames", sorted(codes)) + if saved and not saved.get("consumed"): + codes = saved.get("usernames", sorted(codes)) + else: + codes = sorted(codes) client = ApifyClient(self.session.headers["Authorization"].removeprefix("Bearer "), path) run = client._run(hpix_input(posts=[f"https://www.instagram.com/p/{code}/" for code in codes]), {"usernames": codes, "actor_id": HPIX_ACTOR_ID, diff --git a/pipeline/post_recheck.py b/pipeline/post_recheck.py index bd2f393e..79c5dd83 100644 --- a/pipeline/post_recheck.py +++ b/pipeline/post_recheck.py @@ -120,7 +120,12 @@ def main() -> None: registry = assessed_events.load_registry() state = read_json(STATE_FILE) if STATE_FILE.exists() else {} due = due_sources(events, registry, state, now) - if not due: + details_file = RUN_FILE.with_name(RUN_FILE.stem + "-details.json") + saved = read_json(details_file) if details_file.exists() else {} + # Finish already-paid work before buying a new batch, even if its events + # have expired or its check state was saved just before an interruption. + recovery = saved.get("usernames", []) if saved and not saved.get("consumed") else [] + if not due and not recovery: log.info("post recheck: nothing due") return media_ids = [key.rsplit(":", 1)[-1] for key in due] @@ -131,12 +136,12 @@ def main() -> None: token = os.environ.get("APIFY_TOKEN", "") api = ApifyClient(token, RUN_FILE) results: dict[str, str] = {} - pending = sorted(by_code) - for i in range(0, len(pending), BATCH_SIZE): - batch = pending[i:i + BATCH_SIZE] + withdrawn = 0 + pending = sorted(set(by_code) - set(recovery)) + batches = ([recovery] if recovery else []) + [ + pending[i:i + BATCH_SIZE] for i in range(0, len(pending), BATCH_SIZE)] + for batch in batches: run, rows = api.details(batch, max_charge=MAX_CHARGE_USD, timeout=600) - details_file = RUN_FILE.with_name(RUN_FILE.stem + "-details.json") - write_json(details_file, {**read_json(details_file), "consumed": True}) # Count against the lookup's own ceiling; a charge-limited run may # have stopped before reaching every post, so nothing can be concluded. if run.get("status") != "SUCCEEDED": @@ -144,33 +149,50 @@ def main() -> None: reason = charge_limit_reason(run, MAX_CHARGE_USD, len(rows), details=True) if reason: raise CollectionDeferred(f"Post recheck: {reason}") - batch_result = classify(rows, set(pending)) + if not rows: + raise RuntimeError(f"Post recheck run {run['id']}: empty output; paid batch retained") + for row in rows: + node = row.get("data") if isinstance(row, dict) else None + code = (node.get("code") or node.get("shortcode")) if isinstance(node, dict) else None + match = POST_URL.search(str(row.get("input") or "")) if isinstance(row, dict) else None + identities = {value for value in (code, match.group(1) if match else None) if value} + if not identities or not identities.issubset(batch): + raise RuntimeError(f"Post recheck run {run['id']}: unrelated or unidentifiable output; paid batch retained") + batch_result = classify(rows, set(batch)) gone = sum(1 for v in batch_result.values() if v == "missing") if len(batch) >= 4 and gone * 2 > len(batch): # Posts this old almost never vanish together; the lookup is broken. raise RuntimeError(f"Post recheck: {gone} of {len(batch)} posts reported deleted; " "treating the response as an outage and changing nothing") results.update(batch_result) - - updates = [] - for code, outcome in results.items(): - media_id = by_code[code] - key = f"instagram:post:{media_id}" - entry = state.get(media_id) or {} - if outcome == "present": - state[media_id] = {"status": "present", "checked_at": iso(now)} - continue - misses = entry.get("misses", 0) + 1 - state[media_id] = {"status": "missing", "misses": misses, "checked_at": iso(now)} - log.info("post %s (%s): reported deleted (%d/%d)", media_id, code, misses, CONFIRMATIONS) - if misses >= CONFIRMATIONS: - updates.append(withdrawal(key, registry[key], now)) - state[media_id]["status"] = "withdrawn" - write_json(STATE_FILE, state) - if updates: - log.info("post recheck: withdrawing %d deleted post(s): %s", len(updates), - ", ".join(u["source_key"] for u in updates)) - log.info("Assessed publication: %s", assessed_events.publish(updates)) + updates = [] + for code, outcome in batch_result.items(): + # Recovery may contain sources that are no longer due. Its paid + # dataset is still consumed, but expired listings are left alone. + if code not in by_code: + continue + media_id = by_code[code] + key = f"instagram:post:{media_id}" + entry = state.get(media_id) or {} + if entry.get("run_id") == run["id"]: + continue + state[media_id] = {"status": outcome, "checked_at": iso(now), "run_id": run["id"]} + if outcome == "missing": + misses = entry.get("misses", 0) + 1 + state[media_id]["misses"] = misses + log.info("post %s (%s): reported deleted (%d/%d)", media_id, code, misses, CONFIRMATIONS) + if misses >= CONFIRMATIONS: + updates.append(withdrawal(key, registry[key], now)) + state[media_id]["status"] = "withdrawn" + if updates: + log.info("post recheck: withdrawing %d deleted post(s): %s", len(updates), + ", ".join(u["source_key"] for u in updates)) + log.info("Assessed publication: %s", assessed_events.publish(updates)) + withdrawn += len(updates) + # Persist each batch before marking its paid run consumed. If either + # write fails, replay retains the run ID and cannot double-count misses. + write_json(STATE_FILE, state) + write_json(details_file, {**read_json(details_file), "consumed": True}) log.info("post recheck: %d looked up, %d present, %d reported deleted, %d withdrawn", len(results), sum(v == "present" for v in results.values()), - sum(v == "missing" for v in results.values()), len(updates)) + sum(v == "missing" for v in results.values()), withdrawn) diff --git a/pipeline/tests/test_post_recheck.py b/pipeline/tests/test_post_recheck.py index d076c1e8..e58aa76d 100644 --- a/pipeline/tests/test_post_recheck.py +++ b/pipeline/tests/test_post_recheck.py @@ -2,15 +2,18 @@ from __future__ import annotations import sys +import tempfile import unittest from datetime import datetime, timedelta, timezone from pathlib import Path +from unittest.mock import MagicMock, patch PIPELINE_ROOT = Path(__file__).resolve().parents[1] if str(PIPELINE_ROOT) not in sys.path: sys.path.insert(0, str(PIPELINE_ROOT)) import post_recheck as pr # noqa: E402 +import apify_posts as apify # noqa: E402 NOW = datetime(2026, 10, 9, 12, tzinfo=timezone.utc) @@ -62,5 +65,219 @@ def test_publishes_nothing_and_marks_the_source_deleted(self): self.assertEqual("complete", update["assessment"]["status"]) +class RecheckIntegration(unittest.TestCase): + """Exercise real batching, paid-run recovery and durable check state.""" + + def setUp(self): + directory = self.enterContext(tempfile.TemporaryDirectory()) + self.state_file = Path(directory) / "state.json" + self.run_file = Path(directory) / "recheck.json" + self.details_file = Path(directory) / "recheck-details.json" + self.enterContext(patch.object(pr, "STATE_FILE", self.state_file)) + self.enterContext(patch.object(pr, "RUN_FILE", self.run_file)) + self.enterContext(patch.dict(pr.os.environ, {"APIFY_TOKEN": "test"})) + self.clock = self.enterContext(patch.object(pr, "datetime", wraps=datetime)) + self.clock.now.return_value = NOW + self.events = [] + self.registry = {} + self.codes = {} + self.database = MagicMock() + query = self.database.table.return_value.select.return_value.eq.return_value + query.gte.return_value.lte.return_value.execute.return_value.data = self.events + self.enterContext(patch("db.client", return_value=self.database)) + self.enterContext(patch("assessed_events.load_registry", return_value=self.registry)) + self.publish = self.enterContext(patch("assessed_events.publish")) + self.enterContext(patch.object(pr, "shortcodes", side_effect=lambda ids: { + key: self.codes[key] for key in ids if key in self.codes})) + self.purchases = [] + self.runs = {} + self.datasets = {} + self.requests = [] + self.response_rows = None + self.fail_purchase = None + self.fail_dataset = None + self.run_updates = {} + self.enterContext(patch.object(apify.ApifyClient, "request", new=self.request)) + + def add_posts(self, count, offset=0): + for i in range(offset, offset + count): + key = str(i) + self.codes[key] = f"POST_{i:03d}" + self.events.append(event(key, 2)) + self.registry[f"instagram:post:{key}"] = source(key) + + def request(self, method, path, **kwargs): + self.requests.append((method, path)) + if method == "POST": + if len(self.purchases) == self.fail_purchase: + raise TimeoutError("ambiguous paid start") + codes = [url.rsplit("/", 2)[1] for url in kwargs["json"]["posts"]] + run_id = str(len(self.purchases)) + self.purchases.append(codes) + self.datasets[run_id] = (self.response_rows if self.response_rows is not None else + [{"kind": "post", "data": {"code": code}} for code in codes]) + run = {"id": run_id, "actId": apify.HPIX_ACTOR_ID, "status": "SUCCEEDED", + "startedAt": NOW.isoformat(), "defaultDatasetId": run_id, + "options": {"maxTotalChargeUsd": kwargs["params"]["maxTotalChargeUsd"]}, + "chargedEventCounts": {"individual_post_scraped": len(codes), "apify-actor-start": 1}, + "pricingInfo": {"pricingPerEvent": {"actorChargeEvents": { + "individual_post_scraped": {"eventPriceUsd": .00149}, + "apify-actor-start": {"eventPriceUsd": .00005}}}}} + run.update(self.run_updates) + self.runs[run_id] = run + return {"data": run} + if path.startswith("actor-runs/"): + return {"data": self.runs[path.split("/")[1]]} + if path.startswith("datasets/"): + if path.split("/")[1] == self.fail_dataset: + raise RuntimeError("dataset temporarily unavailable") + return self.datasets[path.split("/")[1]] + raise AssertionError((method, path)) + + def test_distinct_batches_and_later_cycle_do_not_recheck_present_posts(self): + self.add_posts(60) + pr.main() + self.assertEqual([50, 10], [len(batch) for batch in self.purchases]) + self.assertEqual(60, len(set(code for batch in self.purchases for code in batch))) + self.assertEqual(60, len(pr.read_json(self.state_file))) + pr.main() + self.assertEqual(2, len(self.purchases)) + self.add_posts(1, offset=60) + pr.main() + self.assertEqual(["POST_060"], self.purchases[-1]) + + def test_completed_legacy_list_does_not_replace_new_work(self): + pr.write_json(self.details_file, {"consumed": True, "usernames": ["EXPIRED"]}) + self.add_posts(1) + pr.main() + self.assertEqual([["POST_000"]], self.purchases) + + def test_state_write_failure_replays_paid_run_without_another_purchase(self): + self.add_posts(1) + write = pr.write_json + def fail_state(path, value): + if path == self.state_file: + raise OSError("disk unavailable") + write(path, value) + with patch.object(pr, "write_json", side_effect=fail_state): + with self.assertRaises(OSError): + pr.main() + self.assertFalse(pr.read_json(self.details_file)["consumed"]) + pr.main() + self.assertEqual(1, len(self.purchases)) + self.assertEqual("present", pr.read_json(self.state_file)["0"]["status"]) + + def test_later_batch_failure_keeps_completed_checks(self): + self.add_posts(60) + self.fail_purchase = 1 + with self.assertRaises(TimeoutError): + pr.main() + self.assertEqual(50, len(pr.read_json(self.state_file))) + with self.assertRaisesRegex(RuntimeError, "outcome unknown"): + pr.main() + self.assertEqual(1, len(self.purchases)) + + def test_pending_paid_run_is_consumed_even_when_its_events_expired(self): + self.add_posts(1) + with patch.object(pr, "write_json", side_effect=OSError("disk unavailable")): + with self.assertRaises(OSError): + pr.main() + self.events.clear() + pr.main() + self.assertEqual(1, len(self.purchases)) + self.assertTrue(pr.read_json(self.details_file)["consumed"]) + + def test_second_batch_resumes_original_list_after_due_work_changes(self): + self.add_posts(60) + self.fail_dataset = "1" + with self.assertRaisesRegex(RuntimeError, "dataset temporarily unavailable"): + pr.main() + self.assertEqual(50, len(pr.read_json(self.state_file))) + self.add_posts(1, offset=60) + self.fail_dataset = None + pr.main() + self.assertEqual([50, 10, 1], [len(batch) for batch in self.purchases]) + self.assertEqual(["POST_060"], self.purchases[-1]) + self.assertEqual(61, len(pr.read_json(self.state_file))) + + def test_failed_or_charge_limited_run_is_not_consumed_or_rebought(self): + self.add_posts(60) + for changes in ({"status": "FAILED"}, {"options": {"maxTotalChargeUsd": .05}}): + with self.subTest(changes=changes): + self.run_updates = changes + if self.details_file.exists(): + self.details_file.unlink() + before = len(self.purchases) + with self.assertRaises(RuntimeError): + pr.main() + self.assertFalse(pr.read_json(self.details_file)["consumed"]) + with self.assertRaises(RuntimeError): + pr.main() + self.assertEqual(before + 1, len(self.purchases)) + self.assertFalse(self.state_file.exists()) + + def test_withdrawal_failure_replays_paid_results_before_marking_withdrawn(self): + self.add_posts(1) + pr.write_json(self.state_file, {"0": {"status": "missing", "misses": 1, + "checked_at": (NOW - timedelta(hours=8)).isoformat(), "run_id": "old"}}) + self.response_rows = [{"kind": "post", "input": "https://www.instagram.com/p/POST_000/", + "error": "Post deleted"}] + self.publish.side_effect = RuntimeError("database unavailable") + with self.assertRaisesRegex(RuntimeError, "database unavailable"): + pr.main() + self.assertEqual("missing", pr.read_json(self.state_file)["0"]["status"]) + self.assertFalse(pr.read_json(self.details_file)["consumed"]) + self.publish.side_effect = None + pr.main() + self.assertEqual(1, len(self.purchases)) + self.assertEqual("withdrawn", pr.read_json(self.state_file)["0"]["status"]) + + def test_matching_rate_limit_stays_unknown_without_withdrawing(self): + self.add_posts(1) + self.response_rows = [{"input": "https://www.instagram.com/p/POST_000/", "error": "Rate limited"}] + pr.main() + self.assertEqual({}, pr.read_json(self.state_file)) + self.assertTrue(pr.read_json(self.details_file)["consumed"]) + self.publish.assert_not_called() + + def test_unrelated_or_empty_output_stops_without_buying_more_batches(self): + self.add_posts(60) + for rows in ([{"kind": "post", "data": {"code": "UNRELATED"}}], []): + with self.subTest(rows=rows): + self.response_rows = rows + if self.details_file.exists(): + self.details_file.unlink() + before = len(self.purchases) + with self.assertRaisesRegex(RuntimeError, "output"): + pr.main() + self.assertEqual(before + 1, len(self.purchases)) + with self.assertRaisesRegex(RuntimeError, "output"): + pr.main() + self.assertEqual(before + 1, len(self.purchases)) + self.assertFalse(self.state_file.exists()) + + def test_missing_confirmation_is_not_counted_twice_after_consumption_write_failure(self): + self.add_posts(1) + self.response_rows = [{"kind": "post", "input": "https://www.instagram.com/p/POST_000/", + "error": "Post deleted"}] + write = pr.write_json + def fail_consumption(path, value): + if path == self.details_file and value.get("consumed"): + raise OSError("disk unavailable") + write(path, value) + with patch.object(pr, "write_json", side_effect=fail_consumption): + with self.assertRaises(OSError): + pr.main() + self.clock.now.return_value = NOW + timedelta(hours=8) + pr.main() + self.assertEqual(1, len(self.purchases)) + self.assertEqual(1, pr.read_json(self.state_file)["0"]["misses"]) + self.publish.assert_not_called() + pr.main() + self.assertEqual(2, len(self.purchases)) + self.assertEqual("withdrawn", pr.read_json(self.state_file)["0"]["status"]) + self.publish.assert_called_once() + + if __name__ == "__main__": unittest.main()