feat: Enhance FaceAI functionality with storage management and update deployment instructions
All checks were successful
Publish FaceAI Container / publish (push) Successful in 5m45s
All checks were successful
Publish FaceAI Container / publish (push) Successful in 5m45s
This commit is contained in:
parent
c0d072c6ea
commit
23f811e465
14 changed files with 500 additions and 22 deletions
1
.github/copilot-instructions.md
vendored
1
.github/copilot-instructions.md
vendored
|
|
@ -11,6 +11,7 @@ Use this file only for rules that apply across the whole repository.
|
||||||
## Current Site-Specific Instructions
|
## Current Site-Specific Instructions
|
||||||
|
|
||||||
- `83.149.164.4` deployment and promotion rules live in `.github/instructions/regalamiunsorriso-83-149-164-4.instructions.md`.
|
- `83.149.164.4` deployment and promotion rules live in `.github/instructions/regalamiunsorriso-83-149-164-4.instructions.md`.
|
||||||
|
- `pve02docker.maddo.science` FaceAI Docker debugging and container-management rules live in `.github/instructions/regalamiunsorriso-faceai-pve02docker.instructions.md`.
|
||||||
|
|
||||||
## Deployment Documentation
|
## Deployment Documentation
|
||||||
|
|
||||||
|
|
|
||||||
162
.github/instructions/regalamiunsorriso-faceai-pve02docker.instructions.md
vendored
Normal file
162
.github/instructions/regalamiunsorriso-faceai-pve02docker.instructions.md
vendored
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
---
|
||||||
|
description: 'Use when: debugging, inspecting, or updating the FaceAI Docker deployment on root@pve02docker.maddo.science, especially for faceai/** and stacks/faceai.yml changes.'
|
||||||
|
applyTo: 'faceai/**, stacks/faceai.yml'
|
||||||
|
---
|
||||||
|
|
||||||
|
# Regalami Un Sorriso FaceAI Docker Host
|
||||||
|
|
||||||
|
Instructions in this file are specific to the FaceAI Docker deployment reachable as `root@pve02docker.maddo.science` through the preconfigured SSH tunnel and stored credentials.
|
||||||
|
|
||||||
|
## Host Access
|
||||||
|
|
||||||
|
- SSH target: `root@pve02docker.maddo.science`
|
||||||
|
- Plain `ssh root@pve02docker.maddo.science` works in the current environment.
|
||||||
|
- The remote shell is `/bin/bash`, not `tcsh`.
|
||||||
|
- Do not add manual tunnel, key, or credential flags unless the current workflow is revalidated.
|
||||||
|
|
||||||
|
## Preferred SSH Workflow
|
||||||
|
|
||||||
|
For routine inspection, use plain SSH:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ssh root@pve02docker.maddo.science
|
||||||
|
```
|
||||||
|
|
||||||
|
From PowerShell on Windows, prefer invoking the SSH binary directly instead of wrapping it in `cmd /c`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& 'C:\Windows\System32\OpenSSH\ssh.exe' 'root@pve02docker.maddo.science'
|
||||||
|
```
|
||||||
|
|
||||||
|
When you must run a single remote command non-interactively, pass the whole remote command as one SSH argument.
|
||||||
|
|
||||||
|
## Shell Behavior From PowerShell
|
||||||
|
|
||||||
|
- Prefer one remote command per SSH invocation when doing reconnaissance. Complex commands with pipes, grouped expressions, or escaped parentheses are more likely to break under PowerShell-to-SSH quoting.
|
||||||
|
- On Windows PowerShell, avoid `cmd /c "ssh ..."` wrappers for anything nontrivial. Nested quoting can collapse before SSH runs and spill later tokens into the local PowerShell session.
|
||||||
|
- Prefer the PowerShell call operator form `& 'C:\Windows\System32\OpenSSH\ssh.exe' ...` and pass the remote command as a single argument when you must stay non-interactive.
|
||||||
|
- If PowerShell shows the continuation prompt `? >`, the command was malformed locally before SSH executed it. Cancel it and rerun a simpler command instead of trying to answer the prompt.
|
||||||
|
- When running remote commands from PowerShell, quoting can break if the command contains both nested quotes and file paths with spaces.
|
||||||
|
- For read-only verification commands from PowerShell, prefer `ssh ... --% <remote command>` so the remote command is passed verbatim.
|
||||||
|
- If repeated SSH commands start cancelling or interleaving poorly in the same terminal, rerun them sequentially instead of in parallel.
|
||||||
|
- The remote shell is normal `bash`, so standard POSIX shell constructs usually work once they reach the host intact.
|
||||||
|
|
||||||
|
## Docker Runtime Facts
|
||||||
|
|
||||||
|
- The FaceAI deployment is currently managed by Docker Compose, not Docker Swarm.
|
||||||
|
- Compose project: `faceai`
|
||||||
|
- Compose working directory: `/data/compose/4`
|
||||||
|
- Compose file: `/data/compose/4/docker-compose.yml`
|
||||||
|
- Main containers:
|
||||||
|
- `regalami-faceai`
|
||||||
|
- `regalami-faceai-processor`
|
||||||
|
- `regalami-faceai-redis`
|
||||||
|
|
||||||
|
## Container Roles
|
||||||
|
|
||||||
|
- `regalami-faceai`: public HTTP application
|
||||||
|
- `regalami-faceai-processor`: background queue worker that runs the matcher jobs
|
||||||
|
- `regalami-faceai-redis`: Redis queue and state store
|
||||||
|
|
||||||
|
## Useful Runtime Paths
|
||||||
|
|
||||||
|
- Runtime data: `/mnt/storage/data/faceai/runtime`
|
||||||
|
- Persistent logs: `/mnt/storage/data/faceai/logs`
|
||||||
|
- Read-only PKL dataset: `/mnt/nas12/nas2/RUS`
|
||||||
|
- In-container runtime root: `/data/runtime`
|
||||||
|
- In-container log root: `/data/logs`
|
||||||
|
- In-container PKL root: `/data/pkl`
|
||||||
|
|
||||||
|
## Logs And Health Checks
|
||||||
|
|
||||||
|
Prefer Docker logs first for quick inspection:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs --tail 200 regalami-faceai
|
||||||
|
docker logs --tail 200 regalami-faceai-processor
|
||||||
|
docker logs --tail 200 regalami-faceai-redis
|
||||||
|
```
|
||||||
|
|
||||||
|
Persistent host logs are also available directly:
|
||||||
|
|
||||||
|
- `/mnt/storage/data/faceai/logs/backend.log`
|
||||||
|
- `/mnt/storage/data/faceai/logs/processor.log`
|
||||||
|
- `/mnt/storage/data/faceai/logs/searches/<searchId>/worker.log`
|
||||||
|
- `/mnt/storage/data/faceai/logs/searches/<searchId>/matcher.log`
|
||||||
|
|
||||||
|
Operational notes:
|
||||||
|
|
||||||
|
- `regalami-faceai` has shown `ECONNREFUSED` errors when Redis was not yet reachable, then recovered once Redis became healthy.
|
||||||
|
- `docker ps` health output is meaningful here because the public app has an HTTP healthcheck and Redis has a readiness check.
|
||||||
|
- If the public app is running but marked `unhealthy`, inspect both `docker logs regalami-faceai` and `/mnt/storage/data/faceai/logs/backend.log` before changing anything.
|
||||||
|
|
||||||
|
## Read-Only Debugging Commands
|
||||||
|
|
||||||
|
Use these patterns before considering any state-changing action:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'
|
||||||
|
docker compose -f /data/compose/4/docker-compose.yml ps
|
||||||
|
docker inspect regalami-faceai
|
||||||
|
docker inspect regalami-faceai-processor
|
||||||
|
docker inspect regalami-faceai-redis
|
||||||
|
```
|
||||||
|
|
||||||
|
When you need recent logs without attaching to a live stream:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs --tail 200 regalami-faceai
|
||||||
|
docker logs --tail 200 regalami-faceai-processor
|
||||||
|
docker logs --tail 200 regalami-faceai-redis
|
||||||
|
```
|
||||||
|
|
||||||
|
When you need file-backed diagnostics:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tail -n 200 /mnt/storage/data/faceai/logs/backend.log
|
||||||
|
tail -n 200 /mnt/storage/data/faceai/logs/processor.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updates And Container Management
|
||||||
|
|
||||||
|
If the user explicitly approves a rollout or image refresh, the Compose-managed update path is from `/data/compose/4`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /data/compose/4
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
After any approved update, verify with:
|
||||||
|
|
||||||
|
- `docker compose -f /data/compose/4/docker-compose.yml ps`
|
||||||
|
- `docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.RunningFor}}'`
|
||||||
|
- `docker logs --tail 200 regalami-faceai`
|
||||||
|
- `docker logs --tail 200 regalami-faceai-processor`
|
||||||
|
|
||||||
|
## Consent Rules
|
||||||
|
|
||||||
|
Read-only inspection may proceed without additional confirmation.
|
||||||
|
|
||||||
|
Any destructive or service-affecting action requires the user's express consent, and that consent must be collected through the `vscode_askQuestions` tool before running the command.
|
||||||
|
|
||||||
|
Treat all of the following as consent-gated actions:
|
||||||
|
|
||||||
|
- `docker compose pull`
|
||||||
|
- `docker compose up -d`
|
||||||
|
- `docker compose down`
|
||||||
|
- `docker restart`, `docker stop`, `docker kill`, `docker rm`
|
||||||
|
- `docker exec` commands that modify files, data, or runtime state
|
||||||
|
- deleting logs, runtime files, volumes, images, networks, or containers
|
||||||
|
- `docker system prune`, `docker volume prune`, and similar cleanup commands
|
||||||
|
- editing files under `/data/compose/4` or the mounted FaceAI host paths
|
||||||
|
|
||||||
|
Before any consent-gated action, ask a concise confirmation question that names the exact action and the affected services.
|
||||||
|
|
||||||
|
## Safety Boundaries
|
||||||
|
|
||||||
|
- Default to inspection first. Do not jump directly to restarts or updates when logs or status already explain the issue.
|
||||||
|
- Do not run cleanup commands opportunistically.
|
||||||
|
- Do not edit Compose files on the host unless the user explicitly asks for that change.
|
||||||
|
- Do not assume that `regalami-faceai` startup errors are frontend issues. Check Redis reachability, processor availability, and mounted log files first.
|
||||||
|
- If a command may produce a long interactive stream, prefer a bounded `docker logs --tail ...` read before following with a live stream.
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
const path = require('path');
|
||||||
const { test, expect } = require('@playwright/test');
|
const { test, expect } = require('@playwright/test');
|
||||||
const {
|
const {
|
||||||
LIVE_EXPECTED_RACE_STORAGE,
|
LIVE_EXPECTED_RACE_STORAGE,
|
||||||
|
|
@ -112,6 +113,16 @@ async function readVisibleLegacyPhotoIds(page) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function readFaceAiMatchState(page) {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
if (typeof getFaceAiMatchState === 'function') {
|
||||||
|
return getFaceAiMatchState();
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.faceAiMatchState || null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function waitForVisibleLegacyPhotoIds(page, expectedCount) {
|
async function waitForVisibleLegacyPhotoIds(page, expectedCount) {
|
||||||
await expect.poll(async () => {
|
await expect.poll(async () => {
|
||||||
const visiblePhotoIds = await readVisibleLegacyPhotoIds(page);
|
const visiblePhotoIds = await readVisibleLegacyPhotoIds(page);
|
||||||
|
|
@ -345,7 +356,7 @@ test('accepts the supplied portrait image for the live upload flow', async ({ pa
|
||||||
await expect(fileInput).toBeEnabled();
|
await expect(fileInput).toBeEnabled();
|
||||||
await fileInput.setInputFiles(LIVE_SITE_PORTRAIT_PATH);
|
await fileInput.setInputFiles(LIVE_SITE_PORTRAIT_PATH);
|
||||||
|
|
||||||
await expect(page.locator('.faceai-file-name')).toContainText('test_portrait_1.png');
|
await expect(page.locator('.faceai-file-name')).toContainText(path.basename(LIVE_SITE_PORTRAIT_PATH));
|
||||||
|
|
||||||
const searchResponsePromise = page.waitForResponse((response) => {
|
const searchResponsePromise = page.waitForResponse((response) => {
|
||||||
return response.request().method() === 'POST' && response.url().includes('/api/searches');
|
return response.request().method() === 'POST' && response.url().includes('/api/searches');
|
||||||
|
|
@ -376,15 +387,26 @@ test('accepts the supplied portrait image for the live upload flow', async ({ pa
|
||||||
await expect.poll(async () => page.url(), {
|
await expect.poll(async () => page.url(), {
|
||||||
timeout: 30 * 1000,
|
timeout: 30 * 1000,
|
||||||
message: 'Expected the browser to land on the legacy race page with FaceAI filter parameters after FaceAI completed.'
|
message: 'Expected the browser to land on the legacy race page with FaceAI filter parameters after FaceAI completed.'
|
||||||
}).toMatch(new RegExp(`^${escapeRegExp(LIVE_SITE_BASE_URL)}/.*faceaiPhotoIds=`));
|
}).toMatch(new RegExp(`^${escapeRegExp(LIVE_SITE_BASE_URL)}/.*(?:faceaiPhotoIds=|faceaiMatchStorageKey=)`));
|
||||||
|
|
||||||
await expect(page.locator('form[onsubmit="return searching()"]')).toBeVisible();
|
await expect(page.locator('form[onsubmit="return searching()"]')).toBeVisible();
|
||||||
await expect(page.locator('#faceAiFilterBanner')).toContainText(/Face ID filter active|Filtro Face ID attivo/i);
|
await expect(page.locator('#faceAiFilterBanner')).toContainText(/Face ID filter active|Filtro Face ID attivo/i);
|
||||||
await expect(page.locator('.gallery-card')).toHaveCount(0);
|
await expect(page.locator('.gallery-card')).toHaveCount(0);
|
||||||
|
|
||||||
const finalUrl = new URL(page.url());
|
const finalUrl = new URL(page.url());
|
||||||
const expectedPhotoIds = (finalUrl.searchParams.get('faceaiPhotoIds') || '').split(',').map((value) => value.trim()).filter(Boolean);
|
await expect.poll(async () => {
|
||||||
|
const matchState = await readFaceAiMatchState(page);
|
||||||
|
return Array.isArray(matchState && matchState.photoIds) ? matchState.photoIds.length : 0;
|
||||||
|
}, {
|
||||||
|
timeout: 30 * 1000,
|
||||||
|
message: 'Expected the legacy race page to resolve FaceAI match state after redirect.'
|
||||||
|
}).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const matchState = await readFaceAiMatchState(page);
|
||||||
|
|
||||||
|
const expectedPhotoIds = Array.isArray(matchState.photoIds) ? matchState.photoIds.map((value) => String(value || '').trim()).filter(Boolean) : [];
|
||||||
expect(expectedPhotoIds.length, 'Expected the final race page URL to include at least one FaceAI photo identifier.').toBeGreaterThan(0);
|
expect(expectedPhotoIds.length, 'Expected the final race page URL to include at least one FaceAI photo identifier.').toBeGreaterThan(0);
|
||||||
|
expect(Number(finalUrl.searchParams.get('faceaiMatchCount') || 0)).toBe(expectedPhotoIds.length);
|
||||||
|
|
||||||
for (const photoKey of expectedPhotoIds) {
|
for (const photoKey of expectedPhotoIds) {
|
||||||
const lookup = await lookupLivePhoto(page, photoKey);
|
const lookup = await lookupLivePhoto(page, photoKey);
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,15 @@ All files in this rollout are deployed from the current working tree.
|
||||||
|
|
||||||
## Updated Files
|
## Updated Files
|
||||||
|
|
||||||
- `www/fotoCR.jsp`
|
- `www/_inc_header.jsp`
|
||||||
- `www/fotoCR-en.jsp`
|
- `www/_inc_headerNoCr.jsp`
|
||||||
|
- `www/_inc_headerNoCr-en.jsp`
|
||||||
|
- `www/_inc_headerNoCrNews.jsp`
|
||||||
|
- `www/_inc_headerNoCrNews-en.jsp`
|
||||||
|
- `www/_js/rus-ecom-240621.js`
|
||||||
|
- `www/faceai_simulator_view.php`
|
||||||
|
- `www/faceai_config.php`
|
||||||
|
- `www/faceai_return.php`
|
||||||
|
|
||||||
## Excluded Files
|
## Excluded Files
|
||||||
|
|
||||||
|
|
@ -27,7 +34,7 @@ All files in this rollout are deployed from the current working tree.
|
||||||
- Remote host: `marco@83.149.164.4:410`
|
- Remote host: `marco@83.149.164.4:410`
|
||||||
- Remote staging path: `/home/marco/regalamiunsorriso/incoming/www`
|
- Remote staging path: `/home/marco/regalamiunsorriso/incoming/www`
|
||||||
- Remote live path: `/home/sites/regalamiunsorriso/www`
|
- Remote live path: `/home/sites/regalamiunsorriso/www`
|
||||||
- Total files in this manifest: `2`
|
- Total files in this manifest: `9`
|
||||||
|
|
||||||
## Transfer Method
|
## Transfer Method
|
||||||
|
|
||||||
|
|
|
||||||
BIN
test_pkl/test_images/photo_2026-04-20_12-10-59.jpg
Normal file
BIN
test_pkl/test_images/photo_2026-04-20_12-10-59.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
|
|
@ -43,7 +43,7 @@
|
||||||
<script src="vendor/jquery/jquery.min.js"></script>
|
<script src="vendor/jquery/jquery.min.js"></script>
|
||||||
<script src="vendor/popper/popper.min.js"></script>
|
<script src="vendor/popper/popper.min.js"></script>
|
||||||
<script src="admin/_V4/_js/_acxent.js"></script>
|
<script src="admin/_V4/_js/_acxent.js"></script>
|
||||||
<script src="_js/rus-ecom-240621.js"></script>
|
<script src="_js/rus-ecom-240621.js?v=20260420-faceai-2"></script>
|
||||||
<script src="addons/datepicker/js/bootstrap-datepicker.min.js"></script>
|
<script src="addons/datepicker/js/bootstrap-datepicker.min.js"></script>
|
||||||
<script src="addons/datepicker/locales/bootstrap-datepicker.it.min.js"></script>
|
<script src="addons/datepicker/locales/bootstrap-datepicker.it.min.js"></script>
|
||||||
<script src="vendor/bootstrap/js/bootstrap.min.js"></script>
|
<script src="vendor/bootstrap/js/bootstrap.min.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
<script src="vendor/jquery/jquery.min.js"></script>
|
<script src="vendor/jquery/jquery.min.js"></script>
|
||||||
<script src="vendor/popper/popper.min.js"></script>
|
<script src="vendor/popper/popper.min.js"></script>
|
||||||
<script src="admin/_V4/_js/_acxent.js"></script>
|
<script src="admin/_V4/_js/_acxent.js"></script>
|
||||||
<script src="_js/rus-ecom-240621.js"></script>
|
<script src="_js/rus-ecom-240621.js?v=20260420-faceai-2"></script>
|
||||||
<script src="addons/datepicker/js/bootstrap-datepicker.min.js"></script>
|
<script src="addons/datepicker/js/bootstrap-datepicker.min.js"></script>
|
||||||
<script src="addons/datepicker/locales/bootstrap-datepicker.it.min.js"></script>
|
<script src="addons/datepicker/locales/bootstrap-datepicker.it.min.js"></script>
|
||||||
<script src="vendor/bootstrap/js/bootstrap.min.js"></script>
|
<script src="vendor/bootstrap/js/bootstrap.min.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
<script src="vendor/jquery/jquery.min.js"></script>
|
<script src="vendor/jquery/jquery.min.js"></script>
|
||||||
<script src="vendor/popper/popper.min.js"></script>
|
<script src="vendor/popper/popper.min.js"></script>
|
||||||
<script src="admin/_V4/_js/_acxent.js"></script>
|
<script src="admin/_V4/_js/_acxent.js"></script>
|
||||||
<script src="_js/rus-ecom-240621.js"></script>
|
<script src="_js/rus-ecom-240621.js?v=20260420-faceai-2"></script>
|
||||||
<script src="addons/datepicker/js/bootstrap-datepicker.min.js"></script>
|
<script src="addons/datepicker/js/bootstrap-datepicker.min.js"></script>
|
||||||
<script src="addons/datepicker/locales/bootstrap-datepicker.it.min.js"></script>
|
<script src="addons/datepicker/locales/bootstrap-datepicker.it.min.js"></script>
|
||||||
<script src="vendor/bootstrap/js/bootstrap.min.js"></script>
|
<script src="vendor/bootstrap/js/bootstrap.min.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
|
|
||||||
<script src="vendor/popper/popper.min.js"></script>
|
<script src="vendor/popper/popper.min.js"></script>
|
||||||
<script src="admin/_V4/_js/_acxent.js"></script>
|
<script src="admin/_V4/_js/_acxent.js"></script>
|
||||||
<script src="_js/rus-ecom-240621.js"></script>
|
<script src="_js/rus-ecom-240621.js?v=20260420-faceai-2"></script>
|
||||||
<script src="addons/datepicker/js/bootstrap-datepicker.min.js"></script>
|
<script src="addons/datepicker/js/bootstrap-datepicker.min.js"></script>
|
||||||
<script src="addons/datepicker/locales/bootstrap-datepicker.it.min.js"></script>
|
<script src="addons/datepicker/locales/bootstrap-datepicker.it.min.js"></script>
|
||||||
<script src="vendor/bootstrap/js/bootstrap.min.js"></script>
|
<script src="vendor/bootstrap/js/bootstrap.min.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
|
|
||||||
<script src="vendor/popper/popper.min.js"></script>
|
<script src="vendor/popper/popper.min.js"></script>
|
||||||
<script src="admin/_V4/_js/_acxent.js"></script>
|
<script src="admin/_V4/_js/_acxent.js"></script>
|
||||||
<script src="_js/rus-ecom-240621.js"></script>
|
<script src="_js/rus-ecom-240621.js?v=20260420-faceai-2"></script>
|
||||||
<script src="addons/datepicker/js/bootstrap-datepicker.min.js"></script>
|
<script src="addons/datepicker/js/bootstrap-datepicker.min.js"></script>
|
||||||
<script src="addons/datepicker/locales/bootstrap-datepicker.it.min.js"></script>
|
<script src="addons/datepicker/locales/bootstrap-datepicker.it.min.js"></script>
|
||||||
<script src="vendor/bootstrap/js/bootstrap.min.js"></script>
|
<script src="vendor/bootstrap/js/bootstrap.min.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,197 @@ function clearFaceAiErrorState() {
|
||||||
window.history.replaceState({}, document.title, cleanUrl.pathname + cleanUrl.search + cleanUrl.hash);
|
window.history.replaceState({}, document.title, cleanUrl.pathname + cleanUrl.search + cleanUrl.hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getFaceAiMatchStorageEntryKey(storageKey) {
|
||||||
|
return "faceai-match-state:" + String(storageKey || "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFaceAiPendingMatchEntryKey() {
|
||||||
|
return "faceai-pending-match-state";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFaceAiPathForComparison(value) {
|
||||||
|
if (!value) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(String(value));
|
||||||
|
} catch (error) {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFaceAiCurrentRaceId() {
|
||||||
|
return String($("#id_gara").val() || "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFaceAiStoredPayloadFresh(storedAt, maxAgeMs) {
|
||||||
|
var timestamp = Date.parse(String(storedAt || ""));
|
||||||
|
|
||||||
|
if (!timestamp) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Date.now() - timestamp) <= maxAgeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canUseFaceAiWebStorage(storage) {
|
||||||
|
var testKey = "__faceai_storage_test__";
|
||||||
|
|
||||||
|
if (!storage) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
storage.setItem(testKey, "1");
|
||||||
|
storage.removeItem(testKey);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFaceAiStoredMatchPayload(storageKey) {
|
||||||
|
var entryKey = getFaceAiMatchStorageEntryKey(storageKey);
|
||||||
|
var rawPayload = "";
|
||||||
|
var windowNamePayload;
|
||||||
|
|
||||||
|
if (!storageKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canUseFaceAiWebStorage(window.sessionStorage)) {
|
||||||
|
rawPayload = window.sessionStorage.getItem(entryKey) || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rawPayload && canUseFaceAiWebStorage(window.localStorage)) {
|
||||||
|
rawPayload = window.localStorage.getItem(entryKey) || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rawPayload && window.name) {
|
||||||
|
try {
|
||||||
|
windowNamePayload = JSON.parse(window.name);
|
||||||
|
if (windowNamePayload && windowNamePayload.faceAiStorageKey === storageKey && windowNamePayload.faceAiMatchState) {
|
||||||
|
rawPayload = JSON.stringify(windowNamePayload.faceAiMatchState);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
rawPayload = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rawPayload) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(rawPayload);
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFaceAiPendingMatchPayload() {
|
||||||
|
var entryKey = getFaceAiPendingMatchEntryKey();
|
||||||
|
var rawPayload = "";
|
||||||
|
var parsedPayload = null;
|
||||||
|
var windowNamePayload;
|
||||||
|
var currentPath = getFaceAiPathForComparison(window.location.pathname || "");
|
||||||
|
var currentRaceId = getFaceAiCurrentRaceId();
|
||||||
|
|
||||||
|
if (canUseFaceAiWebStorage(window.sessionStorage)) {
|
||||||
|
rawPayload = window.sessionStorage.getItem(entryKey) || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rawPayload && canUseFaceAiWebStorage(window.localStorage)) {
|
||||||
|
rawPayload = window.localStorage.getItem(entryKey) || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rawPayload && window.name) {
|
||||||
|
try {
|
||||||
|
windowNamePayload = JSON.parse(window.name);
|
||||||
|
if (windowNamePayload && windowNamePayload.faceAiPendingMatchState) {
|
||||||
|
rawPayload = JSON.stringify(windowNamePayload.faceAiPendingMatchState);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
rawPayload = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rawPayload) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
parsedPayload = JSON.parse(rawPayload);
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsedPayload || !parsedPayload.payload || !parsedPayload.payload.photoIds || !parsedPayload.payload.photoIds.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFaceAiStoredPayloadFresh(parsedPayload.payload.storedAt, 15 * 60 * 1000)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(parsedPayload.raceId || "").trim() && currentRaceId && String(parsedPayload.raceId || "").trim() !== currentRaceId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!String(parsedPayload.raceId || "").trim() && getFaceAiPathForComparison(parsedPayload.targetPath || "") !== currentPath) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFaceAiStoredMatchPayload(storageKey) {
|
||||||
|
var entryKey = getFaceAiMatchStorageEntryKey(storageKey);
|
||||||
|
var pendingEntryKey = getFaceAiPendingMatchEntryKey();
|
||||||
|
var windowNamePayload;
|
||||||
|
var pendingPayload = readFaceAiPendingMatchPayload();
|
||||||
|
|
||||||
|
if (!storageKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canUseFaceAiWebStorage(window.sessionStorage)) {
|
||||||
|
window.sessionStorage.removeItem(entryKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canUseFaceAiWebStorage(window.localStorage)) {
|
||||||
|
window.localStorage.removeItem(entryKey);
|
||||||
|
if (pendingPayload && pendingPayload.storageKey === storageKey) {
|
||||||
|
window.localStorage.removeItem(pendingEntryKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canUseFaceAiWebStorage(window.sessionStorage) && pendingPayload && pendingPayload.storageKey === storageKey) {
|
||||||
|
window.sessionStorage.removeItem(pendingEntryKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
windowNamePayload = JSON.parse(window.name);
|
||||||
|
if (windowNamePayload && windowNamePayload.faceAiStorageKey === storageKey) {
|
||||||
|
delete windowNamePayload.faceAiStorageKey;
|
||||||
|
delete windowNamePayload.faceAiMatchState;
|
||||||
|
}
|
||||||
|
if (windowNamePayload && windowNamePayload.faceAiPendingMatchState && windowNamePayload.faceAiPendingMatchState.storageKey === storageKey) {
|
||||||
|
delete windowNamePayload.faceAiPendingMatchState;
|
||||||
|
}
|
||||||
|
if (!windowNamePayload || !Object.keys(windowNamePayload).length) {
|
||||||
|
window.name = "";
|
||||||
|
} else {
|
||||||
|
window.name = JSON.stringify(windowNamePayload);
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
|
||||||
function showFaceAiErrorModal(title, message) {
|
function showFaceAiErrorModal(title, message) {
|
||||||
var modal = $("#faceAiErrorModal");
|
var modal = $("#faceAiErrorModal");
|
||||||
|
|
||||||
|
|
@ -266,6 +457,7 @@ function stripFaceAiStateFromUrl(url) {
|
||||||
cleanUrl.searchParams.delete("faceaiMatchSource");
|
cleanUrl.searchParams.delete("faceaiMatchSource");
|
||||||
cleanUrl.searchParams.delete("faceaiMatchCount");
|
cleanUrl.searchParams.delete("faceaiMatchCount");
|
||||||
cleanUrl.searchParams.delete("faceaiPhotoIds");
|
cleanUrl.searchParams.delete("faceaiPhotoIds");
|
||||||
|
cleanUrl.searchParams.delete("faceaiMatchStorageKey");
|
||||||
return cleanUrl.toString();
|
return cleanUrl.toString();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return url || fallbackUrl;
|
return url || fallbackUrl;
|
||||||
|
|
@ -279,6 +471,26 @@ function getFaceAiMatchState() {
|
||||||
|
|
||||||
var params = new URLSearchParams(window.location.search || "");
|
var params = new URLSearchParams(window.location.search || "");
|
||||||
var rawIds = params.get("faceaiPhotoIds") || "";
|
var rawIds = params.get("faceaiPhotoIds") || "";
|
||||||
|
var storageKey = String(params.get("faceaiMatchStorageKey") || "").trim();
|
||||||
|
var storedPayload = null;
|
||||||
|
var pendingPayload = null;
|
||||||
|
|
||||||
|
if (!rawIds && storageKey) {
|
||||||
|
storedPayload = readFaceAiStoredMatchPayload(storageKey);
|
||||||
|
if (storedPayload && storedPayload.photoIds && storedPayload.photoIds.length) {
|
||||||
|
rawIds = storedPayload.photoIds.join(",");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rawIds) {
|
||||||
|
pendingPayload = readFaceAiPendingMatchPayload();
|
||||||
|
if (pendingPayload && pendingPayload.payload && pendingPayload.payload.photoIds && pendingPayload.payload.photoIds.length) {
|
||||||
|
rawIds = pendingPayload.payload.photoIds.join(",");
|
||||||
|
storageKey = storageKey || String(pendingPayload.storageKey || "").trim();
|
||||||
|
storedPayload = storedPayload || pendingPayload.payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!rawIds) {
|
if (!rawIds) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -303,7 +515,8 @@ function getFaceAiMatchState() {
|
||||||
return {
|
return {
|
||||||
photoIds: photoIds,
|
photoIds: photoIds,
|
||||||
photoIdSet: photoIdSet,
|
photoIdSet: photoIdSet,
|
||||||
matchCount: isNaN(parsedCount) ? photoIds.length : parsedCount,
|
matchCount: isNaN(parsedCount) ? ((storedPayload && storedPayload.matchCount) ? storedPayload.matchCount : photoIds.length) : parsedCount,
|
||||||
|
storageKey: storageKey,
|
||||||
clearUrl: stripFaceAiStateFromUrl(window.location.href)
|
clearUrl: stripFaceAiStateFromUrl(window.location.href)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -499,12 +712,17 @@ function renderFaceAiLegacyBanner(state) {
|
||||||
}
|
}
|
||||||
|
|
||||||
banner = $("#faceAiFilterBanner");
|
banner = $("#faceAiFilterBanner");
|
||||||
$(document).off("click.faceAiClearFilter", "#faceAiClearFilterButton");
|
|
||||||
$(document).on("click.faceAiClearFilter", "#faceAiClearFilterButton", function() {
|
|
||||||
window.location.href = state.clearUrl;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$(document).off("click.faceAiClearFilter", "#faceAiClearFilterButton");
|
||||||
|
$(document).on("click.faceAiClearFilter", "#faceAiClearFilterButton", function() {
|
||||||
|
var activeState = window.faceAiMatchState || state;
|
||||||
|
if (activeState && activeState.storageKey) {
|
||||||
|
clearFaceAiStoredMatchPayload(activeState.storageKey);
|
||||||
|
}
|
||||||
|
window.location.href = activeState && activeState.clearUrl ? activeState.clearUrl : state.clearUrl;
|
||||||
|
});
|
||||||
|
|
||||||
$("#faceAiFilterBannerTitle").text(copy.bannerTitle);
|
$("#faceAiFilterBannerTitle").text(copy.bannerTitle);
|
||||||
$("#faceAiFilterBannerText").html(message);
|
$("#faceAiFilterBannerText").html(message);
|
||||||
$("#faceAiClearFilterButton").text(copy.clearText);
|
$("#faceAiClearFilterButton").text(copy.clearText);
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,63 @@ function faceai_render_message_page($title, $message, array $details = array(),
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function faceai_render_storage_redirect_page($targetUrl, $storageKey, array $photoIds, $matchCount, $raceId = '')
|
||||||
|
{
|
||||||
|
http_response_code(200);
|
||||||
|
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||||
|
header('Pragma: no-cache');
|
||||||
|
header('Content-Type: text/html; charset=UTF-8');
|
||||||
|
|
||||||
|
$payload = array(
|
||||||
|
'photoIds' => array_values($photoIds),
|
||||||
|
'matchCount' => (int) $matchCount,
|
||||||
|
'storedAt' => gmdate('c')
|
||||||
|
);
|
||||||
|
$pendingPayload = array(
|
||||||
|
'storageKey' => (string) $storageKey,
|
||||||
|
'raceId' => trim((string) $raceId),
|
||||||
|
'targetPath' => (string) (parse_url((string) $targetUrl, PHP_URL_PATH) ?: ''),
|
||||||
|
'payload' => $payload
|
||||||
|
);
|
||||||
|
|
||||||
|
echo '<!doctype html><html lang="it"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">';
|
||||||
|
echo '<title>FaceAI redirect</title>';
|
||||||
|
echo '<style>body{font-family:Georgia,serif;background:#f7f1e8;color:#2a231b;margin:0;padding:32px}main{max-width:900px;margin:0 auto;background:#fff;border:1px solid #ddcbb5;padding:24px}p{margin:0 0 12px}</style>';
|
||||||
|
echo '</head><body><main><h1>Reindirizzamento FaceAI in corso</h1><p>Sto preparando i risultati e torno alla galleria della gara.</p><noscript><p>JavaScript e richiesto per completare il reindirizzamento FaceAI.</p></noscript>';
|
||||||
|
echo '<script>';
|
||||||
|
echo '(function () {';
|
||||||
|
echo 'var targetUrl = ' . json_encode((string) $targetUrl) . ';';
|
||||||
|
echo 'var storageKey = ' . json_encode((string) $storageKey) . ';';
|
||||||
|
echo 'var payload = ' . json_encode($payload) . ';';
|
||||||
|
echo 'var pendingPayload = ' . json_encode($pendingPayload) . ';';
|
||||||
|
echo 'var entryKey = "faceai-match-state:" + storageKey;';
|
||||||
|
echo 'var pendingEntryKey = "faceai-pending-match-state";';
|
||||||
|
echo 'var stored = false;';
|
||||||
|
echo 'function persist(storageName, key, value) {';
|
||||||
|
echo ' try {';
|
||||||
|
echo ' var storage = window[storageName];';
|
||||||
|
echo ' if (!storage) { return false; }';
|
||||||
|
echo ' storage.setItem(key, JSON.stringify(value));';
|
||||||
|
echo ' return storage.getItem(key) !== null;';
|
||||||
|
echo ' } catch (error) {';
|
||||||
|
echo ' return false;';
|
||||||
|
echo ' }';
|
||||||
|
echo '}';
|
||||||
|
echo 'stored = persist("sessionStorage", entryKey, payload) || persist("localStorage", entryKey, payload);';
|
||||||
|
echo 'stored = persist("sessionStorage", pendingEntryKey, pendingPayload) || persist("localStorage", pendingEntryKey, pendingPayload) || stored;';
|
||||||
|
echo 'try {';
|
||||||
|
echo ' window.name = JSON.stringify({ faceAiStorageKey: storageKey, faceAiMatchState: payload, faceAiPendingMatchState: pendingPayload });';
|
||||||
|
echo '} catch (error) {}';
|
||||||
|
echo 'if (!stored && !window.name) {';
|
||||||
|
echo ' document.body.innerHTML = "<main><h1>FaceAI non disponibile</h1><p>Il browser non permette di trasferire i risultati FaceAI verso la galleria.</p></main>";';
|
||||||
|
echo ' return;';
|
||||||
|
echo '}';
|
||||||
|
echo 'window.location.replace(targetUrl);';
|
||||||
|
echo '}());';
|
||||||
|
echo '</script></main></body></html>';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
function faceai_fetch_json($url)
|
function faceai_fetch_json($url)
|
||||||
{
|
{
|
||||||
$context = stream_context_create(array(
|
$context = stream_context_create(array(
|
||||||
|
|
|
||||||
|
|
@ -54,12 +54,23 @@ try {
|
||||||
$photoIds[$photoId] = true;
|
$photoIds[$photoId] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
header('Location: ' . faceai_build_url($returnUrl, array(
|
$matchCount = count($photoIds);
|
||||||
|
if ($matchCount === 0) {
|
||||||
|
header('Location: ' . faceai_build_url($returnUrl, array(
|
||||||
|
'faceaiMatchSource' => 'faceai',
|
||||||
|
'faceaiMatchCount' => 0
|
||||||
|
)), true, 302);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$storageKey = 'faceai-result-' . preg_replace('/[^a-zA-Z0-9_-]/', '', $resultId);
|
||||||
|
$redirectUrl = faceai_build_url($returnUrl, array(
|
||||||
'faceaiMatchSource' => 'faceai',
|
'faceaiMatchSource' => 'faceai',
|
||||||
'faceaiMatchCount' => count($photoIds),
|
'faceaiMatchCount' => $matchCount,
|
||||||
'faceaiPhotoIds' => implode(',', array_keys($photoIds))
|
'faceaiMatchStorageKey' => $storageKey
|
||||||
)), true, 302);
|
));
|
||||||
exit;
|
|
||||||
|
faceai_render_storage_redirect_page($redirectUrl, $storageKey, array_keys($photoIds), $matchCount, (string) ($result['raceId'] ?? ($payload['raceId'] ?? '')));
|
||||||
} catch (Throwable $error) {
|
} catch (Throwable $error) {
|
||||||
faceai_render_message_page('Errore return FaceAI', $error->getMessage(), array(), 500);
|
faceai_render_message_page('Errore return FaceAI', $error->getMessage(), array(), 500);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@ window.faceAiSimulator = {
|
||||||
<script src="vendor/jquery/jquery.min.js"></script>
|
<script src="vendor/jquery/jquery.min.js"></script>
|
||||||
<script src="vendor/popper/popper.min.js"></script>
|
<script src="vendor/popper/popper.min.js"></script>
|
||||||
<script src="vendor/bootstrap/js/bootstrap.min.js"></script>
|
<script src="vendor/bootstrap/js/bootstrap.min.js"></script>
|
||||||
<script src="_js/rus-ecom-240621.js"></script>
|
<script src="_js/rus-ecom-240621.js?v=20260420-faceai-2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
<?php
|
<?php
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue