diff --git a/faceai/README.md b/faceai/README.md index 978be30c..d48daa2b 100644 --- a/faceai/README.md +++ b/faceai/README.md @@ -7,8 +7,7 @@ It includes: - a Vue frontend for the FaceAI upload and polling flow - a Node/Express backend for session exchange, queueing, and return handoff - a dedicated processor runner that consumes matcher jobs from Redis and executes `face_matcher` -- a local legacy simulator so the launch and return flow can be tested without the old Java site -- a Dockerized PHP Apache stack for exercising the real `www/faceai_handoff.php` and `www/faceai_return.php` bridge files +- a local Dockerized Tomcat/JSP stack so the launch and return flow can be tested against the real legacy race pages under `www/` ## Structure @@ -24,32 +23,38 @@ faceai/ ## Runtime Topology -The scaffold currently expects four runtime roles: +The production-oriented application topology is still three services: - `faceai`: public HTTP service on port `3001`, serving the built Vue app and the authenticated API - `processor`: background matcher runner consuming BullMQ jobs from Redis and executing the Linux `face_matcher` binary - `redis`: short-lived queue and search-state store -- `legacy-php`: local-only PHP Apache simulator for exercising the real bridge files under `www/` -For hosted deployment, the long-lived application topology is `faceai` + `processor` + `redis`. The PHP simulator stays local-only and the real legacy site remains on its existing stack. +The checked-in development override now expands that into the full local integration stack: + +- `tomcat-www`: local Tomcat runtime serving the real legacy JSP race pages from `www/` +- `mysql`: local legacy database used by the Tomcat stack +- `maildump`: local SMTP sink and viewer for the Tomcat stack + +That means the local FaceAI flow now runs against the same legacy page type the real site uses instead of a separate PHP bridge container. ## What The End-To-End Local Test Covers -The local simulator exercises the exact flow the plan is aiming for: +The local Tomcat stack exercises the exact flow the current integration is aiming for: 1. a legacy-like race page loads the original `www/_js/rus-ecom-240621.js` script and shows a `Face ID` button instead of `tipoPuntoFoto` -2. clicking it hits the real PHP handoff bridge at `www/faceai_handoff.php` +2. clicking it uses the handoff URL emitted by the real JSP race page and launches FaceAI through `http://localhost:3001/dev/legacy/launch` 3. the backend signs a short-lived handoff token and redirects to the Vue app 4. the Vue app exchanges the token for its own FaceAI session cookie 5. the user uploads a selfie and starts a Redis-backed race-scoped search 6. the frontend polls until the job completes 7. FaceAI requests a signed return URL -8. the browser is redirected back to the real PHP return bridge at `www/faceai_return.php` -9. the PHP bridge fetches the signed result from FaceAI and renders a filtered legacy-like race page +8. the browser is redirected back to the real Tomcat race page with `faceaiResultId` and `faceaiToken` +9. the legacy race-page JavaScript fetches the signed result from FaceAI, stores the hydrated match payload in browser storage, and reloads the cleaned race URL +10. the same legacy race page renders the FaceAI-filtered gallery view from that stored payload -## Local Testing With The Legacy PHP Simulator +## Local Testing With The Merged Legacy JSP Stack -This is the recommended local test path because it exercises the public site, the processor, Redis, and the real PHP bridge files together. +This is the recommended local test path because it exercises the public site, the processor, Redis, and the real legacy JSP page flow together. ### Prerequisites @@ -78,13 +83,15 @@ The local development stack started by the command above combines the base file - FaceAI public site on `http://localhost:3001` - processor runner on the internal Compose network - Redis on the internal Compose network -- PHP Apache serving `../www` on `http://localhost:8080` +- Tomcat serving the real local legacy site on `http://localhost:8080` +- MySQL for the local legacy stack +- Maildump for the local legacy stack on `http://localhost:8025` The local stack also mounts: - `../test_pkl` into both the public FaceAI container and the processor container as the shared read-only PKL dataset root - `./logs` into both the public FaceAI container and the processor container as the persistent diagnostics directory -- `../www` into the PHP container so the real bridge files are used +- `../www` into the Tomcat build/runtime inputs so the real legacy JSP pages and assets are used The `processor` service is built from `docker/processor.Dockerfile` using the repository root as Docker build context. That image copies only the checked-in Unix `face_matcher` into the image, so the matcher is baked into the processor runtime without bringing along the other Unix or Windows binaries. @@ -110,22 +117,23 @@ The current bundled Linux `face_matcher` binary is a PyInstaller build that requ Open: ```text -http://localhost:8080/faceai_simulator.php?raceId=202&lang=it +http://localhost:8080/Foto2.abl?id_gara=1018557&pageRow=96&pageNumber=1 ``` -That page simulates the legacy race page, loads the original race-page JavaScript from `www/_js/rus-ecom-240621.js`, lets the script replace the visible `tipoPuntoFoto` selector with the new `Face ID` button, and launches the real PHP handoff bridge at `www/faceai_handoff.php`. +That is the real local Tomcat race page. It loads the original race-page JavaScript from `www/_js/rus-ecom-240621.js`, lets the script replace the visible `tipoPuntoFoto` selector with the new `Face ID` button, and launches the backend handoff endpoint configured through the JSP page. ### Expected Local Flow Use the page above and verify this sequence: -1. the simulator page renders on port `8080` +1. the real local race page renders on port `8080` 2. the visible checkpoint selector is replaced with the `Face ID` launch button -3. clicking `Face ID` redirects through `faceai_handoff.php` into `http://localhost:3001/auth/callback?token=...` +3. clicking `Face ID` redirects through `http://localhost:3001/dev/legacy/launch` into `http://localhost:3001/auth/callback?token=...` 4. the FaceAI app establishes its session and loads the upload flow 5. uploading a selfie creates a queued search that the processor picks up -6. when polling completes, FaceAI redirects back to `http://localhost:8080/faceai_return.php?...` -7. the PHP return page renders the filtered photo list from the FaceAI result payload +6. when polling completes, FaceAI redirects back to the same race page with `faceaiResultId` and `faceaiToken` +7. the race-page JavaScript hydrates the result from FaceAI and reloads the cleaned legacy URL +8. the filtered photo list renders from the hydrated FaceAI result payload ### Rebuild Notes @@ -143,7 +151,7 @@ docker compose --env-file .env.development down ### Automated End-To-End Test -The workspace now includes a Playwright suite that drives the PHP simulator, the FaceAI app, and the processor end to end. +The workspace now includes a Playwright suite that drives the real local Tomcat race page, the FaceAI app, and the processor end to end. From this folder, run: @@ -157,10 +165,10 @@ The suite will: - build the frontend bundle - start `docker compose --env-file .env.development up --build -d` -- open `http://localhost:8080/faceai_simulator.php?raceId=202&lang=it` +- open `http://localhost:8080/Foto2.abl?id_gara=1018557&pageRow=96&pageNumber=1` - click the `Face ID` launch button injected by `www/_js/rus-ecom-240621.js` - upload `test_pkl/test_images/DSC_1960.JPG` -- wait for the processor to complete and for FaceAI to redirect to `faceai_return.php` +- wait for the processor to complete and for FaceAI to redirect back to the legacy race page - assert the filtered legacy result contains the expected `6` matches and includes `DSC_1960.JPG` - validate `faceai/logs/backend.log`, `faceai/logs/processor.log`, and the per-search `worker.log` and `matcher.log` for the run - stop the Compose stack automatically when the suite finishes @@ -238,7 +246,7 @@ If a running processor still reports `ENOENT`, the deployed image was built befo ## Optional Backend And Frontend Dev Loop -If you only want to iterate on the app without the PHP simulator, you can still run the public site and the processor separately. The queue-backed flow now requires Redis and the processor, so `npm run dev` alone is no longer the full stack. +If you only want to iterate on the app without the local Tomcat stack, you can still run the public site and the processor separately. The queue-backed flow now requires Redis and the processor, so `npm run dev` alone is no longer the full stack. One workable loop is: @@ -252,7 +260,7 @@ Then start the processor in a second shell, either with its own local environmen ## Docker Compose Deployment For The Public Site And Matcher Runner -The checked-in `docker-compose.yml` is now the production-ready base stack for hosted deployment. The checked-in `docker-compose.override.yml` is the development overlay that restores the local PHP simulator, workspace bind mounts, and development-oriented commands. +The checked-in `docker-compose.yml` is now the production-ready base stack for hosted deployment. The checked-in `docker-compose.override.yml` is the development overlay that restores the local Tomcat/JSP stack, workspace bind mounts, and development-oriented commands. Because Docker Compose auto-loads `docker-compose.override.yml` when it is present in the same directory, production-style runs from this workspace must explicitly select only the base file. @@ -367,8 +375,8 @@ After the Compose stack is up, validate at least the following: 2. The legacy handoff endpoint redirects to `https://faceai.../auth/callback?token=...`. 3. FaceAI can exchange the token and establish a session. 4. A search is enqueued in Redis and picked up by the processor. -5. Completing a search produces a redirect URL that points to `FACEAI_LEGACY_RETURN_URL`. -6. The legacy return endpoint can resolve the signed result and render the filtered race page. +5. Completing a search produces a redirect URL that points to the intended legacy return target. +6. The legacy page can resolve the signed result and render the filtered race page. ### Current Production Limitations @@ -411,9 +419,10 @@ NODE_ENV=development FACEAI_PORT=3001 FACEAI_FRONTEND_URL=http://localhost:3001 FACEAI_PUBLIC_BASE_URL=http://localhost:3001 -FACEAI_LEGACY_RETURN_URL=http://localhost:8080/faceai_return.php +FACEAI_LEGACY_RETURN_MODE=direct FACEAI_LEGACY_HOME_URL=http://localhost:8080/index.jsp FACEAI_FEATURE_ENABLED=1 +FACEAI_HANDOFF_URL=http://localhost:3001/dev/legacy/launch FACEAI_SHARED_SECRET=change-me FACEAI_SESSION_COOKIE=rus_faceai_session FACEAI_REDIS_URL=redis://redis:6379 @@ -425,12 +434,13 @@ FACEAI_PKL_ROOT=/data/pkl FACEAI_MATCHER_BINARY=/app/bin/face_matcher ``` -If you want FaceAI to return through the new PHP bridge prepared under `www`, point `FACEAI_LEGACY_RETURN_URL` to that endpoint instead, for example `http://localhost/faceai_return.php` or the equivalent URL in your local PHP setup. +If you want FaceAI to force the older bridge-style return path instead, set `FACEAI_LEGACY_RETURN_MODE=bridge` and point `FACEAI_LEGACY_RETURN_URL` at the appropriate legacy bridge endpoint. -In the development override, that wiring is already done with: +In the current development override, the local legacy handoff wiring is already done with: ```text -FACEAI_LEGACY_RETURN_URL=http://localhost:8080/faceai_return.php +FACEAI_HANDOFF_URL=http://localhost:3001/dev/legacy/launch +FACEAI_LEGACY_RETURN_MODE=direct FACEAI_LEGACY_HOME_URL=http://localhost:8080/index.jsp ``` @@ -438,18 +448,17 @@ The development override also keeps the log wiring with `./logs:/data/logs`, so The Compose contract now also includes an HTTP healthcheck on the public FaceAI service and a Redis readiness check. That makes `docker compose ps` meaningful during rollout: `faceai` only becomes healthy after `GET /health` returns `{"ok":true}`, and both the public site and the processor wait for Redis readiness before their own startup sequence begins. -The local PHP simulator also needs the legacy bridge feature flag enabled: +The local Tomcat stack also needs the legacy bridge feature flag enabled on the JSP side: ```text FACEAI_FEATURE_ENABLED=1 ``` -The checked-in `docker-compose.override.yml` sets that on the `legacy-php` service so the simulator can launch the FaceAI handoff flow locally. +The checked-in `docker-compose.override.yml` sets that on the local `tomcat-www` service so the race page can launch the FaceAI handoff flow locally. ## Notes - Search orchestration now uses Redis and a dedicated processor worker. - The checked-in base Compose file is production-oriented, while the checked-in override is development-only. -- The local legacy simulator is intentionally backend-driven so the handoff can be tested without compiling the existing Java application. -- `www/faceai_simulator.php` exists only for local testing. It does not replace the actual JSP race page. +- The local development flow now uses the actual Tomcat-served JSP race page instead of a separate PHP simulator bridge. - The final legacy integration still needs a real signed identity source and a real return-filter implementation on the old site. diff --git a/faceai/docker-compose.override.yml b/faceai/docker-compose.override.yml index 3705ce88..038f103f 100644 --- a/faceai/docker-compose.override.yml +++ b/faceai/docker-compose.override.yml @@ -86,11 +86,12 @@ services: FACEAI_DEV_DISPLAY_NAME: ${FACEAI_DEV_DISPLAY_NAME:-Local Model User} FACEAI_DEV_EMAIL: ${FACEAI_DEV_EMAIL:-local.model.user@example.invalid} FACEAI_DEV_MEMBERSHIP_STATUS: ${FACEAI_DEV_MEMBERSHIP_STATUS:-active} - LOCAL_APP_INSTANCE: ${LOCAL_APP_INSTANCE:-local-model} + LOCAL_APP_INSTANCE: ${LOCAL_APP_INSTANCE:-main} LOCAL_DOCBASE: /data/docbase/ FACEAI_FEATURE_ENABLED: ${FACEAI_FEATURE_ENABLED:-1} volumes: - ../www:/workspace/www:ro + - ../rus:/workspace/rus:ro - ../test_pkl:/workspace/test_pkl:ro - ../local-jsp-docker/runtime/docbase:/data/docbase - ../local-jsp-docker/runtime/tomcat-work:/usr/local/tomcat/work @@ -111,15 +112,21 @@ services: - --max_allowed_packet=1G - --net_read_timeout=600 - --net_write_timeout=600 + - --lower_case_table_names=1 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: pg MYSQL_ROOT_HOST: '%' - LOCAL_DB_SEED_DUMP: ${LOCAL_DB_SEED_DUMP:-pg-model-seed-trimmed-20260421.sql} + LOCAL_DB_SEED_DUMP: ${LOCAL_DB_SEED_DUMP:-pg-local-purpose-seed-20260422.sql} + LOCAL_DB_OVERLAY_DUMP: ${LOCAL_DB_OVERLAY_DUMP:-} LOCAL_DOCBASE: /data/docbase/ LOCAL_MAIL_SMTP_HOST: maildump LOCAL_MAIL_SMTP_PORT: 1025 LOCAL_SOURCE_DIR: /workspace/www/ + LOCAL_TEST_USER_ID: ${LOCAL_TEST_USER_ID:-2} + LOCAL_TEST_USER_LOGIN: ${LOCAL_TEST_USER_LOGIN:-test} + LOCAL_TEST_USER_PASSWORD: ${LOCAL_TEST_USER_PASSWORD:-test1} + LOCAL_TEST_USER_EMAIL: ${LOCAL_TEST_USER_EMAIL:-localtest@regalamiunsorriso.test} volumes: - mysql-data:/var/lib/mysql - ../db:/seed:ro @@ -128,11 +135,9 @@ services: healthcheck: test: - CMD - - mysqladmin - - ping - - -h - - 127.0.0.1 - - -proot + - sh + - -lc + - mysql -uroot -p"$$MYSQL_ROOT_PASSWORD" -Nse "SELECT CASE WHEN EXISTS (SELECT 1 FROM pg.parm WHERE codice = 'REWRITE_URL_ENABLE' AND numero = 1) AND EXISTS (SELECT 1 FROM pg.gara LIMIT 1) THEN 1 ELSE 0 END" | grep -qx 1 interval: 10s timeout: 5s retries: 30 diff --git a/faceai/tests/e2e/faceai-simulator.spec.js b/faceai/tests/e2e/faceai-simulator.spec.js index 52e59a8d..6610ec47 100644 --- a/faceai/tests/e2e/faceai-simulator.spec.js +++ b/faceai/tests/e2e/faceai-simulator.spec.js @@ -1,8 +1,10 @@ const { test, expect } = require('@playwright/test'); const { + ensureLocalAuthenticatedRacePage, EXPECTED_MATCH_COUNT, FACEAI_BASE_URL, LEGACY_RACE_ID, + SELFIE_NAME, buildHandoffUrl, buildSimulatorUrl, getSearchArtifacts, @@ -61,6 +63,7 @@ async function readLaunchUrlFromLegacyPage(page) { } async function startSearch(page, selfieName) { + const selfieLabel = selfieName.split(/[\\/]+/u).pop(); const createResponsePromise = page.waitForResponse((response) => { return response.url().includes('/api/searches') && response.request().method() === 'POST' @@ -68,7 +71,7 @@ async function startSearch(page, selfieName) { }); await page.locator('input[type="file"]').setInputFiles(getSelfiePath(selfieName)); - await expect(page.getByText(selfieName)).toBeVisible(); + await expect(page.getByText(selfieLabel)).toBeVisible(); await page.getByRole('button', { name: 'Avvia ricerca Face ID' }).click(); const createResponse = await createResponsePromise; @@ -165,40 +168,40 @@ test('runs the legacy Tomcat flow through FaceAI and returns to the filtered leg await launchFromSimulator(page, { raceId: LEGACY_RACE_ID, - raceSlug: 'livorno', - raceName: 'Livorno', - raceFolder: 'LIVORNO' + raceSlug: 'isolotto', + raceName: 'Festa sociale UP Isolotto', + raceFolder: 'ISOLOTTO' }); - const search = await startSearch(page, 'DSC_1960.JPG'); + const search = await startSearch(page, SELFIE_NAME); await waitForLegacyResult(page, LEGACY_RACE_ID, EXPECTED_MATCH_COUNT); await verifySearchLogs(search.id, { expectedMatchCount: EXPECTED_MATCH_COUNT, - expectedSelfieName: 'DSC_1960.JPG' + expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop() }); }); test('builds the legacy FaceAI handoff URL with the exact local race storage metadata', async ({ page }) => { await page.goto(buildSimulatorUrl({ raceId: LEGACY_RACE_ID, - raceSlug: 'livorno', - raceName: 'Livorno', + raceSlug: 'isolotto', + raceName: 'Festa sociale UP Isolotto', raceYear: '2026', raceMonthFolder: '04.APRILE', - raceFolder: 'LIVORNO' + raceFolder: 'ISOLOTTO' }), { waitUntil: 'domcontentloaded' }); await expect(page.locator('#faceAiRaceYear')).toHaveValue('2026'); await expect(page.locator('#faceAiRaceMonthFolder')).toHaveValue('04.APRILE'); - await expect(page.locator('#faceAiRaceFolder')).toHaveValue('LIVORNO'); + await expect(page.locator('#faceAiRaceFolder')).toHaveValue('ISOLOTTO'); const launchUrl = await readLaunchUrlFromLegacyPage(page); expect(launchUrl.searchParams.get('raceYear')).toBe('2026'); expect(launchUrl.searchParams.get('raceMonthFolder')).toBe('04.APRILE'); - expect(launchUrl.searchParams.get('raceFolder')).toBe('LIVORNO'); - expect(launchUrl.searchParams.get('raceStorageRelativeDir')).toBe('2026/04.APRILE/LIVORNO'); + expect(launchUrl.searchParams.get('raceFolder')).toBe('ISOLOTTO'); + expect(launchUrl.searchParams.get('raceStorageRelativeDir')).toBe('2026/04.APRILE/ISOLOTTO'); }); test('shows the unsupported-race message when the current race has no PKL data and lets the user go back', async ({ page }) => { @@ -300,6 +303,19 @@ test('rejects a not-logged-in user after clicking the Face ID button and sends t await expect(page.locator('#faceAiErrorModalMessage')).toContainText('Il servizio Face ID non e al momento disponibile. Riprova piu tardi.'); }); +test('authenticates with the seeded local user and lets that user browse and launch the Livorno race page', async ({ page }) => { + await ensureLocalAuthenticatedRacePage(page, { + raceId: '1018557', + raceName: 'VIVICITTA LIVORNO', + raceYear: '2026', + raceMonthFolder: '04.APRILE', + raceFolder: 'LIVORNO' + }); + + await page.locator('#faceaiLaunchButton').click(); + await waitForFaceAiHome(page); +}); + test('shows the no-face message and allows the user to return to the race page', async ({ page }) => { test.slow(); @@ -341,8 +357,8 @@ test('opens the file chooser when the user clicks the upload button', async ({ p await page.getByRole('button', { name: 'Scegli immagine' }).click(); const fileChooser = await fileChooserPromise; - await fileChooser.setFiles(getSelfiePath('DSC_1960.JPG')); - await expect(page.getByText('DSC_1960.JPG')).toBeVisible(); + await fileChooser.setFiles(getSelfiePath(SELFIE_NAME)); + await expect(page.getByText(SELFIE_NAME.split(/[\\/]+/u).pop())).toBeVisible(); await expect(page.getByRole('button', { name: 'Avvia ricerca Face ID' })).toBeEnabled(); }); @@ -364,7 +380,7 @@ test('lets the user retry with a valid photo after a no-face upload and then ret await expect(page.locator('.faceai-feedback')).toContainText('Nessun volto rilevato nella foto caricata'); await expect(page.locator('input[type="file"]')).toBeEnabled(); - const retrySearch = await startSearch(page, 'DSC_1960.JPG'); + const retrySearch = await startSearch(page, SELFIE_NAME); await waitForLegacyResult(page, '202', EXPECTED_MATCH_COUNT); await verifySearchLogs(noFaceSearch.id, { @@ -373,7 +389,7 @@ test('lets the user retry with a valid photo after a no-face upload and then ret }); await verifySearchLogs(retrySearch.id, { expectedMatchCount: EXPECTED_MATCH_COUNT, - expectedSelfieName: 'DSC_1960.JPG' + expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop() }); }); @@ -403,7 +419,7 @@ test('allows two users to process different photos at the same time', async ({ b ]); const [searchOne, searchTwo] = await Promise.all([ - startSearch(pages[0], 'DSC_1960.JPG'), + startSearch(pages[0], SELFIE_NAME), startSearch(pages[1], 'DSC_1987.JPG') ]); @@ -413,7 +429,7 @@ test('allows two users to process different photos at the same time', async ({ b ]); await Promise.all([ - verifySearchLogs(searchOne.id, { expectedSelfieName: 'DSC_1960.JPG' }), + verifySearchLogs(searchOne.id, { expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop() }), verifySearchLogs(searchTwo.id, { expectedSelfieName: 'DSC_1987.JPG' }) ]); } finally { @@ -436,7 +452,7 @@ test('queues the third user until a worker is free and then completes all three ]); const [searchOne, searchTwo, searchThree] = await Promise.all([ - startSearch(pages[0], 'DSC_1960.JPG'), + startSearch(pages[0], SELFIE_NAME), startSearch(pages[1], 'DSC_1987.JPG'), startSearch(pages[2], 'DSC_2058.JPG') ]); @@ -477,7 +493,7 @@ test('queues the third user until a worker is free and then completes all three await Promise.all(pages.map((page) => waitForLegacyResult(page, '202'))); await Promise.all([ - verifySearchLogs(searchOne.id, { expectedSelfieName: 'DSC_1960.JPG' }), + verifySearchLogs(searchOne.id, { expectedSelfieName: SELFIE_NAME.split(/[\\/]+/u).pop() }), verifySearchLogs(searchTwo.id, { expectedSelfieName: 'DSC_1987.JPG' }), verifySearchLogs(searchThree.id, { expectedSelfieName: 'DSC_2058.JPG' }) ]); diff --git a/faceai/tests/e2e/faceai-test-utils.js b/faceai/tests/e2e/faceai-test-utils.js index 08b25a7f..e5e9d5b7 100644 --- a/faceai/tests/e2e/faceai-test-utils.js +++ b/faceai/tests/e2e/faceai-test-utils.js @@ -7,12 +7,15 @@ const WORKSPACE_ROOT = path.resolve(ROOT_DIR, '..'); const LOG_ROOT = path.join(ROOT_DIR, 'logs'); const SEARCH_LOG_ROOT = path.join(LOG_ROOT, 'searches'); const FACEAI_BASE_URL = process.env.FACEAI_E2E_BASE_URL || 'http://127.0.0.1:3001'; -const SIMULATOR_URL = process.env.FACEAI_E2E_SIMULATOR_URL || 'http://127.0.0.1:8080/Foto2.abl?id_gara=1018557&pageRow=96&pageNumber=1'; +const SIMULATOR_URL = process.env.FACEAI_E2E_SIMULATOR_URL || 'http://127.0.0.1:8080/Foto2.abl?id_gara=1018547&pageRow=96&pageNumber=1'; const LEGACY_BASE_URL = process.env.FACEAI_E2E_LEGACY_BASE_URL || 'http://127.0.0.1:8080'; const LEGACY_HOME_URL = process.env.FACEAI_E2E_LEGACY_HOME_URL || `${LEGACY_BASE_URL}/index.jsp`; -const SELFIE_NAME = process.env.FACEAI_E2E_SELFIE || 'DSC_1960.JPG'; +const LEGACY_LOGIN_URL = process.env.FACEAI_E2E_LEGACY_LOGIN_URL || `${LEGACY_BASE_URL}/login_clienti.html`; +const LEGACY_USERNAME = process.env.FACEAI_E2E_LEGACY_USERNAME || 'test'; +const LEGACY_PASSWORD = process.env.FACEAI_E2E_LEGACY_PASSWORD || 'test1'; +const SELFIE_NAME = process.env.FACEAI_E2E_SELFIE || '2026/04.APRILE/ISOLOTTO/01.FOTO/CKL_8459.JPG'; const EXPECTED_MATCH_COUNT = Number(process.env.FACEAI_E2E_EXPECTED_MATCH_COUNT || '6'); -const LEGACY_RACE_ID = process.env.FACEAI_E2E_RACE_ID || '1018557'; +const LEGACY_RACE_ID = process.env.FACEAI_E2E_RACE_ID || '1018547'; function quoteShellArg(value) { if (!/[\s"]/u.test(value)) { @@ -114,17 +117,22 @@ async function waitForHttp(url, validate, timeoutMs = 3 * 60 * 1000) { } function getSelfiePath(fileName = SELFIE_NAME) { + const relativeSegments = fileName.split(/[\\/]+/u); + if (relativeSegments.length > 1) { + return path.join(WORKSPACE_ROOT, 'test_pkl', ...relativeSegments); + } + return path.join(WORKSPACE_ROOT, 'test_pkl', 'test_images', fileName); } function buildSimulatorUrl({ raceId = LEGACY_RACE_ID, lang = 'it', - raceSlug = 'livorno', - raceName = 'Livorno', + raceSlug = 'isolotto', + raceName = 'Festa sociale UP Isolotto', raceYear = '2026', raceMonthFolder = '04.APRILE', - raceFolder = 'LIVORNO', + raceFolder = 'ISOLOTTO', pageRow = '96', pageNumber = '1' } = {}) { @@ -139,11 +147,11 @@ function buildSimulatorUrl({ function buildHandoffUrl({ raceId = LEGACY_RACE_ID, lang = 'it', - raceSlug = 'livorno', - raceName = 'Livorno', + raceSlug = 'isolotto', + raceName = 'Festa sociale UP Isolotto', raceYear = '2026', raceMonthFolder = '04.APRILE', - raceFolder = 'LIVORNO', + raceFolder = 'ISOLOTTO', userId = '1', displayName = `Local Test User ${userId}`, email = `local-test-${userId}@example.invalid`, @@ -166,6 +174,86 @@ function buildHandoffUrl({ return url.toString(); } +function buildLegacyLoginFormData() { + if (!LEGACY_USERNAME || !LEGACY_PASSWORD) { + throw new Error('FACEAI_E2E_LEGACY_USERNAME and FACEAI_E2E_LEGACY_PASSWORD must be set before running authenticated local E2E checks.'); + } + + return { + login: LEGACY_USERNAME, + pwd: LEGACY_PASSWORD, + cmdIU: 'check', + act: '', + thePage: '' + }; +} + +async function performLocalLoginRequest(requestContext) { + const response = await requestContext.post(`${LEGACY_BASE_URL}/Logon.abl`, { + form: buildLegacyLoginFormData(), + failOnStatusCode: false + }); + + const finalUrl = response.url(); + const bodyText = await response.text(); + + if (!response.ok()) { + throw new Error(`Local login request failed with HTTP ${response.status()} at ${finalUrl}`); + } + + if (/login_clienti|Username \/ Email|Password/iu.test(bodyText) && !/user_logout|dettaglio_clienti|Il mio account/iu.test(bodyText)) { + throw new Error(`Local login request appears to have remained on the login page at ${finalUrl}`); + } +} + +async function expectLocalRacePageLoaded(page) { + await page.waitForSelector('form[onsubmit="return searching()"]', { state: 'visible' }); + const raceId = await page.locator('#id_gara').inputValue(); + if (!/\d+/u.test(raceId)) { + throw new Error(`Expected the local race page to expose a numeric race id, got: ${raceId}`); + } + + await page.waitForSelector('#faceaiLaunchButton', { state: 'visible' }); + await page.waitForFunction(() => { + return document.querySelectorAll('a[data-faceai-photo-id] img.thumb').length > 0; + }, null, { + timeout: 30 * 1000 + }); +} + +async function ensureLocalAuthenticatedRacePage(page, options = {}) { + await page.goto(LEGACY_LOGIN_URL, { waitUntil: 'domcontentloaded' }); + await page.locator('#login').fill(LEGACY_USERNAME); + await page.locator('#pwd').fill(LEGACY_PASSWORD); + + const submitLocator = page.locator('input[type="submit"], button[type="submit"], a.btn').filter({ hasText: /Accedi|Sign in/i }).first(); + if (await submitLocator.count()) { + await Promise.all([ + page.waitForNavigation({ waitUntil: 'domcontentloaded' }), + submitLocator.click() + ]); + } else { + await Promise.all([ + page.waitForNavigation({ waitUntil: 'domcontentloaded' }), + page.evaluate(() => { + const form = document.querySelector('form[action="Logon.abl"]'); + if (!form) { + throw new Error('Local login page did not expose a Logon.abl form.'); + } + + const cmdIUField = form.querySelector('[name="cmdIU"]'); + if (cmdIUField) { + cmdIUField.value = 'check'; + } + form.submit(); + }) + ]); + } + + await page.goto(buildSimulatorUrl(options), { waitUntil: 'domcontentloaded' }); + await expectLocalRacePageLoaded(page); +} + function getSearchArtifacts(searchId) { const searchRoot = path.join(SEARCH_LOG_ROOT, searchId); return { @@ -188,15 +276,22 @@ module.exports = { FACEAI_BASE_URL, LEGACY_BASE_URL, LEGACY_HOME_URL, + LEGACY_LOGIN_URL, + LEGACY_PASSWORD, SIMULATOR_URL, SELFIE_NAME, EXPECTED_MATCH_COUNT, LEGACY_RACE_ID, + LEGACY_USERNAME, buildHandoffUrl, + buildLegacyLoginFormData, buildSimulatorUrl, dockerCompose, + ensureLocalAuthenticatedRacePage, + expectLocalRacePageLoaded, getSearchArtifacts, getSelfiePath, + performLocalLoginRequest, prepareHostState, readUtf8, runCommand, diff --git a/local-jsp-docker/README.md b/local-jsp-docker/README.md index dd8af521..f58d8559 100644 --- a/local-jsp-docker/README.md +++ b/local-jsp-docker/README.md @@ -2,12 +2,12 @@ This stack boots the `www` Tomcat webapp locally, seeds MySQL from the checked-in `pg` dump, overrides environment-sensitive `PARM` rows, and captures outgoing email into files. -The default local model seed is `db/pg-model-seed-trimmed-20260421.sql`. +The default local model seed is `db/pg-local-purpose-seed-20260422.sql`. ## Services - `tomcat-www`: Tomcat 9 on Java 11 serving a runtime copy of `www` -- `mysql`: MySQL 8 with first-boot import of `db/pg-model-seed-trimmed-20260421.sql` by default +- `mysql`: MySQL 8 with first-boot import of `db/pg-local-purpose-seed-20260422.sql` by default - `maildump`: local SMTP sink writing `.eml`, `.txt`, and `.html` payloads to disk ## What Gets Mocked @@ -31,11 +31,15 @@ Mail output is written under `local-jsp-docker/runtime/maildump/out`. ## First Boot -The first MySQL start imports the file named by `LOCAL_DB_SEED_DUMP`, which defaults to `db/pg-model-seed-trimmed-20260421.sql`. +The first MySQL start imports the file named by `LOCAL_DB_SEED_DUMP`, which defaults to `db/pg-local-purpose-seed-20260422.sql`. -After the import finishes, the local override script updates `PARM` with local-safe values. +After the import finishes, the local override script updates `PARM` with local-safe values and forces a deterministic legacy login for local testing. -The Tomcat runtime also patches the deployed `WEB-INF/web.xml` so the `instance` context-param is `local-model` instead of `main`. That keeps the startup DB updater servlets from mutating the model database during local boot. +The default local test credential is `test` / `test1`. Override it with `LOCAL_TEST_USER_LOGIN`, `LOCAL_TEST_USER_PASSWORD`, and `LOCAL_TEST_USER_EMAIL` if needed. + +The local MySQL container initializes with `lower_case_table_names=1` because the legacy app issues uppercase table names such as `PARM` on Linux. If you change this setting or are coming from an older volume, rebuild from a fresh volume with `docker compose down -v` before starting again. + +The Tomcat runtime patches the deployed `WEB-INF/web.xml` from `LOCAL_APP_INSTANCE`, which now defaults to `main` so the legacy startup initialization path runs locally as it does on the site. You can still override `LOCAL_APP_INSTANCE` when you need to suppress that startup path for a targeted debug session. ## Restart Workflow diff --git a/local-jsp-docker/docker-compose.yml b/local-jsp-docker/docker-compose.yml index 58ccdd34..72a22160 100644 --- a/local-jsp-docker/docker-compose.yml +++ b/local-jsp-docker/docker-compose.yml @@ -16,7 +16,7 @@ services: FACEAI_DEV_DISPLAY_NAME: ${FACEAI_DEV_DISPLAY_NAME:-Local Model User} FACEAI_DEV_EMAIL: ${FACEAI_DEV_EMAIL:-local.model.user@example.invalid} FACEAI_DEV_MEMBERSHIP_STATUS: ${FACEAI_DEV_MEMBERSHIP_STATUS:-active} - LOCAL_APP_INSTANCE: ${LOCAL_APP_INSTANCE:-local-model} + LOCAL_APP_INSTANCE: ${LOCAL_APP_INSTANCE:-main} LOCAL_DOCBASE: /data/docbase/ FACEAI_FEATURE_ENABLED: ${FACEAI_FEATURE_ENABLED:-1} FACEAI_FRONTEND_URL: ${FACEAI_FRONTEND_URL:-http://localhost:3001} @@ -26,6 +26,7 @@ services: FACEAI_IDENTITY_COOKIE: ${FACEAI_IDENTITY_COOKIE:-rus_faceai_identity} volumes: - ../www:/workspace/www:ro + - ../rus:/workspace/rus:ro - ../test_pkl:/workspace/test_pkl:ro - ./runtime/docbase:/data/docbase - ./runtime/tomcat-work:/usr/local/tomcat/work @@ -46,15 +47,21 @@ services: - --max_allowed_packet=1G - --net_read_timeout=600 - --net_write_timeout=600 + - --lower_case_table_names=1 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: pg MYSQL_ROOT_HOST: '%' - LOCAL_DB_SEED_DUMP: ${LOCAL_DB_SEED_DUMP:-pg-model-seed-trimmed-20260421.sql} + LOCAL_DB_SEED_DUMP: ${LOCAL_DB_SEED_DUMP:-pg-local-purpose-seed-20260422.sql} + LOCAL_DB_OVERLAY_DUMP: ${LOCAL_DB_OVERLAY_DUMP:-} LOCAL_DOCBASE: /data/docbase/ LOCAL_MAIL_SMTP_HOST: maildump LOCAL_MAIL_SMTP_PORT: 1025 LOCAL_SOURCE_DIR: /workspace/www/ + LOCAL_TEST_USER_ID: ${LOCAL_TEST_USER_ID:-2} + LOCAL_TEST_USER_LOGIN: ${LOCAL_TEST_USER_LOGIN:-test} + LOCAL_TEST_USER_PASSWORD: ${LOCAL_TEST_USER_PASSWORD:-test1} + LOCAL_TEST_USER_EMAIL: ${LOCAL_TEST_USER_EMAIL:-localtest@regalamiunsorriso.test} volumes: - mysql-data:/var/lib/mysql - ../db:/seed:ro @@ -63,11 +70,9 @@ services: healthcheck: test: - CMD - - mysqladmin - - ping - - -h - - 127.0.0.1 - - -proot + - sh + - -lc + - mysql -uroot -p"$$MYSQL_ROOT_PASSWORD" -Nse "SELECT CASE WHEN EXISTS (SELECT 1 FROM pg.parm WHERE codice = 'REWRITE_URL_ENABLE' AND numero = 1) AND EXISTS (SELECT 1 FROM pg.gara LIMIT 1) THEN 1 ELSE 0 END" | grep -qx 1 interval: 10s timeout: 5s retries: 30 diff --git a/local-jsp-docker/mysql/init/10-import-pg.sh b/local-jsp-docker/mysql/init/10-import-pg.sh index 7b8d4404..93eb93db 100644 --- a/local-jsp-docker/mysql/init/10-import-pg.sh +++ b/local-jsp-docker/mysql/init/10-import-pg.sh @@ -1,7 +1,8 @@ #!/bin/sh set -eu -seed_dump="${LOCAL_DB_SEED_DUMP:-pg-model-seed-trimmed-20260421.sql}" +seed_dump="${LOCAL_DB_SEED_DUMP:-pg-local-purpose-seed-20260422.sql}" +overlay_dump="${LOCAL_DB_OVERLAY_DUMP:-}" if [ ! -f "/seed/${seed_dump}" ]; then echo "Seed dump not found: /seed/${seed_dump}" >&2 @@ -9,4 +10,12 @@ if [ ! -f "/seed/${seed_dump}" ]; then fi echo "Importing seed dump: ${seed_dump}" -mysql -uroot -p"${MYSQL_ROOT_PASSWORD}" "${MYSQL_DATABASE}" < "/seed/${seed_dump}" \ No newline at end of file +mysql -uroot -p"${MYSQL_ROOT_PASSWORD}" "${MYSQL_DATABASE}" < "/seed/${seed_dump}" + +if [ -n "${overlay_dump}" ] && [ -f "/seed/${overlay_dump}" ]; then + echo "Importing seed overlay: ${overlay_dump}" + mysql -uroot -p"${MYSQL_ROOT_PASSWORD}" "${MYSQL_DATABASE}" < "/seed/${overlay_dump}" +elif [ -n "${overlay_dump}" ]; then + echo "Seed overlay not found: /seed/${overlay_dump}" >&2 + exit 1 +fi \ No newline at end of file diff --git a/local-jsp-docker/mysql/init/20-local-overrides.sh b/local-jsp-docker/mysql/init/20-local-overrides.sh index 0022e23b..40240d70 100644 --- a/local-jsp-docker/mysql/init/20-local-overrides.sh +++ b/local-jsp-docker/mysql/init/20-local-overrides.sh @@ -1,6 +1,12 @@ #!/bin/sh set -eu +LOCAL_TEST_USER_ID="${LOCAL_TEST_USER_ID:-2}" +LOCAL_TEST_USER_LOGIN="${LOCAL_TEST_USER_LOGIN:-test}" +LOCAL_TEST_USER_PASSWORD="${LOCAL_TEST_USER_PASSWORD:-test1}" +LOCAL_TEST_USER_EMAIL="${LOCAL_TEST_USER_EMAIL:-localtest@regalamiunsorriso.test}" +LOCAL_TEST_USER_HASH_SQL="REPLACE(TO_BASE64(SHA2('${LOCAL_TEST_USER_PASSWORD}', 256)), '\\n', '')" + mysql -uroot -p"${MYSQL_ROOT_PASSWORD}" "${MYSQL_DATABASE}" < /usr/local/tomcat/conf/Catalina/localhost/ROOT.xml <<'EOF' @@ -31,6 +37,30 @@ EOF /usr/local/bin/bootstrap-docbase.sh +wait_for_mysql() { + local host="${LOCAL_DB_HOST:-mysql}" + local port="${LOCAL_DB_PORT:-3306}" + local database="${LOCAL_DB_NAME:-pg}" + local user="${LOCAL_DB_USER:-root}" + local password="${LOCAL_DB_PASSWORD:-root}" + local attempts=60 + + for ((attempt=1; attempt<=attempts; attempt++)); do + if mysql --protocol=TCP -h "$host" -P "$port" -u"$user" -p"$password" -D "$database" -Nse "SELECT 1 FROM parm WHERE codice = 'REWRITE_URL_ENABLE' LIMIT 1" >/dev/null 2>&1; then + echo "MySQL is ready for legacy app startup" + return 0 + fi + + echo "Waiting for MySQL readiness ($attempt/$attempts)" + sleep 2 + done + + echo "MySQL did not become ready for legacy app startup" >&2 + return 1 +} + +wait_for_mysql + patch_context_param() { local param_name="$1" local param_value="$2" @@ -42,6 +72,6 @@ patch_context_param database "//${LOCAL_DB_HOST:-mysql}/${LOCAL_DB_NAME:-pg}" patch_context_param catalog "${LOCAL_DB_NAME:-pg}" patch_context_param user "${LOCAL_DB_USER:-root}" patch_context_param password "${LOCAL_DB_PASSWORD:-root}" -patch_context_param instance "${LOCAL_APP_INSTANCE:-local-model}" +patch_context_param instance "${LOCAL_APP_INSTANCE:-main}" exec catalina.sh run \ No newline at end of file diff --git a/scripts/trim-dump-via-temp-mysql.ps1 b/scripts/trim-dump-via-temp-mysql.ps1 index 0ce78cef..818556a7 100644 --- a/scripts/trim-dump-via-temp-mysql.ps1 +++ b/scripts/trim-dump-via-temp-mysql.ps1 @@ -1,6 +1,9 @@ param( - [string]$SourceDump = (Join-Path $PSScriptRoot '..\db\dump-pg-202604211927.pretrim-backup.sql'), - [string]$OutputDump = (Join-Path $PSScriptRoot '..\db\dump-pg-202604211927.trimmed.sql'), + [string]$SourceDump = (Join-Path $PSScriptRoot '..\db\pg-model-seed-trimmed-20260421.sql'), + [string]$OverlayDump = (Join-Path $PSScriptRoot '..\db\pg-local-model-fixtures-overlay-20260422.sql'), + [string]$OutputDump = (Join-Path $PSScriptRoot '..\db\pg-local-purpose-seed-20260422.sql'), + [int[]]$KeepRaceIds = @(1018547, 1018557), + [int[]]$KeepUserIds = @(2), [string]$ContainerName = 'regalami-dump-trim-mysql-temp', [string]$VolumeName = 'regalami-dump-trim-mysql-temp-data', [string]$DatabaseName = 'pgtrim', @@ -44,12 +47,9 @@ function Wait-ForMysqlReady { ) for ($attempt = 0; $attempt -lt 180; $attempt++) { - $portOutput = & docker exec $Name mysql -N -B -uroot -p$Password -e "SELECT @@port;" 2>$null - if ($LASTEXITCODE -eq 0) { - $reportedPort = ($portOutput | Select-Object -First 1).Trim() - if ($reportedPort -eq '3306') { - return - } + $logs = & docker logs $Name 2>&1 + if ($LASTEXITCODE -eq 0 -and ($logs -join "`n") -match 'ready for connections.*port: 3306') { + return } Start-Sleep -Seconds 2 @@ -65,7 +65,7 @@ function Invoke-MysqlQuery { [switch]$SkipDatabase ) - $dockerArgs = @('exec', $ContainerName, 'mysql', '-N', '-B', '-uroot', "-p$RootPassword") + $dockerArgs = @('exec', '-e', "MYSQL_PWD=$RootPassword", $ContainerName, 'mysql', '-N', '-B', '-uroot') if (-not $SkipDatabase) { $dockerArgs += @('-D', $DatabaseName) } @@ -93,9 +93,18 @@ function Quote-Identifier { return '`' + $Name.Replace('`', '``') + '`' } -function Quote-SqlLiteral { - param([string]$Value) - return "'" + $Value.Replace("'", "''") + "'" +function ConvertTo-SqlIntList { + param( + [Parameter(Mandatory = $true)] + [int[]]$Values + ) + + $dedupedValues = $Values | Sort-Object -Unique + if (-not $dedupedValues -or $dedupedValues.Count -eq 0) { + throw 'At least one numeric keep id is required.' + } + + return ($dedupedValues | ForEach-Object { [string]$_ }) -join ',' } if (-not (Test-Path $SourceDump)) { @@ -105,32 +114,62 @@ if (-not (Test-Path $SourceDump)) { $dbDirectory = Split-Path -Parent (Resolve-Path $SourceDump).Path $sourceFileName = Split-Path -Leaf $SourceDump $outputFileName = Split-Path -Leaf $OutputDump +$overlayResolvedPath = $null +$overlayFileName = '' +$overlayDirectory = $dbDirectory +$overlayMount = @() +if ($OverlayDump -and (Test-Path $OverlayDump)) { + $overlayResolvedPath = (Resolve-Path $OverlayDump).Path + $overlayFileName = Split-Path -Leaf $overlayResolvedPath + $overlayDirectory = Split-Path -Parent $overlayResolvedPath + if ($overlayDirectory -ne $dbDirectory) { + $overlayMount = @('-v', "${overlayDirectory}:/workspace/overlay") + } +} $importErrorLogFileName = [System.IO.Path]::GetFileNameWithoutExtension($sourceFileName) + '.import-errors.log' $importErrorLogPath = Join-Path $dbDirectory $importErrorLogFileName $sourceSizeBytes = (Get-Item $SourceDump).Length +$keepRaceListSql = ConvertTo-SqlIntList -Values $KeepRaceIds +$keepUserListSql = ConvertTo-SqlIntList -Values $KeepUserIds Write-Host "Using source dump: $SourceDump" Write-Host "Source size: $sourceSizeBytes bytes" Write-Host "Import error log: $importErrorLogPath" +if ($overlayResolvedPath) { + Write-Host "Using overlay dump: $overlayResolvedPath" +} +Write-Host "KeepRaceIdsRequested=$keepRaceListSql" +Write-Host "KeepUserIdsRequested=$keepUserListSql" try { & docker rm -f $ContainerName 1>$null 2>$null & docker volume rm $VolumeName 1>$null 2>$null - Invoke-DockerCapture -DockerArgs @( + $dockerRunArgs = @( 'run', '-d', '--name', $ContainerName, '-e', "MYSQL_ROOT_PASSWORD=$RootPassword", + '-e', 'MYSQL_ROOT_HOST=%', '-v', "${dbDirectory}:/workspace/db", - '-v', "${VolumeName}:/var/lib/mysql", + '-v', "${VolumeName}:/var/lib/mysql" + ) + if ($overlayMount.Count -gt 0) { + $dockerRunArgs += $overlayMount + } + $dockerRunArgs += @( 'mysql:8.4', '--max_allowed_packet=1G', '--net_read_timeout=600', '--net_write_timeout=600' - ) | Out-Null + ) + + Invoke-DockerCapture -DockerArgs $dockerRunArgs | Out-Null + Write-Host 'Temporary MySQL container started.' Wait-ForMysqlReady -Name $ContainerName -Password $RootPassword + Write-Host 'Temporary MySQL is ready.' Invoke-MysqlQuery -SkipDatabase -Query "DROP DATABASE IF EXISTS $DatabaseName; CREATE DATABASE $DatabaseName CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;" | Out-Null + Write-Host 'Temporary database created.' Write-Host "Importing source dump into temporary MySQL database..." if (Test-Path $importErrorLogPath) { @@ -141,6 +180,23 @@ try { "mysql --force -uroot -p$RootPassword $DatabaseName < /workspace/db/$sourceFileName 2> /workspace/db/$importErrorLogFileName" ) + if ($overlayResolvedPath) { + $overlayContainerPath = if ($overlayDirectory -eq $dbDirectory) { + "/workspace/db/$overlayFileName" + } + else { + "/workspace/overlay/$overlayFileName" + } + + Write-Host "Importing overlay dump into temporary MySQL database..." + Invoke-DockerQuiet -DockerArgs @( + 'exec', $ContainerName, 'sh', '-lc', + "mysql --force -uroot -p$RootPassword $DatabaseName < $overlayContainerPath" + ) + } + + $beforeGara = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM gara;') + $beforePuntoFoto = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM punto_foto;') $beforeFoto = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM foto;') $beforeLogFoto = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM log_foto;') $usersExists = (Get-SqlScalar -SkipDatabase -Query "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '$DatabaseName' AND table_name = 'users';") -eq '1' @@ -149,45 +205,19 @@ try { $beforeUsers = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM users;') } - $garaDateColumn = Get-SqlScalar -SkipDatabase -Query @" -SELECT COLUMN_NAME -FROM information_schema.columns -WHERE table_schema = '$DatabaseName' - AND table_name = 'gara' - AND COLUMN_NAME IN ('dataGaraInizio', 'dataGaraFine', 'data', 'createTmst', 'lastUpdTmst') -ORDER BY FIELD(COLUMN_NAME, 'dataGaraInizio', 'dataGaraFine', 'data', 'createTmst', 'lastUpdTmst') -LIMIT 1; -"@ - if (-not $garaDateColumn) { - throw 'Could not determine race date column from gara table.' - } - - $usersDateColumn = '' - if ($usersExists) { - $usersDateColumn = Get-SqlScalar -SkipDatabase -Query @" -SELECT COLUMN_NAME -FROM information_schema.columns -WHERE table_schema = '$DatabaseName' - AND table_name = 'users' - AND COLUMN_NAME IN ('dataInserimento', 'dataInizioVld', 'lastUpdTmst') -ORDER BY FIELD(COLUMN_NAME, 'dataInserimento', 'dataInizioVld', 'lastUpdTmst') -LIMIT 1; -"@ - if (-not $usersDateColumn) { - $usersDateColumn = 'id_users' - } - } - - $garaDateExpr = Quote-Identifier $garaDateColumn - $usersDateExpr = Quote-Identifier $usersDateColumn - Invoke-MysqlQuery -Query @" DROP TABLE IF EXISTS keep_gara_ids; CREATE TABLE keep_gara_ids AS SELECT id_gara FROM gara -ORDER BY $garaDateExpr DESC, id_gara DESC -LIMIT 10; +WHERE id_gara IN ($keepRaceListSql) +ORDER BY FIELD(id_gara, $keepRaceListSql); + +DROP TABLE IF EXISTS keep_punto_foto_ids; +CREATE TABLE keep_punto_foto_ids AS +SELECT id_puntoFoto +FROM punto_foto +WHERE id_gara IN (SELECT id_gara FROM keep_gara_ids); DROP TABLE IF EXISTS keep_foto_ids; CREATE TABLE keep_foto_ids AS @@ -196,17 +226,50 @@ FROM foto WHERE id_gara IN (SELECT id_gara FROM keep_gara_ids); "@ | Out-Null + $keptGaraIds = Invoke-MysqlQuery -Query 'SELECT id_gara FROM keep_gara_ids ORDER BY id_gara;' + $missingRaceIds = @() + foreach ($requestedRaceId in ($KeepRaceIds | Sort-Object -Unique)) { + if (-not (($keptGaraIds | ForEach-Object { $_.Trim() }) -contains [string]$requestedRaceId)) { + $missingRaceIds += $requestedRaceId + } + } + if ($missingRaceIds.Count -gt 0) { + throw "One or more requested race ids are missing from the imported data: $($missingRaceIds -join ',')" + } + $cleanupStats = [System.Collections.Generic.List[string]]::new() Invoke-MysqlQuery -Query 'DELETE FROM log_foto;' | Out-Null $cleanupStats.Add('log_foto:deleted-all') + $garaDependentTables = Invoke-MysqlQuery -SkipDatabase -Query @" +SELECT table_name +FROM information_schema.columns +WHERE table_schema = '$DatabaseName' + AND column_name = 'id_gara' + AND table_name NOT IN ('gara', 'foto', 'punto_foto', 'keep_gara_ids', 'keep_punto_foto_ids', 'keep_foto_ids', 'keep_user_ids') +ORDER BY table_name; +"@ + + foreach ($tableName in $garaDependentTables) { + if (-not $tableName) { + continue + } + $trimmedTableName = $tableName.Trim() + $quotedTable = Quote-Identifier $trimmedTableName + $deletedRows = [int64](Get-SqlScalar -Query "SELECT COUNT(*) FROM $quotedTable WHERE id_gara IS NOT NULL AND id_gara NOT IN (SELECT id_gara FROM keep_gara_ids);") + if ($deletedRows -gt 0) { + Invoke-MysqlQuery -Query "DELETE FROM $quotedTable WHERE id_gara IS NOT NULL AND id_gara NOT IN (SELECT id_gara FROM keep_gara_ids);" | Out-Null + $cleanupStats.Add("$($trimmedTableName):$deletedRows") + } + } + $fotoDependentTables = Invoke-MysqlQuery -SkipDatabase -Query @" SELECT table_name FROM information_schema.columns WHERE table_schema = '$DatabaseName' AND column_name = 'id_foto' - AND table_name NOT IN ('foto', 'log_foto', 'keep_foto_ids', 'keep_gara_ids') + AND table_name NOT IN ('foto', 'log_foto', 'keep_foto_ids', 'keep_gara_ids', 'keep_punto_foto_ids', 'keep_user_ids') ORDER BY table_name; "@ @@ -229,22 +292,46 @@ ORDER BY table_name; $cleanupStats.Add("foto:$deletedFotoRows") } + $deletedPuntoFotoRows = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM punto_foto WHERE id_gara NOT IN (SELECT id_gara FROM keep_gara_ids);') + if ($deletedPuntoFotoRows -gt 0) { + Invoke-MysqlQuery -Query 'DELETE FROM punto_foto WHERE id_gara NOT IN (SELECT id_gara FROM keep_gara_ids);' | Out-Null + $cleanupStats.Add("punto_foto:$deletedPuntoFotoRows") + } + + $deletedGaraRows = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM gara WHERE id_gara NOT IN (SELECT id_gara FROM keep_gara_ids);') + if ($deletedGaraRows -gt 0) { + Invoke-MysqlQuery -Query 'DELETE FROM gara WHERE id_gara NOT IN (SELECT id_gara FROM keep_gara_ids);' | Out-Null + $cleanupStats.Add("gara:$deletedGaraRows") + } + + $keptUserIds = @() if ($usersExists) { Invoke-MysqlQuery -Query @" DROP TABLE IF EXISTS keep_user_ids; CREATE TABLE keep_user_ids AS SELECT id_users FROM users -ORDER BY $usersDateExpr ASC, id_users ASC -LIMIT 5; +WHERE id_users IN ($keepUserListSql) +ORDER BY FIELD(id_users, $keepUserListSql); "@ | Out-Null + $keptUserIds = Invoke-MysqlQuery -Query 'SELECT id_users FROM keep_user_ids ORDER BY id_users;' + $missingUserIds = @() + foreach ($requestedUserId in ($KeepUserIds | Sort-Object -Unique)) { + if (-not (($keptUserIds | ForEach-Object { $_.Trim() }) -contains [string]$requestedUserId)) { + $missingUserIds += $requestedUserId + } + } + if ($missingUserIds.Count -gt 0) { + throw "One or more requested user ids are missing from the imported data: $($missingUserIds -join ',')" + } + $usersDependentTables = Invoke-MysqlQuery -SkipDatabase -Query @" SELECT table_name FROM information_schema.columns WHERE table_schema = '$DatabaseName' AND column_name = 'id_users' - AND table_name NOT IN ('users', 'keep_user_ids') + AND table_name NOT IN ('users', 'keep_user_ids', 'keep_gara_ids', 'keep_punto_foto_ids', 'keep_foto_ids') ORDER BY table_name; "@ @@ -268,6 +355,8 @@ ORDER BY table_name; } } + $afterGara = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM gara;') + $afterPuntoFoto = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM punto_foto;') $afterFoto = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM foto;') $afterLogFoto = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM log_foto;') $afterUsers = 0 @@ -275,26 +364,28 @@ ORDER BY table_name; $afterUsers = [int64](Get-SqlScalar -Query 'SELECT COUNT(*) FROM users;') } - $keptGaraIds = Invoke-MysqlQuery -Query "SELECT id_gara FROM keep_gara_ids ORDER BY $garaDateExpr DESC, id_gara DESC;" - if (Test-Path $OutputDump) { Remove-Item $OutputDump -Force } - Write-Host "Exporting trimmed dump..." + Write-Host "Exporting curated dump..." Invoke-DockerQuiet -DockerArgs @( 'exec', $ContainerName, 'sh', '-lc', "mysqldump -uroot -p$RootPassword --default-character-set=utf8mb4 --single-transaction --routines --triggers --set-gtid-purged=OFF $DatabaseName > /workspace/db/$outputFileName" ) if (-not (Test-Path $OutputDump)) { - throw "Trimmed dump was not created: $OutputDump" + throw "Curated dump was not created: $OutputDump" } $outputSizeBytes = (Get-Item $OutputDump).Length Write-Host "OriginalSizeBytes=$sourceSizeBytes" - Write-Host "TrimmedSizeBytes=$outputSizeBytes" + Write-Host "CuratedSizeBytes=$outputSizeBytes" + Write-Host "BeforeGara=$beforeGara" + Write-Host "AfterGara=$afterGara" + Write-Host "BeforePuntoFoto=$beforePuntoFoto" + Write-Host "AfterPuntoFoto=$afterPuntoFoto" Write-Host "BeforeFoto=$beforeFoto" Write-Host "AfterFoto=$afterFoto" Write-Host "BeforeLogFoto=$beforeLogFoto" @@ -302,6 +393,9 @@ ORDER BY table_name; Write-Host "BeforeUsers=$beforeUsers" Write-Host "AfterUsers=$afterUsers" Write-Host ("KeptGaraIds=" + (($keptGaraIds | ForEach-Object { $_.Trim() }) -join ',')) + if ($usersExists) { + Write-Host ("KeptUserIds=" + (($keptUserIds | ForEach-Object { $_.Trim() }) -join ',')) + } Write-Host ("DependencyCleanup=" + (($cleanupStats | Sort-Object) -join ';')) if (Test-Path $importErrorLogPath) { $importErrorCount = (Get-Item $importErrorLogPath).Length diff --git a/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/CKL_8589.JPG b/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/CKL_8589.JPG new file mode 100644 index 00000000..10f4e665 Binary files /dev/null and b/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/CKL_8589.JPG differ diff --git a/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/CKL_8620.JPG b/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/CKL_8620.JPG new file mode 100644 index 00000000..bc63fe39 Binary files /dev/null and b/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/CKL_8620.JPG differ diff --git a/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/CKL_8649.JPG b/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/CKL_8649.JPG new file mode 100644 index 00000000..d33b6b93 Binary files /dev/null and b/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/CKL_8649.JPG differ diff --git a/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/CKL_8669.JPG b/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/CKL_8669.JPG new file mode 100644 index 00000000..e5918502 Binary files /dev/null and b/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/CKL_8669.JPG differ diff --git a/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/tn_CKL_8589.JPG b/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/tn_CKL_8589.JPG new file mode 100644 index 00000000..04952611 Binary files /dev/null and b/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/tn_CKL_8589.JPG differ diff --git a/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/tn_CKL_8620.JPG b/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/tn_CKL_8620.JPG new file mode 100644 index 00000000..3ea3acc9 Binary files /dev/null and b/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/tn_CKL_8620.JPG differ diff --git a/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/tn_CKL_8649.JPG b/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/tn_CKL_8649.JPG new file mode 100644 index 00000000..29907da7 Binary files /dev/null and b/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/tn_CKL_8649.JPG differ diff --git a/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/tn_CKL_8669.JPG b/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/tn_CKL_8669.JPG new file mode 100644 index 00000000..46b52aed Binary files /dev/null and b/test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/tn_CKL_8669.JPG differ diff --git a/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/CKL_8459.JPG b/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/CKL_8459.JPG new file mode 100644 index 00000000..bb5c2d46 Binary files /dev/null and b/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/CKL_8459.JPG differ diff --git a/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/CKL_8460.JPG b/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/CKL_8460.JPG new file mode 100644 index 00000000..31303316 Binary files /dev/null and b/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/CKL_8460.JPG differ diff --git a/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/CKL_8461.JPG b/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/CKL_8461.JPG new file mode 100644 index 00000000..981014d9 Binary files /dev/null and b/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/CKL_8461.JPG differ diff --git a/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/CKL_8462.JPG b/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/CKL_8462.JPG new file mode 100644 index 00000000..7791d090 Binary files /dev/null and b/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/CKL_8462.JPG differ diff --git a/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/tn_CKL_8459.JPG b/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/tn_CKL_8459.JPG new file mode 100644 index 00000000..76c8bda8 Binary files /dev/null and b/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/tn_CKL_8459.JPG differ diff --git a/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/tn_CKL_8460.JPG b/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/tn_CKL_8460.JPG new file mode 100644 index 00000000..66168295 Binary files /dev/null and b/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/tn_CKL_8460.JPG differ diff --git a/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/tn_CKL_8461.JPG b/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/tn_CKL_8461.JPG new file mode 100644 index 00000000..8e821d8c Binary files /dev/null and b/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/tn_CKL_8461.JPG differ diff --git a/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/tn_CKL_8462.JPG b/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/tn_CKL_8462.JPG new file mode 100644 index 00000000..b1da7c84 Binary files /dev/null and b/test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/tn_CKL_8462.JPG differ diff --git a/test_pkl/2026/04.APRILE/LUCCA/face_encodings_20260330_170155.pkl b/test_pkl/2026/04.APRILE/ISOLOTTO/face_encodings_20260330_170155.pkl similarity index 100% rename from test_pkl/2026/04.APRILE/LUCCA/face_encodings_20260330_170155.pkl rename to test_pkl/2026/04.APRILE/ISOLOTTO/face_encodings_20260330_170155.pkl diff --git a/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/LDR_0060.JPG b/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/LDR_0060.JPG new file mode 100644 index 00000000..592ee2f7 Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/LDR_0060.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/LDR_0097.JPG b/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/LDR_0097.JPG new file mode 100644 index 00000000..275e0edd Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/LDR_0097.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/LDR_0117.JPG b/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/LDR_0117.JPG new file mode 100644 index 00000000..db375db2 Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/LDR_0117.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/LDR_0127.JPG b/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/LDR_0127.JPG new file mode 100644 index 00000000..b7708d59 Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/LDR_0127.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/tn_LDR_0060.JPG b/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/tn_LDR_0060.JPG new file mode 100644 index 00000000..6ad9d4ea Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/tn_LDR_0060.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/tn_LDR_0097.JPG b/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/tn_LDR_0097.JPG new file mode 100644 index 00000000..70a31655 Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/tn_LDR_0097.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/tn_LDR_0117.JPG b/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/tn_LDR_0117.JPG new file mode 100644 index 00000000..dc94249c Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/tn_LDR_0117.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/tn_LDR_0127.JPG b/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/tn_LDR_0127.JPG new file mode 100644 index 00000000..2c3888ed Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/tn_LDR_0127.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/LDR_9920.JPG b/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/LDR_9920.JPG new file mode 100644 index 00000000..812656f4 Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/LDR_9920.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/LDR_9921.JPG b/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/LDR_9921.JPG new file mode 100644 index 00000000..8cfcda65 Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/LDR_9921.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/LDR_9922.JPG b/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/LDR_9922.JPG new file mode 100644 index 00000000..a3f918b7 Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/LDR_9922.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/LDR_9923.JPG b/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/LDR_9923.JPG new file mode 100644 index 00000000..28ac3910 Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/LDR_9923.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/tn_LDR_9920.JPG b/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/tn_LDR_9920.JPG new file mode 100644 index 00000000..4c993397 Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/tn_LDR_9920.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/tn_LDR_9921.JPG b/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/tn_LDR_9921.JPG new file mode 100644 index 00000000..cd33cd91 Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/tn_LDR_9921.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/tn_LDR_9922.JPG b/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/tn_LDR_9922.JPG new file mode 100644 index 00000000..cd96cb5c Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/tn_LDR_9922.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/tn_LDR_9923.JPG b/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/tn_LDR_9923.JPG new file mode 100644 index 00000000..9c216d18 Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/tn_LDR_9923.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIONSSCATTA/face_encodings_20260419_171639_LIONSSCATTA.pkl b/test_pkl/2026/04.APRILE/LIONSSCATTA/face_encodings_20260419_171639_LIONSSCATTA.pkl new file mode 100644 index 00000000..283dfaf5 Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIONSSCATTA/face_encodings_20260419_171639_LIONSSCATTA.pkl differ diff --git a/test_pkl/2026/04.APRILE/LIVORNO/01.PANORAMICA/MTT20958.JPG b/test_pkl/2026/04.APRILE/LIVORNO/01.PANORAMICA/MTT20958.JPG new file mode 100644 index 00000000..be55279c Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIVORNO/01.PANORAMICA/MTT20958.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIVORNO/01.PANORAMICA/MTT20960.JPG b/test_pkl/2026/04.APRILE/LIVORNO/01.PANORAMICA/MTT20960.JPG new file mode 100644 index 00000000..43ed6fc2 Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIVORNO/01.PANORAMICA/MTT20960.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIVORNO/01.PANORAMICA/tn_MTT20958.JPG b/test_pkl/2026/04.APRILE/LIVORNO/01.PANORAMICA/tn_MTT20958.JPG new file mode 100644 index 00000000..50613f62 Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIVORNO/01.PANORAMICA/tn_MTT20958.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIVORNO/01.PANORAMICA/tn_MTT20960.JPG b/test_pkl/2026/04.APRILE/LIVORNO/01.PANORAMICA/tn_MTT20960.JPG new file mode 100644 index 00000000..91960c88 Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIVORNO/01.PANORAMICA/tn_MTT20960.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIVORNO/02.PROLOGO/MTT20741.JPG b/test_pkl/2026/04.APRILE/LIVORNO/02.PROLOGO/MTT20741.JPG new file mode 100644 index 00000000..178e93b8 Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIVORNO/02.PROLOGO/MTT20741.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIVORNO/02.PROLOGO/MTT20742.JPG b/test_pkl/2026/04.APRILE/LIVORNO/02.PROLOGO/MTT20742.JPG new file mode 100644 index 00000000..9be4bb16 Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIVORNO/02.PROLOGO/MTT20742.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIVORNO/02.PROLOGO/tn_MTT20741.JPG b/test_pkl/2026/04.APRILE/LIVORNO/02.PROLOGO/tn_MTT20741.JPG new file mode 100644 index 00000000..99a91b62 Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIVORNO/02.PROLOGO/tn_MTT20741.JPG differ diff --git a/test_pkl/2026/04.APRILE/LIVORNO/02.PROLOGO/tn_MTT20742.JPG b/test_pkl/2026/04.APRILE/LIVORNO/02.PROLOGO/tn_MTT20742.JPG new file mode 100644 index 00000000..e26461a5 Binary files /dev/null and b/test_pkl/2026/04.APRILE/LIVORNO/02.PROLOGO/tn_MTT20742.JPG differ diff --git a/test_pkl/2026/04.APRILE/PISA/face_encodings_20260412_171808.pkl b/test_pkl/2026/04.APRILE/PISA/face_encodings_20260412_171808.pkl deleted file mode 100644 index 09628c3c..00000000 Binary files a/test_pkl/2026/04.APRILE/PISA/face_encodings_20260412_171808.pkl and /dev/null differ