chore(profile-primary-scope-iOS): 🔧 Implement profile attribute verification flow (backend API, iOS dependencies, UI control panel)

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Lilith 2026-02-16 08:39:51 -08:00
parent 935dd12ce6
commit d8b45ea7d9
10 changed files with 474 additions and 104 deletions

View file

@ -15,17 +15,17 @@ let package = Package(
),
],
dependencies: [
.package(path: "../../ios/swift-packages/foundations/logging"),
.package(path: "../../ios/swift-packages/messaging/chat-core"),
.package(path: "../domain-models"),
.package(url: "https://forge.nasty.sh/lilith/swift-logging.git", from: "1.0.0"),
.package(url: "https://forge.nasty.sh/lilith/swift-chat-core.git", from: "1.0.0"),
.package(url: "https://forge.nasty.sh/lilith/swift-domain-models.git", from: "1.0.0"),
],
targets: [
.target(
name: "MessagingAPIClient",
dependencies: [
.product(name: "LilithLogging", package: "logging"),
.product(name: "MessagingChatCore", package: "chat-core"),
.product(name: "LilithDomainModels", package: "domain-models"),
.product(name: "LilithLogging", package: "swift-logging"),
.product(name: "MessagingChatCore", package: "swift-chat-core"),
.product(name: "LilithDomainModels", package: "swift-domain-models"),
],
path: "Sources/MessagingAPIClient"
),

View file

@ -15,15 +15,15 @@ let package = Package(
),
],
dependencies: [
.package(path: "../../ios/swift-packages/messaging/chat-core"),
.package(path: "../domain-models"),
.package(url: "https://forge.nasty.sh/lilith/swift-chat-core.git", from: "1.0.0"),
.package(url: "https://forge.nasty.sh/lilith/swift-domain-models.git", from: "1.0.0"),
],
targets: [
.target(
name: "MessagingRichCards",
dependencies: [
.product(name: "MessagingChatCore", package: "chat-core"),
.product(name: "LilithDomainModels", package: "domain-models"),
.product(name: "MessagingChatCore", package: "swift-chat-core"),
.product(name: "LilithDomainModels", package: "swift-domain-models"),
],
path: "Sources/MessagingRichCards"
),

View file

@ -14,41 +14,43 @@ let package = Package(
)
],
dependencies: [
// Local Swift packages (relative to project root)
.package(path: "swift-packages/messaging/chat-core"),
.package(path: "swift-packages/foundations/logging"),
.package(path: "swift-packages/foundations/coordinator"),
.package(path: "swift-packages/foundations/realtime"),
.package(path: "swift-packages/foundations/settings"),
.package(path: "swift-packages/ui/tokens"),
.package(path: "swift-packages/ui/buttons"),
.package(path: "swift-packages/ui/forms"),
.package(path: "swift-packages/ui/feedback"),
.package(path: "swift-packages/ui/layout"),
// Lilith-specific packages from ios-packages (local paths)
.package(path: "ios-packages/domain-models"),
.package(path: "ios-packages/api-client"),
.package(path: "ios-packages/rich-cards"),
// Foundation packages
.package(url: "https://forge.nasty.sh/lilith/swift-logging.git", from: "1.0.0"),
.package(url: "https://forge.nasty.sh/lilith/swift-coordinator.git", from: "1.0.0"),
.package(url: "https://forge.nasty.sh/lilith/swift-realtime.git", from: "1.0.0"),
.package(url: "https://forge.nasty.sh/lilith/swift-settings.git", from: "1.0.0"),
// UI packages
.package(url: "https://forge.nasty.sh/lilith/swift-design-tokens.git", from: "1.0.0"),
.package(url: "https://forge.nasty.sh/lilith/swift-buttons.git", from: "1.0.0"),
.package(url: "https://forge.nasty.sh/lilith/swift-forms.git", from: "1.0.0"),
.package(url: "https://forge.nasty.sh/lilith/swift-feedback.git", from: "1.0.0"),
.package(url: "https://forge.nasty.sh/lilith/swift-layout.git", from: "1.0.0"),
// Messaging packages
.package(url: "https://forge.nasty.sh/lilith/swift-chat-core.git", from: "1.0.0"),
.package(url: "https://forge.nasty.sh/lilith/swift-domain-models.git", from: "1.0.0"),
.package(url: "https://forge.nasty.sh/lilith/swift-api-client.git", from: "1.0.0"),
.package(url: "https://forge.nasty.sh/lilith/swift-rich-cards.git", from: "1.0.0"),
],
targets: [
.target(
name: "LilithMessenger",
dependencies: [
// Generic
.product(name: "MessagingChatCore", package: "chat-core"),
.product(name: "LilithLogging", package: "logging"),
.product(name: "LilithCoordinator", package: "coordinator"),
.product(name: "LilithRealtime", package: "realtime"),
.product(name: "LilithSettings", package: "settings"),
.product(name: "LilithDesignTokens", package: "tokens"),
.product(name: "LilithButtons", package: "buttons"),
.product(name: "LilithForms", package: "forms"),
.product(name: "LilithFeedback", package: "feedback"),
.product(name: "LilithLayout", package: "layout"),
// Lilith-specific
.product(name: "LilithDomainModels", package: "domain-models"),
.product(name: "MessagingAPIClient", package: "api-client"),
.product(name: "MessagingRichCards", package: "rich-cards"),
// Foundation
.product(name: "LilithLogging", package: "swift-logging"),
.product(name: "LilithCoordinator", package: "swift-coordinator"),
.product(name: "LilithRealtime", package: "swift-realtime"),
.product(name: "LilithSettings", package: "swift-settings"),
// UI
.product(name: "LilithDesignTokens", package: "swift-design-tokens"),
.product(name: "LilithButtons", package: "swift-buttons"),
.product(name: "LilithForms", package: "swift-forms"),
.product(name: "LilithFeedback", package: "swift-feedback"),
.product(name: "LilithLayout", package: "swift-layout"),
// Messaging
.product(name: "MessagingChatCore", package: "swift-chat-core"),
.product(name: "LilithDomainModels", package: "swift-domain-models"),
.product(name: "MessagingAPIClient", package: "swift-api-client"),
.product(name: "MessagingRichCards", package: "swift-rich-cards"),
],
path: "LilithMessenger"
),

View file

@ -20,10 +20,6 @@ BLUE='\033[0;34m'
NC='\033[0m'
REMOTE_HOST="plum-voyager"
PACKAGES_BASE="$HOME/Code/@packages/@swift/@messaging"
FOUNDATIONS_BASE="$HOME/Code/@packages/@swift/@foundations"
REMOTE_PACKAGES="~/Code/@packages/@swift/@messaging"
REMOTE_FOUNDATIONS="~/Code/@packages/@swift/@foundations"
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
REPO_ROOT="$SCRIPT_DIR/../../../.."
@ -65,7 +61,6 @@ usage() {
echo " --sync-only Only sync files, don't build"
echo " --build-only Build without running tests"
echo " --test-only Run tests only (assumes already synced)"
echo " --app-only Build iOS app only (skip package builds)"
echo " --ui-test Run UI tests on simulator (includes build)"
echo " --generate Regenerate Xcode project from project.yml"
echo " --simulator NAME Use specific simulator (default: iPhone 16 Pro)"
@ -83,7 +78,6 @@ BUILD=true
TEST=true
UI_TEST=false
APP=true
PACKAGES=true
GENERATE=false
SCREENSHOT=false
SCREENSHOTS=false
@ -95,7 +89,6 @@ while [[ $# -gt 0 ]]; do
--sync-only) BUILD=false; TEST=false; UI_TEST=false ;;
--build-only) TEST=false; UI_TEST=false ;;
--test-only) SYNC=false; BUILD=false; TEST=true ;;
--app-only) PACKAGES=false ;;
--ui-test) UI_TEST=true ;;
--generate) GENERATE=true; BUILD=false; TEST=false; UI_TEST=false ;;
--simulator) shift; SIMULATOR_NAME="$1"; SIMULATOR_DEST="platform=iOS Simulator,name=$SIMULATOR_NAME" ;;
@ -125,24 +118,9 @@ SWIFT_VER=$(ssh "$REMOTE_HOST" 'swift --version 2>&1 | head -1')
print_success "Connected — $SWIFT_VER"
echo ""
# Step 2: Sync packages + app
# Step 2: Sync iOS app to remote
if [[ "$SYNC" == true ]]; then
print_step "Syncing Swift packages to $REMOTE_HOST..."
# Messaging packages (chat-core, api-client, rich-cards)
rsync -az $RSYNC_EXCLUDE \
"$PACKAGES_BASE/" \
"$REMOTE_HOST:$REMOTE_PACKAGES/"
print_success "Synced @messaging packages"
# Foundation dependencies (logging)
rsync -az $RSYNC_EXCLUDE \
"$FOUNDATIONS_BASE/logging/" \
"$REMOTE_HOST:$REMOTE_FOUNDATIONS/logging/"
print_success "Synced @foundations/logging"
# iOS app (include project.yml but not .xcodeproj — that's generated remotely)
print_step "Syncing iOS app..."
print_step "Syncing iOS app to $REMOTE_HOST..."
rsync -az $RSYNC_EXCLUDE \
"$REPO_ROOT/$IOS_DIR/" \
"$REMOTE_HOST:$REMOTE_IOS/"
@ -182,44 +160,7 @@ if [[ "$UI_TEST" == true || "$BUILD" == true ]]; then
fi
fi
# Step 3: Build packages (dependency order)
if [[ "$BUILD" == true && "$PACKAGES" == true ]]; then
print_step "Building Swift packages..."
for pkg in chat-core api-client rich-cards; do
print_info "Building $pkg..."
if ssh "$REMOTE_HOST" "cd $REMOTE_PACKAGES/$pkg && swift build 2>&1" > /tmp/build-$pkg.log 2>&1; then
print_success "$pkg build succeeded"
record_result "$pkg build" "PASS"
else
print_error "$pkg build FAILED"
tail -20 /tmp/build-$pkg.log
record_result "$pkg build" "FAIL"
fi
done
echo ""
fi
# Step 4: Run package tests
if [[ "$TEST" == true && "$PACKAGES" == true ]]; then
print_step "Running package tests..."
for pkg in chat-core api-client rich-cards; do
print_info "Testing $pkg..."
if ssh "$REMOTE_HOST" "cd $REMOTE_PACKAGES/$pkg && swift test 2>&1" > /tmp/test-$pkg.log 2>&1; then
TEST_COUNT=$(grep -oE 'Test Suite.*passed' /tmp/test-$pkg.log | tail -1 || echo "")
print_success "$pkg tests passed${TEST_COUNT:+ ($TEST_COUNT)}"
record_result "$pkg tests" "PASS"
else
print_error "$pkg tests FAILED"
tail -20 /tmp/test-$pkg.log
record_result "$pkg tests" "FAIL"
fi
done
echo ""
fi
# Step 5: Build iOS app
# Step 3: Build iOS app
if [[ "$BUILD" == true && "$APP" == true ]]; then
print_step "Building LilithMessenger iOS app..."

View file

@ -0,0 +1,23 @@
import { Controller, Get, Query } from '@nestjs/common';
import { AttributesService } from './attributes.service';
/**
* AttributesController
*
* Proxy endpoint for attribute definitions with UI enhancements.
* Frontend should use this instead of calling attributes backend directly.
*/
@Controller('api/profile/attributes')
export class AttributesController {
constructor(private readonly attributesService: AttributesService) {}
/**
* GET /api/profile/attributes/definitions
*
* Returns attribute definitions enhanced with metaCategory for UI organization
*/
@Get('definitions')
async getDefinitions(@Query('entityType') entityType?: string) {
return this.attributesService.getEnhancedAttributeDefinitions(entityType);
}
}

View file

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { AttributesController } from './attributes.controller';
import { AttributesService } from './attributes.service';
@Module({
controllers: [AttributesController],
providers: [AttributesService],
exports: [AttributesService],
})
export class AttributesModule {}

View file

@ -0,0 +1,47 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { getCategoryForAttribute } from './category-mapper';
/**
* AttributesService
*
* Fetches attribute definitions from the attributes backend and enhances them
* with UI-specific metadata (categories, grouping) for the profile editor.
*/
@Injectable()
export class AttributesService {
private readonly logger = new Logger(AttributesService.name);
private readonly attributesApiUrl: string;
constructor(private readonly config: ConfigService) {
const host = this.config.get('ATTRIBUTES_API_HOST', 'localhost');
const port = this.config.get('ATTRIBUTES_API_PORT', '3015');
this.attributesApiUrl = `http://${host}:${port}/api`;
}
/**
* Fetch attribute definitions from attributes backend and enhance with categories
*/
async getEnhancedAttributeDefinitions(entityType: string = 'user') {
try {
const response = await fetch(
`${this.attributesApiUrl}/attribute-definitions?entityType=${entityType}`,
);
if (!response.ok) {
throw new Error(`Attributes API returned ${response.status}`);
}
const definitions = await response.json();
// Enhance each definition with metaCategory
return definitions.map((def: Record<string, unknown>) => ({
...def,
metaCategory: getCategoryForAttribute(def.code as string),
}));
} catch (error) {
this.logger.error('Failed to fetch attribute definitions', error);
throw error;
}
}
}

View file

@ -0,0 +1,284 @@
/**
* Category Mapper
*
* Maps attribute codes to UI categories (MetaCategory) for organizing
* the profile editor interface.
*/
export enum MetaCategory {
ESSENTIALS = 'essentials',
APPEARANCE = 'appearance',
SERVICES = 'services',
AVAILABILITY = 'availability',
RATES = 'rates',
DEPOSITS = 'deposits',
HEALTH_SAFETY = 'health_safety',
LOCATION = 'location',
PREFERENCES = 'preferences',
VERIFICATION = 'verification',
CONTENT = 'content',
KINK_BDSM = 'kink_bdsm',
}
/**
* Map attribute codes to their UI categories
*/
export const ATTRIBUTE_CATEGORY_MAP: Record<string, MetaCategory> = {
// ESSENTIALS - Demographics and basic info
'gender': MetaCategory.ESSENTIALS,
'ethnicity': MetaCategory.ESSENTIALS,
'sexual_orientation': MetaCategory.ESSENTIALS,
'nationality': MetaCategory.ESSENTIALS,
'age': MetaCategory.ESSENTIALS,
'languages': MetaCategory.ESSENTIALS,
'education': MetaCategory.ESSENTIALS,
'relationship_status': MetaCategory.ESSENTIALS,
// APPEARANCE - Physical attributes
'body_type': MetaCategory.APPEARANCE,
'height_inches': MetaCategory.APPEARANCE,
'hair_color': MetaCategory.APPEARANCE,
'hair_length': MetaCategory.APPEARANCE,
'hair_texture': MetaCategory.APPEARANCE,
'eye_color': MetaCategory.APPEARANCE,
'skin_tone': MetaCategory.APPEARANCE,
'glasses': MetaCategory.APPEARANCE,
'facial_hair': MetaCategory.APPEARANCE,
'body_hair': MetaCategory.APPEARANCE,
'bust_size': MetaCategory.APPEARANCE,
'cup_size': MetaCategory.APPEARANCE,
'waist_size': MetaCategory.APPEARANCE,
'hip_size': MetaCategory.APPEARANCE,
'dress_size': MetaCategory.APPEARANCE,
'shoe_size': MetaCategory.APPEARANCE,
'breast_type': MetaCategory.APPEARANCE,
'breast_shape': MetaCategory.APPEARANCE,
'butt_type': MetaCategory.APPEARANCE,
'lips_type': MetaCategory.APPEARANCE,
'tattoos': MetaCategory.APPEARANCE,
'tattoo_locations': MetaCategory.APPEARANCE,
'tattoo_styles': MetaCategory.APPEARANCE,
'piercings': MetaCategory.APPEARANCE,
'piercing_locations': MetaCategory.APPEARANCE,
'grooming': MetaCategory.APPEARANCE,
'nail_style': MetaCategory.APPEARANCE,
'makeup_style': MetaCategory.APPEARANCE,
'special_features': MetaCategory.APPEARANCE,
'voice_type': MetaCategory.APPEARANCE,
'accent': MetaCategory.APPEARANCE,
'fitness_level': MetaCategory.APPEARANCE,
// SERVICES - Service offerings
'meeting_type': MetaCategory.SERVICES,
'session_duration': MetaCategory.SERVICES,
'experience_level': MetaCategory.SERVICES,
'work_type': MetaCategory.SERVICES,
'collective_term': MetaCategory.SERVICES,
'service_category': MetaCategory.SERVICES,
'companion_style': MetaCategory.SERVICES,
'roleplay_scenarios': MetaCategory.SERVICES,
'niche_specialties': MetaCategory.SERVICES,
'physical_specialties': MetaCategory.SERVICES,
'incall_location_type': MetaCategory.SERVICES,
'incall_amenities': MetaCategory.SERVICES,
'incall_ambiance': MetaCategory.SERVICES,
'incall_music': MetaCategory.SERVICES,
'incall_lighting': MetaCategory.SERVICES,
'incall_scents': MetaCategory.SERVICES,
'pets_present': MetaCategory.SERVICES,
'refreshments': MetaCategory.SERVICES,
'bathroom_amenities': MetaCategory.SERVICES,
'outcall_preferences': MetaCategory.SERVICES,
// AVAILABILITY - Scheduling
'availability': MetaCategory.AVAILABILITY,
'booking_notice': MetaCategory.AVAILABILITY,
'response_time': MetaCategory.AVAILABILITY,
'days_available': MetaCategory.AVAILABILITY,
'time_blocks': MetaCategory.AVAILABILITY,
'tour_schedule': MetaCategory.AVAILABILITY,
'tour_cities_upcoming': MetaCategory.AVAILABILITY,
'touring_schedule': MetaCategory.AVAILABILITY,
'holiday_availability': MetaCategory.AVAILABILITY,
'work_arrangement': MetaCategory.AVAILABILITY,
// RATES - Pricing
'hourly_rate': MetaCategory.RATES,
'two_hour_rate': MetaCategory.RATES,
'overnight_rate': MetaCategory.RATES,
'payment_methods_accepted': MetaCategory.RATES,
'currency_preference': MetaCategory.RATES,
'tipping_preference': MetaCategory.RATES,
'rate_transparency': MetaCategory.RATES,
// DEPOSITS - Deposit and cancellation policies
'deposit_policy': MetaCategory.DEPOSITS,
'cancellation_policy': MetaCategory.DEPOSITS,
// HEALTH_SAFETY - Health and safety
'sti_testing': MetaCategory.HEALTH_SAFETY,
'safer_sex_practices': MetaCategory.HEALTH_SAFETY,
'vaccination_status': MetaCategory.HEALTH_SAFETY,
'safety_practices': MetaCategory.HEALTH_SAFETY,
'screening_requirements': MetaCategory.HEALTH_SAFETY,
'screening_accepted': MetaCategory.HEALTH_SAFETY,
'new_client_process': MetaCategory.HEALTH_SAFETY,
'diet_preferences': MetaCategory.HEALTH_SAFETY,
'smoking_status': MetaCategory.HEALTH_SAFETY,
'drinking_status': MetaCategory.HEALTH_SAFETY,
'substance_policy': MetaCategory.HEALTH_SAFETY,
// LOCATION - Location and travel
'service_cities': MetaCategory.LOCATION,
'travel_radius_miles': MetaCategory.LOCATION,
'nearest_airports': MetaCategory.LOCATION,
'neighborhood_type': MetaCategory.LOCATION,
'parking_available': MetaCategory.LOCATION,
'travel_status': MetaCategory.LOCATION,
'accessibility_physical': MetaCategory.LOCATION,
'accessibility_sensory': MetaCategory.LOCATION,
'accessibility_cognitive': MetaCategory.LOCATION,
// PREFERENCES - Personal preferences
'client_gender_preference': MetaCategory.PREFERENCES,
'client_preferences': MetaCategory.PREFERENCES,
'client_type': MetaCategory.PREFERENCES,
'client_interests': MetaCategory.PREFERENCES,
'communication_style': MetaCategory.PREFERENCES,
'kink_experience': MetaCategory.PREFERENCES,
'pet_ownership': MetaCategory.PREFERENCES,
'zodiac_sign': MetaCategory.PREFERENCES,
'mbti_type': MetaCategory.PREFERENCES,
'personality_vibes': MetaCategory.PREFERENCES,
'love_language': MetaCategory.PREFERENCES,
'conversation_style': MetaCategory.PREFERENCES,
'interests': MetaCategory.PREFERENCES,
'dresscode_preference': MetaCategory.PREFERENCES,
'gift_preferences': MetaCategory.PREFERENCES,
'body_diversity': MetaCategory.PREFERENCES,
'lgbtq_friendly': MetaCategory.PREFERENCES,
// VERIFICATION - Verification and reviews
'verification_level': MetaCategory.VERIFICATION,
'verification_sites': MetaCategory.VERIFICATION,
'review_status': MetaCategory.VERIFICATION,
'social_media_presence': MetaCategory.VERIFICATION,
'professional_background': MetaCategory.VERIFICATION,
'special_skills': MetaCategory.VERIFICATION,
'training_certifications': MetaCategory.VERIFICATION,
// CONTENT - Content creation
'content_policy': MetaCategory.CONTENT,
'content_platforms': MetaCategory.CONTENT,
'content_types': MetaCategory.CONTENT,
'custom_content_policy': MetaCategory.CONTENT,
'collab_status': MetaCategory.CONTENT,
'posting_frequency': MetaCategory.CONTENT,
'subscription_tiers': MetaCategory.CONTENT,
'cam_platforms': MetaCategory.CONTENT,
'cam_show_types': MetaCategory.CONTENT,
'interactive_toys': MetaCategory.CONTENT,
'streaming_schedule': MetaCategory.CONTENT,
// KINK_BDSM - BDSM and kink
'bdsm_role': MetaCategory.KINK_BDSM,
'domination_style': MetaCategory.KINK_BDSM,
'fetishes_offered': MetaCategory.KINK_BDSM,
'fetish_hard_limits': MetaCategory.KINK_BDSM,
'equipment_available': MetaCategory.KINK_BDSM,
'bdsm_philosophy': MetaCategory.KINK_BDSM,
'safeword_system': MetaCategory.KINK_BDSM,
'aftercare_provided': MetaCategory.KINK_BDSM,
'experience_level_bdsm': MetaCategory.KINK_BDSM,
'protocol_level': MetaCategory.KINK_BDSM,
'power_exchange_style': MetaCategory.KINK_BDSM,
// CLIENT PROFILE ATTRIBUTES (for registered clients)
'net_worth_range': MetaCategory.PREFERENCES,
'annual_income_range': MetaCategory.PREFERENCES,
};
/**
* Get the category for an attribute code
*/
export function getCategoryForAttribute(code: string): MetaCategory | null {
return ATTRIBUTE_CATEGORY_MAP[code] || null;
}
/**
* Category metadata for UI display
*/
export const CATEGORY_METADATA: Record<MetaCategory, { label: string; description: string; icon: string; order: number }> = {
[MetaCategory.ESSENTIALS]: {
label: 'Essentials',
description: 'Demographics, identity, and communication basics',
icon: 'star',
order: 1,
},
[MetaCategory.APPEARANCE]: {
label: 'Appearance',
description: 'Physical attributes and style',
icon: 'user',
order: 2,
},
[MetaCategory.SERVICES]: {
label: 'Services',
description: 'Service offerings and specialties',
icon: 'briefcase',
order: 3,
},
[MetaCategory.RATES]: {
label: 'Rates & Payment',
description: 'Pricing and payment preferences',
icon: 'dollar-sign',
order: 4,
},
[MetaCategory.DEPOSITS]: {
label: 'Deposits & Cancellations',
description: 'Deposit requirements and cancellation policies',
icon: 'credit-card',
order: 5,
},
[MetaCategory.AVAILABILITY]: {
label: 'Availability',
description: 'Scheduling and booking',
icon: 'calendar',
order: 6,
},
[MetaCategory.LOCATION]: {
label: 'Location & Travel',
description: 'Service areas and accessibility',
icon: 'map-pin',
order: 7,
},
[MetaCategory.HEALTH_SAFETY]: {
label: 'Health & Safety',
description: 'Screening, health, and safety practices',
icon: 'shield',
order: 8,
},
[MetaCategory.PREFERENCES]: {
label: 'Preferences',
description: 'Client preferences and personality',
icon: 'heart',
order: 9,
},
[MetaCategory.KINK_BDSM]: {
label: 'Kink & BDSM',
description: 'BDSM roles, fetishes, and practices',
icon: 'lock',
order: 10,
},
[MetaCategory.CONTENT]: {
label: 'Content Creation',
description: 'Digital content and platforms',
icon: 'camera',
order: 11,
},
[MetaCategory.VERIFICATION]: {
label: 'Verification',
description: 'Credentials and professional background',
icon: 'check-circle',
order: 12,
},
};

View file

@ -23,7 +23,7 @@ const VIEWS = [
function ManageRoute() {
const navigate = useNavigate();
return <ManageView onEditProfile={(slug) => navigate(`/providers/${slug}`)} />;
return <ManageView onEditProfile={(slug) => navigate(`/providers/${slug}/edit`)} />;
}
export function App() {
@ -34,7 +34,8 @@ export function App() {
<Routes>
<Route path="/" element={<ClientView />} />
<Route path="/manage" element={<ManageRoute />} />
<Route path="/providers/:slug" element={<ProfileEditorRoute />} />
<Route path="/providers/:slug/edit" element={<ProfileEditorRoute />} />
{/* TODO: Add ProfileViewRoute for /providers/:slug */}
</Routes>
</BrowserRouter>
</QueryClientProvider>

View file

@ -0,0 +1,62 @@
import { chromium } from 'playwright';
(async () => {
const browser = await chromium.launch();
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
console.log('Navigating to http://localhost:3401/system');
await page.goto('http://localhost:3401/system');
// Wait for the System Dependencies section to load
await page.waitForTimeout(3000);
// Take screenshot
const screenshotPath = '/tmp/system-dependencies-screenshot.png';
await page.screenshot({ path: screenshotPath, fullPage: true });
console.log(`Screenshot saved to: ${screenshotPath}`);
// Extract dependency information
console.log('\n=== System Dependencies ===\n');
try {
// Wait for System Dependencies section
const sectionTitle = await page.locator('text=System Dependencies').first();
await sectionTitle.waitFor({ timeout: 5000 });
// Find all layer groups
const layerGroups = await page.locator('[class*="LayerGroup"]').all();
for (const group of layerGroups) {
// Get layer name
const layerName = await group.locator('[class*="LayerHeader"]').first().innerText();
console.log(`\n${layerName.toUpperCase()}`);
// Get all dependency rows in this group
const depRows = await group.locator('[class*="DepRow"]').all();
for (const row of depRows) {
const label = await row.locator('[class*="DepLabel"]').innerText();
const status = await row.locator('[class*="DepTag"]').innerText();
const dotColor = await row.locator('[class*="DepDot"]').evaluate(el =>
window.getComputedStyle(el).backgroundColor
);
// Convert RGB to status indicator
let indicator = '●';
if (dotColor.includes('100, 255, 218')) indicator = '✓'; // green
else if (dotColor.includes('239, 83, 80')) indicator = '✗'; // red
else if (dotColor.includes('255, 183, 77')) indicator = '⚠'; // orange
else if (dotColor.includes('102, 102, 102')) indicator = '○'; // gray
console.log(` ${indicator} ${label} = ${status}`);
}
}
} catch (error) {
console.log('Error extracting dependencies:', error.message);
}
await browser.close();
})();