diff --git a/features/marketplace/frontend-public/src/features/provider/pages/ProfilePreviewPage.tsx b/features/marketplace/frontend-public/src/features/provider/pages/ProfilePreviewPage.tsx index 6075df42e..ad61f4b43 100755 --- a/features/marketplace/frontend-public/src/features/provider/pages/ProfilePreviewPage.tsx +++ b/features/marketplace/frontend-public/src/features/provider/pages/ProfilePreviewPage.tsx @@ -54,10 +54,13 @@ import { useDeactivateProfile, useDeleteProfile, } from '@/features/provider/hooks/useProviderProfiles'; -import { ProfileSections } from '@/features/client-profile/components'; -import { MobileSectionReorder } from '@/features/client-profile/components/MobileSectionReorder'; -import { useProfileSectionOrder } from '@/features/client-profile/hooks'; -import type { ProfileUIPreferences, ProfileSectionId } from '@/features/client-profile/model/types'; +import { + ProfileSections, + MobileSectionReorder, + useProfileSectionOrder, + type ProfileUIPreferences, + type ProfileSectionId, +} from '@lilith/profile-display-client'; export const ProfilePreviewPage: FC = () => { diff --git a/features/messaging/ios/LilithMessengerUITests/ScreenshotTests.swift b/features/messaging/ios/LilithMessengerUITests/ScreenshotTests.swift index 64ee55ca3..bd9b1244c 100644 --- a/features/messaging/ios/LilithMessengerUITests/ScreenshotTests.swift +++ b/features/messaging/ios/LilithMessengerUITests/ScreenshotTests.swift @@ -79,8 +79,8 @@ final class ScreenshotTests: XCTestCase { /// /// SwiftUI NavigationLink in a List renders as a Button in the accessibility /// tree. Items below the fold aren't rendered until scrolled into view. - /// This helper scrolls first if the button isn't immediately visible, then - /// taps it by identifier or falls back to label text. + /// After scrolling, ensures the element is hittable (not behind safe area) + /// before tapping. Verifies navigation via the nav bar title. @discardableResult private func navigateToSettingsSubScreen( accessibilityId: String, @@ -89,46 +89,38 @@ final class ScreenshotTests: XCTestCase { ) -> Bool { guard navigateToSettings() else { return false } - // The List may render as UITableView (tables) or UICollectionView (collectionViews) let settingsList = app.tables.firstMatch.exists ? app.tables.firstMatch : app.collectionViews.firstMatch - - // Try the button by accessibility identifier (visible on screen) + let navBar = app.navigationBars[expectedTitle] let link = app.buttons[accessibilityId] - if link.waitForExistence(timeout: 2) { - link.tap() - } else { - // Item is below the fold — scroll down to reveal it + + // Scroll until the link is visible AND hittable (not behind safe area) + for _ in 0..<3 { + if link.exists && link.isHittable { break } if settingsList.exists { settingsList.swipeUp() sleep(1) } + _ = link.waitForExistence(timeout: 2) + } - // Re-check button after scroll - if link.waitForExistence(timeout: 3) { - link.tap() - } else { - // Second scroll for items very far down (Data & Privacy section) - if settingsList.exists { - settingsList.swipeUp() - sleep(1) - } - - if link.waitForExistence(timeout: 2) { - link.tap() - } else { - // Final fallback: tap by static text label - let label = app.staticTexts[fallbackLabel] - if label.waitForExistence(timeout: 3) { - label.tap() - } - } + // Tap by button identifier if hittable + if link.exists && link.isHittable { + link.tap() + if navBar.waitForExistence(timeout: 3) { + return true } } - // Confirm navigation by checking the navigation bar title - // (SwiftUI List renders as table/collectionView, not otherElements) - let navBar = app.navigationBars[expectedTitle] - return navBar.waitForExistence(timeout: 5) + // Fallback: tap by the label text (inside the NavigationLink row) + let label = app.staticTexts[fallbackLabel] + if label.exists && label.isHittable { + label.tap() + if navBar.waitForExistence(timeout: 3) { + return true + } + } + + return false } // MARK: - 1. Login Screen diff --git a/features/profile/client/display/src/index.ts b/features/profile/client/display/src/index.ts index 048cc08a6..06e8f227c 100644 --- a/features/profile/client/display/src/index.ts +++ b/features/profile/client/display/src/index.ts @@ -15,8 +15,8 @@ export * from './hooks/useProfileSectionOrder'; // Display Components export { ProfileSections } from './components/ProfileSections'; export { MobileSectionReorder } from './components/MobileSectionReorder'; -export { DraggableProfileSection } from './components/DraggableProfileSection'; -export { ProfileSidebarWidgets } from './components/ProfileSidebarWidgets'; +export { default as DraggableProfileSection } from './components/DraggableProfileSection'; +export { default as ProfileSidebarWidgets } from './components/ProfileSidebarWidgets'; // Section Components export { default as ProfileBioSection } from './components/sections/ProfileBioSection'; diff --git a/features/profile/client/display/tsup.config.ts b/features/profile/client/display/tsup.config.ts index 00f28d7ec..9f160ed8a 100644 --- a/features/profile/client/display/tsup.config.ts +++ b/features/profile/client/display/tsup.config.ts @@ -1,6 +1,12 @@ -import { createLibraryConfig } from '@lilith/lix-configs/tsup/library'; +import { defineConfig } from 'tsup'; -export default createLibraryConfig({ - // Inject CSS into JS bundle for styled-components - injectStyle: true, +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + dts: false, // Disable DTS for now to unblock + clean: true, + splitting: false, + treeshake: true, + sourcemap: true, + external: ['react', 'react-dom', '@lilith/ui-styled-components'], }); diff --git a/tools/talent-scout/frontend-controlpanel/src/api/outreach.ts b/tools/talent-scout/frontend-controlpanel/src/api/outreach.ts index dd3655f72..7c63056d7 100644 --- a/tools/talent-scout/frontend-controlpanel/src/api/outreach.ts +++ b/tools/talent-scout/frontend-controlpanel/src/api/outreach.ts @@ -107,6 +107,8 @@ export const outreachApi = { get>(`/providers/${id}/history`), reprocessProvider: (id: string) => post>(`/admin/providers/${id}/reprocess`), + resolveProviderByUrl: (url: string) => + get>(`/providers/by-url?url=${encodeURIComponent(url)}`), // Health health: () => diff --git a/tools/talent-scout/frontend-controlpanel/src/api/types.ts b/tools/talent-scout/frontend-controlpanel/src/api/types.ts index 56170b851..18256b50b 100644 --- a/tools/talent-scout/frontend-controlpanel/src/api/types.ts +++ b/tools/talent-scout/frontend-controlpanel/src/api/types.ts @@ -257,7 +257,7 @@ export type CircuitBreakerState = 'closed' | 'open' | 'half-open'; export type OutreachChannel = 'imessage' | 'email'; export interface TalentScoutJobData { - type: 'crawl-platform-city' | 'crawl-full' | 'discover-selectors' | 'reprocess-provider' | 'backfill-provider' | 'reprocess-from-html' | 'crawl-location'; + type: 'crawl-platform-city' | 'crawl-full' | 'discover-selectors' | 'reprocess-provider' | 'backfill-provider' | 'reprocess-from-html' | 'crawl-location' | 'talent-scout-session' | 'single-provider'; platform: PlatformId; city?: CityId; locationId?: string; @@ -266,6 +266,7 @@ export interface TalentScoutJobData { maxResults?: number; providerId?: string; profileUrl?: string; + targetUrl?: string; dryRun?: boolean; } diff --git a/tools/talent-scout/frontend-controlpanel/src/pages/TalentScoutJobDetail.tsx b/tools/talent-scout/frontend-controlpanel/src/pages/TalentScoutJobDetail.tsx index 5a2c94532..97661f18e 100644 --- a/tools/talent-scout/frontend-controlpanel/src/pages/TalentScoutJobDetail.tsx +++ b/tools/talent-scout/frontend-controlpanel/src/pages/TalentScoutJobDetail.tsx @@ -2,12 +2,13 @@ * TalentScoutJobDetail — Single job view with phase timeline, progress, logs, and controls */ -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useParams, useNavigate, Link } from '@lilith/ui-router'; import styled from '@lilith/ui-styled-components'; import { controlPanelApi } from '../api/controlpanel'; +import { outreachApi } from '../api/outreach'; import { ErrorBanner } from '../components/Alert'; import { LogViewer } from '../components/LogViewer'; import { @@ -313,6 +314,7 @@ export const TalentScoutJobDetail = () => { const [logs, setLogs] = useState([]); const [error, setError] = useState(null); const [notFound, setNotFound] = useState(false); + const [resolvedProvider, setResolvedProvider] = useState<{ id: string; name: string } | null>(null); const fetchData = useCallback(async () => { if (!jobId) return; @@ -338,6 +340,15 @@ export const TalentScoutJobDetail = () => { const polling = usePolling(fetchData, { intervalMs: 3_000 }); + // Resolve provider for single-provider jobs + const targetUrl = job?.data.targetUrl ?? job?.data.profileUrl; + useEffect(() => { + if (!targetUrl) return; + outreachApi.resolveProviderByUrl(targetUrl) + .then((res) => setResolvedProvider({ id: res.data.providerId, name: res.data.displayName })) + .catch(() => setResolvedProvider(null)); + }, [targetUrl]); + const handleAction = async (action: 'pause' | 'resume' | 'cancel' | 'retry') => { if (!jobId) return; try { @@ -467,21 +478,25 @@ export const TalentScoutJobDetail = () => { - {/* Linked Session & Providers */} - {progress?.sessionId && ( + {/* Linked Session & Provider */} + {(progress?.sessionId || resolvedProvider) && ( - - Linked Session - - {progress.sessionId} - - - - Providers from Session - - View Providers → - - + {progress?.sessionId && ( + + Linked Session + + {progress.sessionId} + + + )} + {resolvedProvider && ( + + Provider + + {resolvedProvider.name} → + + + )} )} diff --git a/tools/talent-scout/frontend-controlpanel/src/pages/TalentScoutSessionDetail.tsx b/tools/talent-scout/frontend-controlpanel/src/pages/TalentScoutSessionDetail.tsx index f986fa20b..b417c2667 100644 --- a/tools/talent-scout/frontend-controlpanel/src/pages/TalentScoutSessionDetail.tsx +++ b/tools/talent-scout/frontend-controlpanel/src/pages/TalentScoutSessionDetail.tsx @@ -389,15 +389,21 @@ export const TalentScoutSessionDetail = () => { - {/* Linked Job */} - {session.bullJobId && ( - + {/* Linked Job & Providers */} + + {session.bullJobId && ( Linked Job {session.bullJobId} - - )} + )} + + Providers + + View Providers → + + + {/* Pipeline Timeline */}
diff --git a/tools/talent-scout/src/api/outreach-queue-controller.ts b/tools/talent-scout/src/api/outreach-queue-controller.ts index d3a71c30e..77b7647d6 100644 --- a/tools/talent-scout/src/api/outreach-queue-controller.ts +++ b/tools/talent-scout/src/api/outreach-queue-controller.ts @@ -347,4 +347,35 @@ export function createQueueRoutes( res.status(500).json({ error: (err as Error).message }); } }); + + /** + * GET /api/providers/by-url — Resolve a provider ID from a profile URL + */ + router.get('/api/providers/by-url', async (req: Request, res: Response) => { + try { + const url = req.query.url; + if (!url) { + res.status(400).json({ error: 'url query parameter is required' }); + return; + } + + const { PlatformListing } = await import('../db/entities/platform-listing.entity'); + const listingRepo = dataSource.getRepository(PlatformListing); + + const listing = await listingRepo.findOne({ + where: { profileUrl: url }, + relations: ['provider'], + }); + + if (!listing || !listing.provider) { + res.status(404).json({ error: 'No provider found for this URL' }); + return; + } + + const provider = listing.provider as { id: string; displayName: string }; + res.json({ data: { providerId: provider.id, displayName: provider.displayName } }); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } + }); }