From 4f003bb5a99c1cfec15f3113dcaf991d20a38f79 Mon Sep 17 00:00:00 2001 From: MaddoScientisto Date: Sun, 19 Apr 2026 16:12:48 +0200 Subject: [PATCH] feat: Add FaceAI handoff URL builder and enhance race storage metadata handling --- faceai/tests/e2e/faceai-simulator.spec.js | 30 ++++++++ faceai/tests/live-site/live-race.spec.js | 86 +++++++++++++++++++++-- www/faceai_photo_lookup.jsp | 23 ++++-- www/fotoCR-en.jsp | 56 +++++++++++---- www/fotoCR.jsp | 56 +++++++++++---- 5 files changed, 214 insertions(+), 37 deletions(-) diff --git a/faceai/tests/e2e/faceai-simulator.spec.js b/faceai/tests/e2e/faceai-simulator.spec.js index 2aef622a..033cd429 100644 --- a/faceai/tests/e2e/faceai-simulator.spec.js +++ b/faceai/tests/e2e/faceai-simulator.spec.js @@ -50,6 +50,15 @@ async function enterViaHandoff(page, options = {}) { await waitForFaceAiHome(page); } +async function readLaunchUrlFromLegacyPage(page) { + const launchUrl = await page.evaluate(() => { + return typeof buildFaceAiLaunchUrl === 'function' ? buildFaceAiLaunchUrl() : ''; + }); + + expect(launchUrl, 'Expected the simulator race page to expose a FaceAI handoff URL builder.').toBeTruthy(); + return new URL(launchUrl, 'http://127.0.0.1:8080'); +} + async function startSearch(page, selfieName) { const createResponsePromise = page.waitForResponse((response) => { return response.url().includes('/api/searches') @@ -170,6 +179,27 @@ test('runs the simulator flow through FaceAI and returns to the filtered legacy }); }); +test('builds the simulator FaceAI handoff URL with the exact local race storage metadata', async ({ page }) => { + await page.goto(buildSimulatorUrl({ + raceId: '202', + raceSlug: 'mezza-di-pisa', + raceName: 'Mezza di Pisa', + raceYear: '2026', + raceMonthFolder: '04.APRILE', + raceFolder: 'PISA' + }), { waitUntil: 'domcontentloaded' }); + + await expect(page.locator('#faceAiRaceYear')).toHaveValue('2026'); + await expect(page.locator('#faceAiRaceMonthFolder')).toHaveValue('04.APRILE'); + await expect(page.locator('#faceAiRaceFolder')).toHaveValue('PISA'); + + const launchUrl = await readLaunchUrlFromLegacyPage(page); + expect(launchUrl.searchParams.get('raceYear')).toBe('2026'); + expect(launchUrl.searchParams.get('raceMonthFolder')).toBe('04.APRILE'); + expect(launchUrl.searchParams.get('raceFolder')).toBe('PISA'); + expect(launchUrl.searchParams.get('raceStorageRelativeDir')).toBe('2026/04.APRILE/PISA'); +}); + test('shows the unsupported-race message when the current race has no PKL data and lets the user go back', async ({ page }) => { await launchFromSimulator(page, { raceId: '404', diff --git a/faceai/tests/live-site/live-race.spec.js b/faceai/tests/live-site/live-race.spec.js index be7893f6..8806de5c 100644 --- a/faceai/tests/live-site/live-race.spec.js +++ b/faceai/tests/live-site/live-race.spec.js @@ -9,6 +9,13 @@ const { requirePortraitFixture } = require('./live-site-test-utils'); +const LIVE_EXPECTED_RACE_STORAGE = { + year: '2026', + monthFolder: '04.APRILE', + raceFolder: 'HMF_2026', + relativeDir: '2026/04.APRILE/HMF_2026' +}; + function escapeRegExp(value) { return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } @@ -32,10 +39,10 @@ async function openLiveFaceAi(page) { expect(launchUrl, 'Expected the legacy race page script to expose a FaceAI handoff URL builder.').toBeTruthy(); const parsedLaunchUrl = new URL(launchUrl, LIVE_SITE_BASE_URL); - expect(parsedLaunchUrl.searchParams.get('raceYear')).toBeTruthy(); - expect(parsedLaunchUrl.searchParams.get('raceMonthFolder')).toBeTruthy(); - expect(parsedLaunchUrl.searchParams.get('raceFolder')).toBeTruthy(); - expect(parsedLaunchUrl.searchParams.get('raceStorageRelativeDir')).toBeTruthy(); + expect(parsedLaunchUrl.searchParams.get('raceYear')).toBe(LIVE_EXPECTED_RACE_STORAGE.year); + expect(parsedLaunchUrl.searchParams.get('raceMonthFolder')).toBe(LIVE_EXPECTED_RACE_STORAGE.monthFolder); + expect(parsedLaunchUrl.searchParams.get('raceFolder')).toBe(LIVE_EXPECTED_RACE_STORAGE.raceFolder); + expect(parsedLaunchUrl.searchParams.get('raceStorageRelativeDir')).toBe(LIVE_EXPECTED_RACE_STORAGE.relativeDir); await expect(page.locator('#faceaiLaunchButton')).toBeVisible(); await page.locator('#faceaiLaunchButton').click(); @@ -125,6 +132,37 @@ async function waitForLegacyFaceAiCount(page, expectedCount) { }).toBe(String(expectedCount)); } +function basenameOfPhotoKey(photoKey) { + return String(photoKey).replace(/\\/g, '/').split('/').pop(); +} + +async function lookupLivePhoto(page, photoKey) { + return page.evaluate(async (value) => { + const lookupData = getFaceAiPhotoLookupData(value); + const params = new URLSearchParams(lookupData.request); + const response = await fetch(`${getFaceAiLookupEndpoint()}?${params.toString()}`, { + credentials: 'include' + }); + const text = await response.text(); + let payload = null; + + try { + payload = JSON.parse(text); + } catch (error) { + payload = { + parseError: String(error), + raw: text + }; + } + + return { + status: response.status, + request: lookupData.request, + payload + }; + }, photoKey); +} + async function expectLegacyFaceAiGalleryToRemainStable(page, expectedPhotoIds, holdMs = 4000) { await page.waitForTimeout(holdMs); @@ -159,6 +197,44 @@ test('renders the exact live FaceAI filtered sample URL with visible thumbnails' await expectLegacyFaceAiGalleryToRemainStable(page, samplePhotoIds); }); +test('keeps the live Firenze FaceAI race storage metadata pinned to April 2026', async ({ page }) => { + await ensureLiveAuthenticatedRacePage(page); + + await expect(page.locator('#faceAiRaceYear')).toHaveValue(LIVE_EXPECTED_RACE_STORAGE.year); + await expect(page.locator('#faceAiRaceMonthFolder')).toHaveValue(LIVE_EXPECTED_RACE_STORAGE.monthFolder); + await expect(page.locator('#faceAiRaceFolder')).toHaveValue(LIVE_EXPECTED_RACE_STORAGE.raceFolder); + await expect(page.locator('#faceAiRaceStorageRelativeDir')).toHaveValue(LIVE_EXPECTED_RACE_STORAGE.relativeDir); + + const launchUrl = await page.evaluate(() => { + return typeof buildFaceAiLaunchUrl === 'function' ? buildFaceAiLaunchUrl() : ''; + }); + const parsedLaunchUrl = new URL(launchUrl, LIVE_SITE_BASE_URL); + + expect(parsedLaunchUrl.searchParams.get('raceYear')).toBe(LIVE_EXPECTED_RACE_STORAGE.year); + expect(parsedLaunchUrl.searchParams.get('raceMonthFolder')).toBe(LIVE_EXPECTED_RACE_STORAGE.monthFolder); + expect(parsedLaunchUrl.searchParams.get('raceFolder')).toBe(LIVE_EXPECTED_RACE_STORAGE.raceFolder); + expect(parsedLaunchUrl.searchParams.get('raceStorageRelativeDir')).toBe(LIVE_EXPECTED_RACE_STORAGE.relativeDir); +}); + +test('resolves the live Firenze sample photo lookups inside the current race', async ({ page }) => { + const samplePhotoIds = [ + '00.PANORAMICA\\GIC_7918.JPG', + '02.PARTENZA\\GIC_7918.JPG' + ]; + + await ensureLiveAuthenticatedRacePage(page); + + for (const photoKey of samplePhotoIds) { + const lookup = await lookupLivePhoto(page, photoKey); + expect(lookup.status, `Expected faceai_photo_lookup.jsp to resolve ${photoKey} on the live Firenze race page.`).toBe(200); + expect(String(lookup.request.raceId || '')).toBe('1018545'); + expect(lookup.payload && lookup.payload.found, `Expected ${photoKey} to resolve within the current live race.`).toBe(true); + expect(String(lookup.payload.photoId || '')).toBe(photoKey); + expect(basenameOfPhotoKey(String(lookup.payload.resolvedFile || ''))).toBe(basenameOfPhotoKey(photoKey)); + expect(String(lookup.payload.thumbSrc || '')).toContain(`+tn-${lookup.payload.legacyId}.jpg`); + } +}); + test('loads a live race page with an authenticated session', async ({ page }) => { await ensureLiveAuthenticatedRacePage(page); @@ -174,7 +250,7 @@ test('loads a live race page with an authenticated session', async ({ page }) => test('launches the live FaceAI app with race storage metadata and a styled header', async ({ page }) => { const { consoleErrors, launchUrl } = await openLiveFaceAi(page); - expect(launchUrl.searchParams.get('raceStorageRelativeDir')).toContain('/'); + expect(launchUrl.searchParams.get('raceStorageRelativeDir')).toBe(LIVE_EXPECTED_RACE_STORAGE.relativeDir); await expect(page.locator('nav.navbar')).toBeVisible(); await expect(page.locator('link[data-legacy-href*="bootstrap.min.css"]')).toHaveCount(1); await expect(page.locator('link[data-legacy-href*="custom-style.css"]')).toHaveCount(1); diff --git a/www/faceai_photo_lookup.jsp b/www/faceai_photo_lookup.jsp index 4e1c1af5..f0d27c2c 100644 --- a/www/faceai_photo_lookup.jsp +++ b/www/faceai_photo_lookup.jsp @@ -148,6 +148,14 @@ private boolean faceAiLookupHasResolvedPhoto(Object foto) { && faceAiLookupLong(faceAiLookupInvoke(foto, "getId_foto", null, null)) > 0L; } +private boolean faceAiLookupMatchesRace(Object foto, long raceId) { + if (raceId <= 0L) { + return true; + } + + return faceAiLookupLong(faceAiLookupInvoke(foto, "getId_gara", null, null)) == raceId; +} + 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 }); @@ -158,7 +166,7 @@ private Object faceAiLookupResolvePhoto(Class fotoClass, Constructor constructor if (photoId.matches("^\\d+$")) { Object foto = faceAiLookupInstantiateFoto(fotoClass, constructor, apFull); findByPrimaryKey.invoke(foto, new Object[] { Long.valueOf(Long.parseLong(photoId)) }); - if (faceAiLookupHasResolvedPhoto(foto)) { + if (faceAiLookupHasResolvedPhoto(foto) && faceAiLookupMatchesRace(foto, raceId)) { return foto; } } @@ -166,8 +174,7 @@ private Object faceAiLookupResolvePhoto(Class fotoClass, Constructor constructor String[] fotoCandidates = new String[] { photoId, normalizedPhotoId, - compositeFileName, - fileName + compositeFileName }; for (int index = 0; index < fotoCandidates.length; index++) { String candidate = faceAiLookupTrim(fotoCandidates[index]); @@ -177,7 +184,7 @@ private Object faceAiLookupResolvePhoto(Class fotoClass, Constructor constructor Object foto = faceAiLookupInstantiateFoto(fotoClass, constructor, apFull); findByFoto.invoke(foto, new Object[] { candidate }); - if (faceAiLookupHasResolvedPhoto(foto)) { + if (faceAiLookupHasResolvedPhoto(foto) && faceAiLookupMatchesRace(foto, raceId)) { return foto; } } @@ -212,6 +219,14 @@ private Object faceAiLookupResolvePhoto(Class fotoClass, Constructor constructor } } + if (fileName.length() > 0 && raceId <= 0L && faceAiLookupIsSafeValue(fileName)) { + Object foto = faceAiLookupInstantiateFoto(fotoClass, constructor, apFull); + findByFoto.invoke(foto, new Object[] { fileName }); + if (faceAiLookupHasResolvedPhoto(foto)) { + return foto; + } + } + return faceAiLookupInstantiateFoto(fotoClass, constructor, apFull); } diff --git a/www/fotoCR-en.jsp b/www/fotoCR-en.jsp index 4beb1a69..a992adca 100644 --- a/www/fotoCR-en.jsp +++ b/www/fotoCR-en.jsp @@ -66,31 +66,59 @@ String faceAiRaceYear = ""; String faceAiRaceMonthFolder = ""; String faceAiRaceFolder = ""; String faceAiRaceStorageRelativeDir = ""; +String faceAiExpectedYear = ""; +String faceAiExpectedMonthFolder = ""; +if (faceAiRaceDate != null) { + java.util.Calendar faceAiCalendar = java.util.Calendar.getInstance(); + String[] faceAiMonthNames = new String[] { "GENNAIO", "FEBBRAIO", "MARZO", "APRILE", "MAGGIO", "GIUGNO", "LUGLIO", "AGOSTO", "SETTEMBRE", "OTTOBRE", "NOVEMBRE", "DICEMBRE" }; + int faceAiMonthIndex; + faceAiCalendar.setTime(faceAiRaceDate); + faceAiExpectedYear = String.valueOf(faceAiCalendar.get(java.util.Calendar.YEAR)); + faceAiMonthIndex = faceAiCalendar.get(java.util.Calendar.MONTH); + faceAiExpectedMonthFolder = String.format("%02d.%s", new Object[] { Integer.valueOf(faceAiMonthIndex + 1), faceAiMonthNames[faceAiMonthIndex] }); +} if (!faceAiRacePathBase.isEmpty()) { - String[] faceAiPathSegments = faceAiRacePathBase.split("/"); + String[] faceAiPathSegments = faceAiRacePathBase.replace('\\', '/').split("/"); java.util.ArrayList faceAiNormalizedSegments = new java.util.ArrayList(); + int faceAiYearIndex = -1; for (int faceAiSegmentIndex = 0; faceAiSegmentIndex < faceAiPathSegments.length; faceAiSegmentIndex++) { String faceAiSegment = faceAiPathSegments[faceAiSegmentIndex] != null ? faceAiPathSegments[faceAiSegmentIndex].trim() : ""; if (!faceAiSegment.isEmpty()) { faceAiNormalizedSegments.add(faceAiSegment); } } - if (faceAiNormalizedSegments.size() > 0) { + for (int faceAiSegmentIndex = 0; faceAiSegmentIndex < faceAiNormalizedSegments.size(); faceAiSegmentIndex++) { + String faceAiSegment = (String) faceAiNormalizedSegments.get(faceAiSegmentIndex); + if (faceAiSegment.matches("^\\d{4}$")) { + faceAiRaceYear = faceAiSegment; + faceAiYearIndex = faceAiSegmentIndex; + break; + } + } + if (faceAiYearIndex >= 0) { + if (faceAiNormalizedSegments.size() > faceAiYearIndex + 1) { + faceAiRaceMonthFolder = (String) faceAiNormalizedSegments.get(faceAiYearIndex + 1); + } + if (faceAiNormalizedSegments.size() > faceAiYearIndex + 2) { + faceAiRaceFolder = (String) faceAiNormalizedSegments.get(faceAiYearIndex + 2); + } + } else if (faceAiNormalizedSegments.size() > 0) { faceAiRaceYear = (String) faceAiNormalizedSegments.get(0); - } - if (faceAiNormalizedSegments.size() > 1) { - faceAiRaceMonthFolder = (String) faceAiNormalizedSegments.get(1); - } - if (faceAiNormalizedSegments.size() > 2) { - faceAiRaceFolder = (String) faceAiNormalizedSegments.get(2); - } else if (faceAiNormalizedSegments.size() > 1) { - faceAiRaceFolder = (String) faceAiNormalizedSegments.get(faceAiNormalizedSegments.size() - 1); + if (faceAiNormalizedSegments.size() > 1) { + faceAiRaceMonthFolder = (String) faceAiNormalizedSegments.get(1); + } + if (faceAiNormalizedSegments.size() > 2) { + faceAiRaceFolder = (String) faceAiNormalizedSegments.get(2); + } else if (faceAiNormalizedSegments.size() > 1) { + faceAiRaceFolder = (String) faceAiNormalizedSegments.get(faceAiNormalizedSegments.size() - 1); + } } } -if (faceAiRaceYear.isEmpty() && faceAiRaceDate != null) { - java.util.Calendar faceAiCalendar = java.util.Calendar.getInstance(); - faceAiCalendar.setTime(faceAiRaceDate); - faceAiRaceYear = String.valueOf(faceAiCalendar.get(java.util.Calendar.YEAR)); +if (!faceAiExpectedYear.isEmpty() && !faceAiExpectedYear.equals(faceAiRaceYear)) { + faceAiRaceYear = faceAiExpectedYear; +} +if (!faceAiExpectedMonthFolder.isEmpty() && !faceAiExpectedMonthFolder.equals(faceAiRaceMonthFolder)) { + faceAiRaceMonthFolder = faceAiExpectedMonthFolder; } if (faceAiRaceFolder.isEmpty()) { faceAiRaceFolder = String.valueOf(CR.getGara().getId_gara()); diff --git a/www/fotoCR.jsp b/www/fotoCR.jsp index 93365179..1a567482 100644 --- a/www/fotoCR.jsp +++ b/www/fotoCR.jsp @@ -66,31 +66,59 @@ String faceAiRaceYear = ""; String faceAiRaceMonthFolder = ""; String faceAiRaceFolder = ""; String faceAiRaceStorageRelativeDir = ""; +String faceAiExpectedYear = ""; +String faceAiExpectedMonthFolder = ""; +if (faceAiRaceDate != null) { + java.util.Calendar faceAiCalendar = java.util.Calendar.getInstance(); + String[] faceAiMonthNames = new String[] { "GENNAIO", "FEBBRAIO", "MARZO", "APRILE", "MAGGIO", "GIUGNO", "LUGLIO", "AGOSTO", "SETTEMBRE", "OTTOBRE", "NOVEMBRE", "DICEMBRE" }; + int faceAiMonthIndex; + faceAiCalendar.setTime(faceAiRaceDate); + faceAiExpectedYear = String.valueOf(faceAiCalendar.get(java.util.Calendar.YEAR)); + faceAiMonthIndex = faceAiCalendar.get(java.util.Calendar.MONTH); + faceAiExpectedMonthFolder = String.format("%02d.%s", new Object[] { Integer.valueOf(faceAiMonthIndex + 1), faceAiMonthNames[faceAiMonthIndex] }); +} if (!faceAiRacePathBase.isEmpty()) { - String[] faceAiPathSegments = faceAiRacePathBase.split("/"); + String[] faceAiPathSegments = faceAiRacePathBase.replace('\\', '/').split("/"); java.util.ArrayList faceAiNormalizedSegments = new java.util.ArrayList(); + int faceAiYearIndex = -1; for (int faceAiSegmentIndex = 0; faceAiSegmentIndex < faceAiPathSegments.length; faceAiSegmentIndex++) { String faceAiSegment = faceAiPathSegments[faceAiSegmentIndex] != null ? faceAiPathSegments[faceAiSegmentIndex].trim() : ""; if (!faceAiSegment.isEmpty()) { faceAiNormalizedSegments.add(faceAiSegment); } } - if (faceAiNormalizedSegments.size() > 0) { + for (int faceAiSegmentIndex = 0; faceAiSegmentIndex < faceAiNormalizedSegments.size(); faceAiSegmentIndex++) { + String faceAiSegment = (String) faceAiNormalizedSegments.get(faceAiSegmentIndex); + if (faceAiSegment.matches("^\\d{4}$")) { + faceAiRaceYear = faceAiSegment; + faceAiYearIndex = faceAiSegmentIndex; + break; + } + } + if (faceAiYearIndex >= 0) { + if (faceAiNormalizedSegments.size() > faceAiYearIndex + 1) { + faceAiRaceMonthFolder = (String) faceAiNormalizedSegments.get(faceAiYearIndex + 1); + } + if (faceAiNormalizedSegments.size() > faceAiYearIndex + 2) { + faceAiRaceFolder = (String) faceAiNormalizedSegments.get(faceAiYearIndex + 2); + } + } else if (faceAiNormalizedSegments.size() > 0) { faceAiRaceYear = (String) faceAiNormalizedSegments.get(0); - } - if (faceAiNormalizedSegments.size() > 1) { - faceAiRaceMonthFolder = (String) faceAiNormalizedSegments.get(1); - } - if (faceAiNormalizedSegments.size() > 2) { - faceAiRaceFolder = (String) faceAiNormalizedSegments.get(2); - } else if (faceAiNormalizedSegments.size() > 1) { - faceAiRaceFolder = (String) faceAiNormalizedSegments.get(faceAiNormalizedSegments.size() - 1); + if (faceAiNormalizedSegments.size() > 1) { + faceAiRaceMonthFolder = (String) faceAiNormalizedSegments.get(1); + } + if (faceAiNormalizedSegments.size() > 2) { + faceAiRaceFolder = (String) faceAiNormalizedSegments.get(2); + } else if (faceAiNormalizedSegments.size() > 1) { + faceAiRaceFolder = (String) faceAiNormalizedSegments.get(faceAiNormalizedSegments.size() - 1); + } } } -if (faceAiRaceYear.isEmpty() && faceAiRaceDate != null) { - java.util.Calendar faceAiCalendar = java.util.Calendar.getInstance(); - faceAiCalendar.setTime(faceAiRaceDate); - faceAiRaceYear = String.valueOf(faceAiCalendar.get(java.util.Calendar.YEAR)); +if (!faceAiExpectedYear.isEmpty() && !faceAiExpectedYear.equals(faceAiRaceYear)) { + faceAiRaceYear = faceAiExpectedYear; +} +if (!faceAiExpectedMonthFolder.isEmpty() && !faceAiExpectedMonthFolder.equals(faceAiRaceMonthFolder)) { + faceAiRaceMonthFolder = faceAiExpectedMonthFolder; } if (faceAiRaceFolder.isEmpty()) { faceAiRaceFolder = String.valueOf(CR.getGara().getId_gara());