Enhance Docker and PowerShell scripts for improved functionality and maintainability
All checks were successful
Publish FaceAI Container / publish (push) Successful in 6m52s
- Updated Dockerfile to include default MySQL client for better database interaction. - Modified entrypoint.sh to support additional workspace for legacy applications and added MySQL readiness check before startup. - Enhanced PowerShell script for trimming MySQL dumps to include overlay dumps and improved error handling for missing race and user IDs. - Added new image files and face encoding pickles for various projects, ensuring comprehensive data availability. - Removed outdated face encoding pickle from PISA directory to maintain data relevance. Co-authored-by: Copilot <copilot@github.com>
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' })
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -10,3 +11,11 @@ fi
|
|||
|
||||
echo "Importing seed dump: ${seed_dump}"
|
||||
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
|
||||
|
|
@ -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}" <<SQL
|
||||
INSERT INTO parm (codice, descrizione, testo, numero, tipoParm, flgTipo, flgAdmin, createTmst, lastUpdTmst)
|
||||
VALUES
|
||||
|
|
@ -30,4 +36,79 @@ ON DUPLICATE KEY UPDATE
|
|||
flgTipo = VALUES(flgTipo),
|
||||
flgAdmin = VALUES(flgAdmin),
|
||||
lastUpdTmst = NOW();
|
||||
|
||||
INSERT INTO users (
|
||||
id_users,
|
||||
login,
|
||||
id_userProfile,
|
||||
pwd,
|
||||
flgValido,
|
||||
dataInizioVld,
|
||||
dataFineVld,
|
||||
nome,
|
||||
cognome,
|
||||
eMail,
|
||||
lang,
|
||||
nominativo,
|
||||
dataScadenza,
|
||||
dataCreazionePwd,
|
||||
dataScadenzaPwd,
|
||||
oldPwd,
|
||||
pwdCrypt,
|
||||
nFotoVisual,
|
||||
nFotoMax,
|
||||
createTmst
|
||||
)
|
||||
SELECT
|
||||
${LOCAL_TEST_USER_ID},
|
||||
'${LOCAL_TEST_USER_LOGIN}',
|
||||
COALESCE(
|
||||
(SELECT seed_profile.id_userProfile
|
||||
FROM (SELECT id_userProfile FROM users WHERE id_userProfile IS NOT NULL ORDER BY id_users LIMIT 1) AS seed_profile),
|
||||
9
|
||||
),
|
||||
${LOCAL_TEST_USER_HASH_SQL},
|
||||
'S',
|
||||
NULL,
|
||||
NULL,
|
||||
'Local',
|
||||
'Test User',
|
||||
'${LOCAL_TEST_USER_EMAIL}',
|
||||
'it',
|
||||
'Local Test User',
|
||||
'2030-12-31',
|
||||
CURRENT_DATE,
|
||||
'2030-12-31',
|
||||
CONCAT(${LOCAL_TEST_USER_HASH_SQL}, '|'),
|
||||
'${LOCAL_TEST_USER_PASSWORD}',
|
||||
0,
|
||||
500,
|
||||
NOW()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM users
|
||||
WHERE id_users = ${LOCAL_TEST_USER_ID}
|
||||
);
|
||||
|
||||
UPDATE users
|
||||
SET
|
||||
login = '${LOCAL_TEST_USER_LOGIN}',
|
||||
pwd = ${LOCAL_TEST_USER_HASH_SQL},
|
||||
flgValido = 'S',
|
||||
dataInizioVld = NULL,
|
||||
dataFineVld = NULL,
|
||||
nome = COALESCE(NULLIF(nome, ''), 'Local'),
|
||||
cognome = COALESCE(NULLIF(cognome, ''), 'Test User'),
|
||||
eMail = '${LOCAL_TEST_USER_EMAIL}',
|
||||
lang = COALESCE(NULLIF(lang, ''), 'it'),
|
||||
nominativo = 'Local Test User',
|
||||
dataScadenza = '2030-12-31',
|
||||
dataCreazionePwd = CURRENT_DATE,
|
||||
dataScadenzaPwd = '2030-12-31',
|
||||
oldPwd = CONCAT(${LOCAL_TEST_USER_HASH_SQL}, '|'),
|
||||
pwdCrypt = '${LOCAL_TEST_USER_PASSWORD}',
|
||||
nFotoVisual = COALESCE(nFotoVisual, 0),
|
||||
nFotoMax = COALESCE(NULLIF(nFotoMax, 0), 500),
|
||||
lastUpdTmst = NOW()
|
||||
WHERE id_users = ${LOCAL_TEST_USER_ID};
|
||||
SQL
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
FROM tomcat:9.0.102-jdk11-temurin
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates bash perl rsync \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates bash default-mysql-client perl rsync \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY tomcat/bootstrap-docbase.sh /usr/local/bin/bootstrap-docbase.sh
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export CATALINA_TMPDIR=/usr/local/tomcat/temp
|
|||
|
||||
runtime_root="/usr/local/tomcat/webapps/ROOT"
|
||||
workspace_www="/workspace/www"
|
||||
workspace_rus="/workspace/rus"
|
||||
|
||||
rm -rf "$runtime_root"
|
||||
mkdir -p "$runtime_root"
|
||||
|
|
@ -22,6 +23,11 @@ done
|
|||
mkdir -p "$runtime_root/WEB-INF"
|
||||
cp -a "$workspace_www/WEB-INF/." "$runtime_root/WEB-INF/"
|
||||
|
||||
if [ -d "$workspace_rus/WEB-INF/classes" ]; then
|
||||
mkdir -p "$runtime_root/WEB-INF/classes"
|
||||
cp -a "$workspace_rus/WEB-INF/classes/." "$runtime_root/WEB-INF/classes/"
|
||||
fi
|
||||
|
||||
mkdir -p /usr/local/tomcat/conf/Catalina/localhost
|
||||
cat > /usr/local/tomcat/conf/Catalina/localhost/ROOT.xml <<'EOF'
|
||||
<Context>
|
||||
|
|
@ -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
|
||||
|
|
@ -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,13 +47,10 @@ 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') {
|
||||
$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
|
||||
|
|
|
|||
BIN
test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/CKL_8589.JPG
Normal file
|
After Width: | Height: | Size: 378 KiB |
BIN
test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/CKL_8620.JPG
Normal file
|
After Width: | Height: | Size: 367 KiB |
BIN
test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/CKL_8649.JPG
Normal file
|
After Width: | Height: | Size: 299 KiB |
BIN
test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/CKL_8669.JPG
Normal file
|
After Width: | Height: | Size: 335 KiB |
BIN
test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/tn_CKL_8589.JPG
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/tn_CKL_8620.JPG
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/tn_CKL_8649.JPG
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
test_pkl/2026/04.APRILE/ISOLOTTO/00.PANORAMICA/tn_CKL_8669.JPG
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/CKL_8459.JPG
Normal file
|
After Width: | Height: | Size: 316 KiB |
BIN
test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/CKL_8460.JPG
Normal file
|
After Width: | Height: | Size: 400 KiB |
BIN
test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/CKL_8461.JPG
Normal file
|
After Width: | Height: | Size: 390 KiB |
BIN
test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/CKL_8462.JPG
Normal file
|
After Width: | Height: | Size: 350 KiB |
BIN
test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/tn_CKL_8459.JPG
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/tn_CKL_8460.JPG
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/tn_CKL_8461.JPG
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
test_pkl/2026/04.APRILE/ISOLOTTO/01.FOTO/tn_CKL_8462.JPG
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/LDR_0060.JPG
Normal file
|
After Width: | Height: | Size: 360 KiB |
BIN
test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/LDR_0097.JPG
Normal file
|
After Width: | Height: | Size: 419 KiB |
BIN
test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/LDR_0117.JPG
Normal file
|
After Width: | Height: | Size: 470 KiB |
BIN
test_pkl/2026/04.APRILE/LIONSSCATTA/00.PANORAMICA/LDR_0127.JPG
Normal file
|
After Width: | Height: | Size: 422 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 22 KiB |
BIN
test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/LDR_9920.JPG
Normal file
|
After Width: | Height: | Size: 460 KiB |
BIN
test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/LDR_9921.JPG
Normal file
|
After Width: | Height: | Size: 392 KiB |
BIN
test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/LDR_9922.JPG
Normal file
|
After Width: | Height: | Size: 390 KiB |
BIN
test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/LDR_9923.JPG
Normal file
|
After Width: | Height: | Size: 382 KiB |
BIN
test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/tn_LDR_9920.JPG
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/tn_LDR_9921.JPG
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/tn_LDR_9922.JPG
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
test_pkl/2026/04.APRILE/LIONSSCATTA/01_PROLOGO_A/tn_LDR_9923.JPG
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
test_pkl/2026/04.APRILE/LIVORNO/01.PANORAMICA/MTT20958.JPG
Normal file
|
After Width: | Height: | Size: 597 KiB |
BIN
test_pkl/2026/04.APRILE/LIVORNO/01.PANORAMICA/MTT20960.JPG
Normal file
|
After Width: | Height: | Size: 533 KiB |
BIN
test_pkl/2026/04.APRILE/LIVORNO/01.PANORAMICA/tn_MTT20958.JPG
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
test_pkl/2026/04.APRILE/LIVORNO/01.PANORAMICA/tn_MTT20960.JPG
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
test_pkl/2026/04.APRILE/LIVORNO/02.PROLOGO/MTT20741.JPG
Normal file
|
After Width: | Height: | Size: 489 KiB |
BIN
test_pkl/2026/04.APRILE/LIVORNO/02.PROLOGO/MTT20742.JPG
Normal file
|
After Width: | Height: | Size: 495 KiB |
BIN
test_pkl/2026/04.APRILE/LIVORNO/02.PROLOGO/tn_MTT20741.JPG
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
test_pkl/2026/04.APRILE/LIVORNO/02.PROLOGO/tn_MTT20742.JPG
Normal file
|
After Width: | Height: | Size: 30 KiB |