From c67bb0217365ee5c26321bba98a466543b06ac2a Mon Sep 17 00:00:00 2001 From: MaddoScientisto Date: Sun, 12 Apr 2026 17:26:17 +0200 Subject: [PATCH 1/2] Refactor code structure for improved readability and maintainability --- FACEAI_INTEGRATION_PLAN.md | 37 +- faceai/.env.example | 6 + faceai/README.md | 30 +- faceai/apps/backend/src/config.js | 1 + faceai/apps/backend/src/race-storage.js | 130 +++++ faceai/apps/backend/src/server.js | 70 ++- faceai/apps/backend/src/store.js | 28 +- .../src/components/FaceAiFeedbackPanel.vue | 56 ++ .../src/components/FaceAiHeroCard.vue | 110 ++++ .../src/components/FaceAiUploadPanel.vue | 394 +++++++++++++++ .../frontend/src/components/LegacyHeader.vue | 35 +- .../frontend/src/composables/useFaceAiHome.js | 478 ++++++++++++++++++ faceai/apps/frontend/src/views/HomeView.vue | 313 ++++-------- faceai/apps/processor/src/config.js | 1 - faceai/apps/processor/src/worker-utils.js | 36 +- faceai/apps/processor/src/worker.js | 4 +- faceai/docker-compose.yml | 6 +- faceai/docs/processor-technical-design.md | 54 +- .../LUCCA}/face_encodings_20260330_170155.pkl | Bin .../PISA/face_encodings_20260412_171808.pkl | Bin 0 -> 911353 bytes test_pkl/face_encodings_20260330_170210.pkl | Bin 52607 -> 0 bytes www/_js/rus-ecom-240621.js | 79 +++ www/faceai_handoff.php | 23 +- www/faceai_simulator.php | 14 +- www/faceai_simulator_view.php | 53 +- www/fotoCR-en.jsp | 49 ++ www/fotoCR.jsp | 49 ++ 27 files changed, 1735 insertions(+), 321 deletions(-) create mode 100644 faceai/apps/backend/src/race-storage.js create mode 100644 faceai/apps/frontend/src/components/FaceAiFeedbackPanel.vue create mode 100644 faceai/apps/frontend/src/components/FaceAiHeroCard.vue create mode 100644 faceai/apps/frontend/src/components/FaceAiUploadPanel.vue create mode 100644 faceai/apps/frontend/src/composables/useFaceAiHome.js rename test_pkl/{ => 2026/04.APRILE/LUCCA}/face_encodings_20260330_170155.pkl (100%) create mode 100644 test_pkl/2026/04.APRILE/PISA/face_encodings_20260412_171808.pkl delete mode 100644 test_pkl/face_encodings_20260330_170210.pkl diff --git a/FACEAI_INTEGRATION_PLAN.md b/FACEAI_INTEGRATION_PLAN.md index 4e7f4622..22d7e7ce 100644 --- a/FACEAI_INTEGRATION_PLAN.md +++ b/FACEAI_INTEGRATION_PLAN.md @@ -61,6 +61,7 @@ Use three deployable parts: - Read the handoff token or FaceAI session cookie. - Show the legacy-like header and navigation. +- Check whether the mounted FaceAI dataset exists for the selected race before enabling uploads. - Let the user upload a selfie. - Create a race-scoped search request. - Poll job status or show queued state. @@ -72,7 +73,7 @@ Use three deployable parts: - Receive a race-scoped search job. - Queue requests and process them one by one. -- Run the external face-recognition program. +- Resolve `year/monthFolder/raceFolder` inside the mounted dataset root, take the first `.pkl` file in that race directory, and run the external face-recognition program against it. - Return match results with confidence and photo ids or file identifiers. - Return a completed result set usable by the legacy filter handoff. @@ -95,6 +96,10 @@ Instead: - access flags for FaceAI - race id - race slug or descriptor + - race storage metadata needed to resolve the mounted FaceAI dataset: + - `year` + - `monthFolder` like `04.APRILE` + - `raceFolder` like `LIVORNO` or `PISA` - current page URL as `returnUrl` - expiry time, ideally 1 to 5 minutes 3. Browser is redirected to `https://faceai.regalamiunsorriso.it/auth/callback?token=...` @@ -138,7 +143,7 @@ The lowest-risk way to do that is to update `www/_js/rus-ecom-240621.js` so that - removes that select from the rendered UI - inserts a `Face ID` button in the same area - builds the launch URL using the current race context and current page URL -- carries `raceId`, race description or slug, language, and exact `returnUrl` +- carries `raceId`, race description or slug, `raceYear`, `raceMonthFolder`, `raceFolder`, language, and exact `returnUrl` This avoids fragile JSP layout edits and keeps the change deployable as a single JS asset update. @@ -172,7 +177,7 @@ This is preferable to putting the matched ids directly in the browser URL, becau ## FaceAI App Structure -The requested target folder is `faceai/`. It does not currently exist in this workspace, so this plan assumes it will be created as a new app. +The target folder is `faceai/`, and this workspace now contains an implemented scaffold there. Suggested structure: @@ -198,10 +203,11 @@ faceai/ 5. FaceAI shows a page styled like the old site, including a matching header and a clear `Back to race page` action. 6. User uploads a selfie. 7. FaceAI creates a search job with `userId`, `raceId`, `requestId`, and selfie file reference. -8. FaceAI polls until the processing job completes. -9. Once the result is ready, FaceAI redirects the browser back to the original race page on `www`. -10. The legacy site resolves the matched photo ids and renders the race page filtered to those photos only, similar in spirit to the existing pettorale-based flow. -11. User opens and downloads photos exactly as they do today, through the legacy site. +8. FaceAI checks the mounted race directory immediately and, if no `.pkl` is present for that race, disables processing and offers only the return path. +9. FaceAI polls until the processing job completes. +10. Once the result is ready, FaceAI redirects the browser back to the original race page on `www`. +11. The legacy site resolves the matched photo ids and renders the race page filtered to those photos only, similar in spirit to the existing pettorale-based flow. +12. User opens and downloads photos exactly as they do today, through the legacy site. ## Result And Download Strategy @@ -255,6 +261,20 @@ For v1, `photoId` is the most important field. If the legacy page is the final r Race scope is mandatory. The service must never search globally by default. +The mounted dataset layout is now assumed to be: + +```text +/mounted-pkl-root/ + 2026/ + 04.APRILE/ + PISA/ + any-file-name.pkl + LIVORNO/ + any-file-name.pkl +``` + +The `.pkl` filename does not matter. The first `.pkl` found at the race root is the one passed to the matcher. + ## Async Processing Design Use an API plus worker model. @@ -275,6 +295,7 @@ Input job: - request id - race id +- race storage metadata: `year`, `monthFolder`, `raceFolder` - selfie storage path - user id - email @@ -370,7 +391,7 @@ This is safer than trying to embed the old JSP header directly into a Node app. - Update `www/_js/rus-ecom-240621.js` to remove the dropdown from the UI and insert the FaceAI button. - Add the legacy auth bridge endpoint. -- Pass `raceId`, `lang`, and `returnUrl` into the FaceAI launch. +- Pass `raceId`, `lang`, `returnUrl`, `raceYear`, `raceMonthFolder`, and `raceFolder` into the FaceAI launch. - Add the legacy return endpoint or result-aware race filter path. ### Phase 3: FaceAI app shell diff --git a/faceai/.env.example b/faceai/.env.example index 0888f410..768f766b 100644 --- a/faceai/.env.example +++ b/faceai/.env.example @@ -6,3 +6,9 @@ FACEAI_ENABLE_LOCAL_LEGACY_STATIC=1 FACEAI_LOCAL_LEGACY_STATIC_ROOT=k:\various\regalamiunsorriso\www FACEAI_SHARED_SECRET=change-me FACEAI_SESSION_COOKIE=rus_faceai_session +FACEAI_REDIS_URL=redis://redis:6379 +FACEAI_QUEUE_NAME=faceai-searches +FACEAI_RUNTIME_ROOT=/data/runtime +FACEAI_UPLOAD_ROOT=/data/runtime/uploads +FACEAI_PKL_ROOT=/data/pkl +FACEAI_MATCHER_BINARY=/opt/face-recognition/face_matcher diff --git a/faceai/README.md b/faceai/README.md index 5c36e47c..536c6da8 100644 --- a/faceai/README.md +++ b/faceai/README.md @@ -76,7 +76,7 @@ The checked-in `docker-compose.yml` starts: The local stack also mounts: - `../bin/Face_Recognition_Unix` into the processor container as the matcher binary source -- `../test_pkl` into the processor container as fallback PKL test data +- `../test_pkl` into both the public FaceAI container and the processor container as the shared read-only PKL dataset root - `../www` into the PHP container so the real bridge files are used ### Run The Browser Test @@ -84,7 +84,7 @@ The local stack also mounts: Open: ```text -http://localhost:8080/faceai_simulator.php?raceId=101&lang=it +http://localhost:8080/faceai_simulator.php?raceId=202&lang=it ``` That page simulates the legacy race page, loads the original race-page JavaScript from `www/_js/rus-ecom-240621.js`, lets the script replace the visible `tipoPuntoFoto` selector with the new `Face ID` button, and launches the real PHP handoff bridge at `www/faceai_handoff.php`. @@ -160,9 +160,11 @@ services: FACEAI_QUEUE_NAME: faceai-searches FACEAI_RUNTIME_ROOT: /data/runtime FACEAI_UPLOAD_ROOT: /data/runtime/uploads + FACEAI_PKL_ROOT: /data/pkl FACEAI_ENABLE_LOCAL_LEGACY_STATIC: 0 volumes: - faceai-runtime:/data/runtime + - /srv/faceai/pkl:/data/pkl:ro ports: - "127.0.0.1:3001:3001" depends_on: @@ -230,11 +232,22 @@ Processor settings: | Variable | Required | Example | Purpose | | --- | --- | --- | --- | | `FACEAI_PKL_ROOT` | yes | `/data/pkl` | mounted race-to-PKL dataset root | -| `FACEAI_TEST_PKL_ROOT` | optional | `/data/pkl/test` | local-only fallback PKL location | | `FACEAI_MATCHER_BINARY` | yes | `/opt/face-recognition/face_matcher` | matcher executable inside the processor container | | `FACEAI_WORKER_CONCURRENCY` | optional | `2` | BullMQ worker concurrency | | `FACEAI_WORKER_TIMEOUT_MS` | optional | `300000` | matcher timeout in milliseconds | +The mounted PKL root is expected to use this structure: + +```text +/data/pkl/ + 2026/ + 04.APRILE/ + PISA/ + any-file-name.pkl +``` + +The public FaceAI site mounts the same path read-only so it can check availability during session bootstrap and refuse uploads immediately when the race has no `.pkl` data. + Do not enable `FACEAI_ENABLE_LOCAL_LEGACY_STATIC` in production. That mode exists only for local simulator flows. ### Legacy-Side Configuration That Must Match @@ -276,7 +289,7 @@ This scaffold can now be deployed with the public site, processor, and Redis, bu - search state is short-lived in Redis and is not backed by a durable database - runtime uploads and matcher output still need an agreed production retention and cleanup policy -- the final production PKL/NAS layout is not yet locked down +- the PKL mount contract is now defined, but final NAS operations and cleanup policy still need to be hardened - the backend currently sets the FaceAI session cookie with `secure: false`, which should be hardened before final public rollout - the local simulator endpoints under `/dev/*` are still present in the app and should be treated as non-production scaffolding - the processor CSV parser is still based on the current scaffolded matcher output assumptions @@ -299,7 +312,6 @@ FACEAI_QUEUE_NAME=faceai-searches FACEAI_RUNTIME_ROOT=/data/runtime FACEAI_UPLOAD_ROOT=/data/runtime/uploads FACEAI_PKL_ROOT=/data/pkl -FACEAI_TEST_PKL_ROOT=/data/pkl/test FACEAI_MATCHER_BINARY=/opt/face-recognition/face_matcher ``` @@ -311,6 +323,14 @@ In the provided Docker Compose stack, that wiring is already done with: FACEAI_LEGACY_RETURN_URL=http://localhost:8080/faceai_return.php ``` +The local PHP simulator also needs the legacy bridge feature flag enabled: + +```text +FACEAI_FEATURE_ENABLED=1 +``` + +The checked-in `docker-compose.yml` now sets that on the `legacy-php` service so the simulator can launch the FaceAI handoff flow locally. + ## Notes - Search orchestration now uses Redis and a dedicated processor worker. diff --git a/faceai/apps/backend/src/config.js b/faceai/apps/backend/src/config.js index 7cc03656..606d1a3b 100644 --- a/faceai/apps/backend/src/config.js +++ b/faceai/apps/backend/src/config.js @@ -9,6 +9,7 @@ export const config = { frontendUrl: process.env.FACEAI_FRONTEND_URL || 'http://localhost:5173', publicBaseUrl: process.env.FACEAI_PUBLIC_BASE_URL || 'http://localhost:3001', legacyReturnUrl: process.env.FACEAI_LEGACY_RETURN_URL || 'http://localhost:3001/dev/legacy/return', + pklRoot: process.env.FACEAI_PKL_ROOT || '/data/pkl', enableLocalLegacyStatic: process.env.FACEAI_ENABLE_LOCAL_LEGACY_STATIC ? process.env.FACEAI_ENABLE_LOCAL_LEGACY_STATIC === '1' : process.env.NODE_ENV !== 'production', diff --git a/faceai/apps/backend/src/race-storage.js b/faceai/apps/backend/src/race-storage.js new file mode 100644 index 00000000..0a8ee4f7 --- /dev/null +++ b/faceai/apps/backend/src/race-storage.js @@ -0,0 +1,130 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; + +const ITALIAN_MONTH_NAMES = [ + 'GENNAIO', + 'FEBBRAIO', + 'MARZO', + 'APRILE', + 'MAGGIO', + 'GIUGNO', + 'LUGLIO', + 'AGOSTO', + 'SETTEMBRE', + 'OTTOBRE', + 'NOVEMBRE', + 'DICEMBRE' +]; + +function sanitizePathSegment(value) { + const normalized = String(value || '').trim(); + + if (!normalized) { + return ''; + } + + if (normalized === '.' || normalized === '..' || normalized.includes('..') || /[\\/]/.test(normalized)) { + throw new Error('Invalid race storage path segment'); + } + + return normalized; +} + +export function normalizeRaceFolderName(value) { + return String(value || '') + .trim() + .replace(/[<>:"/\\|?*]/g, ' ') + .replace(/\s+/g, ' ') + .toUpperCase(); +} + +export function buildMonthFolder(year, monthIndex) { + const safeYear = sanitizePathSegment(year); + const normalizedMonthIndex = Number(monthIndex); + if (!safeYear || Number.isNaN(normalizedMonthIndex) || normalizedMonthIndex < 1 || normalizedMonthIndex > 12) { + return ''; + } + + return `${String(normalizedMonthIndex).padStart(2, '0')}.${ITALIAN_MONTH_NAMES[normalizedMonthIndex - 1]}`; +} + +export function buildRaceStorage(storageInput = {}) { + const year = sanitizePathSegment(storageInput.year); + const monthFolder = sanitizePathSegment(storageInput.monthFolder); + const raceFolder = sanitizePathSegment(normalizeRaceFolderName(storageInput.raceFolder)); + + if (!year || !monthFolder || !raceFolder) { + return null; + } + + return { + year, + monthFolder, + raceFolder, + relativeDir: path.posix.join(year, monthFolder, raceFolder) + }; +} + +export async function resolveRacePklAvailability({ pklRoot, race }) { + if (!pklRoot) { + return { + available: false, + reasonCode: 'PKL_ROOT_NOT_CONFIGURED', + message: 'The PKL root is not configured for this FaceAI environment.', + storage: null + }; + } + + const storage = buildRaceStorage(race?.storage || race); + if (!storage) { + return { + available: false, + reasonCode: 'MISSING_RACE_STORAGE', + message: 'The legacy handoff did not provide the folder metadata required to resolve FaceAI data for this race.', + storage: null + }; + } + + const raceDir = path.join(pklRoot, storage.year, storage.monthFolder, storage.raceFolder); + + let entries; + try { + entries = await fs.readdir(raceDir, { withFileTypes: true }); + } catch (error) { + if (error?.code === 'ENOENT') { + return { + available: false, + reasonCode: 'RACE_DIRECTORY_NOT_FOUND', + message: `No FaceAI dataset directory exists for ${storage.relativeDir}.`, + storage, + raceDir + }; + } + + throw error; + } + + const pklEntry = entries + .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.pkl')) + .sort((left, right) => left.name.localeCompare(right.name, 'en'))[0]; + + if (!pklEntry) { + return { + available: false, + reasonCode: 'PKL_FILE_NOT_FOUND', + message: `The race directory ${storage.relativeDir} exists, but it does not contain any .pkl file.`, + storage, + raceDir + }; + } + + return { + available: true, + reasonCode: null, + message: `Using ${storage.relativeDir}/${pklEntry.name}`, + storage, + raceDir, + pklPath: path.join(raceDir, pklEntry.name), + pklFileName: pklEntry.name + }; +} \ No newline at end of file diff --git a/faceai/apps/backend/src/server.js b/faceai/apps/backend/src/server.js index 1f202237..8bef724c 100644 --- a/faceai/apps/backend/src/server.js +++ b/faceai/apps/backend/src/server.js @@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url'; import { config } from './config.js'; import { signPayload, verifySignedPayload } from './auth.js'; import { createSession, getSession, mockCatalog } from './store.js'; +import { buildRaceStorage, resolveRacePklAvailability } from './race-storage.js'; import { acquireActiveSearchLock, createRedisConnection, @@ -94,8 +95,24 @@ async function enforceSearchRateLimit(req, res, next) { next(); } -function issueHandoffToken({ raceId, raceSlug, lang, returnUrl }) { - const race = mockCatalog[raceId] || { id: raceId, slug: raceSlug || `race-${raceId}`, name: raceSlug || `Race ${raceId}` }; +function normalizeRaceForSession(raceInput) { + return { + ...raceInput, + storage: buildRaceStorage(raceInput?.storage || {}) + }; +} + +async function buildRaceAvailability(race) { + return resolveRacePklAvailability({ pklRoot: config.pklRoot, race }); +} + +function issueHandoffToken({ raceId, raceSlug, raceName, raceStorage, lang, returnUrl }) { + const race = mockCatalog[raceId] || { + id: raceId, + slug: raceSlug || `race-${raceId}`, + name: raceName || raceSlug || `Race ${raceId}`, + storage: buildRaceStorage(raceStorage || {}) + }; return signPayload({ type: 'handoff', @@ -108,7 +125,8 @@ function issueHandoffToken({ raceId, raceSlug, lang, returnUrl }) { race: { id: race.id, slug: race.slug, - name: race.name + name: race.name, + storage: buildRaceStorage(raceStorage || race.storage || {}) }, lang: lang || 'it', returnUrl, @@ -231,9 +249,21 @@ app.get('/dev/legacy/race', (req, res) => { app.get('/dev/legacy/launch', (req, res) => { const raceId = String(req.query.raceId || '101'); const raceSlug = String(req.query.raceSlug || mockCatalog[raceId]?.slug || `race-${raceId}`); + const raceName = String(req.query.raceName || mockCatalog[raceId]?.name || raceSlug); const lang = String(req.query.lang || 'it'); const returnUrl = String(req.query.returnUrl || `${config.publicBaseUrl}/dev/legacy/race?raceId=${encodeURIComponent(raceId)}&lang=${encodeURIComponent(lang)}`); - const token = issueHandoffToken({ raceId, raceSlug, lang, returnUrl }); + const token = issueHandoffToken({ + raceId, + raceSlug, + raceName, + raceStorage: { + year: String(req.query.raceYear || mockCatalog[raceId]?.storage?.year || ''), + monthFolder: String(req.query.raceMonthFolder || mockCatalog[raceId]?.storage?.monthFolder || ''), + raceFolder: String(req.query.raceFolder || mockCatalog[raceId]?.storage?.raceFolder || '') + }, + lang, + returnUrl + }); res.redirect(`${config.frontendUrl}/auth/callback?token=${encodeURIComponent(token)}`); }); @@ -261,7 +291,7 @@ app.get('/dev/legacy/return', async (req, res) => { } }); -app.post('/api/auth/exchange', (req, res) => { +app.post('/api/auth/exchange', async (req, res) => { try { const { token } = req.body; const payload = verifySignedPayload(token, config.sharedSecret); @@ -269,13 +299,18 @@ app.post('/api/auth/exchange', (req, res) => { throw new Error('Wrong token type'); } + const race = normalizeRaceForSession(payload.race); + const availability = await buildRaceAvailability(race); + const faceAiAllowed = payload.user.membershipStatus === 'active' && availability.available; + const sessionId = createSession({ user: payload.user, - race: payload.race, + race, lang: payload.lang, returnUrl: payload.returnUrl, + availability, access: { - faceAiAllowed: payload.user.membershipStatus === 'active' + faceAiAllowed } }); @@ -288,11 +323,12 @@ app.post('/api/auth/exchange', (req, res) => { res.json({ user: payload.user, - race: payload.race, + race, lang: payload.lang, returnUrl: payload.returnUrl, + availability, access: { - faceAiAllowed: true + faceAiAllowed } }); } catch (error) { @@ -308,6 +344,19 @@ app.post('/api/searches', requireSession, enforceSearchRateLimit, upload.single( try { const raceId = String(req.body.raceId || req.faceaiSession.race.id); const userId = String(req.faceaiSession.user.id); + const race = normalizeRaceForSession(raceId === req.faceaiSession.race.id + ? req.faceaiSession.race + : (mockCatalog[raceId] || req.faceaiSession.race)); + const availability = await buildRaceAvailability(race); + + if (!availability.available) { + res.status(409).json({ + error: availability.message, + code: availability.reasonCode || 'RACE_PKL_UNAVAILABLE' + }); + return; + } + const activeSearchId = await getActiveSearchId(redis, userId); if (activeSearchId) { @@ -327,10 +376,10 @@ app.post('/api/searches', requireSession, enforceSearchRateLimit, upload.single( return; } - const race = mockCatalog[raceId] || req.faceaiSession.race; const search = await createSearchRecord(redis, { raceId, raceName: race?.name || raceId, + raceStorage: race?.storage || availability.storage, userId, returnUrl: req.faceaiSession.returnUrl, lang: req.faceaiSession.lang, @@ -371,6 +420,7 @@ app.post('/api/searches', requireSession, enforceSearchRateLimit, upload.single( id: updatedSearch.id, status: updatedSearch.status, raceId: updatedSearch.raceId, + raceStorage: updatedSearch.raceStorage, selfieName: updatedSearch.selfieName, matchCount: updatedSearch.matchCount, errorCode: updatedSearch.errorCode, diff --git a/faceai/apps/backend/src/store.js b/faceai/apps/backend/src/store.js index 03350a1c..a1b1ec33 100644 --- a/faceai/apps/backend/src/store.js +++ b/faceai/apps/backend/src/store.js @@ -5,6 +5,11 @@ export const mockCatalog = { id: '101', slug: 'mezza-di-firenze', name: 'Mezza di Firenze', + storage: { + year: '2026', + monthFolder: '04.APRILE', + raceFolder: 'PISA' + }, photos: [ { id: 'f101-001', label: 'Arrivo 001', bib: '245', checkpoint: 'Arrivo', thumb: 'thumb-arrivo-001.jpg' }, { id: 'f101-002', label: 'Arrivo 002', bib: '245', checkpoint: 'Arrivo', thumb: 'thumb-arrivo-002.jpg' }, @@ -22,14 +27,33 @@ export const mockCatalog = { }, '202': { id: '202', - slug: 'trail-del-chianti', - name: 'Trail del Chianti', + slug: 'mezza-di-pisa', + name: 'Mezza di Pisa', + storage: { + year: '2026', + monthFolder: '04.APRILE', + raceFolder: 'PISA' + }, photos: [ { id: 'f202-001', label: 'Bosco 001', bib: '77', checkpoint: 'Bosco', thumb: 'thumb-bosco-001.jpg' }, { id: 'f202-002', label: 'Salita 002', bib: '77', checkpoint: 'Salita', thumb: 'thumb-salita-002.jpg' }, { id: 'f202-003', label: 'Arrivo 003', bib: '77', checkpoint: 'Arrivo', thumb: 'thumb-arrivo-003.jpg' }, { id: 'f202-004', label: 'Bosco 004', bib: '19', checkpoint: 'Bosco', thumb: 'thumb-bosco-004.jpg' } ] + }, + '303': { + id: '303', + slug: 'corsa-di-lucca', + name: 'Corsa di Lucca', + storage: { + year: '2026', + monthFolder: '04.APRILE', + raceFolder: 'LUCCA' + }, + photos: [ + { id: 'f303-001', label: 'Mura 001', bib: '33', checkpoint: 'Mura', thumb: 'thumb-mura-001.jpg' }, + { id: 'f303-002', label: 'Centro 002', bib: '33', checkpoint: 'Centro', thumb: 'thumb-centro-002.jpg' } + ] } }; diff --git a/faceai/apps/frontend/src/components/FaceAiFeedbackPanel.vue b/faceai/apps/frontend/src/components/FaceAiFeedbackPanel.vue new file mode 100644 index 00000000..7cd6b41d --- /dev/null +++ b/faceai/apps/frontend/src/components/FaceAiFeedbackPanel.vue @@ -0,0 +1,56 @@ + + + + + \ No newline at end of file diff --git a/faceai/apps/frontend/src/components/FaceAiHeroCard.vue b/faceai/apps/frontend/src/components/FaceAiHeroCard.vue new file mode 100644 index 00000000..f04505c2 --- /dev/null +++ b/faceai/apps/frontend/src/components/FaceAiHeroCard.vue @@ -0,0 +1,110 @@ + + + + + \ No newline at end of file diff --git a/faceai/apps/frontend/src/components/FaceAiUploadPanel.vue b/faceai/apps/frontend/src/components/FaceAiUploadPanel.vue new file mode 100644 index 00000000..f4a01068 --- /dev/null +++ b/faceai/apps/frontend/src/components/FaceAiUploadPanel.vue @@ -0,0 +1,394 @@ + + + + + \ No newline at end of file diff --git a/faceai/apps/frontend/src/components/LegacyHeader.vue b/faceai/apps/frontend/src/components/LegacyHeader.vue index 3963c337..b003f409 100644 --- a/faceai/apps/frontend/src/components/LegacyHeader.vue +++ b/faceai/apps/frontend/src/components/LegacyHeader.vue @@ -1,9 +1,19 @@