feat: Enhance FaceAI functionality and improve login process
All checks were successful
Publish FaceAI Container / publish (push) Successful in 4m43s
All checks were successful
Publish FaceAI Container / publish (push) Successful in 4m43s
- Added a retry mechanism for page navigation in `live-site-test-utils.js` to handle transient errors during login. - Introduced a new function `performLiveLoginRequest` to handle login requests via API, improving the login flow. - Updated the login process to utilize the new API request method, ensuring a more robust authentication. - Implemented new utility functions in `rus-ecom-240621.js` for managing FaceAI state and filtering. - Created `faceai_photo_lookup.jsp` to handle photo lookups, returning JSON responses for better integration with the frontend. - Updated `faceai_return.php` to redirect users with appropriate parameters after FaceAI processing. - Modified `fotoCR.jsp` and `fotoCR-en.jsp` to include FaceAI photo IDs in the image elements for better tracking. - Enhanced the UI to display the number of matched photos dynamically based on FaceAI results.
This commit is contained in:
parent
6f191de115
commit
bba8026b7c
17 changed files with 1077 additions and 95 deletions
|
|
@ -28,7 +28,7 @@ export const config = {
|
||||||
uploadRoot: process.env.FACEAI_UPLOAD_ROOT || path.join(process.env.FACEAI_RUNTIME_ROOT || '/data/runtime', 'uploads'),
|
uploadRoot: process.env.FACEAI_UPLOAD_ROOT || path.join(process.env.FACEAI_RUNTIME_ROOT || '/data/runtime', 'uploads'),
|
||||||
searchTtlSeconds: Number(process.env.FACEAI_SEARCH_TTL_SECONDS || 24 * 60 * 60),
|
searchTtlSeconds: Number(process.env.FACEAI_SEARCH_TTL_SECONDS || 24 * 60 * 60),
|
||||||
resultTtlSeconds: Number(process.env.FACEAI_RESULT_TTL_SECONDS || 24 * 60 * 60),
|
resultTtlSeconds: Number(process.env.FACEAI_RESULT_TTL_SECONDS || 24 * 60 * 60),
|
||||||
processorHeartbeatGraceMs: Number(process.env.FACEAI_PROCESSOR_HEARTBEAT_GRACE_MS || 20 * 1000),
|
processorHeartbeatGraceMs: Number(process.env.FACEAI_PROCESSOR_HEARTBEAT_GRACE_MS || 60 * 1000),
|
||||||
rateLimitWindowSeconds: Number(process.env.FACEAI_RATE_LIMIT_WINDOW_SECONDS || 10 * 60),
|
rateLimitWindowSeconds: Number(process.env.FACEAI_RATE_LIMIT_WINDOW_SECONDS || 10 * 60),
|
||||||
rateLimitMaxRequests: Number(process.env.FACEAI_RATE_LIMIT_MAX_REQUESTS || 5)
|
rateLimitMaxRequests: Number(process.env.FACEAI_RATE_LIMIT_MAX_REQUESTS || 20)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
getSearchRecord,
|
getSearchRecord,
|
||||||
incrementRateLimit,
|
incrementRateLimit,
|
||||||
markSearchFailed,
|
markSearchFailed,
|
||||||
|
releaseActiveSearchLock,
|
||||||
saveSearchRecord
|
saveSearchRecord
|
||||||
} from './redis-store.js';
|
} from './redis-store.js';
|
||||||
import { getSearchQueue } from './queue.js';
|
import { getSearchQueue } from './queue.js';
|
||||||
|
|
@ -126,6 +127,27 @@ async function failSearchIfProcessorUnavailable(search) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveBlockingActiveSearch(userId) {
|
||||||
|
const activeSearchId = await getActiveSearchId(redis, userId);
|
||||||
|
if (!activeSearchId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingSearch = await getSearchRecord(redis, activeSearchId);
|
||||||
|
if (!existingSearch) {
|
||||||
|
await releaseActiveSearchLock(redis, userId, activeSearchId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedSearch = await failSearchIfProcessorUnavailable(existingSearch);
|
||||||
|
if (!normalizedSearch || normalizedSearch.status === 'completed' || normalizedSearch.status === 'failed') {
|
||||||
|
await releaseActiveSearchLock(redis, userId, activeSearchId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedSearch;
|
||||||
|
}
|
||||||
|
|
||||||
function logHealthFailure(details) {
|
function logHealthFailure(details) {
|
||||||
const signature = JSON.stringify(details);
|
const signature = JSON.stringify(details);
|
||||||
if (signature === lastHealthFailureSignature) {
|
if (signature === lastHealthFailureSignature) {
|
||||||
|
|
@ -528,13 +550,14 @@ app.post('/api/searches', requireSession, enforceSearchRateLimit, upload.single(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeSearchId = await getActiveSearchId(redis, userId);
|
const activeSearch = await resolveBlockingActiveSearch(userId);
|
||||||
|
|
||||||
if (activeSearchId) {
|
if (activeSearch) {
|
||||||
res.status(409).json({
|
res.status(409).json({
|
||||||
error: 'There is already an operation being processed.',
|
error: 'There is already an operation being processed.',
|
||||||
code: 'ACTIVE_SEARCH_EXISTS',
|
code: 'ACTIVE_SEARCH_EXISTS',
|
||||||
activeSearchId
|
activeSearchId: activeSearch.id,
|
||||||
|
activeSearchStatus: activeSearch.status
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
defineProps({
|
const fileInputId = 'faceai-upload-input';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: true
|
||||||
|
|
@ -54,6 +56,10 @@ defineProps({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function assignFileInput(element) {
|
||||||
|
props.fileInput.value = element;
|
||||||
|
}
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
'open-file-picker',
|
'open-file-picker',
|
||||||
'file-change',
|
'file-change',
|
||||||
|
|
@ -108,7 +114,8 @@ const emit = defineEmits([
|
||||||
@drop="emit('drop', $event)"
|
@drop="emit('drop', $event)"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
ref="fileInput"
|
:ref="assignFileInput"
|
||||||
|
:id="fileInputId"
|
||||||
class="d-none"
|
class="d-none"
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
|
|
@ -141,9 +148,18 @@ const emit = defineEmits([
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="faceai-dropzone-actions" @click.stop>
|
<div class="faceai-dropzone-actions" @click.stop>
|
||||||
<button class="btn btn-outline-warning" type="button" :disabled="!canPickFile" @click="emit('open-file-picker')">
|
<label
|
||||||
|
class="btn btn-outline-warning mb-0"
|
||||||
|
:class="{ disabled: !canPickFile }"
|
||||||
|
:for="canPickFile ? fileInputId : null"
|
||||||
|
role="button"
|
||||||
|
:aria-disabled="!canPickFile"
|
||||||
|
tabindex="0"
|
||||||
|
@keydown.enter.prevent="emit('open-file-picker')"
|
||||||
|
@keydown.space.prevent="emit('open-file-picker')"
|
||||||
|
>
|
||||||
{{ selectedFile ? t('uploaderReplace') : t('uploaderBrowse') }}
|
{{ selectedFile ? t('uploaderReplace') : t('uploaderBrowse') }}
|
||||||
</button>
|
</label>
|
||||||
<button v-if="selectedFile" class="btn btn-link" type="button" @click="emit('clear-file')">
|
<button v-if="selectedFile" class="btn btn-link" type="button" @click="emit('clear-file')">
|
||||||
{{ t('uploaderRemove') }}
|
{{ t('uploaderRemove') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ const copy = {
|
||||||
raceDataUnavailable: 'I dati FaceAI non sono disponibili per questa gara.',
|
raceDataUnavailable: 'I dati FaceAI non sono disponibili per questa gara.',
|
||||||
invalidRaceData: 'I dati della gara ricevuti non sono validi. Torna alla pagina gara e riapri Face ID dalla gara corretta.',
|
invalidRaceData: 'I dati della gara ricevuti non sono validi. Torna alla pagina gara e riapri Face ID dalla gara corretta.',
|
||||||
searchCreateError: 'Impossibile avviare la ricerca.',
|
searchCreateError: 'Impossibile avviare la ricerca.',
|
||||||
|
activeSearchExists: 'C\'e gia una ricerca in corso per questo utente. Attendi il completamento oppure ricarica la pagina.',
|
||||||
|
rateLimited: 'Hai avviato troppe ricerche in poco tempo. Attendi un momento e riprova.',
|
||||||
faceAiAlt: 'FaceAI',
|
faceAiAlt: 'FaceAI',
|
||||||
dropzoneDisabled: 'Il caricamento non è disponibile per questa gara.'
|
dropzoneDisabled: 'Il caricamento non è disponibile per questa gara.'
|
||||||
},
|
},
|
||||||
|
|
@ -101,11 +103,23 @@ const copy = {
|
||||||
raceDataUnavailable: 'FaceAI data is not available for this race.',
|
raceDataUnavailable: 'FaceAI data is not available for this race.',
|
||||||
invalidRaceData: 'The race data received for this session is invalid. Go back to the race page and reopen Face ID from the correct race.',
|
invalidRaceData: 'The race data received for this session is invalid. Go back to the race page and reopen Face ID from the correct race.',
|
||||||
searchCreateError: 'Unable to start the search.',
|
searchCreateError: 'Unable to start the search.',
|
||||||
|
activeSearchExists: 'There is already a search running for this user. Wait for it to finish or reload the page.',
|
||||||
|
rateLimited: 'Too many searches were started in a short time. Please wait a moment and try again.',
|
||||||
faceAiAlt: 'FaceAI',
|
faceAiAlt: 'FaceAI',
|
||||||
dropzoneDisabled: 'Upload is not available for this race.'
|
dropzoneDisabled: 'Upload is not available for this race.'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const knownServerCodes = {
|
||||||
|
PROCESSOR_UNAVAILABLE: 'processorUnavailable',
|
||||||
|
ACTIVE_SEARCH_EXISTS: 'activeSearchExists',
|
||||||
|
RATE_LIMITED: 'rateLimited',
|
||||||
|
MISSING_SELFIE: 'chooseSelfie',
|
||||||
|
RACE_PKL_UNAVAILABLE: 'raceDataUnavailable',
|
||||||
|
RACE_DIRECTORY_NOT_FOUND: 'invalidRaceData',
|
||||||
|
MISSING_RACE_STORAGE: 'invalidRaceData'
|
||||||
|
};
|
||||||
|
|
||||||
const knownServerMessages = {
|
const knownServerMessages = {
|
||||||
'No training dataset available for this race.': 'raceDataUnavailable',
|
'No training dataset available for this race.': 'raceDataUnavailable',
|
||||||
'FaceAI data is not available for this race.': 'raceDataUnavailable',
|
'FaceAI data is not available for this race.': 'raceDataUnavailable',
|
||||||
|
|
@ -115,7 +129,9 @@ const knownServerMessages = {
|
||||||
'FaceAI processor is temporarily unavailable. Please try again shortly.': 'processorUnavailable',
|
'FaceAI processor is temporarily unavailable. Please try again shortly.': 'processorUnavailable',
|
||||||
'Unable to build return URL.': 'redirectError',
|
'Unable to build return URL.': 'redirectError',
|
||||||
'Unable to create the search.': 'searchCreateError',
|
'Unable to create the search.': 'searchCreateError',
|
||||||
'Choose a selfie before starting the search.': 'chooseSelfie'
|
'Choose a selfie before starting the search.': 'chooseSelfie',
|
||||||
|
'There is already an operation being processed.': 'activeSearchExists',
|
||||||
|
'Too many search attempts. Please try again later.': 'rateLimited'
|
||||||
};
|
};
|
||||||
|
|
||||||
const simulatorUrl = legacyUrl('/faceai_simulator.php?raceId=101&lang=it');
|
const simulatorUrl = legacyUrl('/faceai_simulator.php?raceId=101&lang=it');
|
||||||
|
|
@ -178,6 +194,15 @@ export function useFaceAiHome() {
|
||||||
return t(fallbackKey);
|
return t(fallbackKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function localizeServerError(payload, fallbackKey) {
|
||||||
|
const mappedCodeKey = payload?.code ? knownServerCodes[payload.code] : null;
|
||||||
|
if (mappedCodeKey) {
|
||||||
|
return t(mappedCodeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return localizeServerMessage(payload?.error, fallbackKey);
|
||||||
|
}
|
||||||
|
|
||||||
function getAvailabilityUserMessage(availability, fallbackKey = 'unavailableDefault') {
|
function getAvailabilityUserMessage(availability, fallbackKey = 'unavailableDefault') {
|
||||||
if (isInvalidRaceAvailability(availability)) {
|
if (isInvalidRaceAvailability(availability)) {
|
||||||
return t('invalidRaceData');
|
return t('invalidRaceData');
|
||||||
|
|
@ -424,7 +449,7 @@ export function useFaceAiHome() {
|
||||||
const response = await fetch(`/api/searches/${searchId}`, { credentials: 'include' });
|
const response = await fetch(`/api/searches/${searchId}`, { credentials: 'include' });
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const payload = await response.json().catch(() => ({}));
|
const payload = await response.json().catch(() => ({}));
|
||||||
errorMessage.value = localizeServerMessage(payload.error, 'pollError');
|
errorMessage.value = localizeServerError(payload, 'pollError');
|
||||||
isSubmitting.value = false;
|
isSubmitting.value = false;
|
||||||
logFaceAiDebug('Search polling failed', { searchId, status: response.status, payload });
|
logFaceAiDebug('Search polling failed', { searchId, status: response.status, payload });
|
||||||
return;
|
return;
|
||||||
|
|
@ -451,7 +476,7 @@ export function useFaceAiHome() {
|
||||||
const redirectResponse = await fetch(`/api/searches/${searchId}/redirect`, { credentials: 'include' });
|
const redirectResponse = await fetch(`/api/searches/${searchId}/redirect`, { credentials: 'include' });
|
||||||
const payload = await redirectResponse.json();
|
const payload = await redirectResponse.json();
|
||||||
if (!redirectResponse.ok) {
|
if (!redirectResponse.ok) {
|
||||||
errorMessage.value = localizeServerMessage(payload.error, 'redirectError');
|
errorMessage.value = localizeServerError(payload, 'redirectError');
|
||||||
logFaceAiDebug('Redirect build failed', { searchId, payload });
|
logFaceAiDebug('Redirect build failed', { searchId, payload });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -498,7 +523,7 @@ export function useFaceAiHome() {
|
||||||
|
|
||||||
const payload = await response.json();
|
const payload = await response.json();
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
errorMessage.value = localizeServerMessage(payload.error, 'searchCreateError');
|
errorMessage.value = localizeServerError(payload, 'searchCreateError');
|
||||||
isSubmitting.value = false;
|
isSubmitting.value = false;
|
||||||
logFaceAiDebug('Search creation failed', { status: response.status, payload });
|
logFaceAiDebug('Search creation failed', { status: response.status, payload });
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export const config = {
|
||||||
pklRoot: process.env.FACEAI_PKL_ROOT || '/data/pkl',
|
pklRoot: process.env.FACEAI_PKL_ROOT || '/data/pkl',
|
||||||
matcherBinary: process.env.FACEAI_MATCHER_BINARY || '/app/bin/face_matcher',
|
matcherBinary: process.env.FACEAI_MATCHER_BINARY || '/app/bin/face_matcher',
|
||||||
processorHeartbeatIntervalMs: Number(process.env.FACEAI_PROCESSOR_HEARTBEAT_INTERVAL_MS || 5 * 1000),
|
processorHeartbeatIntervalMs: Number(process.env.FACEAI_PROCESSOR_HEARTBEAT_INTERVAL_MS || 5 * 1000),
|
||||||
processorHeartbeatTtlSeconds: Number(process.env.FACEAI_PROCESSOR_HEARTBEAT_TTL_SECONDS || 20),
|
processorHeartbeatTtlSeconds: Number(process.env.FACEAI_PROCESSOR_HEARTBEAT_TTL_SECONDS || 60),
|
||||||
searchTtlSeconds: Number(process.env.FACEAI_SEARCH_TTL_SECONDS || 24 * 60 * 60),
|
searchTtlSeconds: Number(process.env.FACEAI_SEARCH_TTL_SECONDS || 24 * 60 * 60),
|
||||||
resultTtlSeconds: Number(process.env.FACEAI_RESULT_TTL_SECONDS || 24 * 60 * 60)
|
resultTtlSeconds: Number(process.env.FACEAI_RESULT_TTL_SECONDS || 24 * 60 * 60)
|
||||||
};
|
};
|
||||||
|
|
@ -2,9 +2,9 @@ const { defineConfig } = require('@playwright/test');
|
||||||
|
|
||||||
module.exports = defineConfig({
|
module.exports = defineConfig({
|
||||||
testDir: './tests/e2e',
|
testDir: './tests/e2e',
|
||||||
timeout: 10 * 60 * 1000,
|
timeout: 60 * 1000,
|
||||||
expect: {
|
expect: {
|
||||||
timeout: 30 * 1000
|
timeout: 15 * 1000
|
||||||
},
|
},
|
||||||
fullyParallel: false,
|
fullyParallel: false,
|
||||||
workers: 1,
|
workers: 1,
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ const authFile = path.join(__dirname, 'tests/live-site/.auth/user.json');
|
||||||
|
|
||||||
module.exports = defineConfig({
|
module.exports = defineConfig({
|
||||||
testDir: './tests/live-site',
|
testDir: './tests/live-site',
|
||||||
timeout: 2 * 60 * 1000,
|
timeout: 60 * 1000,
|
||||||
expect: {
|
expect: {
|
||||||
timeout: 20 * 1000
|
timeout: 15 * 1000
|
||||||
},
|
},
|
||||||
fullyParallel: false,
|
fullyParallel: false,
|
||||||
workers: 1,
|
workers: 1,
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,12 @@ const {
|
||||||
|
|
||||||
const FACEAI_HOME_URL_RE = /http:\/\/(localhost|127\.0\.0\.1):3001\/?(?:\?.*)?$/;
|
const FACEAI_HOME_URL_RE = /http:\/\/(localhost|127\.0\.0\.1):3001\/?(?:\?.*)?$/;
|
||||||
const FACEAI_CALLBACK_URL_RE = /http:\/\/(localhost|127\.0\.0\.1):3001\/auth\/callback\?token=/;
|
const FACEAI_CALLBACK_URL_RE = /http:\/\/(localhost|127\.0\.0\.1):3001\/auth\/callback\?token=/;
|
||||||
const FACEAI_RETURN_URL_RE = /http:\/\/(localhost|127\.0\.0\.1):8080\/faceai_return\.php\?resultId=.*token=.*/;
|
|
||||||
const LEGACY_HOME_URL_RE = /http:\/\/(localhost|127\.0\.0\.1):8080\/index\.jsp$/;
|
const LEGACY_HOME_URL_RE = /http:\/\/(localhost|127\.0\.0\.1):8080\/index\.jsp$/;
|
||||||
|
const LONG_TEST_TIMEOUT_MS = 3 * 60 * 1000;
|
||||||
|
const SHORT_UI_TIMEOUT_MS = 30 * 1000;
|
||||||
|
const SEARCH_COMPLETION_TIMEOUT_MS = 75 * 1000;
|
||||||
|
const LEGACY_RETURN_TIMEOUT_MS = 75 * 1000;
|
||||||
|
const FILE_CHOOSER_TIMEOUT_MS = 8 * 1000;
|
||||||
|
|
||||||
function buildLegacySimulatorReturnMatcher(raceId) {
|
function buildLegacySimulatorReturnMatcher(raceId) {
|
||||||
return new RegExp(`http:\\/\\/(localhost|127\\.0\\.0\\.1):8080\\/faceai_simulator\\.php\\?raceId=${raceId}.*`);
|
return new RegExp(`http:\\/\\/(localhost|127\\.0\\.0\\.1):8080\\/faceai_simulator\\.php\\?raceId=${raceId}.*`);
|
||||||
|
|
@ -26,7 +30,7 @@ function assertLogDoesNotContain(content, patterns, label) {
|
||||||
|
|
||||||
async function waitForFaceAiHome(page) {
|
async function waitForFaceAiHome(page) {
|
||||||
await page.waitForURL((url) => FACEAI_CALLBACK_URL_RE.test(url.toString()) || FACEAI_HOME_URL_RE.test(url.toString()), {
|
await page.waitForURL((url) => FACEAI_CALLBACK_URL_RE.test(url.toString()) || FACEAI_HOME_URL_RE.test(url.toString()), {
|
||||||
timeout: 60 * 1000
|
timeout: SHORT_UI_TIMEOUT_MS
|
||||||
});
|
});
|
||||||
await expect(page.getByRole('heading', { name: 'Trova le tue foto con un selfie' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Trova le tue foto con un selfie' })).toBeVisible();
|
||||||
}
|
}
|
||||||
|
|
@ -89,18 +93,24 @@ async function waitForSearchCondition(page, searchId, predicate, timeoutMs = 30
|
||||||
throw new Error(`Timed out waiting for search ${searchId}. Last payload: ${JSON.stringify(lastPayload)}`);
|
throw new Error(`Timed out waiting for search ${searchId}. Last payload: ${JSON.stringify(lastPayload)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForLegacyResult(page, expectedMatchCount = null) {
|
async function waitForLegacyResult(page, raceId, expectedMatchCount = null) {
|
||||||
await page.waitForURL(FACEAI_RETURN_URL_RE, {
|
await page.waitForURL(buildLegacySimulatorReturnMatcher(raceId), {
|
||||||
timeout: 6 * 60 * 1000
|
timeout: LEGACY_RETURN_TIMEOUT_MS,
|
||||||
|
waitUntil: 'commit'
|
||||||
});
|
});
|
||||||
await expect(page.locator('.sim-banner')).toContainText('Vista filtrata da FaceAI');
|
|
||||||
|
await expect.poll(() => page.url(), {
|
||||||
|
timeout: 15 * 1000,
|
||||||
|
message: 'Expected the legacy simulator return URL to include FaceAI filter parameters.'
|
||||||
|
}).toMatch(/faceaiPhotoIds=/);
|
||||||
|
|
||||||
if (expectedMatchCount === null) {
|
if (expectedMatchCount === null) {
|
||||||
await expect(page.locator('.gallery-card').first()).toBeVisible();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await expect(page.locator('.sim-banner')).toContainText(String(expectedMatchCount));
|
const finalUrl = new URL(page.url());
|
||||||
await expect(page.locator('.gallery-card')).toHaveCount(expectedMatchCount);
|
const photoIds = (finalUrl.searchParams.get('faceaiPhotoIds') || '').split(',').map((value) => value.trim()).filter(Boolean);
|
||||||
|
expect(photoIds.length).toBe(expectedMatchCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function verifySearchLogs(searchId, { expectedMatchCount, expectedSelfieName }) {
|
async function verifySearchLogs(searchId, { expectedMatchCount, expectedSelfieName }) {
|
||||||
|
|
@ -129,10 +139,20 @@ async function verifySearchLogs(searchId, { expectedMatchCount, expectedSelfieNa
|
||||||
}
|
}
|
||||||
|
|
||||||
async function closeContexts(contexts) {
|
async function closeContexts(contexts) {
|
||||||
await Promise.all(contexts.map((context) => context.close()));
|
await Promise.all(contexts.map(async (context) => {
|
||||||
|
try {
|
||||||
|
await context.close();
|
||||||
|
} catch (error) {
|
||||||
|
if (!/ENOENT|Target page, context or browser has been closed/i.test(String(error))) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
test('runs the simulator flow through FaceAI and returns to the filtered legacy result', async ({ page }) => {
|
test('runs the simulator flow through FaceAI and returns to the filtered legacy result', async ({ page }) => {
|
||||||
|
test.slow();
|
||||||
|
|
||||||
await launchFromSimulator(page, {
|
await launchFromSimulator(page, {
|
||||||
raceId: '202',
|
raceId: '202',
|
||||||
raceSlug: 'mezza-di-pisa',
|
raceSlug: 'mezza-di-pisa',
|
||||||
|
|
@ -142,8 +162,7 @@ test('runs the simulator flow through FaceAI and returns to the filtered legacy
|
||||||
|
|
||||||
const search = await startSearch(page, 'DSC_1960.JPG');
|
const search = await startSearch(page, 'DSC_1960.JPG');
|
||||||
|
|
||||||
await waitForLegacyResult(page, EXPECTED_MATCH_COUNT);
|
await waitForLegacyResult(page, '202', EXPECTED_MATCH_COUNT);
|
||||||
await expect(page.locator('.gallery-card').filter({ hasText: 'DSC_1960.JPG' }).first()).toBeVisible();
|
|
||||||
|
|
||||||
await verifySearchLogs(search.id, {
|
await verifySearchLogs(search.id, {
|
||||||
expectedMatchCount: EXPECTED_MATCH_COUNT,
|
expectedMatchCount: EXPECTED_MATCH_COUNT,
|
||||||
|
|
@ -191,7 +210,7 @@ test('shows a localized invalid-race error when session race data points to a mi
|
||||||
await page.locator('#faceaiLaunchButton').click();
|
await page.locator('#faceaiLaunchButton').click();
|
||||||
|
|
||||||
await page.waitForURL(FACEAI_HOME_URL_RE, {
|
await page.waitForURL(FACEAI_HOME_URL_RE, {
|
||||||
timeout: 60 * 1000
|
timeout: SHORT_UI_TIMEOUT_MS
|
||||||
});
|
});
|
||||||
await expect(page.getByRole('heading', { name: 'Find your photos with a selfie' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Find your photos with a selfie' })).toBeVisible();
|
||||||
await expect(page.locator('.faceai-feedback')).toContainText('The race data received for this session is invalid. Go back to the race page and reopen Face ID from the correct race.');
|
await expect(page.locator('.faceai-feedback')).toContainText('The race data received for this session is invalid. Go back to the race page and reopen Face ID from the correct race.');
|
||||||
|
|
@ -240,6 +259,8 @@ test('rejects a not-logged-in user after clicking the Face ID button and sends t
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shows the no-face message and allows the user to return to the race page', async ({ page }) => {
|
test('shows the no-face message and allows the user to return to the race page', async ({ page }) => {
|
||||||
|
test.slow();
|
||||||
|
|
||||||
await launchFromSimulator(page, {
|
await launchFromSimulator(page, {
|
||||||
raceId: '202',
|
raceId: '202',
|
||||||
raceSlug: 'mezza-di-pisa',
|
raceSlug: 'mezza-di-pisa',
|
||||||
|
|
@ -251,7 +272,7 @@ test('shows the no-face message and allows the user to return to the race page',
|
||||||
|
|
||||||
await waitForSearchCondition(page, search.id, (payload) => {
|
await waitForSearchCondition(page, search.id, (payload) => {
|
||||||
return payload.status === 'completed' && payload.completionCode === 'NO_FACES_FOUND';
|
return payload.status === 'completed' && payload.completionCode === 'NO_FACES_FOUND';
|
||||||
}, 2 * 60 * 1000);
|
}, SEARCH_COMPLETION_TIMEOUT_MS);
|
||||||
|
|
||||||
await expect(page.locator('.faceai-feedback')).toContainText('Nessun volto rilevato nella foto caricata');
|
await expect(page.locator('.faceai-feedback')).toContainText('Nessun volto rilevato nella foto caricata');
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(2000);
|
||||||
|
|
@ -266,7 +287,26 @@ test('shows the no-face message and allows the user to return to the race page',
|
||||||
await expect(page).toHaveURL(buildLegacySimulatorReturnMatcher('202'));
|
await expect(page).toHaveURL(buildLegacySimulatorReturnMatcher('202'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('opens the file chooser when the user clicks the upload button', async ({ page }) => {
|
||||||
|
await launchFromSimulator(page, {
|
||||||
|
raceId: '202',
|
||||||
|
raceSlug: 'mezza-di-pisa',
|
||||||
|
raceName: 'Mezza di Pisa',
|
||||||
|
raceFolder: 'PISA'
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileChooserPromise = page.waitForEvent('filechooser', { timeout: FILE_CHOOSER_TIMEOUT_MS });
|
||||||
|
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 expect(page.getByRole('button', { name: 'Avvia ricerca Face ID' })).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
test('lets the user retry with a valid photo after a no-face upload and then returns to the filtered legacy result', async ({ page }) => {
|
test('lets the user retry with a valid photo after a no-face upload and then returns to the filtered legacy result', async ({ page }) => {
|
||||||
|
test.slow();
|
||||||
|
|
||||||
await launchFromSimulator(page, {
|
await launchFromSimulator(page, {
|
||||||
raceId: '202',
|
raceId: '202',
|
||||||
raceSlug: 'mezza-di-pisa',
|
raceSlug: 'mezza-di-pisa',
|
||||||
|
|
@ -277,13 +317,13 @@ test('lets the user retry with a valid photo after a no-face upload and then ret
|
||||||
const noFaceSearch = await startSearch(page, 'DSC_1994.JPG');
|
const noFaceSearch = await startSearch(page, 'DSC_1994.JPG');
|
||||||
await waitForSearchCondition(page, noFaceSearch.id, (payload) => {
|
await waitForSearchCondition(page, noFaceSearch.id, (payload) => {
|
||||||
return payload.status === 'completed' && payload.completionCode === 'NO_FACES_FOUND';
|
return payload.status === 'completed' && payload.completionCode === 'NO_FACES_FOUND';
|
||||||
}, 2 * 60 * 1000);
|
}, SEARCH_COMPLETION_TIMEOUT_MS);
|
||||||
|
|
||||||
await expect(page.locator('.faceai-feedback')).toContainText('Nessun volto rilevato nella foto caricata');
|
await expect(page.locator('.faceai-feedback')).toContainText('Nessun volto rilevato nella foto caricata');
|
||||||
await expect(page.locator('input[type="file"]')).toBeEnabled();
|
await expect(page.locator('input[type="file"]')).toBeEnabled();
|
||||||
|
|
||||||
const retrySearch = await startSearch(page, 'DSC_1960.JPG');
|
const retrySearch = await startSearch(page, 'DSC_1960.JPG');
|
||||||
await waitForLegacyResult(page, EXPECTED_MATCH_COUNT);
|
await waitForLegacyResult(page, '202', EXPECTED_MATCH_COUNT);
|
||||||
|
|
||||||
await verifySearchLogs(noFaceSearch.id, {
|
await verifySearchLogs(noFaceSearch.id, {
|
||||||
expectedMatchCount: 0,
|
expectedMatchCount: 0,
|
||||||
|
|
@ -301,13 +341,16 @@ test('redirects direct-entry users without FaceAI session data back to the legac
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await page.goto(`${FACEAI_BASE_URL}/`, { waitUntil: 'domcontentloaded' });
|
await page.goto(`${FACEAI_BASE_URL}/`, { waitUntil: 'domcontentloaded' });
|
||||||
await page.waitForURL(LEGACY_HOME_URL_RE, { timeout: 30 * 1000 });
|
await page.waitForURL(LEGACY_HOME_URL_RE, { timeout: SHORT_UI_TIMEOUT_MS });
|
||||||
} finally {
|
} finally {
|
||||||
await context.close();
|
await context.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('allows two users to process different photos at the same time', async ({ browser }) => {
|
test('allows two users to process different photos at the same time', async ({ browser }) => {
|
||||||
|
test.slow();
|
||||||
|
test.setTimeout(LONG_TEST_TIMEOUT_MS);
|
||||||
|
|
||||||
const contexts = [await browser.newContext(), await browser.newContext()];
|
const contexts = [await browser.newContext(), await browser.newContext()];
|
||||||
const pages = await Promise.all(contexts.map((context) => context.newPage()));
|
const pages = await Promise.all(contexts.map((context) => context.newPage()));
|
||||||
|
|
||||||
|
|
@ -323,8 +366,8 @@ test('allows two users to process different photos at the same time', async ({ b
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
waitForLegacyResult(pages[0]),
|
waitForLegacyResult(pages[0], '202'),
|
||||||
waitForLegacyResult(pages[1])
|
waitForLegacyResult(pages[1], '202')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
|
@ -337,6 +380,9 @@ test('allows two users to process different photos at the same time', async ({ b
|
||||||
});
|
});
|
||||||
|
|
||||||
test('queues the third user until a worker is free and then completes all three searches normally', async ({ browser }) => {
|
test('queues the third user until a worker is free and then completes all three searches normally', async ({ browser }) => {
|
||||||
|
test.slow();
|
||||||
|
test.setTimeout(LONG_TEST_TIMEOUT_MS);
|
||||||
|
|
||||||
const contexts = [await browser.newContext(), await browser.newContext(), await browser.newContext()];
|
const contexts = [await browser.newContext(), await browser.newContext(), await browser.newContext()];
|
||||||
const pages = await Promise.all(contexts.map((context) => context.newPage()));
|
const pages = await Promise.all(contexts.map((context) => context.newPage()));
|
||||||
|
|
||||||
|
|
@ -384,9 +430,9 @@ test('queues the third user until a worker is free and then completes all three
|
||||||
|
|
||||||
await waitForSearchCondition(queuedSearch.page, queuedSearch.searchId, (payload) => {
|
await waitForSearchCondition(queuedSearch.page, queuedSearch.searchId, (payload) => {
|
||||||
return payload.status === 'processing' || payload.status === 'completed';
|
return payload.status === 'processing' || payload.status === 'completed';
|
||||||
}, 2 * 60 * 1000);
|
}, SEARCH_COMPLETION_TIMEOUT_MS);
|
||||||
|
|
||||||
await Promise.all(pages.map((page) => waitForLegacyResult(page)));
|
await Promise.all(pages.map((page) => waitForLegacyResult(page, '202')));
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
verifySearchLogs(searchOne.id, { expectedSelfieName: 'DSC_1960.JPG' }),
|
verifySearchLogs(searchOne.id, { expectedSelfieName: 'DSC_1960.JPG' }),
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ const { test } = require('@playwright/test');
|
||||||
const {
|
const {
|
||||||
AUTH_FILE,
|
AUTH_FILE,
|
||||||
ensureAuthDirectory,
|
ensureAuthDirectory,
|
||||||
performLiveLogin
|
performLiveLoginRequest
|
||||||
} = require('./live-site-test-utils');
|
} = require('./live-site-test-utils');
|
||||||
|
|
||||||
test('authenticate against the live site', async ({ page }) => {
|
test('authenticate against the live site', async ({ request }) => {
|
||||||
ensureAuthDirectory();
|
ensureAuthDirectory();
|
||||||
await performLiveLogin(page);
|
await performLiveLoginRequest(request);
|
||||||
await page.context().storageState({ path: AUTH_FILE });
|
await request.storageState({ path: AUTH_FILE });
|
||||||
});
|
});
|
||||||
|
|
@ -4,7 +4,6 @@ const {
|
||||||
LIVE_SITE_BASE_URL,
|
LIVE_SITE_BASE_URL,
|
||||||
LIVE_SITE_PORTRAIT_PATH,
|
LIVE_SITE_PORTRAIT_PATH,
|
||||||
LIVE_SITE_RACE_URL,
|
LIVE_SITE_RACE_URL,
|
||||||
LIVE_SITE_RESULT_URL_PATTERN,
|
|
||||||
LIVE_SITE_RUN_UPLOAD_FLOW,
|
LIVE_SITE_RUN_UPLOAD_FLOW,
|
||||||
ensureLiveAuthenticatedRacePage,
|
ensureLiveAuthenticatedRacePage,
|
||||||
requirePortraitFixture
|
requirePortraitFixture
|
||||||
|
|
@ -80,6 +79,86 @@ async function waitForSearchCompletion(page, searchId) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function readVisibleLegacyPhotoIds(page) {
|
||||||
|
return page.locator('#demo [data-faceai-photo-id]').evaluateAll((elements) => {
|
||||||
|
return elements.map((element) => String(element.getAttribute('data-faceai-photo-id') || '').trim()).filter(Boolean);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForVisibleLegacyPhotoIds(page, expectedCount) {
|
||||||
|
await expect.poll(async () => {
|
||||||
|
const visiblePhotoIds = await readVisibleLegacyPhotoIds(page);
|
||||||
|
return visiblePhotoIds.length;
|
||||||
|
}, {
|
||||||
|
timeout: 30 * 1000,
|
||||||
|
message: 'Expected the legacy FaceAI gallery to finish loading matched thumbnails.'
|
||||||
|
}).toBe(expectedCount);
|
||||||
|
|
||||||
|
return readVisibleLegacyPhotoIds(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForVisibleLegacyThumbs(page, expectedCount) {
|
||||||
|
const thumbs = page.locator('#demo [data-faceai-photo-id] img.thumb');
|
||||||
|
|
||||||
|
await expect(thumbs).toHaveCount(expectedCount);
|
||||||
|
await expect.poll(async () => {
|
||||||
|
return thumbs.evaluateAll((elements) => {
|
||||||
|
return elements.every((element) => {
|
||||||
|
const src = String(element.getAttribute('src') || '').trim();
|
||||||
|
return src.length > 0 && src.indexOf('_imgNotFound') === -1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
timeout: 30 * 1000,
|
||||||
|
message: 'Expected the legacy FaceAI gallery to resolve real thumbnail image URLs.'
|
||||||
|
}).toBe(true);
|
||||||
|
|
||||||
|
return thumbs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForLegacyFaceAiCount(page, expectedCount) {
|
||||||
|
await expect.poll(async () => {
|
||||||
|
return page.locator('#faceAiPhotoCountValue').textContent();
|
||||||
|
}, {
|
||||||
|
timeout: 30 * 1000,
|
||||||
|
message: 'Expected the legacy FaceAI count to match the resolved gallery size.'
|
||||||
|
}).toBe(String(expectedCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectLegacyFaceAiGalleryToRemainStable(page, expectedPhotoIds, holdMs = 4000) {
|
||||||
|
await page.waitForTimeout(holdMs);
|
||||||
|
|
||||||
|
const visiblePhotoIds = await readVisibleLegacyPhotoIds(page);
|
||||||
|
expect(visiblePhotoIds.sort()).toEqual(expectedPhotoIds.slice().sort());
|
||||||
|
|
||||||
|
await expect(page.locator('#demo [data-faceai-photo-id]')).toHaveCount(expectedPhotoIds.length);
|
||||||
|
await waitForVisibleLegacyThumbs(page, expectedPhotoIds.length);
|
||||||
|
await waitForLegacyFaceAiCount(page, expectedPhotoIds.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('renders the exact live FaceAI filtered sample URL with visible thumbnails', async ({ page }) => {
|
||||||
|
const samplePhotoIds = [
|
||||||
|
'00.PANORAMICA\\GIC_7918.JPG',
|
||||||
|
'02.PARTENZA\\GIC_7918.JPG'
|
||||||
|
];
|
||||||
|
const sampleUrl = `${LIVE_SITE_BASE_URL}/42%20HALF%20MARATHON%20FIRENZE_gara-1018545---48-1.html?faceaiMatchSource=faceai&faceaiMatchCount=2&faceaiPhotoIds=${encodeURIComponent(samplePhotoIds.join(','))}`;
|
||||||
|
|
||||||
|
await ensureLiveAuthenticatedRacePage(page);
|
||||||
|
await page.goto(sampleUrl, {
|
||||||
|
waitUntil: 'domcontentloaded'
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.locator('form[onsubmit="return searching()"]')).toBeVisible();
|
||||||
|
await expect(page.locator('#faceAiFilterBanner')).toContainText(/Face ID filter active|Filtro Face ID attivo/i);
|
||||||
|
|
||||||
|
const visiblePhotoIds = await waitForVisibleLegacyPhotoIds(page, samplePhotoIds.length);
|
||||||
|
expect(visiblePhotoIds.sort()).toEqual(samplePhotoIds.slice().sort());
|
||||||
|
|
||||||
|
await waitForVisibleLegacyThumbs(page, samplePhotoIds.length);
|
||||||
|
await waitForLegacyFaceAiCount(page, samplePhotoIds.length);
|
||||||
|
await expectLegacyFaceAiGalleryToRemainStable(page, samplePhotoIds);
|
||||||
|
});
|
||||||
|
|
||||||
test('loads a live race page with an authenticated session', async ({ page }) => {
|
test('loads a live race page with an authenticated session', async ({ page }) => {
|
||||||
await ensureLiveAuthenticatedRacePage(page);
|
await ensureLiveAuthenticatedRacePage(page);
|
||||||
|
|
||||||
|
|
@ -129,7 +208,8 @@ test('returns to the live race page from FaceAI without leaving the legacy spinn
|
||||||
await page.waitForURL((url) => {
|
await page.waitForURL((url) => {
|
||||||
return url.toString().startsWith(LIVE_SITE_BASE_URL) && !url.toString().startsWith(LIVE_FACEAI_BASE_URL);
|
return url.toString().startsWith(LIVE_SITE_BASE_URL) && !url.toString().startsWith(LIVE_FACEAI_BASE_URL);
|
||||||
}, {
|
}, {
|
||||||
timeout: 60 * 1000
|
timeout: 60 * 1000,
|
||||||
|
waitUntil: 'commit'
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(page.locator('form[onsubmit="return searching()"]')).toBeVisible();
|
await expect(page.locator('form[onsubmit="return searching()"]')).toBeVisible();
|
||||||
|
|
@ -149,6 +229,8 @@ test('returns to the live race page from FaceAI without leaving the legacy spinn
|
||||||
test.skip(!LIVE_SITE_RUN_UPLOAD_FLOW, 'Set LIVE_SITE_RUN_UPLOAD_FLOW=1 to exercise the live upload flow.');
|
test.skip(!LIVE_SITE_RUN_UPLOAD_FLOW, 'Set LIVE_SITE_RUN_UPLOAD_FLOW=1 to exercise the live upload flow.');
|
||||||
|
|
||||||
test('accepts the supplied portrait image for the live upload flow', async ({ page }) => {
|
test('accepts the supplied portrait image for the live upload flow', async ({ page }) => {
|
||||||
|
test.slow();
|
||||||
|
|
||||||
requirePortraitFixture();
|
requirePortraitFixture();
|
||||||
|
|
||||||
await openLiveFaceAi(page);
|
await openLiveFaceAi(page);
|
||||||
|
|
@ -185,18 +267,24 @@ test('accepts the supplied portrait image for the live upload flow', async ({ pa
|
||||||
|
|
||||||
await waitForSearchCompletion(page, searchId);
|
await waitForSearchCompletion(page, searchId);
|
||||||
|
|
||||||
await page.waitForURL(new RegExp(`^${escapeRegExp(LIVE_SITE_RESULT_URL_PATTERN)}`), {
|
|
||||||
timeout: 3 * 60 * 1000
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect.poll(async () => page.url(), {
|
await expect.poll(async () => page.url(), {
|
||||||
timeout: 15 * 1000,
|
timeout: 30 * 1000,
|
||||||
message: 'Expected the browser to land on the legacy result page 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)}/`));
|
}).toMatch(new RegExp(`^${escapeRegExp(LIVE_SITE_BASE_URL)}/.*faceaiPhotoIds=`));
|
||||||
|
|
||||||
await expect(page.locator('body')).toContainText(/Vista filtrata da FaceAI|foto da FaceAI|ID foto:/i);
|
await expect(page.locator('form[onsubmit="return searching()"]')).toBeVisible();
|
||||||
await expect.poll(async () => page.locator('.gallery-card').count(), {
|
await expect(page.locator('#faceAiFilterBanner')).toContainText(/Face ID filter active|Filtro Face ID attivo/i);
|
||||||
timeout: 15 * 1000,
|
await expect(page.locator('.gallery-card')).toHaveCount(0);
|
||||||
message: 'Expected the legacy return page to render at least one FaceAI result card.'
|
|
||||||
}).toBeGreaterThan(0);
|
const finalUrl = new URL(page.url());
|
||||||
});
|
const expectedPhotoIds = (finalUrl.searchParams.get('faceaiPhotoIds') || '').split(',').map((value) => value.trim()).filter(Boolean);
|
||||||
|
expect(expectedPhotoIds.length, 'Expected the final race page URL to include at least one FaceAI photo identifier.').toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const visiblePhotoIds = await waitForVisibleLegacyPhotoIds(page, expectedPhotoIds.length);
|
||||||
|
expect(visiblePhotoIds.length, 'Expected at least one legacy race thumbnail to remain visible after FaceAI filtering.').toBeGreaterThan(0);
|
||||||
|
expect(visiblePhotoIds.sort()).toEqual(expectedPhotoIds.slice().sort());
|
||||||
|
|
||||||
|
await waitForVisibleLegacyThumbs(page, expectedPhotoIds.length);
|
||||||
|
await waitForLegacyFaceAiCount(page, expectedPhotoIds.length);
|
||||||
|
await expectLegacyFaceAiGalleryToRemainStable(page, expectedPhotoIds);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,39 @@ function loginSubmitLocator(page) {
|
||||||
return page.locator('a.btn').filter({ hasText: /Accedi|Sign in/i }).first();
|
return page.locator('a.btn').filter({ hasText: /Accedi|Sign in/i }).first();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildLoginFormData() {
|
||||||
|
requireCredentials();
|
||||||
|
return {
|
||||||
|
login: LIVE_SITE_USERNAME,
|
||||||
|
pwd: LIVE_SITE_PASSWORD,
|
||||||
|
cmdIU: 'check',
|
||||||
|
act: '',
|
||||||
|
thePage: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function gotoWithRetry(page, url, options = {}, maxAttempts = 3) {
|
||||||
|
let lastError = null;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||||
|
try {
|
||||||
|
await page.goto(url, options);
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
if (!/ERR_ABORTED/i.test(String(error)) || attempt === maxAttempts) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
async function performLiveLogin(page) {
|
async function performLiveLogin(page) {
|
||||||
requireCredentials();
|
requireCredentials();
|
||||||
|
|
||||||
await page.goto(LIVE_SITE_LOGIN_URL, { waitUntil: 'domcontentloaded' });
|
await gotoWithRetry(page, LIVE_SITE_LOGIN_URL, { waitUntil: 'commit' });
|
||||||
await dismissCookieBanner(page);
|
await dismissCookieBanner(page);
|
||||||
await page.locator('#login').fill(LIVE_SITE_USERNAME);
|
await page.locator('#login').fill(LIVE_SITE_USERNAME);
|
||||||
await page.locator('#pwd').fill(LIVE_SITE_PASSWORD);
|
await page.locator('#pwd').fill(LIVE_SITE_PASSWORD);
|
||||||
|
|
@ -52,6 +81,24 @@ async function performLiveLogin(page) {
|
||||||
await waitForLoggedInUi(page);
|
await waitForLoggedInUi(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function performLiveLoginRequest(requestContext) {
|
||||||
|
const response = await requestContext.post(`${LIVE_SITE_BASE_URL}/Logon.abl`, {
|
||||||
|
form: buildLoginFormData(),
|
||||||
|
failOnStatusCode: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalUrl = response.url();
|
||||||
|
const bodyText = await response.text();
|
||||||
|
|
||||||
|
if (!response.ok()) {
|
||||||
|
throw new Error(`Live 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(`Live login request appears to have remained on the login page at ${finalUrl}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function waitForLoggedInUi(page) {
|
async function waitForLoggedInUi(page) {
|
||||||
const accountMenu = page.locator('#navbarDropdownMenuLink');
|
const accountMenu = page.locator('#navbarDropdownMenuLink');
|
||||||
const accountLink = page.locator('a[href*="dettaglio_clienti"]');
|
const accountLink = page.locator('a[href*="dettaglio_clienti"]');
|
||||||
|
|
@ -81,14 +128,14 @@ async function expectRacePageLoaded(page) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureLiveAuthenticatedRacePage(page) {
|
async function ensureLiveAuthenticatedRacePage(page) {
|
||||||
await page.goto(LIVE_SITE_RACE_URL, { waitUntil: 'domcontentloaded' });
|
await gotoWithRetry(page, LIVE_SITE_RACE_URL, { waitUntil: 'commit' });
|
||||||
await dismissCookieBanner(page);
|
await dismissCookieBanner(page);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await waitForLoggedInUi(page);
|
await waitForLoggedInUi(page);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await performLiveLogin(page);
|
await performLiveLoginRequest(page.context().request);
|
||||||
await page.goto(LIVE_SITE_RACE_URL, { waitUntil: 'domcontentloaded' });
|
await gotoWithRetry(page, LIVE_SITE_RACE_URL, { waitUntil: 'commit' });
|
||||||
await dismissCookieBanner(page);
|
await dismissCookieBanner(page);
|
||||||
await waitForLoggedInUi(page);
|
await waitForLoggedInUi(page);
|
||||||
}
|
}
|
||||||
|
|
@ -119,6 +166,7 @@ module.exports = {
|
||||||
expectRacePageLoaded,
|
expectRacePageLoaded,
|
||||||
loginSubmitLocator,
|
loginSubmitLocator,
|
||||||
performLiveLogin,
|
performLiveLogin,
|
||||||
|
performLiveLoginRequest,
|
||||||
requirePortraitFixture,
|
requirePortraitFixture,
|
||||||
requireCredentials,
|
requireCredentials,
|
||||||
waitForLoggedInUi
|
waitForLoggedInUi
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,10 @@ All files in this rollout are deployed from the current working tree.
|
||||||
## Updated Files
|
## Updated Files
|
||||||
|
|
||||||
- `www/_js/rus-ecom-240621.js`
|
- `www/_js/rus-ecom-240621.js`
|
||||||
- `www/faceai_handoff.php`
|
- `www/faceai_return.php`
|
||||||
|
- `www/faceai_photo_lookup.jsp`
|
||||||
|
- `www/fotoCR.jsp`
|
||||||
|
- `www/fotoCR-en.jsp`
|
||||||
|
|
||||||
## Excluded Files
|
## Excluded Files
|
||||||
|
|
||||||
|
|
@ -27,7 +30,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: `5`
|
||||||
|
|
||||||
## Transfer Method
|
## Transfer Method
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -251,6 +251,439 @@ function initFaceAiErrorModal() {
|
||||||
clearFaceAiErrorState();
|
clearFaceAiErrorState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripFaceAiStateFromUrl(url) {
|
||||||
|
var fallbackUrl = window.location.href;
|
||||||
|
|
||||||
|
if (typeof URL === "undefined") {
|
||||||
|
return url || fallbackUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var cleanUrl = new URL(url || fallbackUrl, window.location.href);
|
||||||
|
cleanUrl.searchParams.delete("faceaiError");
|
||||||
|
cleanUrl.searchParams.delete("faceaiErrorTitle");
|
||||||
|
cleanUrl.searchParams.delete("faceaiErrorMessage");
|
||||||
|
cleanUrl.searchParams.delete("faceaiMatchSource");
|
||||||
|
cleanUrl.searchParams.delete("faceaiMatchCount");
|
||||||
|
cleanUrl.searchParams.delete("faceaiPhotoIds");
|
||||||
|
return cleanUrl.toString();
|
||||||
|
} catch (error) {
|
||||||
|
return url || fallbackUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFaceAiMatchState() {
|
||||||
|
if (typeof URLSearchParams === "undefined") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var params = new URLSearchParams(window.location.search || "");
|
||||||
|
var rawIds = params.get("faceaiPhotoIds") || "";
|
||||||
|
if (!rawIds) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var photoIds = rawIds.split(",").map(function(value) {
|
||||||
|
return String(value || "").trim();
|
||||||
|
}).filter(function(value) {
|
||||||
|
return value.length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!photoIds.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var photoIdSet = {};
|
||||||
|
for (var index = 0; index < photoIds.length; index += 1) {
|
||||||
|
photoIdSet[photoIds[index]] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsedCount = parseInt(params.get("faceaiMatchCount"), 10);
|
||||||
|
|
||||||
|
return {
|
||||||
|
photoIds: photoIds,
|
||||||
|
photoIdSet: photoIdSet,
|
||||||
|
matchCount: isNaN(parsedCount) ? photoIds.length : parsedCount,
|
||||||
|
clearUrl: stripFaceAiStateFromUrl(window.location.href)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFaceAiFilterCopy() {
|
||||||
|
var lang = String(getCurrentLangValue() || "it").toLowerCase();
|
||||||
|
if (lang.indexOf("en") === 0) {
|
||||||
|
return {
|
||||||
|
bannerTitle: "Face ID filter active",
|
||||||
|
bannerText: "Showing <strong>{count}</strong> matched photos on the normal race page.",
|
||||||
|
clearText: "Show all race photos",
|
||||||
|
emptyText: "No matched FaceAI photos are available in this current race view."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
bannerTitle: "Filtro Face ID attivo",
|
||||||
|
bannerText: "Mostro <strong>{count}</strong> foto corrispondenti nella normale pagina gara.",
|
||||||
|
clearText: "Mostra tutte le foto della gara",
|
||||||
|
emptyText: "Nessuna foto FaceAI corrispondente e disponibile in questa vista della gara."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFaceAiLegacyCount(visibleCount) {
|
||||||
|
var countValue = $("#faceAiPhotoCountValue");
|
||||||
|
if (!countValue.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
countValue.text(String(visibleCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeFaceAiAttribute(value) {
|
||||||
|
return String(value || "").replace(/&/g, "&").replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFaceAiNumericPhotoId(value) {
|
||||||
|
return /^\d+$/.test(String(value || "").trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFaceAiLookupEndpoint() {
|
||||||
|
return "faceai_photo_lookup.jsp";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFaceAiGalleryLayoutOptions() {
|
||||||
|
var config = window.faceAiConfig || {};
|
||||||
|
if (config.galleryMode === "compact") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rowHeight: 150,
|
||||||
|
margins: 8
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshFaceAiLegacyGalleryLayout(gallery) {
|
||||||
|
var options;
|
||||||
|
|
||||||
|
if (!gallery || !gallery.length || typeof $.fn.justifiedGallery !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gallery.data("jg.controller")) {
|
||||||
|
gallery.justifiedGallery("destroy");
|
||||||
|
}
|
||||||
|
|
||||||
|
options = getFaceAiGalleryLayoutOptions();
|
||||||
|
if (options) {
|
||||||
|
gallery.justifiedGallery(options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gallery.justifiedGallery();
|
||||||
|
gallery.justifiedGallery("norewind");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFaceAiLookupToken(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.toUpperCase()
|
||||||
|
.replace(/\\+/g, "/")
|
||||||
|
.replace(/\s+/g, "_")
|
||||||
|
.replace(/[^A-Z0-9._/-]+/g, "")
|
||||||
|
.replace(/^_+|_+$/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectFaceAiCheckpointTokens(value) {
|
||||||
|
var normalized = normalizeFaceAiLookupToken(value);
|
||||||
|
var tokens = {};
|
||||||
|
var current = normalized;
|
||||||
|
|
||||||
|
while (current) {
|
||||||
|
tokens[current] = true;
|
||||||
|
current = current.replace(/^\d+\./, "");
|
||||||
|
if (tokens[current]) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current = normalized.replace(/^\d+\./, "");
|
||||||
|
while (current) {
|
||||||
|
tokens[current] = true;
|
||||||
|
if (current.indexOf("_") === -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = current.replace(/_[^_]+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findFaceAiPointIdsForCheckpoint(checkpointLabel) {
|
||||||
|
var field = $("#id_puntoFoto");
|
||||||
|
var lookupTokens = collectFaceAiCheckpointTokens(checkpointLabel);
|
||||||
|
var ids = [];
|
||||||
|
|
||||||
|
if (!field.length || !Object.keys(lookupTokens).length) {
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
field.find("option").each(function() {
|
||||||
|
var option = $(this);
|
||||||
|
var value = String(option.val() || "").trim();
|
||||||
|
var optionTokens;
|
||||||
|
var token;
|
||||||
|
|
||||||
|
if (!/^\d+$/.test(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
optionTokens = collectFaceAiCheckpointTokens(option.text());
|
||||||
|
for (token in optionTokens) {
|
||||||
|
if (optionTokens.hasOwnProperty(token) && lookupTokens[token]) {
|
||||||
|
ids.push(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFaceAiPhotoLookupData(photoKey) {
|
||||||
|
var value = String(photoKey || "").trim();
|
||||||
|
var normalizedPath = value.replace(/\\+/g, "/");
|
||||||
|
var segments = normalizedPath.split("/");
|
||||||
|
var fileName = segments.length ? String(segments[segments.length - 1] || "").trim() : "";
|
||||||
|
var checkpointLabel = segments.length > 1 ? String(segments[0] || "").trim() : "";
|
||||||
|
var raceId = String($("#id_gara").val() || "").trim();
|
||||||
|
var puntoFotoIds = checkpointLabel ? findFaceAiPointIdsForCheckpoint(checkpointLabel) : [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
photoKey: value,
|
||||||
|
request: {
|
||||||
|
photoId: value,
|
||||||
|
normalizedPhotoId: normalizedPath,
|
||||||
|
fileName: fileName,
|
||||||
|
raceId: raceId,
|
||||||
|
checkpointLabel: checkpointLabel,
|
||||||
|
puntoFotoIds: puntoFotoIds.join(",")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideFaceAiLegacyPagination() {
|
||||||
|
$("nav[aria-label='paginatore']").hide();
|
||||||
|
$("#pageRow").closest(".col-md-3").hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFaceAiLegacyBanner(state) {
|
||||||
|
var copy = getFaceAiFilterCopy();
|
||||||
|
var banner = $("#faceAiFilterBanner");
|
||||||
|
var message = copy.bannerText.replace("{count}", String(state.visibleCount));
|
||||||
|
|
||||||
|
if (!banner.length) {
|
||||||
|
var bannerHtml = ''
|
||||||
|
+ '<div class="col-12" id="faceAiFilterBannerWrap">'
|
||||||
|
+ '<div id="faceAiFilterBanner" class="alert alert-warning d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-3" role="status">'
|
||||||
|
+ '<div class="mb-2 mb-md-0">'
|
||||||
|
+ '<strong id="faceAiFilterBannerTitle"></strong> '
|
||||||
|
+ '<span id="faceAiFilterBannerText"></span>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<button type="button" id="faceAiClearFilterButton" class="btn btn-sm btn-outline-dark"></button>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '</div>';
|
||||||
|
|
||||||
|
var insertTarget = $("#faceAiResultsSummary").closest(".col-md-4").parent();
|
||||||
|
if (insertTarget.length) {
|
||||||
|
insertTarget.before(bannerHtml);
|
||||||
|
} else {
|
||||||
|
$(".container.my-3 .row.mb-5").first().prepend(bannerHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
banner = $("#faceAiFilterBanner");
|
||||||
|
$(document).off("click.faceAiClearFilter", "#faceAiClearFilterButton");
|
||||||
|
$(document).on("click.faceAiClearFilter", "#faceAiClearFilterButton", function() {
|
||||||
|
window.location.href = state.clearUrl;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#faceAiFilterBannerTitle").text(copy.bannerTitle);
|
||||||
|
$("#faceAiFilterBannerText").html(message);
|
||||||
|
$("#faceAiClearFilterButton").text(copy.clearText);
|
||||||
|
$("#faceAiEmptyResultsMessage").remove();
|
||||||
|
|
||||||
|
if (state.visibleCount > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var emptyMessage = '<div class="col-12" id="faceAiEmptyResultsMessage"><div class="alert alert-light border mb-3">' + faceAiEscapeHtml(copy.emptyText) + '</div></div>';
|
||||||
|
var gallery = $("#demo");
|
||||||
|
if (gallery.length) {
|
||||||
|
gallery.closest(".col-md-10").before(emptyMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFaceAiLegacyPhotoCard(photoKey, legacyId, imageSrc) {
|
||||||
|
var safePhotoKey = escapeFaceAiAttribute(photoKey);
|
||||||
|
var safeLegacyId = escapeFaceAiAttribute(legacyId);
|
||||||
|
var safeImageSrc = escapeFaceAiAttribute(imageSrc);
|
||||||
|
var captionLabel = String(getCurrentLangValue() || "it").toLowerCase().indexOf("en") === 0 ? "Photo ID" : "ID foto";
|
||||||
|
|
||||||
|
return $(
|
||||||
|
'<a href="javascript:mostraFoto(' + safeLegacyId + ')" data-faceai-photo-id="' + safePhotoKey + '" data-faceai-legacy-id="' + safeLegacyId + '">' +
|
||||||
|
'<img src="' + safeImageSrc + '" alt="" class="thumb" />' +
|
||||||
|
'<div class="caption">' + captionLabel + ': ' + faceAiEscapeHtml(photoKey) + '</div>' +
|
||||||
|
'</a>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFaceAiImageSrc(htmlPayload) {
|
||||||
|
var parsedNodes = $.parseHTML(htmlPayload || "", document, true) || [];
|
||||||
|
var wrapper = $("<div></div>").append(parsedNodes);
|
||||||
|
var image = wrapper.find("img").first();
|
||||||
|
if (!image.length) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return image.attr("src") || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchFaceAiLegacyPhotoCard(photoKey) {
|
||||||
|
var lookupData = getFaceAiPhotoLookupData(photoKey);
|
||||||
|
|
||||||
|
return $.getJSON(getFaceAiLookupEndpoint(), lookupData.request).then(function(payload) {
|
||||||
|
if (!payload || !payload.found || !payload.legacyId || !payload.thumbSrc) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildFaceAiLegacyPhotoCard(payload.photoId || lookupData.photoKey, payload.legacyId, payload.thumbSrc);
|
||||||
|
}, function() {
|
||||||
|
if (!isFaceAiNumericPhotoId(photoKey)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $.get("Foto2.abl?cmd=mostraFoto&id_foto=" + encodeURIComponent(photoKey)).then(function(htmlPayload) {
|
||||||
|
var imageSrc = extractFaceAiImageSrc(htmlPayload);
|
||||||
|
if (!imageSrc) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildFaceAiLegacyPhotoCard(photoKey, photoKey, imageSrc);
|
||||||
|
}, function() {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneFaceAiExistingPhotoCard(item) {
|
||||||
|
if (!item || !item.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var clone = item.clone(true, true);
|
||||||
|
clone.attr("data-faceai-photo-id", item.attr("data-faceai-photo-id") || "");
|
||||||
|
clone.attr("data-faceai-legacy-id", item.attr("data-faceai-legacy-id") || "");
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectFaceAiExistingPhotoCards(gallery, matchState) {
|
||||||
|
var cardsById = {};
|
||||||
|
|
||||||
|
gallery.find("[data-faceai-photo-id]").each(function() {
|
||||||
|
var item = $(this);
|
||||||
|
var photoId = String(item.attr("data-faceai-photo-id") || "").trim();
|
||||||
|
if (!photoId || !matchState.photoIdSet[photoId] || cardsById[photoId]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cardsById[photoId] = cloneFaceAiExistingPhotoCard(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
return cardsById;
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeFaceAiLegacyGallery(gallery, matchState, cards) {
|
||||||
|
var visibleCards = [];
|
||||||
|
var index;
|
||||||
|
var activeRequestId = window.faceAiLegacyGalleryRequestId || 0;
|
||||||
|
|
||||||
|
if (matchState.requestId !== activeRequestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gallery.empty();
|
||||||
|
for (index = 0; index < cards.length; index += 1) {
|
||||||
|
if (!cards[index] || !cards[index].length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visibleCards.push(cards[index]);
|
||||||
|
gallery.append(cards[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
matchState.visibleCount = visibleCards.length;
|
||||||
|
window.faceAiMatchState = matchState;
|
||||||
|
updateFaceAiLegacyCount(matchState.visibleCount);
|
||||||
|
renderFaceAiLegacyBanner(matchState);
|
||||||
|
refreshFaceAiLegacyGalleryLayout(gallery);
|
||||||
|
|
||||||
|
$(document).trigger("faceai:gallery-ready", [matchState]);
|
||||||
|
|
||||||
|
logFaceAiDebug("Applied legacy FaceAI filter", {
|
||||||
|
matchCount: matchState.matchCount,
|
||||||
|
visibleCount: matchState.visibleCount,
|
||||||
|
photoIds: matchState.photoIds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFaceAiLegacyGallery(gallery, matchState) {
|
||||||
|
var existingCards = collectFaceAiExistingPhotoCards(gallery, matchState);
|
||||||
|
var orderedCards = [];
|
||||||
|
var index;
|
||||||
|
|
||||||
|
updateFaceAiLegacyCount(0);
|
||||||
|
gallery.empty().append('<div class="faceai-loading text-center w-100 py-4">FaceAI loading...</div>');
|
||||||
|
|
||||||
|
for (index = 0; index < matchState.photoIds.length; index += 1) {
|
||||||
|
(function(photoId) {
|
||||||
|
if (existingCards[photoId]) {
|
||||||
|
orderedCards.push($.Deferred().resolve(existingCards[photoId]).promise());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
orderedCards.push(fetchFaceAiLegacyPhotoCard(photoId));
|
||||||
|
}(matchState.photoIds[index]));
|
||||||
|
}
|
||||||
|
|
||||||
|
$.when.apply($, orderedCards).then(function() {
|
||||||
|
var resolvedCards = orderedCards.length === 1 ? [arguments[0]] : Array.prototype.slice.call(arguments);
|
||||||
|
finalizeFaceAiLegacyGallery(gallery, matchState, resolvedCards);
|
||||||
|
}, function() {
|
||||||
|
finalizeFaceAiLegacyGallery(gallery, matchState, []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareFaceAiLegacyRacePage() {
|
||||||
|
var matchState = getFaceAiMatchState();
|
||||||
|
var gallery = $("#demo");
|
||||||
|
var preparationKey;
|
||||||
|
if (!matchState || !gallery.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
preparationKey = window.location.pathname + "?" + window.location.search;
|
||||||
|
if (window.faceAiLegacyPreparationKey === preparationKey) {
|
||||||
|
return window.faceAiMatchState || matchState;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.faceAiLegacyPreparationKey = preparationKey;
|
||||||
|
window.faceAiLegacyGalleryRequestId = (window.faceAiLegacyGalleryRequestId || 0) + 1;
|
||||||
|
matchState.requestId = window.faceAiLegacyGalleryRequestId;
|
||||||
|
matchState.visibleCount = 0;
|
||||||
|
hideFaceAiLegacyPagination();
|
||||||
|
renderFaceAiLegacyBanner(matchState);
|
||||||
|
loadFaceAiLegacyGallery(gallery, matchState);
|
||||||
|
|
||||||
|
return matchState;
|
||||||
|
}
|
||||||
|
|
||||||
function buildFaceAiLaunchUrl() {
|
function buildFaceAiLaunchUrl() {
|
||||||
var raceId = $("#id_gara").val() || "";
|
var raceId = $("#id_gara").val() || "";
|
||||||
var raceSlug = $("#garaDesc").val() || "";
|
var raceSlug = $("#garaDesc").val() || "";
|
||||||
|
|
@ -261,7 +694,7 @@ function buildFaceAiLaunchUrl() {
|
||||||
var raceStorageRelativeDir = $("#faceAiRaceStorageRelativeDir").val() || [raceYear, raceMonthFolder, raceFolder].filter(Boolean).join("/");
|
var raceStorageRelativeDir = $("#faceAiRaceStorageRelativeDir").val() || [raceYear, raceMonthFolder, raceFolder].filter(Boolean).join("/");
|
||||||
var lang = getCurrentLangValue();
|
var lang = getCurrentLangValue();
|
||||||
var handoffUrl = (window.faceAiSimulator && window.faceAiSimulator.handoffUrl) || "faceai_handoff.php";
|
var handoffUrl = (window.faceAiSimulator && window.faceAiSimulator.handoffUrl) || "faceai_handoff.php";
|
||||||
var returnUrl = (window.faceAiSimulator && window.faceAiSimulator.returnUrl) || window.location.href;
|
var returnUrl = stripFaceAiStateFromUrl((window.faceAiSimulator && window.faceAiSimulator.returnUrl) || window.location.href);
|
||||||
var query = [
|
var query = [
|
||||||
"raceId=" + encodeURIComponent(raceId),
|
"raceId=" + encodeURIComponent(raceId),
|
||||||
"raceSlug=" + encodeURIComponent(raceSlug),
|
"raceSlug=" + encodeURIComponent(raceSlug),
|
||||||
|
|
@ -558,6 +991,7 @@ function goPage()
|
||||||
|
|
||||||
$(function() {
|
$(function() {
|
||||||
clearLegacyLoadingState();
|
clearLegacyLoadingState();
|
||||||
|
prepareFaceAiLegacyRacePage();
|
||||||
initFaceAiRaceSearchButton();
|
initFaceAiRaceSearchButton();
|
||||||
initFaceAiErrorModal();
|
initFaceAiErrorModal();
|
||||||
logFaceAiDebug("Legacy race page ready");
|
logFaceAiDebug("Legacy race page ready");
|
||||||
|
|
|
||||||
299
www/faceai_photo_lookup.jsp
Normal file
299
www/faceai_photo_lookup.jsp
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
<%@ page contentType="application/json; charset=UTF-8" %>
|
||||||
|
<%@ page language="java" import="java.lang.reflect.Constructor" %>
|
||||||
|
<%@ page language="java" import="java.lang.reflect.Method" %>
|
||||||
|
<%!
|
||||||
|
private String faceAiLookupTrim(String value) {
|
||||||
|
return value == null ? "" : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String faceAiLookupEscapeJson(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder(value.length() + 16);
|
||||||
|
for (int index = 0; index < value.length(); index++) {
|
||||||
|
char current = value.charAt(index);
|
||||||
|
switch (current) {
|
||||||
|
case '\\':
|
||||||
|
builder.append("\\\\");
|
||||||
|
break;
|
||||||
|
case '"':
|
||||||
|
builder.append("\\\"");
|
||||||
|
break;
|
||||||
|
case '\b':
|
||||||
|
builder.append("\\b");
|
||||||
|
break;
|
||||||
|
case '\f':
|
||||||
|
builder.append("\\f");
|
||||||
|
break;
|
||||||
|
case '\n':
|
||||||
|
builder.append("\\n");
|
||||||
|
break;
|
||||||
|
case '\r':
|
||||||
|
builder.append("\\r");
|
||||||
|
break;
|
||||||
|
case '\t':
|
||||||
|
builder.append("\\t");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (current < 0x20) {
|
||||||
|
String hex = Integer.toHexString(current);
|
||||||
|
builder.append("\\u");
|
||||||
|
for (int padding = hex.length(); padding < 4; padding++) {
|
||||||
|
builder.append('0');
|
||||||
|
}
|
||||||
|
builder.append(hex);
|
||||||
|
} else {
|
||||||
|
builder.append(current);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object faceAiLookupInvoke(Object target, String methodName, Class[] paramTypes, Object[] args) {
|
||||||
|
if (target == null || methodName == null || methodName.length() == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Method method = target.getClass().getMethod(methodName, paramTypes == null ? new Class[0] : paramTypes);
|
||||||
|
return method.invoke(target, args == null ? new Object[0] : args);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long faceAiLookupLong(Object value) {
|
||||||
|
if (value instanceof Number) {
|
||||||
|
return ((Number) value).longValue();
|
||||||
|
}
|
||||||
|
if (value != null) {
|
||||||
|
try {
|
||||||
|
return Long.parseLong(String.valueOf(value));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String faceAiLookupString(Object value) {
|
||||||
|
return value == null ? "" : String.valueOf(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean faceAiLookupIsSafeValue(String value) {
|
||||||
|
return value == null || value.indexOf('\'') == -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String faceAiLookupNormalizePhotoId(String value) {
|
||||||
|
return faceAiLookupTrim(value).replace('\\', '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
private String faceAiLookupNormalizeFileName(String value) {
|
||||||
|
String normalized = faceAiLookupNormalizePhotoId(value);
|
||||||
|
int lastSlash = normalized.lastIndexOf('/');
|
||||||
|
if (lastSlash >= 0 && lastSlash < normalized.length() - 1) {
|
||||||
|
normalized = normalized.substring(lastSlash + 1);
|
||||||
|
}
|
||||||
|
return faceAiLookupTrim(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String faceAiLookupCompositeFileName(String photoId) {
|
||||||
|
String normalized = faceAiLookupNormalizePhotoId(photoId);
|
||||||
|
if (normalized.length() == 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return normalized.replace('/', '_').replace(':', '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
private long[] faceAiLookupParseLongList(String value) {
|
||||||
|
if (value == null || value.trim().length() == 0) {
|
||||||
|
return new long[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] parts = value.split(",");
|
||||||
|
long[] parsed = new long[parts.length];
|
||||||
|
int count = 0;
|
||||||
|
for (int index = 0; index < parts.length; index++) {
|
||||||
|
String part = parts[index] == null ? "" : parts[index].trim();
|
||||||
|
if (!part.matches("^\\d+$")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed[count] = Long.parseLong(part);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == parsed.length) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
long[] compact = new long[count];
|
||||||
|
for (int index = 0; index < count; index++) {
|
||||||
|
compact[index] = parsed[index];
|
||||||
|
}
|
||||||
|
return compact;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object faceAiLookupInstantiateFoto(Class fotoClass, Constructor constructor, Object apFull) throws Exception {
|
||||||
|
return constructor.newInstance(new Object[] { apFull });
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean faceAiLookupHasResolvedPhoto(Object foto) {
|
||||||
|
return faceAiLookupLong(faceAiLookupInvoke(foto, "getDBState", null, null)) == 1L
|
||||||
|
&& faceAiLookupLong(faceAiLookupInvoke(foto, "getId_foto", null, null)) > 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object faceAiLookupResolvePhoto(Class fotoClass, Constructor constructor, Object apFull, String photoId, String normalizedPhotoId, String fileName, long raceId, long[] puntoFotoIds) throws Exception {
|
||||||
|
Method findByPrimaryKey = fotoClass.getMethod("findByPrimaryKey", new Class[] { long.class });
|
||||||
|
Method findByFoto = fotoClass.getMethod("findByFoto", new Class[] { String.class });
|
||||||
|
Method findByFilenamePuntoFoto = fotoClass.getMethod("findByFilenamePuntoFoto", new Class[] { String.class, long.class });
|
||||||
|
Method findByFilenameGara = fotoClass.getMethod("findByFilenameGara", new Class[] { String.class, long.class });
|
||||||
|
String compositeFileName = faceAiLookupCompositeFileName(photoId);
|
||||||
|
|
||||||
|
if (photoId.matches("^\\d+$")) {
|
||||||
|
Object foto = faceAiLookupInstantiateFoto(fotoClass, constructor, apFull);
|
||||||
|
findByPrimaryKey.invoke(foto, new Object[] { Long.valueOf(Long.parseLong(photoId)) });
|
||||||
|
if (faceAiLookupHasResolvedPhoto(foto)) {
|
||||||
|
return foto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] fotoCandidates = new String[] {
|
||||||
|
photoId,
|
||||||
|
normalizedPhotoId,
|
||||||
|
compositeFileName,
|
||||||
|
fileName
|
||||||
|
};
|
||||||
|
for (int index = 0; index < fotoCandidates.length; index++) {
|
||||||
|
String candidate = faceAiLookupTrim(fotoCandidates[index]);
|
||||||
|
if (candidate.length() == 0 || !faceAiLookupIsSafeValue(candidate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object foto = faceAiLookupInstantiateFoto(fotoClass, constructor, apFull);
|
||||||
|
findByFoto.invoke(foto, new Object[] { candidate });
|
||||||
|
if (faceAiLookupHasResolvedPhoto(foto)) {
|
||||||
|
return foto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileName.length() > 0 && faceAiLookupIsSafeValue(fileName)) {
|
||||||
|
for (int index = 0; index < puntoFotoIds.length; index++) {
|
||||||
|
if (puntoFotoIds[index] <= 0L) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object foto = faceAiLookupInstantiateFoto(fotoClass, constructor, apFull);
|
||||||
|
findByFilenamePuntoFoto.invoke(foto, new Object[] { fileName, Long.valueOf(puntoFotoIds[index]) });
|
||||||
|
if (faceAiLookupHasResolvedPhoto(foto)) {
|
||||||
|
return foto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raceId > 0L) {
|
||||||
|
Object foto = faceAiLookupInstantiateFoto(fotoClass, constructor, apFull);
|
||||||
|
findByFilenameGara.invoke(foto, new Object[] { fileName, Long.valueOf(raceId) });
|
||||||
|
if (faceAiLookupHasResolvedPhoto(foto)) {
|
||||||
|
return foto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compositeFileName.length() > 0 && raceId > 0L && faceAiLookupIsSafeValue(compositeFileName)) {
|
||||||
|
Object foto = faceAiLookupInstantiateFoto(fotoClass, constructor, apFull);
|
||||||
|
findByFilenameGara.invoke(foto, new Object[] { compositeFileName, Long.valueOf(raceId) });
|
||||||
|
if (faceAiLookupHasResolvedPhoto(foto)) {
|
||||||
|
return foto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return faceAiLookupInstantiateFoto(fotoClass, constructor, apFull);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object faceAiLookupResolveApFull(javax.servlet.jsp.PageContext pageContext) {
|
||||||
|
if (pageContext == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object[] candidates = new Object[] {
|
||||||
|
pageContext.findAttribute("user"),
|
||||||
|
pageContext.findAttribute("utenteLogon")
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int index = 0; index < candidates.length; index++) {
|
||||||
|
Object candidate = candidates[index];
|
||||||
|
Object apFull = faceAiLookupInvoke(candidate, "getApFull", null, null);
|
||||||
|
if (apFull != null) {
|
||||||
|
return apFull;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
%>
|
||||||
|
<%
|
||||||
|
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
|
||||||
|
response.setHeader("Pragma", "no-cache");
|
||||||
|
|
||||||
|
String rawPhotoId = request.getParameter("photoId");
|
||||||
|
String photoId = faceAiLookupTrim(rawPhotoId);
|
||||||
|
String normalizedPhotoId = faceAiLookupNormalizePhotoId(request.getParameter("normalizedPhotoId"));
|
||||||
|
String fileName = faceAiLookupNormalizeFileName(request.getParameter("fileName"));
|
||||||
|
long raceId = faceAiLookupLong(request.getParameter("raceId"));
|
||||||
|
long[] puntoFotoIds = faceAiLookupParseLongList(request.getParameter("puntoFotoIds"));
|
||||||
|
|
||||||
|
if (photoId.length() == 0) {
|
||||||
|
response.setStatus(400);
|
||||||
|
out.print("{\"found\":false,\"error\":\"missing-photo-id\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!faceAiLookupIsSafeValue(photoId) || !faceAiLookupIsSafeValue(normalizedPhotoId) || !faceAiLookupIsSafeValue(fileName)) {
|
||||||
|
response.setStatus(400);
|
||||||
|
out.print("{\"found\":false,\"photoId\":\"" + faceAiLookupEscapeJson(photoId) + "\",\"error\":\"invalid-photo-id\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object apFull = faceAiLookupResolveApFull(pageContext);
|
||||||
|
if (apFull == null) {
|
||||||
|
response.setStatus(503);
|
||||||
|
out.print("{\"found\":false,\"photoId\":\"" + faceAiLookupEscapeJson(photoId) + "\",\"error\":\"missing-apfull\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Class fotoClass = Class.forName("it.acxent.pg.Foto");
|
||||||
|
Constructor constructor = fotoClass.getConstructor(new Class[] { apFull.getClass() });
|
||||||
|
Object foto = faceAiLookupResolvePhoto(fotoClass, constructor, apFull, photoId, normalizedPhotoId, fileName, raceId, puntoFotoIds);
|
||||||
|
|
||||||
|
long dbState = faceAiLookupLong(faceAiLookupInvoke(foto, "getDBState", null, null));
|
||||||
|
long legacyId = faceAiLookupLong(faceAiLookupInvoke(foto, "getId_foto", null, null));
|
||||||
|
|
||||||
|
if (dbState != 1L || legacyId <= 0L) {
|
||||||
|
response.setStatus(404);
|
||||||
|
out.print("{\"found\":false,\"photoId\":\"" + faceAiLookupEscapeJson(photoId) + "\"}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Method getFileName = fotoClass.getMethod("getFileName", new Class[] { String.class });
|
||||||
|
String thumbnailBase = faceAiLookupString(getFileName.invoke(foto, new Object[] { ".jpg" }));
|
||||||
|
String thumbSrc = "foto/" + thumbnailBase + "+tn-" + legacyId + ".jpg";
|
||||||
|
|
||||||
|
out.print("{"
|
||||||
|
+ "\"found\":true,"
|
||||||
|
+ "\"photoId\":\"" + faceAiLookupEscapeJson(photoId) + "\","
|
||||||
|
+ "\"resolvedFile\":\"" + faceAiLookupEscapeJson(faceAiLookupString(faceAiLookupInvoke(foto, "getFile", null, null))) + "\","
|
||||||
|
+ "\"legacyId\":" + legacyId + ","
|
||||||
|
+ "\"thumbSrc\":\"" + faceAiLookupEscapeJson(thumbSrc) + "\""
|
||||||
|
+ "}");
|
||||||
|
} catch (Exception error) {
|
||||||
|
log("FaceAI photo lookup failed for " + photoId, error);
|
||||||
|
response.setStatus(500);
|
||||||
|
out.print("{\"found\":false,\"photoId\":\"" + faceAiLookupEscapeJson(photoId) + "\",\"error\":\"lookup-failed\"}");
|
||||||
|
}
|
||||||
|
%>
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
require_once __DIR__ . '/faceai_config.php';
|
require_once __DIR__ . '/faceai_config.php';
|
||||||
require_once __DIR__ . '/faceai_simulator_view.php';
|
|
||||||
|
|
||||||
$config = faceai_config();
|
$config = faceai_config();
|
||||||
|
|
||||||
|
|
@ -39,31 +38,28 @@ try {
|
||||||
'token' => $token
|
'token' => $token
|
||||||
));
|
));
|
||||||
$result = faceai_fetch_json($bridgeUrl);
|
$result = faceai_fetch_json($bridgeUrl);
|
||||||
|
$returnUrl = (string) ($result['returnUrl'] ?? '');
|
||||||
$matches = is_array($result['matches'] ?? null) ? $result['matches'] : array();
|
$matches = is_array($result['matches'] ?? null) ? $result['matches'] : array();
|
||||||
$photos = array_map(static function ($match) {
|
|
||||||
$photoId = (string) ($match['photoId'] ?? ($match['id'] ?? ''));
|
|
||||||
|
|
||||||
return array(
|
if ($returnUrl === '') {
|
||||||
'id' => $photoId,
|
throw new RuntimeException('Missing legacy return URL.');
|
||||||
'photoId' => $photoId,
|
}
|
||||||
'label' => (string) ($match['label'] ?? $photoId),
|
|
||||||
'checkpoint' => (string) ($match['checkpoint'] ?? '-'),
|
|
||||||
'previewUrl' => (string) ($match['previewUrl'] ?? ''),
|
|
||||||
'score' => $match['score'] ?? null,
|
|
||||||
);
|
|
||||||
}, $matches);
|
|
||||||
|
|
||||||
faceai_sim_render_page(array(
|
$photoIds = array();
|
||||||
'raceId' => (string) ($result['raceId'] ?? ($payload['raceId'] ?? '')),
|
foreach ($matches as $match) {
|
||||||
'lang' => (string) ($result['lang'] ?? 'it'),
|
$photoId = trim((string) ($match['photoId'] ?? ($match['id'] ?? '')));
|
||||||
'raceSlug' => (string) ($result['raceId'] ?? ($payload['raceId'] ?? '')),
|
if ($photoId === '') {
|
||||||
'raceName' => (string) ($result['raceName'] ?? ('Race ' . ($payload['raceId'] ?? ''))),
|
continue;
|
||||||
'returnUrl' => (string) ($result['returnUrl'] ?? 'faceai_simulator.php'),
|
}
|
||||||
'banner' => 'Vista filtrata da FaceAI. Sono state trovate <strong>' . count($photos) . '</strong> foto corrispondenti per l utente corrente.',
|
$photoIds[$photoId] = true;
|
||||||
'totalLabel' => count($photos) . ' foto da FaceAI',
|
}
|
||||||
'photos' => $photos,
|
|
||||||
'showSimulatorBootstrap' => false
|
header('Location: ' . faceai_build_url($returnUrl, array(
|
||||||
));
|
'faceaiMatchSource' => 'faceai',
|
||||||
|
'faceaiMatchCount' => count($photoIds),
|
||||||
|
'faceaiPhotoIds' => implode(',', array_keys($photoIds))
|
||||||
|
)), true, 302);
|
||||||
|
exit;
|
||||||
} 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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,7 @@ if (!faceAiRaceYear.isEmpty()) {
|
||||||
<hr>
|
<hr>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 col-inline">
|
<div class="col-md-4 col-inline">
|
||||||
<p class="lead"><acx:lang>Sono state trovate</acx:lang> <%=list.getTotNumberOfRecords()%> <acx:lang>foto</acx:lang>
|
<p class="lead" id="faceAiResultsSummary"><span id="faceAiPhotoCountLabel"><acx:lang>Sono state trovate</acx:lang></span> <span id="faceAiPhotoCountValue"><%=list.getTotNumberOfRecords()%></span> <span id="faceAiPhotoCountSuffix"><acx:lang>foto</acx:lang></span>
|
||||||
<acx:if wherecondition="<%=CR.getFlgVisCompatta()==0%>"><a href='<%=bean.getDescrizioneGaraHtml()+"_garaC-"+bean.getId_gara()+"-"+CR.getId_puntoFoto()+"-"+CR.getTipoPuntoFoto()+"-"+CR.getPageRow()+"-"+CR.getPageNumber()+"-"+langpay ><i class="fa fa-search-minus" title="<acx:lang>visualizzazione compatta</acx:lang>"></i></a></acx:if>
|
<acx:if wherecondition="<%=CR.getFlgVisCompatta()==0%>"><a href='<%=bean.getDescrizioneGaraHtml()+"_garaC-"+bean.getId_gara()+"-"+CR.getId_puntoFoto()+"-"+CR.getTipoPuntoFoto()+"-"+CR.getPageRow()+"-"+CR.getPageNumber()+"-"+langpay ><i class="fa fa-search-minus" title="<acx:lang>visualizzazione compatta</acx:lang>"></i></a></acx:if>
|
||||||
<acx:else><a href='<%=bean.getDescrizioneGaraHtml()+"_garaE-"+bean.getId_gara()+"-"+CR.getId_puntoFoto()+"-"+CR.getTipoPuntoFoto()+"-"+CR.getPageRow()+"-"+CR.getPageNumber()+"-"+lang+".html"%>' ><i class="fa fa-search-plus" title="<acx:lang>visualizzazione standard</acx:lang>"></i></a></acx:else>
|
<acx:else><a href='<%=bean.getDescrizioneGaraHtml()+"_garaE-"+bean.getId_gara()+"-"+CR.getId_puntoFoto()+"-"+CR.getTipoPuntoFoto()+"-"+CR.getPageRow()+"-"+CR.getPageNumber()+"-"+lang+".html"%>' ><i class="fa fa-search-plus" title="<acx:lang>visualizzazione standard</acx:lang>"></i></a></acx:else>
|
||||||
<div id="vis" style="visibility: hidden"></div>
|
<div id="vis" style="visibility: hidden"></div>
|
||||||
|
|
@ -258,10 +258,10 @@ if (!faceAiRaceYear.isEmpty()) {
|
||||||
<div class="col-md-10" style="min-height: 500px">
|
<div class="col-md-10" style="min-height: 500px">
|
||||||
<div id="demo">
|
<div id="demo">
|
||||||
<acx:whilevec rowbeanclass="it.acxent.pg.Foto" vectumerator="list">
|
<acx:whilevec rowbeanclass="it.acxent.pg.Foto" vectumerator="list">
|
||||||
<acx:if_logon_ok> <a href="javascript:mostraFoto(<%=rowBean.getId_foto()%>)"> <img src="foto/<%=rowBean.getFileName(".jpg")%>+tn-<%=rowBean.getId_foto()%>.jpg" alt="" class="thumb" />
|
<acx:if_logon_ok> <a href="javascript:mostraFoto(<%=rowBean.getId_foto()%>)" data-faceai-photo-id="<%=rowBean.getId_foto()%>"> <img src="foto/<%=rowBean.getFileName(".jpg")%>+tn-<%=rowBean.getId_foto()%>.jpg" alt="" class="thumb" />
|
||||||
<div class="caption"><acx:lang>Hits</acx:lang>: <%=rowBean.getImpression()%> - <%=df.format(rowBean.getDataUltimaVisual())%> </div>
|
<div class="caption"><acx:lang>Hits</acx:lang>: <%=rowBean.getImpression()%> - <%=df.format(rowBean.getDataUltimaVisual())%> </div>
|
||||||
</a> </acx:if_logon_ok>
|
</a> </acx:if_logon_ok>
|
||||||
<acx:else_logon> <a data-toggle="modal" data-target="#ModalNoLog"> <img src="foto/<%=rowBean.getFile()%>?id_foto=<%=rowBean.getId_foto()%>" alt="" class="thumb" />
|
<acx:else_logon> <a data-toggle="modal" data-target="#ModalNoLog" data-faceai-photo-id="<%=rowBean.getId_foto()%>"> <img src="foto/<%=rowBean.getFile()%>?id_foto=<%=rowBean.getId_foto()%>" alt="" class="thumb" />
|
||||||
<div class="caption"><acx:lang>Hits</acx:lang>: <%=rowBean.getImpression()%> - <%=df.format(rowBean.getDataUltimaVisual())%> </div>
|
<div class="caption"><acx:lang>Hits</acx:lang>: <%=rowBean.getImpression()%> - <%=df.format(rowBean.getDataUltimaVisual())%> </div>
|
||||||
</a> </acx:else_logon>
|
</a> </acx:else_logon>
|
||||||
</acx:whilevec>
|
</acx:whilevec>
|
||||||
|
|
@ -376,6 +376,8 @@ if (!faceAiRaceYear.isEmpty()) {
|
||||||
<script>
|
<script>
|
||||||
window.faceAiConfig = window.faceAiConfig || {};
|
window.faceAiConfig = window.faceAiConfig || {};
|
||||||
window.faceAiConfig.enabled = <%= faceAiFeatureEnabled ? "true" : "false" %>;
|
window.faceAiConfig.enabled = <%= faceAiFeatureEnabled ? "true" : "false" %>;
|
||||||
|
window.faceAiConfig.lang = "<%= lang %>";
|
||||||
|
window.faceAiConfig.galleryMode = '<%= CR.getFlgVisCompatta()==1 ? "compact" : "standard" %>';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,7 @@ if (!faceAiRaceYear.isEmpty()) {
|
||||||
<hr>
|
<hr>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 col-inline">
|
<div class="col-md-4 col-inline">
|
||||||
<p class="lead"><acx:lang>Sono state trovate</acx:lang> <%=list.getTotNumberOfRecords()%> <acx:lang>foto</acx:lang>
|
<p class="lead" id="faceAiResultsSummary"><span id="faceAiPhotoCountLabel"><acx:lang>Sono state trovate</acx:lang></span> <span id="faceAiPhotoCountValue"><%=list.getTotNumberOfRecords()%></span> <span id="faceAiPhotoCountSuffix"><acx:lang>foto</acx:lang></span>
|
||||||
<acx:if wherecondition="<%=CR.getFlgVisCompatta()==0%>"><a href="<%=bean.getDescrizioneGaraHtml()+"_garaC-"+bean.getId_gara()+"-"+CR.getId_puntoFoto()+"-"+CR.getTipoPuntoFoto()+"-"+CR.getPageRow()+"-"+CR.getPageNumber()+".html"%>"><i class="fa fa-search-minus" title="<acx:lang>visualizzazione compatta</acx:lang>"></i></a></acx:if>
|
<acx:if wherecondition="<%=CR.getFlgVisCompatta()==0%>"><a href="<%=bean.getDescrizioneGaraHtml()+"_garaC-"+bean.getId_gara()+"-"+CR.getId_puntoFoto()+"-"+CR.getTipoPuntoFoto()+"-"+CR.getPageRow()+"-"+CR.getPageNumber()+".html"%>"><i class="fa fa-search-minus" title="<acx:lang>visualizzazione compatta</acx:lang>"></i></a></acx:if>
|
||||||
<acx:else><a href="<%=bean.getDescrizioneGaraHtml()+"_garaE-"+bean.getId_gara()+"-"+CR.getId_puntoFoto()+"-"+CR.getTipoPuntoFoto()+"-"+CR.getPageRow()+"-"+CR.getPageNumber()+".html"%>"><i class="fa fa-search-plus" title="<acx:lang>visualizzazione standard</acx:lang>"></i></a></acx:else>
|
<acx:else><a href="<%=bean.getDescrizioneGaraHtml()+"_garaE-"+bean.getId_gara()+"-"+CR.getId_puntoFoto()+"-"+CR.getTipoPuntoFoto()+"-"+CR.getPageRow()+"-"+CR.getPageNumber()+".html"%>"><i class="fa fa-search-plus" title="<acx:lang>visualizzazione standard</acx:lang>"></i></a></acx:else>
|
||||||
<div id="vis" style="visibility: hidden"></div>
|
<div id="vis" style="visibility: hidden"></div>
|
||||||
|
|
@ -258,10 +258,10 @@ if (!faceAiRaceYear.isEmpty()) {
|
||||||
<div class="col-md-10" style="min-height: 500px">
|
<div class="col-md-10" style="min-height: 500px">
|
||||||
<div id="demo">
|
<div id="demo">
|
||||||
<acx:whilevec rowbeanclass="it.acxent.pg.Foto" vectumerator="list">
|
<acx:whilevec rowbeanclass="it.acxent.pg.Foto" vectumerator="list">
|
||||||
<acx:if_logon_ok> <a href="javascript:mostraFoto(<%=rowBean.getId_foto()%>)"> <img src="foto/<%=rowBean.getFileName(".jpg")%>+tn-<%=rowBean.getId_foto()%>.jpg" alt="" class="thumb" />
|
<acx:if_logon_ok> <a href="javascript:mostraFoto(<%=rowBean.getId_foto()%>)" data-faceai-photo-id="<%=rowBean.getId_foto()%>"> <img src="foto/<%=rowBean.getFileName(".jpg")%>+tn-<%=rowBean.getId_foto()%>.jpg" alt="" class="thumb" />
|
||||||
<div class="caption"><acx:lang>Hits</acx:lang>: <%=rowBean.getImpression()%> - <%=df.format(rowBean.getDataUltimaVisual())%> </div>
|
<div class="caption"><acx:lang>Hits</acx:lang>: <%=rowBean.getImpression()%> - <%=df.format(rowBean.getDataUltimaVisual())%> </div>
|
||||||
</a> </acx:if_logon_ok>
|
</a> </acx:if_logon_ok>
|
||||||
<acx:else_logon> <a data-toggle="modal" data-target="#ModalNoLog"> <img src="foto/<%=rowBean.getFile()%>?id_foto=<%=rowBean.getId_foto()%>" alt="" class="thumb" />
|
<acx:else_logon> <a data-toggle="modal" data-target="#ModalNoLog" data-faceai-photo-id="<%=rowBean.getId_foto()%>"> <img src="foto/<%=rowBean.getFile()%>?id_foto=<%=rowBean.getId_foto()%>" alt="" class="thumb" />
|
||||||
<div class="caption"><acx:lang>Hits</acx:lang>: <%=rowBean.getImpression()%> - <%=df.format(rowBean.getDataUltimaVisual())%> </div>
|
<div class="caption"><acx:lang>Hits</acx:lang>: <%=rowBean.getImpression()%> - <%=df.format(rowBean.getDataUltimaVisual())%> </div>
|
||||||
</a> </acx:else_logon>
|
</a> </acx:else_logon>
|
||||||
</acx:whilevec>
|
</acx:whilevec>
|
||||||
|
|
@ -376,6 +376,8 @@ if (!faceAiRaceYear.isEmpty()) {
|
||||||
<script>
|
<script>
|
||||||
window.faceAiConfig = window.faceAiConfig || {};
|
window.faceAiConfig = window.faceAiConfig || {};
|
||||||
window.faceAiConfig.enabled = <%= faceAiFeatureEnabled ? "true" : "false" %>;
|
window.faceAiConfig.enabled = <%= faceAiFeatureEnabled ? "true" : "false" %>;
|
||||||
|
window.faceAiConfig.lang = "<%= lang %>";
|
||||||
|
window.faceAiConfig.galleryMode = '<%= CR.getFlgVisCompatta()==1 ? "compact" : "standard" %>';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue