From c01c1a563639f558a8a893c3f35d272cb2966617 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Mon, 25 May 2026 21:53:01 -0400 Subject: [PATCH] rebranding work --- .env.example | 4 +- .env.prod.example | 2 +- .github/workflows/ci.yml | 10 +- .../data/repository/HomeTitleRepository.kt | 61 ++++ .../repository/RemoveBrokersRepository.kt | 81 +++++ .../data/repository/SpamShieldRepository.kt | 86 +++++ .../shieldai/android/di/RepositoryModule.kt | 33 ++ .../android/ui/components/ThreatGauge.kt | 107 ++++++ .../ui/screens/dashboard/AlertDetailScreen.kt | 281 +++++++++++++++ .../ui/screens/dashboard/DashboardScreen.kt | 325 +++++++++++++++++ .../ui/screens/services/DarkWatchScreen.kt | 332 ++++++++++++++++++ .../ui/screens/services/VoicePrintScreen.kt | 256 ++++++++++++++ .../android/viewmodel/AlertDetailViewModel.kt | 93 +++++ .../android/viewmodel/DarkWatchViewModel.kt | 100 ++++++ .../android/viewmodel/DashboardViewModel.kt | 154 ++++++++ .../android/viewmodel/HomeTitleViewModel.kt | 84 +++++ .../viewmodel/RemoveBrokersViewModel.kt | 93 +++++ .../android/viewmodel/SettingsViewModel.kt | 109 ++++++ .../android/viewmodel/SpamShieldViewModel.kt | 98 ++++++ .../android/viewmodel/VoicePrintViewModel.kt | 96 +++++ browser-ext/package.json | 2 +- package.json | 8 +- plans/FRE-4499-implementation-plan.md | 2 +- plans/FRE-4523-rate-limit-middleware.md | 6 +- plans/FRE-4524-spamshield-routes.md | 2 +- .../waitlist-email-sequence-implementation.md | 2 +- .../01-update-monorepo-foundation.md | 37 ++ .../02-update-database-connection-strings.md | 39 ++ ...03-update-web-package-scope-and-imports.md | 39 ++ .../04-update-web-ui-brand-text.md | 46 +++ ...update-web-email-notification-templates.md | 46 +++ ...6-update-web-storage-keys-and-constants.md | 37 ++ ...7-update-web-seed-data-and-blog-content.md | 32 ++ .../08-update-browser-extension-branding.md | 47 +++ .../09-update-ios-app-branding.md | 47 +++ .../10-update-android-app-branding.md | 54 +++ .../11-update-ci-cd-and-infrastructure.md | 39 ++ .../12-update-assets-and-ad-creatives.md | 40 +++ .../13-update-documentation-and-plan-files.md | 39 ++ .../14-verify-rebrand-completeness.md | 63 ++++ tasks/rebrand-to-kordant/README.md | 42 +++ web/.env.example | 16 - web/drizzle.config.ts | 2 +- web/src/server/db/index.ts | 2 +- web/src/server/jobs/queue.ts | 2 +- web/src/server/services/alert.publisher.ts | 2 +- .../server/services/email.templates.test.ts | 16 +- web/src/server/services/email.templates.ts | 32 +- .../services/notification.service.test.ts | 2 +- .../server/services/notification.service.ts | 2 +- web/src/server/services/reports.service.ts | 2 +- web/src/server/services/reports/generator.ts | 8 +- 52 files changed, 3090 insertions(+), 70 deletions(-) create mode 100644 android/ShieldAI/app/src/main/java/com/shieldai/android/data/repository/HomeTitleRepository.kt create mode 100644 android/ShieldAI/app/src/main/java/com/shieldai/android/data/repository/RemoveBrokersRepository.kt create mode 100644 android/ShieldAI/app/src/main/java/com/shieldai/android/data/repository/SpamShieldRepository.kt create mode 100644 android/ShieldAI/app/src/main/java/com/shieldai/android/ui/components/ThreatGauge.kt create mode 100644 android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/dashboard/AlertDetailScreen.kt create mode 100644 android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/dashboard/DashboardScreen.kt create mode 100644 android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/services/DarkWatchScreen.kt create mode 100644 android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/services/VoicePrintScreen.kt create mode 100644 android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/AlertDetailViewModel.kt create mode 100644 android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/DarkWatchViewModel.kt create mode 100644 android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/DashboardViewModel.kt create mode 100644 android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/HomeTitleViewModel.kt create mode 100644 android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/RemoveBrokersViewModel.kt create mode 100644 android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/SettingsViewModel.kt create mode 100644 android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/SpamShieldViewModel.kt create mode 100644 android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/VoicePrintViewModel.kt create mode 100644 tasks/rebrand-to-kordant/01-update-monorepo-foundation.md create mode 100644 tasks/rebrand-to-kordant/02-update-database-connection-strings.md create mode 100644 tasks/rebrand-to-kordant/03-update-web-package-scope-and-imports.md create mode 100644 tasks/rebrand-to-kordant/04-update-web-ui-brand-text.md create mode 100644 tasks/rebrand-to-kordant/05-update-web-email-notification-templates.md create mode 100644 tasks/rebrand-to-kordant/06-update-web-storage-keys-and-constants.md create mode 100644 tasks/rebrand-to-kordant/07-update-web-seed-data-and-blog-content.md create mode 100644 tasks/rebrand-to-kordant/08-update-browser-extension-branding.md create mode 100644 tasks/rebrand-to-kordant/09-update-ios-app-branding.md create mode 100644 tasks/rebrand-to-kordant/10-update-android-app-branding.md create mode 100644 tasks/rebrand-to-kordant/11-update-ci-cd-and-infrastructure.md create mode 100644 tasks/rebrand-to-kordant/12-update-assets-and-ad-creatives.md create mode 100644 tasks/rebrand-to-kordant/13-update-documentation-and-plan-files.md create mode 100644 tasks/rebrand-to-kordant/14-verify-rebrand-completeness.md create mode 100644 tasks/rebrand-to-kordant/README.md delete mode 100644 web/.env.example diff --git a/.env.example b/.env.example index 1ff19d0..b560a06 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -DATABASE_URL="postgresql://shieldai:shieldai_dev@localhost:5432/shieldai" +DATABASE_URL="postgresql://kordant:kordant_dev@localhost:5432/kordant" REDIS_URL="redis://localhost:6379" PORT=3000 LOG_LEVEL=info @@ -7,7 +7,7 @@ RESEND_API_KEY="" AWS_REGION="us-east-1" # Datadog APM Configuration -DD_SERVICE="shieldai-api" +DD_SERVICE="kordant-api" DD_ENV="development" DD_VERSION="0.1.0" DD_TRACE_ENABLED="true" diff --git a/.env.prod.example b/.env.prod.example index d47b868..cab6eaf 100644 --- a/.env.prod.example +++ b/.env.prod.example @@ -7,7 +7,7 @@ RESEND_API_KEY="" # Docker (for deployment) DOCKER_TAG=latest -GITHUB_REPOSITORY_OWNER=shieldai +GITHUB_REPOSITORY_OWNER=kordant # Server PORT=3000 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0bf7e22..ca84e69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,13 +58,13 @@ jobs: postgres: image: postgres:16-alpine env: - POSTGRES_DB: shieldai - POSTGRES_USER: shieldai - POSTGRES_PASSWORD: shieldai_dev + POSTGRES_DB: kordant + POSTGRES_USER: kordant + POSTGRES_PASSWORD: kordant_dev ports: - 5432:5432 options: >- - --health-cmd "pg_isready -U shieldai" + --health-cmd "pg_isready -U kordant" --health-interval 5s --health-timeout 5s --health-retries 5 @@ -92,7 +92,7 @@ jobs: - name: Run tests run: pnpm test env: - DATABASE_URL: "postgresql://shieldai:shieldai_dev@localhost:5432/shieldai" + DATABASE_URL: "postgresql://kordant:kordant_dev@localhost:5432/kordant" REDIS_URL: "redis://localhost:6379" - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/data/repository/HomeTitleRepository.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/data/repository/HomeTitleRepository.kt new file mode 100644 index 0000000..f45b91a --- /dev/null +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/data/repository/HomeTitleRepository.kt @@ -0,0 +1,61 @@ +package com.shieldai.android.data.repository + +import android.content.Context +import com.shieldai.android.data.local.CacheManager +import com.shieldai.android.data.model.Property +import com.shieldai.android.data.remote.ApiResult +import com.shieldai.android.data.remote.ErrorHandler +import com.shieldai.android.data.remote.TRPCApiService +import com.shieldai.android.data.remote.TRPCRequest +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +class HomeTitleRepository( + private val api: TRPCApiService, + private val context: Context, +) { + private val _properties = MutableStateFlow>(emptyList()) + + suspend fun getProperties(forceRefresh: Boolean = false): ApiResult> { + if (!forceRefresh) { + val cached: List? = CacheManager.load(context, "properties") + if (cached != null) { + _properties.value = cached + return ApiResult.Success(cached) + } + } + return ErrorHandler.executeWithRetry { + val response = api.propertyList(TRPCRequest.body(buildJsonObject {})) + val properties = response.result.data + CacheManager.save(context, "properties", properties) + _properties.value = properties + properties + } + } + + suspend fun addProperty(address: String, type: String = "residential"): ApiResult { + return ErrorHandler.executeWithRetry { + val body = buildJsonObject { + put("address", address) + put("type", type) + } + val response = api.propertyAdd(TRPCRequest.body(body)) + val property = response.result.data + refreshCache() + property + } + } + + fun observeProperties(): Flow> = _properties + + private suspend fun refreshCache() { + ErrorHandler.executeWithRetry { + val response = api.propertyList(TRPCRequest.body(buildJsonObject {})) + val properties = response.result.data + CacheManager.save(context, "properties", properties) + _properties.value = properties + } + } +} diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/data/repository/RemoveBrokersRepository.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/data/repository/RemoveBrokersRepository.kt new file mode 100644 index 0000000..35c6b71 --- /dev/null +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/data/repository/RemoveBrokersRepository.kt @@ -0,0 +1,81 @@ +package com.shieldai.android.data.repository + +import android.content.Context +import com.shieldai.android.data.local.CacheManager +import com.shieldai.android.data.model.BrokerListing +import com.shieldai.android.data.model.RemovalRequest +import com.shieldai.android.data.remote.ApiResult +import com.shieldai.android.data.remote.ErrorHandler +import com.shieldai.android.data.remote.TRPCApiService +import com.shieldai.android.data.remote.TRPCRequest +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +class RemoveBrokersRepository( + private val api: TRPCApiService, + private val context: Context, +) { + private val _listings = MutableStateFlow>(emptyList()) + private val _removalRequests = MutableStateFlow>(emptyList()) + + suspend fun getListings(forceRefresh: Boolean = false): ApiResult> { + if (!forceRefresh) { + val cached: List? = CacheManager.load(context, "broker_listings") + if (cached != null) { + _listings.value = cached + return ApiResult.Success(cached) + } + } + return ErrorHandler.executeWithRetry { + val response = api.brokerListListings(TRPCRequest.body(buildJsonObject {})) + val listings = response.result.data + CacheManager.save(context, "broker_listings", listings) + _listings.value = listings + listings + } + } + + suspend fun getRemovalRequests(forceRefresh: Boolean = false): ApiResult> { + if (!forceRefresh) { + val cached: List? = CacheManager.load(context, "removal_requests") + if (cached != null) { + _removalRequests.value = cached + return ApiResult.Success(cached) + } + } + return ErrorHandler.executeWithRetry { + val response = api.removalList(TRPCRequest.body(buildJsonObject {})) + val requests = response.result.data + CacheManager.save(context, "removal_requests", requests) + _removalRequests.value = requests + requests + } + } + + suspend fun createRemovalRequest(listingId: String, notes: String? = null): ApiResult { + return ErrorHandler.executeWithRetry { + val body = buildJsonObject { + put("listingId", listingId) + notes?.let { put("notes", it) } + } + val response = api.removalCreate(TRPCRequest.body(body)) + val request = response.result.data + refreshRemovalsCache() + request + } + } + + fun observeListings(): Flow> = _listings + fun observeRemovalRequests(): Flow> = _removalRequests + + private suspend fun refreshRemovalsCache() { + ErrorHandler.executeWithRetry { + val response = api.removalList(TRPCRequest.body(buildJsonObject {})) + val requests = response.result.data + CacheManager.save(context, "removal_requests", requests) + _removalRequests.value = requests + } + } +} diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/data/repository/SpamShieldRepository.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/data/repository/SpamShieldRepository.kt new file mode 100644 index 0000000..499d810 --- /dev/null +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/data/repository/SpamShieldRepository.kt @@ -0,0 +1,86 @@ +package com.shieldai.android.data.repository + +import android.content.Context +import com.shieldai.android.data.local.CacheManager +import com.shieldai.android.data.model.SpamRule +import com.shieldai.android.data.remote.ApiResult +import com.shieldai.android.data.remote.ErrorHandler +import com.shieldai.android.data.remote.TRPCApiService +import com.shieldai.android.data.remote.TRPCRequest +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +class SpamShieldRepository( + private val api: TRPCApiService, + private val context: Context, +) { + private val _rules = MutableStateFlow>(emptyList()) + + data class SpamStats( + val totalBlocked: Int = 0, + val totalFlagged: Int = 0, + val activeRules: Int = 0 + ) + + suspend fun getRules(forceRefresh: Boolean = false): ApiResult> { + if (!forceRefresh) { + val cached: List? = CacheManager.load(context, "spam_rules") + if (cached != null) { + _rules.value = cached + return ApiResult.Success(cached) + } + } + return ErrorHandler.executeWithRetry { + val response = api.spamListRules(TRPCRequest.body(buildJsonObject {})) + val rules = response.result.data + CacheManager.save(context, "spam_rules", rules) + _rules.value = rules + rules + } + } + + suspend fun createRule(pattern: String, action: String, description: String? = null): ApiResult { + return ErrorHandler.executeWithRetry { + val body = buildJsonObject { + put("pattern", pattern) + put("action", action) + description?.let { put("description", it) } + } + val response = api.spamCreateRule(TRPCRequest.body(body)) + val rule = response.result.data + refreshCache() + rule + } + } + + suspend fun toggleRule(id: String, enabled: Boolean): ApiResult { + return ErrorHandler.executeWithRetry { + _rules.value = _rules.value.map { + if (it.id == id) it.copy(enabled = enabled) else it + } + refreshCache() + } + } + + fun getStats(): SpamStats { + val rules = _rules.value + return SpamStats( + totalBlocked = rules.count { it.action == "block" && it.enabled }, + totalFlagged = rules.count { it.action == "flag" && it.enabled }, + activeRules = rules.count { it.enabled } + ) + } + + fun observeRules(): Flow> = _rules + + private suspend fun refreshCache() { + ErrorHandler.executeWithRetry { + val response = api.spamListRules(TRPCRequest.body(buildJsonObject {})) + val rules = response.result.data + CacheManager.save(context, "spam_rules", rules) + _rules.value = rules + } + } +} diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/di/RepositoryModule.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/di/RepositoryModule.kt index 9ded37f..a459794 100644 --- a/android/ShieldAI/app/src/main/java/com/shieldai/android/di/RepositoryModule.kt +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/di/RepositoryModule.kt @@ -3,6 +3,9 @@ package com.shieldai.android.di import android.content.Context import com.shieldai.android.data.repository.AlertRepository import com.shieldai.android.data.repository.DarkWatchRepository +import com.shieldai.android.data.repository.HomeTitleRepository +import com.shieldai.android.data.repository.RemoveBrokersRepository +import com.shieldai.android.data.repository.SpamShieldRepository import com.shieldai.android.data.repository.SubscriptionRepository import com.shieldai.android.data.repository.UserRepository import com.shieldai.android.data.repository.VoicePrintRepository @@ -13,6 +16,9 @@ object RepositoryModule { private var voicePrintRepository: VoicePrintRepository? = null private var alertRepository: AlertRepository? = null private var subscriptionRepository: SubscriptionRepository? = null + private var spamShieldRepository: SpamShieldRepository? = null + private var homeTitleRepository: HomeTitleRepository? = null + private var removeBrokersRepository: RemoveBrokersRepository? = null fun provideUserRepository(context: Context): UserRepository { return userRepository ?: synchronized(this) { @@ -58,4 +64,31 @@ object RepositoryModule { ).also { subscriptionRepository = it } } } + + fun provideSpamShieldRepository(context: Context): SpamShieldRepository { + return spamShieldRepository ?: synchronized(this) { + SpamShieldRepository( + api = NetworkModule.provideApiService(context), + context = context, + ).also { spamShieldRepository = it } + } + } + + fun provideHomeTitleRepository(context: Context): HomeTitleRepository { + return homeTitleRepository ?: synchronized(this) { + HomeTitleRepository( + api = NetworkModule.provideApiService(context), + context = context, + ).also { homeTitleRepository = it } + } + } + + fun provideRemoveBrokersRepository(context: Context): RemoveBrokersRepository { + return removeBrokersRepository ?: synchronized(this) { + RemoveBrokersRepository( + api = NetworkModule.provideApiService(context), + context = context, + ).also { removeBrokersRepository = it } + } + } } diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/components/ThreatGauge.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/components/ThreatGauge.kt new file mode 100644 index 0000000..8fd47dc --- /dev/null +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/components/ThreatGauge.kt @@ -0,0 +1,107 @@ +package com.shieldai.android.ui.components + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.dp +import com.shieldai.android.ui.theme.Error +import com.shieldai.android.ui.theme.Success +import com.shieldai.android.ui.theme.TextPrimaryLight +import com.shieldai.android.ui.theme.Warning + +@Composable +fun ThreatGauge( + score: Int, + modifier: Modifier = Modifier, + size: Int = 160 +) { + val (startColor, endColor) = when { + score <= 30 -> Success to Success.copy(alpha = 0.4f) + score <= 60 -> Warning to Warning.copy(alpha = 0.4f) + else -> Error to Error.copy(alpha = 0.4f) + } + + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Canvas( + modifier = Modifier.size(size.dp) + ) { + val center = Offset(size / 2f, size / 2f) + val radius = (size / 2f - 16f) * dpiScale + val strokeWidth = 16f * dpiScale + + drawArc( + color = Color(0xFFE2E8F0).copy(alpha = 0.3f), + startAngle = -135f, + sweepAngle = 270f, + useCenter = false, + topLeft = Offset( + center.x - radius, + center.y - radius + ), + size = androidx.compose.ui.geometry.Size(radius * 2, radius * 2), + style = Stroke(width = strokeWidth) + ) + + val sweepAngle = (score / 100f) * 270f + if (sweepAngle > 0) { + val gradient = Brush.linearGradient( + colors = listOf(startColor, endColor), + start = Offset(center.x - radius, center.y), + end = Offset(center.x + radius, center.y) + ) + + drawArc( + brush = gradient, + startAngle = -135f, + sweepAngle = sweepAngle, + useCenter = false, + topLeft = Offset( + center.x - radius, + center.y - radius + ), + size = androidx.compose.ui.geometry.Size(radius * 2, radius * 2), + style = Stroke(width = strokeWidth, cap = androidx.compose.ui.graphics.Paint.Cap.Round) + ) + } + } + + Text( + text = "$score", + style = MaterialTheme.typography.displayMedium, + color = when { + score <= 30 -> Success + score <= 60 -> Warning + else -> Error + }, + modifier = Modifier.padding(top = 8.dp) + ) + + Text( + text = when { + score <= 30 -> "Low Risk" + score <= 60 -> "Medium Risk" + else -> "High Risk" + }, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 4.dp) + ) + } +} + +private val Float.dpiScale: Float + get() = 1f diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/dashboard/AlertDetailScreen.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/dashboard/AlertDetailScreen.kt new file mode 100644 index 0000000..a5d0603 --- /dev/null +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/dashboard/AlertDetailScreen.kt @@ -0,0 +1,281 @@ +package com.shieldai.android.ui.screens.dashboard + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.shieldai.android.data.model.Alert +import com.shieldai.android.ui.components.BadgeVariant +import com.shieldai.android.ui.components.ShieldBadge +import com.shieldai.android.ui.components.ShieldButton +import com.shieldai.android.ui.components.ShieldButtonVariant +import com.shieldai.android.ui.components.ShieldCard +import com.shieldai.android.ui.components.ShieldEmptyState +import com.shieldai.android.ui.viewmodel.AlertDetailViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AlertDetailScreen( + alertId: String, + onBack: () -> Unit = {}, + modifier: Modifier = Modifier, + viewModel: AlertDetailViewModel = viewModel(factory = AlertDetailViewModel.Factory) +) { + val uiState by viewModel.uiState.collectAsState() + + if (uiState.alert == null) { + viewModel.loadAlert(alertId) + } + + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + rememberTopAppBarState() + ) + + Scaffold( + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + LargeTopAppBar( + title = { + Text( + text = uiState.alert?.title ?: "Alert Details", + fontWeight = FontWeight.SemiBold + ) + }, + navigationIcon = { + TextButton(onClick = onBack) { + Text("Back") + } + }, + scrollBehavior = scrollBehavior + ) + } + ) { paddingValues -> + when { + uiState.isLoading -> { + Box( + modifier = Modifier.fillMaxSize().padding(paddingValues), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + } + } + uiState.alert == null -> { + ShieldEmptyState( + title = "Alert not found", + description = "The requested alert could not be loaded", + actionButton = { + TextButton(onClick = onBack) { + Text("Go Back") + } + }, + modifier = Modifier.padding(paddingValues) + ) + } + else -> { + AlertDetailContent( + uiState = uiState, + onMarkResolved = { viewModel.markResolved() }, + onMarkFalsePositive = { viewModel.markFalsePositive() }, + modifier = Modifier.padding(paddingValues) + ) + } + } + } +} + +@Composable +private fun AlertDetailContent( + uiState: AlertDetailViewModel.AlertDetailUiState, + onMarkResolved: () -> Unit, + onMarkFalsePositive: () -> Unit, + modifier: Modifier = Modifier +) { + val alert = uiState.alert!! + + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + item { + AlertDetailHeader(alert) + } + + item { + AlertDetailInfo(alert) + } + + if (uiState.correlatedAlerts.isNotEmpty()) { + item { + Text( + text = "Correlated Alerts (${uiState.correlatedAlerts.size})", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Spacer(modifier = Modifier.height(8.dp)) + } + + items(uiState.correlatedAlerts) { correlated -> + CorrelatedAlertItem(correlated) + Spacer(modifier = Modifier.height(8.dp)) + } + } + + item { + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + ShieldButton( + text = "Mark Resolved", + onClick = onMarkResolved, + variant = ShieldButtonVariant.Primary, + modifier = Modifier.weight(1f), + loading = uiState.isResolving + ) + ShieldButton( + text = "False Positive", + onClick = onMarkFalsePositive, + variant = ShieldButtonVariant.Secondary, + modifier = Modifier.weight(1f) + ) + } + } + } +} + +@Composable +private fun AlertDetailHeader(alert: Alert) { + Column { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val variant = when (alert.severity.lowercase()) { + "critical" -> BadgeVariant.Error + "high" -> BadgeVariant.Warning + "medium" -> BadgeVariant.Info + else -> BadgeVariant.Default + } + ShieldBadge( + text = "${alert.severity} severity", + variant = variant + ) + if (!alert.read) { + ShieldBadge( + text = "Unread", + variant = BadgeVariant.Info + ) + } + } + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = alert.title, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = alert.message, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} + +@Composable +private fun AlertDetailInfo(alert: Alert) { + ShieldCard { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "Details", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold + ) + InfoRow(label = "Type", value = alert.type) + InfoRow(label = "Severity", value = alert.severity) + InfoRow(label = "Status", value = if (alert.read) "Read" else "Unread") + alert.date?.let { + InfoRow(label = "Date", value = it) + } + alert.createdAt?.let { + InfoRow(label = "Created", value = it) + } + } + } +} + +@Composable +private fun InfoRow(label: String, value: String) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = label, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium + ) + } +} + +@Composable +private fun CorrelatedAlertItem(alert: Alert) { + ShieldCard( + modifier = Modifier.fillMaxWidth() + ) { + Column { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = alert.title, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + modifier = Modifier.weight(1f) + ) + AlertSeverityBadge(severity = alert.severity) + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = alert.message, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2 + ) + } + } +} diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/dashboard/DashboardScreen.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/dashboard/DashboardScreen.kt new file mode 100644 index 0000000..0f82e76 --- /dev/null +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/dashboard/DashboardScreen.kt @@ -0,0 +1,325 @@ +package com.shieldai.android.ui.screens.dashboard + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PullToRefreshBox +import androidx.compose.material3.Text +import androidx.compose.material3.pullToRefresh +import androidx.compose.material3.pullToRefreshDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.shieldai.android.R +import com.shieldai.android.data.model.Alert +import com.shieldai.android.ui.components.BadgeVariant +import com.shieldai.android.ui.components.ShieldBadge +import com.shieldai.android.ui.components.ShieldCard +import com.shieldai.android.ui.components.ShieldEmptyState +import com.shieldai.android.ui.components.ShieldSkeletonCard +import com.shieldai.android.ui.components.ThreatGauge +import com.shieldai.android.viewmodel.DashboardViewModel +import com.shieldai.android.viewmodel.DashboardViewModel as DashboardVM + +data class ServiceSummary( + val name: String, + val count: Int, + val icon: ImageVector, + val route: String +) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DashboardScreen( + onNavigateToAlert: (String) -> Unit = {}, + onNavigateToService: (String) -> Unit = {}, + modifier: Modifier = Modifier, + viewModel: DashboardViewModel = viewModel(factory = DashboardVM.Factory) +) { + val uiState by viewModel.uiState.collectAsState() + var isRefreshing by remember { mutableStateOf(false) } + var isRefreshingInternal by remember { mutableStateOf(false) } + + if (isRefreshing) { + viewModel.refresh() + isRefreshing = false + } + + PullToRefreshBox( + isRefreshing = isRefreshingInternal || uiState.isLoading, + onRefresh = { + isRefreshingInternal = false + viewModel.refresh() + }, + modifier = modifier, + indicator = { + androidx.compose.material3.pullToRefreshDefaults.indicator( + parent, + transformer, + isRefreshingInternal || uiState.isLoading + ) + } + ) { + when { + uiState.isLoading && uiState.recentAlerts.isEmpty() -> { + DashboardLoadingState() + } + uiState.recentAlerts.isEmpty() && uiState.threatScore == 0 -> { + if (uiState.error != null) { + ShieldEmptyState( + title = "Failed to load", + description = uiState.error ?: "Unknown error", + actionButton = { + androidx.compose.material3.TextButton(onClick = { viewModel.refresh() }) { + Text("Retry") + } + } + ) + } else { + ShieldEmptyState( + title = "No data", + description = "No dashboard data available" + ) + } + } + else -> { + DashboardContent( + uiState = uiState, + onNavigateToAlert = onNavigateToAlert, + onNavigateToService = onNavigateToService + ) + } + } + } +} + +@Composable +private fun DashboardLoadingState() { + LazyColumn( + modifier = Modifier.fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + item { + Box( + modifier = Modifier.fillMaxWidth().padding(16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + } + } + item { ShieldSkeletonCard() } + item { ShieldSkeletonCard() } + item { ShieldSkeletonCard() } + } +} + +@Composable +private fun DashboardContent( + uiState: DashboardViewModel.DashboardUiState, + onNavigateToAlert: (String) -> Unit, + onNavigateToService: (String) -> Unit +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = androidx.compose.foundation.layout.PaddingValues( + horizontal = 16.dp, + vertical = 16.dp + ), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + item { + DashboardHeader(uiState) + } + + item { + ServiceSummaryRow( + uiState = uiState, + onNavigateToService = onNavigateToService + ) + } + + if (uiState.recentAlerts.isNotEmpty()) { + item { + Text( + text = "Recent Alerts", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Spacer(modifier = Modifier.height(8.dp)) + } + + items(uiState.recentAlerts) { alert -> + AlertCard(alert, onNavigateToAlert) + Spacer(modifier = Modifier.height(8.dp)) + } + } + } +} + +@Composable +private fun DashboardHeader(uiState: DashboardViewModel.DashboardUiState) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp) + ) { + Text( + text = "Threat Overview", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(bottom = 16.dp) + ) + ThreatGauge(score = uiState.threatScore) + + if (uiState.unreadCount > 0) { + ShieldBadge( + text = "${uiState.unreadCount} unread alert${if (uiState.unreadCount > 1) "s" else ""}", + variant = BadgeVariant.Warning, + modifier = Modifier.padding(top = 12.dp) + ) + } + } +} + +@Composable +private fun ServiceSummaryRow( + uiState: DashboardViewModel.DashboardUiState, + onNavigateToService: (String) -> Unit +) { + val services = listOf( + ServiceSummary("DarkWatch", uiState.watchlistCount, ImageVector.vectorResource(R.drawable.ic_services), "darkwatch"), + ServiceSummary("VoicePrint", uiState.enrollmentCount, ImageVector.vectorResource(R.drawable.ic_services), "voiceprint"), + ServiceSummary("SpamShield", uiState.spamRulesCount, ImageVector.vectorResource(R.drawable.ic_services), "spamshield"), + ServiceSummary("HomeTitle", uiState.propertiesCount, ImageVector.vectorResource(R.drawable.ic_services), "hometitle"), + ServiceSummary("RemoveBrokers", uiState.removalsCount, ImageVector.vectorResource(R.drawable.ic_services), "removebrokers") + ) + + Text( + text = "Services", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Spacer(modifier = Modifier.height(8.dp)) + + LazyRow( + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(services) { service -> + ServiceCard( + service = service, + onClick = { onNavigateToService(service.route) } + ) + } + } +} + +@Composable +private fun ServiceCard( + service: ServiceSummary, + onClick: () -> Unit +) { + ShieldCard( + onClick = onClick, + modifier = Modifier.width(130.dp) + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.padding(8.dp) + ) { + Icon( + imageVector = service.icon, + contentDescription = service.name, + modifier = Modifier.size(32.dp), + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = service.name, + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Medium + ) + Text( + text = "${service.count}", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + } + } +} + +@Composable +private fun AlertCard( + alert: Alert, + onClick: (String) -> Unit +) { + ShieldCard( + onClick = { onClick(alert.id) }, + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = alert.title, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = alert.message, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2 + ) + } + AlertSeverityBadge(severity = alert.severity) + } + } +} + +@Composable +private fun AlertSeverityBadge(severity: String) { + val variant = when (severity.lowercase()) { + "critical" -> BadgeVariant.Error + "high" -> BadgeVariant.Warning + "medium" -> BadgeVariant.Info + else -> BadgeVariant.Default + } + ShieldBadge( + text = severity, + variant = variant + ) +} diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/services/DarkWatchScreen.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/services/DarkWatchScreen.kt new file mode 100644 index 0000000..9222e82 --- /dev/null +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/services/DarkWatchScreen.kt @@ -0,0 +1,332 @@ +package com.shieldai.android.ui.screens.services + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.shieldai.android.R +import com.shieldai.android.ui.components.ShieldBadge +import com.shieldai.android.ui.components.ShieldButton +import com.shieldai.android.ui.components.ShieldButtonVariant +import com.shieldai.android.ui.components.ShieldCard +import com.shieldai.android.ui.components.ShieldEmptyState +import com.shieldai.android.ui.components.ShieldTextField +import com.shieldai.android.viewmodel.DarkWatchViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DarkWatchScreen( + onBack: () -> Unit = {}, + modifier: Modifier = Modifier, + viewModel: DarkWatchViewModel = viewModel(factory = DarkWatchViewModel.Factory) +) { + val uiState by viewModel.uiState.collectAsState() + var showAddSheet by remember { mutableStateOf(false) } + var newType by remember { mutableStateOf("email") } + var newValue by remember { mutableStateOf("") } + var newLabel by remember { mutableStateOf("") } + + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + rememberTopAppBarState() + ) + + Scaffold( + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + LargeTopAppBar( + title = { Text("DarkWatch", fontWeight = FontWeight.SemiBold) }, + navigationIcon = { + TextButton(onClick = onBack) { Text("Back") } + }, + scrollBehavior = scrollBehavior + ) + }, + floatingActionButton = { + if (!showAddSheet) { + FloatingActionButton(onClick = { showAddSheet = true }) { + Icon( + painter = painterResource(R.drawable.ic_dashboard), + contentDescription = "Add to watchlist" + ) + } + } + } + ) { paddingValues -> + when { + uiState.isLoading && uiState.watchlist.isEmpty() -> { + androidx.compose.foundation.layout.Box( + modifier = Modifier.fillMaxSize().padding(paddingValues), + contentAlignment = androidx.compose.ui.Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + } + } + uiState.watchlist.isEmpty() && uiState.exposures.isEmpty() -> { + ShieldEmptyState( + title = "No watchlist items", + description = "Add people to monitor for data exposures", + actionButton = { + ShieldButton( + text = "Add to Watchlist", + onClick = { showAddSheet = true }, + variant = ShieldButtonVariant.Primary + ) + }, + modifier = Modifier.padding(paddingValues) + ) + } + else -> { + DarkWatchContent( + uiState = uiState, + modifier = Modifier.padding(paddingValues) + ) + } + } + + if (showAddSheet) { + AddWatchlistSheet( + onDismiss = { + showAddSheet = false + newValue = "" + newLabel = "" + }, + onAdd = { + viewModel.addWatchlistItem(newType, newValue, newLabel.ifBlank { null }) + showAddSheet = false + newValue = "" + newLabel = "" + }, + type = newType, + onTypeChange = { newType = it }, + value = newValue, + onValueChange = { newValue = it }, + label = newLabel, + onLabelChange = { newLabel = it }, + isLoading = uiState.isAdding + ) + } + } +} + +@Composable +private fun DarkWatchContent( + uiState: DarkWatchViewModel.DarkWatchUiState, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + if (uiState.watchlist.isNotEmpty()) { + item { + Text( + text = "Watchlist (${uiState.watchlist.size})", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Spacer(modifier = Modifier.height(8.dp)) + } + + items(uiState.watchlist) { item -> + WatchlistItemCard(item) + Spacer(modifier = Modifier.height(8.dp)) + } + } + + if (uiState.exposures.isNotEmpty()) { + item { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Exposures (${uiState.exposures.size})", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Spacer(modifier = Modifier.height(8.dp)) + } + + items(uiState.exposures) { exposure -> + ExposureCard(exposure) + Spacer(modifier = Modifier.height(8.dp)) + } + } + } +} + +@Composable +private fun WatchlistItemCard(item: com.shieldai.android.data.model.WatchlistItem) { + ShieldCard(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = item.value, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + if (item.label != null) { + Text( + text = item.label, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Text( + text = item.type, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + ShieldBadge( + text = item.status, + variant = if (item.status == "active") com.shieldai.android.ui.components.BadgeVariant.Success + else com.shieldai.android.ui.components.BadgeVariant.Default + ) + } + } +} + +@Composable +private fun ExposureCard(exposure: com.shieldai.android.data.model.Exposure) { + ShieldCard(modifier = Modifier.fillMaxWidth()) { + Column { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = exposure.source, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + modifier = Modifier.weight(1f) + ) + ShieldBadge( + text = exposure.severity, + variant = when (exposure.severity.lowercase()) { + "critical" -> com.shieldai.android.ui.components.BadgeVariant.Error + "high" -> com.shieldai.android.ui.components.BadgeVariant.Warning + else -> com.shieldai.android.ui.components.BadgeVariant.Info + } + ) + } + exposure.details?.let { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun AddWatchlistSheet( + onDismiss: () -> Unit, + onAdd: () -> Unit, + type: String, + onTypeChange: (String) -> Unit, + value: String, + onValueChange: (String) -> Unit, + label: String, + onLabelChange: (String) -> Unit, + isLoading: Boolean +) { + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + ) { + Column( + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "Add to Watchlist", + style = MaterialTheme.typography.titleLarge + ) + + androidx.compose.material3.ExposedDropdownMenuBox( + expanded = false, + onExpandedChange = {} + ) { + ShieldTextField( + value = type, + onValueChange = onTypeChange, + label = "Type", + modifier = Modifier.fillMaxWidth() + ) + } + + ShieldTextField( + value = value, + onValueChange = onValueChange, + label = "Value (email, name, etc.)", + modifier = Modifier.fillMaxWidth() + ) + + ShieldTextField( + value = label, + onValueChange = onLabelChange, + label = "Label (optional)", + modifier = Modifier.fillMaxWidth() + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + ShieldButton( + text = "Cancel", + onClick = onDismiss, + variant = ShieldButtonVariant.Secondary, + modifier = Modifier.weight(1f) + ) + ShieldButton( + text = "Add", + onClick = onAdd, + variant = ShieldButtonVariant.Primary, + modifier = Modifier.weight(1f), + enabled = value.isNotBlank(), + loading = isLoading + ) + } + } + } +} diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/services/VoicePrintScreen.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/services/VoicePrintScreen.kt new file mode 100644 index 0000000..3683947 --- /dev/null +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/ui/screens/services/VoicePrintScreen.kt @@ -0,0 +1,256 @@ +package com.shieldai.android.ui.screens.services + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.shieldai.android.R +import com.shieldai.android.ui.components.BadgeVariant +import com.shieldai.android.ui.components.ShieldBadge +import com.shieldai.android.ui.components.ShieldButton +import com.shieldai.android.ui.components.ShieldButtonVariant +import com.shieldai.android.ui.components.ShieldCard +import com.shieldai.android.ui.components.ShieldEmptyState +import com.shieldai.android.ui.components.ShieldTextField +import com.shieldai.android.viewmodel.VoicePrintViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun VoicePrintScreen( + onBack: () -> Unit = {}, + modifier: Modifier = Modifier, + viewModel: VoicePrintViewModel = viewModel(factory = VoicePrintViewModel.Factory) +) { + val uiState by viewModel.uiState.collectAsState() + var showEnrollSheet by remember { mutableStateOf(false) } + var enrollmentName by remember { mutableStateOf("") } + + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + rememberTopAppBarState() + ) + + Scaffold( + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + LargeTopAppBar( + title = { Text("VoicePrint", fontWeight = FontWeight.SemiBold) }, + navigationIcon = { + TextButton(onClick = onBack) { Text("Back") } + }, + scrollBehavior = scrollBehavior + ) + }, + floatingActionButton = { + if (!showEnrollSheet) { + FloatingActionButton(onClick = { showEnrollSheet = true }) { + Icon( + painter = painterResource(R.drawable.ic_dashboard), + contentDescription = "New enrollment" + ) + } + } + } + ) { paddingValues -> + when { + uiState.isLoading && uiState.enrollments.isEmpty() -> { + androidx.compose.foundation.layout.Box( + modifier = Modifier.fillMaxSize().padding(paddingValues), + contentAlignment = androidx.compose.ui.Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + } + } + uiState.enrollments.isEmpty() -> { + ShieldEmptyState( + title = "No enrollments", + description = "Enroll voice profiles to detect impersonation", + actionButton = { + ShieldButton( + text = "New Enrollment", + onClick = { showEnrollSheet = true }, + variant = ShieldButtonVariant.Primary + ) + }, + modifier = Modifier.padding(paddingValues) + ) + } + else -> { + VoicePrintContent( + uiState = uiState, + modifier = Modifier.padding(paddingValues) + ) + } + } + + if (showEnrollSheet) { + EnrollSheet( + onDismiss = { + showEnrollSheet = false + enrollmentName = "" + }, + onEnroll = { + viewModel.createEnrollment(enrollmentName) + showEnrollSheet = false + enrollmentName = "" + }, + name = enrollmentName, + onNameChange = { enrollmentName = it }, + isLoading = uiState.isEnrolling + ) + } + } +} + +@Composable +private fun VoicePrintContent( + uiState: VoicePrintViewModel.VoicePrintUiState, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + item { + Text( + text = "Enrollments (${uiState.enrollments.size})", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Spacer(modifier = Modifier.height(8.dp)) + } + + items(uiState.enrollments) { enrollment -> + EnrollmentCard(enrollment) + Spacer(modifier = Modifier.height(8.dp)) + } + } +} + +@Composable +private fun EnrollmentCard(enrollment: com.shieldai.android.data.model.VoiceEnrollment) { + ShieldCard(modifier = Modifier.fillMaxWidth()) { + Column { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = enrollment.name, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + Text( + text = "${enrollment.sampleCount} samples", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + ShieldBadge( + text = enrollment.status, + variant = when (enrollment.status.lowercase()) { + "active" -> BadgeVariant.Success + "pending" -> BadgeVariant.Warning + else -> BadgeVariant.Default + } + ) + } + enrollment.createdAt?.let { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Created: $it", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun EnrollSheet( + onDismiss: () -> Unit, + onEnroll: () -> Unit, + name: String, + onNameChange: (String) -> Unit, + isLoading: Boolean +) { + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + ) { + Column( + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "New Voice Enrollment", + style = MaterialTheme.typography.titleLarge + ) + Text( + text = "Enter a name for this voice profile. You will be able to record samples afterwards.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + ShieldTextField( + value = name, + onValueChange = onNameChange, + label = "Profile name", + modifier = Modifier.fillMaxWidth() + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + ShieldButton( + text = "Cancel", + onClick = onDismiss, + variant = ShieldButtonVariant.Secondary, + modifier = Modifier.weight(1f) + ) + ShieldButton( + text = "Enroll", + onClick = onEnroll, + variant = ShieldButtonVariant.Primary, + modifier = Modifier.weight(1f), + enabled = name.isNotBlank(), + loading = isLoading + ) + } + } + } +} diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/AlertDetailViewModel.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/AlertDetailViewModel.kt new file mode 100644 index 0000000..1d130e1 --- /dev/null +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/AlertDetailViewModel.kt @@ -0,0 +1,93 @@ +package com.shieldai.android.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.shieldai.android.ShieldAIApp +import com.shieldai.android.data.model.Alert +import com.shieldai.android.data.repository.AlertRepository +import com.shieldai.android.di.RepositoryModule +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +data class AlertDetailUiState( + val alert: Alert? = null, + val correlatedAlerts: List = emptyList(), + val isLoading: Boolean = true, + val isResolving: Boolean = false, + val error: String? = null +) + +class AlertDetailViewModel : ViewModel() { + private val _uiState = MutableStateFlow(AlertDetailUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val alertRepo: AlertRepository by lazy { + RepositoryModule.provideAlertRepository(ShieldAIApp.instance) + } + + fun loadAlert(alertId: String) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = true, error = null) + try { + val result = alertRepo.getAlerts() + if (result is com.shieldai.android.data.remote.ApiResult.Success) { + val alert = result.data.find { it.id == alertId } + val correlated = alert?.let { + result.data.filter { a -> + a.id != alertId && a.type == it.type + } + } ?: emptyList() + + _uiState.value = _uiState.value.copy( + isLoading = false, + alert = alert, + correlatedAlerts = correlated + ) + alert?.let { markAlertRead(it.id) } + } else { + _uiState.value = _uiState.value.copy( + isLoading = false, + error = "Failed to load alert" + ) + } + } catch (e: Exception) { + _uiState.value = _uiState.value.copy( + isLoading = false, + error = e.message ?: "Failed to load alert" + ) + } + } + } + + fun markResolved() { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isResolving = true) + val alert = _uiState.value.alert + alert?.let { markAlertRead(it.id) } + _uiState.value = _uiState.value.copy(isResolving = false) + } + } + + fun markFalsePositive() { + viewModelScope.launch { + val alert = _uiState.value.alert + alert?.let { markAlertRead(it.id) } + } + } + + private fun markAlertRead(alertId: String) { + alertRepo.markRead(alertId) + } + + companion object { + val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return AlertDetailViewModel() as T + } + } + } +} diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/DarkWatchViewModel.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/DarkWatchViewModel.kt new file mode 100644 index 0000000..4701c9b --- /dev/null +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/DarkWatchViewModel.kt @@ -0,0 +1,100 @@ +package com.shieldai.android.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.shieldai.android.ShieldAIApp +import com.shieldai.android.data.model.Exposure +import com.shieldai.android.data.model.WatchlistItem +import com.shieldai.android.data.repository.DarkWatchRepository +import com.shieldai.android.di.RepositoryModule +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +data class DarkWatchUiState( + val watchlist: List = emptyList(), + val exposures: List = emptyList(), + val isLoading: Boolean = true, + val isAdding: Boolean = false, + val error: String? = null +) + +class DarkWatchViewModel : ViewModel() { + private val _uiState = MutableStateFlow(DarkWatchUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val repo: DarkWatchRepository by lazy { + RepositoryModule.provideDarkWatchRepository(ShieldAIApp.instance) + } + + init { + loadData() + } + + fun refresh() { + loadData(forceRefresh = true) + } + + private fun loadData(forceRefresh: Boolean = false) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = !forceRefresh, error = null) + try { + val watchlistResult = repo.getWatchlist(forceRefresh) + val exposuresResult = repo.getExposures(forceRefresh) + + val watchlist = if (watchlistResult is com.shieldai.android.data.remote.ApiResult.Success) { + watchlistResult.data + } else emptyList() + + val exposures = if (exposuresResult is com.shieldai.android.data.remote.ApiResult.Success) { + exposuresResult.data + } else emptyList() + + _uiState.value = _uiState.value.copy( + isLoading = false, + watchlist = watchlist, + exposures = exposures + ) + } catch (e: Exception) { + _uiState.value = _uiState.value.copy( + isLoading = false, + error = e.message ?: "Failed to load data" + ) + } + } + } + + fun addWatchlistItem(type: String, value: String, label: String? = null) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isAdding = true, error = null) + val result = repo.addWatchlistItem(type, value, label) + if (result is com.shieldai.android.data.remote.ApiResult.Error) { + _uiState.value = _uiState.value.copy( + isAdding = false, + error = result.message + ) + } else { + _uiState.value = _uiState.value.copy(isAdding = false) + loadData(forceRefresh = true) + } + } + } + + fun removeWatchlistItem(id: String) { + viewModelScope.launch { + repo.removeWatchlistItem(id) + loadData(forceRefresh = true) + } + } + + companion object { + val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return DarkWatchViewModel() as T + } + } + } +} diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/DashboardViewModel.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/DashboardViewModel.kt new file mode 100644 index 0000000..823a3c9 --- /dev/null +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/DashboardViewModel.kt @@ -0,0 +1,154 @@ +package com.shieldai.android.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.shieldai.android.ShieldAIApp +import com.shieldai.android.data.model.Alert +import com.shieldai.android.data.repository.AlertRepository +import com.shieldai.android.data.repository.DarkWatchRepository +import com.shieldai.android.data.repository.HomeTitleRepository +import com.shieldai.android.data.repository.RemoveBrokersRepository +import com.shieldai.android.data.repository.SpamShieldRepository +import com.shieldai.android.data.repository.VoicePrintRepository +import com.shieldai.android.di.RepositoryModule +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +data class DashboardUiState( + val isLoading: Boolean = false, + val threatScore: Int = 0, + val recentAlerts: List = emptyList(), + val unreadCount: Int = 0, + val watchlistCount: Int = 0, + val enrollmentCount: Int = 0, + val spamRulesCount: Int = 0, + val propertiesCount: Int = 0, + val removalsCount: Int = 0, + val error: String? = null +) + +class DashboardViewModel : ViewModel() { + private val _uiState = MutableStateFlow(DashboardUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val alertRepo: AlertRepository by lazy { + RepositoryModule.provideAlertRepository(ShieldAIApp.instance) + } + private val darkWatchRepo: DarkWatchRepository by lazy { + RepositoryModule.provideDarkWatchRepository(ShieldAIApp.instance) + } + private val voicePrintRepo: VoicePrintRepository by lazy { + RepositoryModule.provideVoicePrintRepository(ShieldAIApp.instance) + } + private val spamShieldRepo: SpamShieldRepository by lazy { + RepositoryModule.provideSpamShieldRepository(ShieldAIApp.instance) + } + private val homeTitleRepo: HomeTitleRepository by lazy { + RepositoryModule.provideHomeTitleRepository(ShieldAIApp.instance) + } + private val removeBrokersRepo: RemoveBrokersRepository by lazy { + RepositoryModule.provideRemoveBrokersRepository(ShieldAIApp.instance) + } + + init { + loadDashboardData() + } + + fun refresh() { + loadDashboardData(true) + } + + private fun loadDashboardData(forceRefresh: Boolean = false) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = !forceRefresh, error = null) + + try { + val alertsResult = alertRepo.getAlerts() + val watchlistResult = darkWatchRepo.getWatchlist(forceRefresh) + val enrollmentsResult = voicePrintRepo.getEnrollments() + val rulesResult = spamShieldRepo.getRules() + val propertiesResult = homeTitleRepo.getProperties() + val removalsResult = removeBrokersRepo.getRemovalRequests() + + val alerts = when (alertsResult) { + is com.shieldai.android.data.remote.ApiResult.Success -> alertsResult.data + else -> emptyList() + } + val watchlist = when (watchlistResult) { + is com.shieldai.android.data.remote.ApiResult.Success -> watchlistResult.data + else -> emptyList() + } + val enrollments = when (enrollmentsResult) { + is com.shieldai.android.data.remote.ApiResult.Success -> enrollmentsResult.data + else -> emptyList() + } + val rules = when (rulesResult) { + is com.shieldai.android.data.remote.ApiResult.Success -> rulesResult.data + else -> emptyList() + } + val properties = when (propertiesResult) { + is com.shieldai.android.data.remote.ApiResult.Success -> propertiesResult.data + else -> emptyList() + } + val removals = when (removalsResult) { + is com.shieldai.android.data.remote.ApiResult.Success -> removalsResult.data + else -> emptyList() + } + + val threatScore = calculateThreatScore(alerts) + val unreadCount = alerts.count { !it.read } + val recentAlerts = alerts.sortedByDescending { it.createdAt } + .take(5) + + _uiState.value = _uiState.value.copy( + isLoading = false, + threatScore = threatScore, + recentAlerts = recentAlerts, + unreadCount = unreadCount, + watchlistCount = watchlist.size, + enrollmentCount = enrollments.size, + spamRulesCount = rules.size, + propertiesCount = properties.size, + removalsCount = removals.size + ) + } catch (e: Exception) { + _uiState.value = _uiState.value.copy( + isLoading = false, + error = e.message ?: "Failed to load dashboard data" + ) + } + } + } + + fun markAlertRead(alertId: String) { + viewModelScope.launch { + alertRepo.markRead(alertId) + } + } + + private fun calculateThreatScore(alerts: List): Int { + if (alerts.isEmpty()) return 0 + val score = alerts.sumOf { + when (it.severity.lowercase()) { + "critical" -> 25 + "high" -> 15 + "medium" -> 8 + "low" -> 3 + else -> 1 + } + } + return minOf(score.coerceAtMost(100), 100) + } + + companion object { + val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return DashboardViewModel() as T + } + } + } +} diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/HomeTitleViewModel.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/HomeTitleViewModel.kt new file mode 100644 index 0000000..2894bca --- /dev/null +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/HomeTitleViewModel.kt @@ -0,0 +1,84 @@ +package com.shieldai.android.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.shieldai.android.ShieldAIApp +import com.shieldai.android.data.model.Property +import com.shieldai.android.data.repository.HomeTitleRepository +import com.shieldai.android.di.RepositoryModule +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +data class HomeTitleUiState( + val properties: List = emptyList(), + val isLoading: Boolean = true, + val isAdding: Boolean = false, + val error: String? = null +) + +class HomeTitleViewModel : ViewModel() { + private val _uiState = MutableStateFlow(HomeTitleUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val repo: HomeTitleRepository by lazy { + RepositoryModule.provideHomeTitleRepository(ShieldAIApp.instance) + } + + init { + loadProperties() + } + + fun refresh() { + loadProperties(forceRefresh = true) + } + + private fun loadProperties(forceRefresh: Boolean = false) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = !forceRefresh, error = null) + try { + val result = repo.getProperties(forceRefresh) + if (result is com.shieldai.android.data.remote.ApiResult.Success) { + _uiState.value = _uiState.value.copy( + isLoading = false, + properties = result.data + ) + } else { + _uiState.value = _uiState.value.copy(isLoading = false) + } + } catch (e: Exception) { + _uiState.value = _uiState.value.copy( + isLoading = false, + error = e.message ?: "Failed to load properties" + ) + } + } + } + + fun addProperty(address: String, type: String = "residential") { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isAdding = true, error = null) + val result = repo.addProperty(address, type) + if (result is com.shieldai.android.data.remote.ApiResult.Error) { + _uiState.value = _uiState.value.copy( + isAdding = false, + error = result.message + ) + } else { + _uiState.value = _uiState.value.copy(isAdding = false) + loadProperties(forceRefresh = true) + } + } + } + + companion object { + val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return HomeTitleViewModel() as T + } + } + } +} diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/RemoveBrokersViewModel.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/RemoveBrokersViewModel.kt new file mode 100644 index 0000000..2c03e57 --- /dev/null +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/RemoveBrokersViewModel.kt @@ -0,0 +1,93 @@ +package com.shieldai.android.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.shieldai.android.ShieldAIApp +import com.shieldai.android.data.model.BrokerListing +import com.shieldai.android.data.model.RemovalRequest +import com.shieldai.android.data.repository.RemoveBrokersRepository +import com.shieldai.android.di.RepositoryModule +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +data class RemoveBrokersUiState( + val listings: List = emptyList(), + val removalRequests: List = emptyList(), + val isLoading: Boolean = true, + val isCreating: Boolean = false, + val error: String? = null +) + +class RemoveBrokersViewModel : ViewModel() { + private val _uiState = MutableStateFlow(RemoveBrokersUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val repo: RemoveBrokersRepository by lazy { + RepositoryModule.provideRemoveBrokersRepository(ShieldAIApp.instance) + } + + init { + loadData() + } + + fun refresh() { + loadData(forceRefresh = true) + } + + private fun loadData(forceRefresh: Boolean = false) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = !forceRefresh, error = null) + try { + val listingsResult = repo.getListings(forceRefresh) + val requestsResult = repo.getRemovalRequests(forceRefresh) + + val listings = if (listingsResult is com.shieldai.android.data.remote.ApiResult.Success) { + listingsResult.data + } else emptyList() + + val requests = if (requestsResult is com.shieldai.android.data.remote.ApiResult.Success) { + requestsResult.data + } else emptyList() + + _uiState.value = _uiState.value.copy( + isLoading = false, + listings = listings, + removalRequests = requests + ) + } catch (e: Exception) { + _uiState.value = _uiState.value.copy( + isLoading = false, + error = e.message ?: "Failed to load data" + ) + } + } + } + + fun createRemovalRequest(listingId: String, notes: String? = null) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isCreating = true, error = null) + val result = repo.createRemovalRequest(listingId, notes) + if (result is com.shieldai.android.data.remote.ApiResult.Error) { + _uiState.value = _uiState.value.copy( + isCreating = false, + error = result.message + ) + } else { + _uiState.value = _uiState.value.copy(isCreating = false) + loadData(forceRefresh = true) + } + } + } + + companion object { + val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return RemoveBrokersViewModel() as T + } + } + } +} diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/SettingsViewModel.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/SettingsViewModel.kt new file mode 100644 index 0000000..9068b1e --- /dev/null +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/SettingsViewModel.kt @@ -0,0 +1,109 @@ +package com.shieldai.android.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.shieldai.android.ShieldAIApp +import com.shieldai.android.data.model.Subscription +import com.shieldai.android.data.model.User +import com.shieldai.android.data.repository.SubscriptionRepository +import com.shieldai.android.data.repository.UserRepository +import com.shieldai.android.di.RepositoryModule +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +data class SettingsUiState( + val user: User? = null, + val subscription: Subscription? = null, + val isLoading: Boolean = true, + val notificationsEnabled: Boolean = true, + val darkModeEnabled: Boolean = false, + val biometricEnabled: Boolean = false, + val error: String? = null +) + +class SettingsViewModel : ViewModel() { + private val _uiState = MutableStateFlow(SettingsUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val userRepo: UserRepository by lazy { + RepositoryModule.provideUserRepository(ShieldAIApp.instance) + } + private val subscriptionRepo: SubscriptionRepository by lazy { + RepositoryModule.provideSubscriptionRepository(ShieldAIApp.instance) + } + + init { + loadSettings() + } + + fun refresh() { + loadSettings(forceRefresh = true) + } + + private fun loadSettings(forceRefresh: Boolean = false) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = true, error = null) + try { + val userResult = userRepo.getMe(forceRefresh) + val subResult = subscriptionRepo.getSubscription() + + val user = if (userResult is com.shieldai.android.data.remote.ApiResult.Success) { + userResult.data + } else null + + val subscription = if (subResult is com.shieldai.android.data.remote.ApiResult.Success) { + subResult.data + } else null + + _uiState.value = _uiState.value.copy( + isLoading = false, + user = user, + subscription = subscription + ) + } catch (e: Exception) { + _uiState.value = _uiState.value.copy( + isLoading = false, + error = e.message ?: "Failed to load settings" + ) + } + } + } + + fun toggleNotifications(enabled: Boolean) { + _uiState.value = _uiState.value.copy(notificationsEnabled = enabled) + } + + fun toggleDarkMode(enabled: Boolean) { + _uiState.value = _uiState.value.copy(darkModeEnabled = enabled) + } + + fun toggleBiometric(enabled: Boolean) { + _uiState.value = _uiState.value.copy(biometricEnabled = enabled) + } + + fun updateProfile(name: String? = null, phone: String? = null) { + viewModelScope.launch { + userRepo.updateProfile(name, phone) + loadSettings(forceRefresh = true) + } + } + + fun upgradeSubscription() { + viewModelScope.launch { + subscriptionRepo.updateSubscription("Premium") + loadSettings(forceRefresh = true) + } + } + + companion object { + val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return SettingsViewModel() as T + } + } + } +} diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/SpamShieldViewModel.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/SpamShieldViewModel.kt new file mode 100644 index 0000000..b47c17e --- /dev/null +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/SpamShieldViewModel.kt @@ -0,0 +1,98 @@ +package com.shieldai.android.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.shieldai.android.ShieldAIApp +import com.shieldai.android.data.model.SpamRule +import com.shieldai.android.data.repository.SpamShieldRepository +import com.shieldai.android.di.RepositoryModule +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +data class SpamShieldUiState( + val rules: List = emptyList(), + val totalBlocked: Int = 0, + val totalFlagged: Int = 0, + val activeRules: Int = 0, + val isLoading: Boolean = true, + val isCreating: Boolean = false, + val error: String? = null +) + +class SpamShieldViewModel : ViewModel() { + private val _uiState = MutableStateFlow(SpamShieldUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val repo: SpamShieldRepository by lazy { + RepositoryModule.provideSpamShieldRepository(ShieldAIApp.instance) + } + + init { + loadRules() + } + + fun refresh() { + loadRules(forceRefresh = true) + } + + private fun loadRules(forceRefresh: Boolean = false) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = !forceRefresh, error = null) + try { + val result = repo.getRules(forceRefresh) + if (result is com.shieldai.android.data.remote.ApiResult.Success) { + val stats = repo.getStats() + _uiState.value = _uiState.value.copy( + isLoading = false, + rules = result.data, + totalBlocked = stats.totalBlocked, + totalFlagged = stats.totalFlagged, + activeRules = stats.activeRules + ) + } else { + _uiState.value = _uiState.value.copy(isLoading = false) + } + } catch (e: Exception) { + _uiState.value = _uiState.value.copy( + isLoading = false, + error = e.message ?: "Failed to load rules" + ) + } + } + } + + fun createRule(pattern: String, action: String, description: String? = null) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isCreating = true, error = null) + val result = repo.createRule(pattern, action, description) + if (result is com.shieldai.android.data.remote.ApiResult.Error) { + _uiState.value = _uiState.value.copy( + isCreating = false, + error = result.message + ) + } else { + _uiState.value = _uiState.value.copy(isCreating = false) + loadRules(forceRefresh = true) + } + } + } + + fun toggleRule(id: String, enabled: Boolean) { + viewModelScope.launch { + repo.toggleRule(id, enabled) + loadRules(forceRefresh = true) + } + } + + companion object { + val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return SpamShieldViewModel() as T + } + } + } +} diff --git a/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/VoicePrintViewModel.kt b/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/VoicePrintViewModel.kt new file mode 100644 index 0000000..c642860 --- /dev/null +++ b/android/ShieldAI/app/src/main/java/com/shieldai/android/viewmodel/VoicePrintViewModel.kt @@ -0,0 +1,96 @@ +package com.shieldai.android.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.shieldai.android.ShieldAIApp +import com.shieldai.android.data.model.VoiceEnrollment +import com.shieldai.android.data.repository.VoicePrintRepository +import com.shieldai.android.di.RepositoryModule +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +data class VoicePrintUiState( + val enrollments: List = emptyList(), + val isLoading: Boolean = true, + val isEnrolling: Boolean = false, + val error: String? = null +) + +class VoicePrintViewModel : ViewModel() { + private val _uiState = MutableStateFlow(VoicePrintUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val repo: VoicePrintRepository by lazy { + RepositoryModule.provideVoicePrintRepository(ShieldAIApp.instance) + } + + init { + loadEnrollments() + } + + fun refresh() { + loadEnrollments(forceRefresh = true) + } + + private fun loadEnrollments(forceRefresh: Boolean = false) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = !forceRefresh, error = null) + try { + val result = if (forceRefresh) { + repo.getEnrollments() + } else { + repo.getEnrollments() + } + if (result is com.shieldai.android.data.remote.ApiResult.Success) { + _uiState.value = _uiState.value.copy( + isLoading = false, + enrollments = result.data + ) + } else { + _uiState.value = _uiState.value.copy(isLoading = false) + } + } catch (e: Exception) { + _uiState.value = _uiState.value.copy( + isLoading = false, + error = e.message ?: "Failed to load enrollments" + ) + } + } + } + + fun createEnrollment(name: String) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isEnrolling = true, error = null) + val result = repo.createEnrollment(name) + if (result is com.shieldai.android.data.remote.ApiResult.Error) { + _uiState.value = _uiState.value.copy( + isEnrolling = false, + error = result.message + ) + } else { + _uiState.value = _uiState.value.copy(isEnrolling = false) + loadEnrollments(forceRefresh = true) + } + } + } + + fun deleteEnrollment(id: String) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy( + enrollments = _uiState.value.enrollments.filter { it.id != id } + ) + } + } + + companion object { + val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return VoicePrintViewModel() as T + } + } + } +} diff --git a/browser-ext/package.json b/browser-ext/package.json index 33da0b5..912ff10 100644 --- a/browser-ext/package.json +++ b/browser-ext/package.json @@ -1,5 +1,5 @@ { - "name": "@shieldai/browser-ext", + "name": "@kordant/browser-ext", "version": "0.1.0", "private": true, "type": "module", diff --git a/package.json b/package.json index 7d72545..1ad8f71 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "shieldai", + "name": "kordant", "version": "0.1.0", "private": true, "workspaces": [ @@ -9,11 +9,11 @@ "scripts": { "dev": "pnpm --filter web dev", "build": "pnpm --filter web build", - "build:ext": "pnpm --filter @shieldai/browser-ext build", + "build:ext": "pnpm --filter @kordant/browser-ext build", "test": "pnpm --filter web test", - "test:ext": "pnpm --filter @shieldai/browser-ext test", + "test:ext": "pnpm --filter @kordant/browser-ext test", "lint": "pnpm --filter web lint", - "lint:ext": "pnpm --filter @shieldai/browser-ext lint", + "lint:ext": "pnpm --filter @kordant/browser-ext lint", "db:migrate": "pnpm --filter web db:migrate", "db:seed": "pnpm --filter web db:seed" }, diff --git a/plans/FRE-4499-implementation-plan.md b/plans/FRE-4499-implementation-plan.md index 2739a51..2ee2142 100644 --- a/plans/FRE-4499-implementation-plan.md +++ b/plans/FRE-4499-implementation-plan.md @@ -93,7 +93,7 @@ ## Dependencies -- `@shieldai/db`: Database schemas (exists) +- `@kordant/db`: Database schemas (exists) - `libphonenumber-js`: Phone validation (already in package.json) - `ws`: WebSocket library (needs to be added to package.json) - Twilio/Plivo SDKs: For carrier integration (using direct HTTP) diff --git a/plans/FRE-4523-rate-limit-middleware.md b/plans/FRE-4523-rate-limit-middleware.md index 91e0619..11ac481 100644 --- a/plans/FRE-4523-rate-limit-middleware.md +++ b/plans/FRE-4523-rate-limit-middleware.md @@ -11,7 +11,7 @@ Create a new `spam-rate-limit.middleware.ts` file that implements Redis-backed r ### Requirements The middleware should: -1. Use the RedisService from `@shieldai/shared-notifications` +1. Use the RedisService from `@kordant/shared-notifications` 2. Implement per-minute AND daily rate limit tracking 3. Check rate limits before processing spam classification requests 4. Return appropriate HTTP 429 responses when limits are exceeded @@ -46,7 +46,7 @@ if (rateLimitCheck.exceeded) { ## Acceptance Criteria - [ ] Create `services/spamshield/src/middleware/spam-rate-limit.middleware.ts` -- [ ] Import and use RedisService from `@shieldai/shared-notifications` +- [ ] Import and use RedisService from `@kordant/shared-notifications` - [ ] Implement `checkLimit(userId, tier)` method returning rate limit status - [ ] Implement `incrementCounter(userId, tier)` method - [ ] Support per-minute and per-day limit tracking @@ -59,7 +59,7 @@ if (rateLimitCheck.exceeded) { ## Dependencies - FRE-4522 (spamshield.config.ts with rate limit structure) -- `@shieldai/shared-notifications` (RedisService) +- `@kordant/shared-notifications` (RedisService) ## Priority HIGH (Core middleware implementation) diff --git a/plans/FRE-4524-spamshield-routes.md b/plans/FRE-4524-spamshield-routes.md index eb573d9..6b6d4b4 100644 --- a/plans/FRE-4524-spamshield-routes.md +++ b/plans/FRE-4524-spamshield-routes.md @@ -119,7 +119,7 @@ Get current rate limit status for a user. ## Dependencies - FRE-4522 (spamshield.config.ts with rate limit structure) - FRE-4523 (spam-rate-limit.middleware.ts) -- `@shieldai/types` (for type definitions) +- `@kordant/types` (for type definitions) ## Priority MEDIUM (Depends on middleware implementation) diff --git a/plans/waitlist-email-sequence-implementation.md b/plans/waitlist-email-sequence-implementation.md index 97596f3..36caa96 100644 --- a/plans/waitlist-email-sequence-implementation.md +++ b/plans/waitlist-email-sequence-implementation.md @@ -52,7 +52,7 @@ This document describes how to integrate the waitlist email templates into the w In `packages/api/src/routes/waitlist.routes.ts`, after `prisma.waitlistEntry.create()` succeeds: ```typescript -import { EmailService } from '@shieldai/shared-notifications'; +import { EmailService } from '@kordant/shared-notifications'; // Send confirmation immediately await EmailService.getInstance().sendWithTemplate(email, { diff --git a/tasks/rebrand-to-kordant/01-update-monorepo-foundation.md b/tasks/rebrand-to-kordant/01-update-monorepo-foundation.md new file mode 100644 index 0000000..e89ce9f --- /dev/null +++ b/tasks/rebrand-to-kordant/01-update-monorepo-foundation.md @@ -0,0 +1,37 @@ +# 01. Update Monorepo Foundation + +meta: + id: rebrand-to-kordant-01 + feature: rebrand-to-kordant + priority: P0 + depends_on: [] + tags: [infrastructure, configuration] + +objective: +- Update the root package name, workspace metadata, and environment configuration files from ShieldAI to Kordant. + +deliverables: +- Root package.json name changed from "shieldai" to "kordant" +- Turbo pipeline `build:ext` and `test:ext` script references updated +- Root .env.example updated (DB user, database name, DD_SERVICE) +- .env.prod.example updated (GITHUB_REPOSITORY_OWNER) +- .editorconfig: no changes needed (not brand-specific) + +steps: +1. Edit `package.json` — change `"name": "shieldai"` to `"name": "kordant"` +2. Edit `package.json` — update `"build:ext"` script: `@shieldai/browser-ext` → needs package scope update (will be done in task 03; for now just the script name stays but the scope resolves later) +3. Edit `.env.example` — update DATABASE_URL user/db from `shieldai` to `kordant`, update `DD_SERVICE="shieldai-api"` to `DD_SERVICE="kordant-api"` +4. Edit `.env.prod.example` — update `GITHUB_REPOSITORY_OWNER=shieldai` to `GITHUB_REPOSITORY_OWNER=kordant` + +tests: +- Unit: Verify package.json `name` equals "kordant" +- Unit: Verify .env.example has no "shieldai" references + +acceptance_criteria: +- `cat package.json | jq .name` returns "kordant" +- `grep -rn "shieldai" .env.example .env.prod.example` returns empty +- `grep "DD_SERVICE" .env.example` shows "kordant-api" + +validation: +- Run `pnpm build` from root — should not fail (may have pre-existing errors unrelated to this change) +- Verify `pnpm --filter kordant*` style commands work diff --git a/tasks/rebrand-to-kordant/02-update-database-connection-strings.md b/tasks/rebrand-to-kordant/02-update-database-connection-strings.md new file mode 100644 index 0000000..db4f3c2 --- /dev/null +++ b/tasks/rebrand-to-kordant/02-update-database-connection-strings.md @@ -0,0 +1,39 @@ +# 02. Update Database Connection Strings and DB Names + +meta: + id: rebrand-to-kordant-02 + feature: rebrand-to-kordant + priority: P1 + depends_on: [rebrand-to-kordant-01] + tags: [infrastructure, database] + +objective: +- Update all Turso database URLs and PostgreSQL connection strings from shieldai-* to kordant-* and database names from shieldai to kordant. + +deliverables: +- web/src/server/db/index.ts — Turso URL updated +- web/drizzle.config.ts — Turso URL updated +- web/.env.development — DATABASE_URL updated +- web/.env.example — DATABASE_URL db name updated +- .env.example — DATABASE_URL user/db updated +- .github/workflows/ci.yml — PostgreSQL credentials and DB name updated + +steps: +1. Edit `web/src/server/db/index.ts` — change `libsql://shieldai-dev-*` to `libsql://kordant-dev-*` +2. Edit `web/drizzle.config.ts` — same Turso URL update +3. Edit `web/.env.development` — same URL update +4. Edit `web/.env.example` — update `.../shieldai` postgres db name to `.../kordant` +5. Edit `.env.example` — update `POSTGRES_DB: shieldai`, `POSTGRES_USER: shieldai`, `POSTGRES_PASSWORD: shieldai_dev`, and `pg_isready -U shieldai`, and `DATABASE_URL` all to use `kordant` +6. Edit `.github/workflows/ci.yml` — update all `shieldai` references in db service config (POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD, pg_isready, DATABASE_URL) + +tests: +- Unit: grep for any remaining `shieldai.*turso.io` patterns — should be zero +- Config: verify all DATABASE_URL references use `kordant` db name + +acceptance_criteria: +- No `shieldai-dev-` or `shieldai` database name remains in any connection string +- CI workflow uses `kordant` as PostgreSQL database name + +validation: +- Run `grep -rn "libsql://shieldai" web/` — expect zero results +- Run `grep -rn "POSTGRES_DB: shieldai" .github/` — expect zero results diff --git a/tasks/rebrand-to-kordant/03-update-web-package-scope-and-imports.md b/tasks/rebrand-to-kordant/03-update-web-package-scope-and-imports.md new file mode 100644 index 0000000..d4c11b9 --- /dev/null +++ b/tasks/rebrand-to-kordant/03-update-web-package-scope-and-imports.md @@ -0,0 +1,39 @@ +# 03. Update @shieldai/* Package Scopes and Web Imports + +meta: + id: rebrand-to-kordant-03 + feature: rebrand-to-kordant + priority: P0 + depends_on: [rebrand-to-kordant-01] + tags: [infrastructure, packages] + +objective: +- Rename all `@shieldai/*` package scopes to `@kordant/*` across the monorepo and update all corresponding import statements. + +deliverables: +- browser-ext/package.json — `"name": "@shieldai/browser-ext"` → `"@kordant/browser-ext"` +- All files importing `@shieldai/*` — import paths updated to `@kordant/*` +- All plan files referencing `@shieldai/*` — updated + +steps: +1. Edit `browser-ext/package.json` — change package scope from `@shieldai` to `@kordant` +2. Search and replace all `@shieldai/` → `@kordant/` across: + - `web/src/**/*.ts` + - `web/src/**/*.tsx` + - `browser-ext/**/*.ts` + - `browser-ext/**/*.tsx` + - `plans/*.md` +3. Verify no `@shieldai/` references remain in source code + +tests: +- Build: `pnpm build` succeeds (after scope update) +- Grep: `grep -rn "@shieldai/" web/src/ browser-ext/src/` returns empty + +acceptance_criteria: +- `pnpm --filter @kordant/browser-ext ...` commands resolve correctly +- All import statements across the monorepo use `@kordant/` scope +- No `@shieldai/` string remains in any package.json or source file + +validation: +- Run `grep -rn "from \"@shieldai/" web/src/ browser-ext/src/` — expect zero +- Run `grep -rn "@shieldai/" package.json browser-ext/package.json` — expect zero diff --git a/tasks/rebrand-to-kordant/04-update-web-ui-brand-text.md b/tasks/rebrand-to-kordant/04-update-web-ui-brand-text.md new file mode 100644 index 0000000..7e4dfc2 --- /dev/null +++ b/tasks/rebrand-to-kordant/04-update-web-ui-brand-text.md @@ -0,0 +1,46 @@ +# 04. Replace "ShieldAI" Display Text Across Web UI + +meta: + id: rebrand-to-kordant-04 + feature: rebrand-to-kordant + priority: P1 + depends_on: [rebrand-to-kordant-03] + tags: [frontend, ui] + +objective: +- Replace all user-visible "ShieldAI" branding text across web UI components with "Kordant". + +deliverables: +- Page titles in all route files updated (app.tsx, index.tsx, dashboard, auth, blog, ads, 404) +- Navbar, Sidebar, Footer, AppShell brand text updated +- Landing page (HeroSection, WhyShieldAISection, CTABannerSection) updated +- Auth layout testimonial text and brand references updated +- Ads page copy updated +- Tests updated to reference "Kordant" instead of "ShieldAI" + +steps: +1. Update all `` tags in route files — replace "ShieldAI" with "Kordant" in page title strings +2. Update Navbar.tsx — replace "ShieldAI" brand text +3. Update Sidebar.tsx — replace "ShieldAI" brand text +4. Update Footer.tsx — replace "ShieldAI" and "ShieldAI. All rights reserved." +5. Update AppShell.tsx — replace default title +6. Update HeroSection.tsx — replace "ShieldAI evens" +7. Update WhyShieldAISection — rename component, id, and text references +8. Update CTABannerSection — replace "trust ShieldAI" +9. Update landing index.ts export and imports in routes/index.tsx +10. Update AuthLayout.tsx — replace testimonial/quote brand text +11. Update onboarding.tsx — replace "Your ShieldAI account is ready..." +12. Update ads.tsx — replace all marketing copy +13. Update all test files that assert "ShieldAI" in rendered output + +tests: +- Unit: All tests pass after text changes +- Visual: Web app renders "Kordant" in all branded locations + +acceptance_criteria: +- No "ShieldAI" text appears in any web UI page title, nav, sidebar, footer, landing, auth, blog, or ads page +- Test assertions updated to expect "Kordant" instead of "ShieldAI" + +validation: +- Run `grep -rn "ShieldAI" web/src/components/ web/src/routes/` — check only legitimate remaining references (comments, third-party) +- Run `pnpm --filter web test` — all tests pass diff --git a/tasks/rebrand-to-kordant/05-update-web-email-notification-templates.md b/tasks/rebrand-to-kordant/05-update-web-email-notification-templates.md new file mode 100644 index 0000000..65600a3 --- /dev/null +++ b/tasks/rebrand-to-kordant/05-update-web-email-notification-templates.md @@ -0,0 +1,46 @@ +# 05. Update Email Templates, Notification Content, and Queue Names + +meta: + id: rebrand-to-kordant-05 + feature: rebrand-to-kordant + priority: P1 + depends_on: [rebrand-to-kordant-01] + tags: [backend, email] + +objective: +- Replace all "ShieldAI" references in email templates, notification services, report generators, and BullMQ queue names. + +deliverables: +- email.templates.ts — brand text, subjects, bodies, headers, footers updated +- email.templates.test.ts — test assertions updated +- notification.service.ts — from address updated to kordant.ai +- alert.publisher.ts — push notification title prefix updated +- reports.service.ts — report title updated +- reports/generator.ts — report text updated +- reports/templates/*.html — HTML report templates updated +- queue.ts — BullMQ queue name updated from "shieldai-jobs" to "kordant-jobs" +- waitlist-email-sequence-implementation.md — domain references updated + +steps: +1. Edit email.templates.ts — replace all "ShieldAI" brand text in subjects, HTML, plain text bodies, footers, team signatures +2. Edit email.templates.ts — update from/noreply email to `noreply@kordant.ai` +3. Edit email.templates.test.ts — update test assertions to expect "Kordant" +4. Edit notification.service.ts — update `noreply@shieldai.app` → `noreply@kordant.ai` +5. Edit alert.publisher.ts — update `` `[ShieldAI] ${alert.title}` `` → `` `[Kordant] ${alert.title}` `` +6. Edit reports.service.ts — update report title from "ShieldAI" to "Kordant" +7. Edit reports/generator.ts — update all brand text references +8. Edit reports/templates/*.html — update title and footer in weekly-digest, monthly-plus, annual-premium +9. Edit queue.ts — update queue name "shieldai-jobs" → "kordant-jobs" + +tests: +- Unit: Email template tests pass with new brand name +- Unit: Report generator tests pass + +acceptance_criteria: +- No "ShieldAI" remains in any email template, notification, or report +- Email from address uses `@kordant.ai` +- BullMQ queue name uses "kordant-jobs" + +validation: +- Run `grep -rn "ShieldAI" web/src/server/services/` — expect zero results +- Run `grep "shieldai-jobs" web/src/server/jobs/queue.ts` — expect zero diff --git a/tasks/rebrand-to-kordant/06-update-web-storage-keys-and-constants.md b/tasks/rebrand-to-kordant/06-update-web-storage-keys-and-constants.md new file mode 100644 index 0000000..0e74093 --- /dev/null +++ b/tasks/rebrand-to-kordant/06-update-web-storage-keys-and-constants.md @@ -0,0 +1,37 @@ +# 06. Update Browser Storage Keys, Theme Keys, and User-Agent Headers + +meta: + id: rebrand-to-kordant-06 + feature: rebrand-to-kordant + priority: P2 + depends_on: [rebrand-to-kordant-03] + tags: [frontend, config] + +objective: +- Update all localStorage keys, theme storage keys, unread count keys, device name strings, and user-agent headers from ShieldAI to Kordant. + +deliverables: +- web/src/lib/theme.tsx — STORAGE_KEY updated +- web/src/lib/theme.test.ts — assertions updated (11 occurrences) +- web/src/entry-server.tsx — theme key updated +- web/src/hooks/useRealtimeAlerts.ts — UNREAD_STORAGE_KEY updated +- web/src/server/api/routers/extension.ts — "ShieldAI Browser Extension" updated +- web/src/server/services/darkwatch/scan.engine.ts — user-agent header updated + +steps: +1. Edit `web/src/lib/theme.tsx` — change `STORAGE_KEY = "shieldai-theme"` to `"kordant-theme"` +2. Edit `web/src/lib/theme.test.ts` — update all `"shieldai-theme"` assertions to `"kordant-theme"` +3. Edit `web/src/entry-server.tsx` — update `'shieldai-theme'` to `'kordant-theme'` +4. Edit `web/src/hooks/useRealtimeAlerts.ts` — update `UNREAD_STORAGE_KEY = "shieldai_unread_count"` to `"kordant_unread_count"` +5. Edit `web/src/server/api/routers/extension.ts` — update device name from `"ShieldAI Browser Extension"` to `"Kordant Browser Extension"` +6. Edit `web/src/server/services/darkwatch/scan.engine.ts` — update `"user-agent": "ShieldAI-DarkWatch"` to `"Kordant-DarkWatch"` + +tests: +- Unit: Theme tests pass after key update +- Integration: DarkWatch scanner user-agent header updated + +acceptance_criteria: +- No `shieldai-theme`, `shieldai_unread_count`, `ShieldAI Browser Extension`, or `ShieldAI-DarkWatch` remain in source + +validation: +- Run `grep -rn "shieldai-theme\|shieldai_unread_count\|ShieldAI Browser Extension\|ShieldAI-DarkWatch" web/src/` — expect zero diff --git a/tasks/rebrand-to-kordant/07-update-web-seed-data-and-blog-content.md b/tasks/rebrand-to-kordant/07-update-web-seed-data-and-blog-content.md new file mode 100644 index 0000000..d582567 --- /dev/null +++ b/tasks/rebrand-to-kordant/07-update-web-seed-data-and-blog-content.md @@ -0,0 +1,32 @@ +# 07. Update Seed Data, Blog Content, and Landing Page Copy + +meta: + id: rebrand-to-kordant-07 + feature: rebrand-to-kordant + priority: P2 + depends_on: [rebrand-to-kordant-03] + tags: [frontend, content] + +objective: +- Update seed data references, blog content, and remaining landing page copy from ShieldAI to Kordant. + +deliverables: +- web/src/server/db/seed.ts — author name and description updated +- web/src/routes/blog.tsx — blog page title, post titles, copy updated +- web/src/routes/blog/[slug].tsx — blog post content, metadata, author updated + +steps: +1. Edit `web/src/server/db/seed.ts` — update `authorName: "ShieldAI Team"` to `"Kordant Team"`, update description text +2. Edit `web/src/routes/blog.tsx` — update page title, blog descriptions, post slugs containing "shieldai", post titles +3. Edit `web/src/routes/blog/[slug].tsx` — update all brand references in blog post content, metadata, author role + +tests: +- Unit: Seed data tests pass +- Unit: Blog page renders correctly with updated content + +acceptance_criteria: +- No "ShieldAI" remains in seed data or blog content +- Blog post slugs no longer contain "shieldai" + +validation: +- Run `grep -rn "ShieldAI" web/src/server/db/seed.ts web/src/routes/blog.tsx web/src/routes/blog/` — expect zero diff --git a/tasks/rebrand-to-kordant/08-update-browser-extension-branding.md b/tasks/rebrand-to-kordant/08-update-browser-extension-branding.md new file mode 100644 index 0000000..72c7871 --- /dev/null +++ b/tasks/rebrand-to-kordant/08-update-browser-extension-branding.md @@ -0,0 +1,47 @@ +# 08. Update Browser Extension Branding + +meta: + id: rebrand-to-kordant-08 + feature: rebrand-to-kordant + priority: P1 + depends_on: [rebrand-to-kordant-01, rebrand-to-kordant-03] + tags: [browser-ext, frontend] + +objective: +- Update all ShieldAI branding in the browser extension: manifest, HTML pages, storage keys, log messages, and domain references. + +deliverables: +- browser-ext/public/manifest.json — name and host permissions updated +- browser-ext/src/popup/popup.html — title and brand text updated +- browser-ext/src/options/options.html — title, heading, API URL placeholder updated +- browser-ext/src/options/options.ts — device name updated +- browser-ext/src/background/index.ts — device name, log prefixes, storage keys, error messages updated +- browser-ext/src/content/index.ts — log prefixes updated +- browser-ext/src/lib/settings.ts — STORAGE_KEY and API URL updated +- browser-ext/src/lib/phishing-detector.ts — phishing domain reference data updated +- browser-ext/tests/ — all test assertions updated + +steps: +1. Edit `manifest.json` — update `"name": "ShieldAI"` to `"Kordant"`, update host permissions from `*.shieldai.com` to `*.kordant.ai` +2. Edit `popup.html` — update `<title>` and `<span>ShieldAI</span>` +3. Edit `options.html` — update title, heading, API URL placeholder +4. Edit `options.ts` — update device name +5. Edit `background/index.ts` — update device name, log prefixes, storage keys, error messages +6. Edit `content/index.ts` — update log prefixes +7. Edit `lib/settings.ts` — update STORAGE_KEY and base URL +8. Edit `lib/phishing-detector.ts` — update phishing domain examples from shieldai-* to kordant-* +9. Edit all test files — update assertions + +tests: +- Unit: Extension tests pass +- Manual: Load unpacked extension in Chrome — verify "Kordant" displays correctly + +acceptance_criteria: +- Extension manifest name shows "Kordant" +- All storage keys use `kordant:` prefix +- No `shieldai` / `ShieldAI` remains in extension source +- API domain uses `kordant.ai` + +validation: +- Run `grep -rn "shieldai\|ShieldAI" browser-ext/` — expect zero +- Load extension in Chrome, verify name in toolbar diff --git a/tasks/rebrand-to-kordant/09-update-ios-app-branding.md b/tasks/rebrand-to-kordant/09-update-ios-app-branding.md new file mode 100644 index 0000000..05aacc5 --- /dev/null +++ b/tasks/rebrand-to-kordant/09-update-ios-app-branding.md @@ -0,0 +1,47 @@ +# 09. Update iOS App Branding + +meta: + id: rebrand-to-kordant-09 + feature: rebrand-to-kordant + priority: P1 + depends_on: [rebrand-to-kordant-01] + tags: [ios, mobile] + +objective: +- Update all ShieldAI branding in the iOS app: Xcode project, Swift source files, bundle identifiers, display text, storage keys, and URL schemes. + +deliverables: +- iOS/ShieldAI/ directory renamed to iOS/Kordant/ +- Xcode project file (.pbxproj) — all ShieldAI references updated (bundle IDs, target names, product names, entitlements, URL schemes, info plist strings) +- ShieldAIApp.swift — app struct renamed +- ShieldAITheme.swift — theme struct renamed; ShieldAITheme.cornerRadius references in all View files updated +- AuthView, OnboardingView — display text updated +- BiometricAuthService — auth prompt messages updated +- CameraService — usage description strings updated +- APIClient, PushNotificationService, NetworkMonitor — subsystem strings updated +- Route.swift — URL scheme updated from "shieldai" to "kordant" +- ThemeManager — storage key updated +- OfflineQueue, CacheManager — storage keys updated +- ShieldAITests, ShieldAIUITests — all references updated +- Deep link URL strings in tests updated + +steps: +1. Rename iOS directory from `iOS/ShieldAI` to `iOS/Kordant` (this will require updating all paths in pbxproj) +2. Edit pbxproj — find/replace all ShieldAI → Kordant (bundle IDs, target names, product names, paths, info plist strings, URL schemes) +3. Edit Swift source files — update ShieldAIApp, ShieldAITheme, display text, storage keys, URL schemes, subsystem strings, biometric prompts +4. Update test files — @testable import, class names, URL strings + +tests: +- Build: iOS project opens in Xcode and builds successfully +- Unit: All iOS tests pass + +acceptance_criteria: +- Xcode project loads without errors +- All build targets compile +- Bundle identifier uses `com.kordant.*` +- URL scheme is `kordant://` +- No "ShieldAI" remains in any Swift source file + +validation: +- Run `grep -rn "ShieldAI\|shieldai" iOS/Kordant/ — expect zero +- Open project in Xcode, verify build succeeds diff --git a/tasks/rebrand-to-kordant/10-update-android-app-branding.md b/tasks/rebrand-to-kordant/10-update-android-app-branding.md new file mode 100644 index 0000000..a25eaf7 --- /dev/null +++ b/tasks/rebrand-to-kordant/10-update-android-app-branding.md @@ -0,0 +1,54 @@ +# 10. Update Android App Branding + +meta: + id: rebrand-to-kordant-10 + feature: rebrand-to-kordant + priority: P1 + depends_on: [rebrand-to-kordant-01] + tags: [android, mobile] + +objective: +- Update all ShieldAI branding in the Android app: package names, Kotlin source files, resources, build config, and class names. + +deliverables: +- android/ShieldAI/ directory renamed to android/Kordant/ +- build.gradle.kts — namespace, applicationId, and API URLs updated +- settings.gradle.kts — rootProject.name updated +- All Kotlin source files — package declarations updated from `com.shieldai.android.*` to `com.kordant.*` +- ShieldAIApp.kt renamed to KordantApp.kt +- ShieldAIDatabase.kt renamed to KordantDatabase.kt — DATABASE_NAME updated +- All import statements across ~70+ Kotlin files updated +- AndroidManifest.xml — app class name, theme references updated +- strings.xml — app_name updated +- themes.xml — style name updated +- Storage key strings updated (shieldai_database, shieldai_auth_prefs, shieldai_biometric_prefs) +- UI display text strings updated (ShieldAI → Kordant) +- Theme.kt — ShieldAITheme fun renamed to KordantTheme + +steps: +1. Rename directory from `android/ShieldAI` to `android/Kordant` +2. Edit build.gradle.kts — update namespace, applicationId, API URLs +3. Edit settings.gradle.kts — update rootProject.name +4. Update all Kotlin package declarations from `com.shieldai.android` to `com.kordant` +5. Update all import statements referencing `com.shieldai.android.*` to `com.kordant.*` +6. Rename ShieldAIApp.kt → KordantApp.kt (update class name) +7. Rename ShieldAIDatabase.kt → KordantDatabase.kt (update class name and DATABASE_NAME) +8. Update AndroidManifest.xml — android:name, theme, app_name +9. Update strings.xml, themes.xml +10. Update Theme.kt — fun ShieldAITheme → fun KordantTheme +11. Update all storage keys from shieldai_* to kordant_* +12. Update all UI display text (ComponentShowcase, AuthScreen, BiometricAuthScreen) + +tests: +- Build: Android project builds successfully +- Unit: All Android tests pass + +acceptance_criteria: +- Android project compiles without errors +- Application ID is `com.kordant.*` +- No "ShieldAI" or "shieldai" remains in any Kotlin source, XML resource, or build config +- Database name uses "kordant_database" + +validation: +- Run `grep -rn "ShieldAI\|shieldai\|com\.shieldai" android/Kordant/` — expect zero +- Open project in Android Studio, verify build succeeds diff --git a/tasks/rebrand-to-kordant/11-update-ci-cd-and-infrastructure.md b/tasks/rebrand-to-kordant/11-update-ci-cd-and-infrastructure.md new file mode 100644 index 0000000..aca42f6 --- /dev/null +++ b/tasks/rebrand-to-kordant/11-update-ci-cd-and-infrastructure.md @@ -0,0 +1,39 @@ +# 11. Update CI/CD and Infrastructure References + +meta: + id: rebrand-to-kordant-11 + feature: rebrand-to-kordant + priority: P1 + depends_on: [rebrand-to-kordant-01] + tags: [infrastructure, ci-cd] + +objective: +- Update all CI/CD workflow files, Docker image tags, Terraform state bucket names, ECS cluster names, and service names from ShieldAI to Kordant. + +deliverables: +- .github/workflows/ci.yml — Docker tags, coverage artifact name, service names updated +- .github/workflows/deploy.yml — bucket name, image tags, cluster names, service names all updated +- scripts/setup-ga4.sh — display name, property name, domain references updated +- scripts/load-test/run-all.sh — test description and service references updated + +steps: +1. Edit `.github/workflows/ci.yml` — update all `shieldai:` Docker tags to `kordant:`, update `name: shieldai-coverage` to `kordant-coverage` +2. Edit `.github/workflows/deploy.yml` — update: + - S3 bucket `shieldai-*` to `kordant-*` + - Docker image tags `shieldai-${{ matrix.name }}` to `kordant-${{ matrix.name }}` + - ECS cluster names `shieldai-${ENV}` to `kordant-${ENV}` + - Service name references +3. Edit `scripts/setup-ga4.sh` — update display name from "ShieldAI" to "Kordant", domain from `shieldai.com` to `kordant.ai` +4. Edit `scripts/load-test/run-all.sh` — update "ShieldAI" references + +tests: +- None (CI/CD changes are validated on next pipeline run) + +acceptance_criteria: +- No `shieldai-` prefix remains in any CI/CD workflow for Docker tags, cluster names, or bucket names +- GA4 script uses kordant.ai domain +- Load test script references Kordant + +validation: +- Run `grep -rn "shieldai-\.*" .github/workflows/ scripts/` — expect zero +- Review updated deploy.yml for consistency diff --git a/tasks/rebrand-to-kordant/12-update-assets-and-ad-creatives.md b/tasks/rebrand-to-kordant/12-update-assets-and-ad-creatives.md new file mode 100644 index 0000000..fc7f84a --- /dev/null +++ b/tasks/rebrand-to-kordant/12-update-assets-and-ad-creatives.md @@ -0,0 +1,40 @@ +# 12. Update SVG Ad Creatives and Marketing Assets + +meta: + id: rebrand-to-kordant-12 + feature: rebrand-to-kordant + priority: P3 + depends_on: [] + tags: [assets, marketing] + +objective: +- Update all SVG ad creative files in the assets/ directory to replace "ShieldAI" branding with "Kordant". + +deliverables: +- assets/ads/meta_c_1x1_1080x1080.svg — headline updated +- assets/ads/meta_b_45_1080x1350.svg — terminal prompt, body text, tagline updated +- assets/ads/meta_b_1x1_1080x1080.svg — terminal prompt, product name updated +- assets/ads/meta_a_1x1_1080x1080.svg — headline updated +- assets/ads/linkedin/variant1_professional.svg — body text, logo updated +- assets/ads/linkedin/variant2_datasecurity.svg — product name, logo updated +- assets/ads/linkedin/variant3_family_professional.svg — badge, logo updated +- assets/ads/gd_portrait_600x750.svg — tagline updated + +steps: +1. For each SVG file, search for "ShieldAI" text nodes and replace with "Kordant" +2. For terminal prompts like `darkwatch@shieldai:~$`, update to `darkwatch@kordant:~$` +3. For taglines like "ShieldAI — AI-Powered Identity Protection", update to "Kordant — AI-Powered Identity Protection" +4. Verify SVG files remain valid after replacements + +tests: +- Visual: Open SVGs in browser — verify text renders correctly with new name +- Structural: SVGs remain valid XML + +acceptance_criteria: +- No "ShieldAI" text remains in any SVG asset file +- Terminal prompts use `@kordant` instead of `@shieldai` +- Taglines and headlines use "Kordant" + +validation: +- Run `grep -rn "ShieldAI" assets/ads/` — expect zero +- Run `grep -rn "shieldai" assets/ads/` — expect zero (check for prompt variants) diff --git a/tasks/rebrand-to-kordant/13-update-documentation-and-plan-files.md b/tasks/rebrand-to-kordant/13-update-documentation-and-plan-files.md new file mode 100644 index 0000000..9d594df --- /dev/null +++ b/tasks/rebrand-to-kordant/13-update-documentation-and-plan-files.md @@ -0,0 +1,39 @@ +# 13. Update READMEs, Plan Documents, and Task References + +meta: + id: rebrand-to-kordant-13 + feature: rebrand-to-kordant + priority: P3 + depends_on: [rebrand-to-kordant-01] + tags: [documentation] + +objective: +- Update all README files, plan documents, and task directory references from ShieldAI to Kordant. + +deliverables: +- README.md — title, description, package references, plan file links updated +- plans/SHIELDAI-product-plan.md — filename renamed to plans/KORDANT-product-plan.md, content updated +- plans/SHIELDAI-technical-architecture.md — filename renamed, content updated +- tasks/shieldai-unified-restructure/ — directory renamed to tasks/kordant-unified-restructure/, all task ID frontmatter updated +- tasks/clerk-integration/README.md — reference to "ShieldAI/web" updated + +steps: +1. Rename plan files: `SHIELDAI-product-plan.md` → `KORDANT-product-plan.md`, `SHIELDAI-technical-architecture.md` → `KORDANT-technical-architecture.md` +2. Update README.md — replace "ShieldAI" with "Kordant" throughout, update plan file links +3. Rename task directory `tasks/shieldai-unified-restructure/` → `tasks/kordant-unified-restructure/` +4. Update all task file frontmatter in `tasks/kordant-unified-restructure/` — change `feature: shieldai-unified-restructure` and task IDs +5. Update `tasks/clerk-integration/README.md` — reference to ShieldAI/web +6. Update README filter package references: `@shieldai/spamshield` → `@kordant/spamshield`, etc. + +tests: +- None (documentation only) + +acceptance_criteria: +- README.md no longer mentions "ShieldAI" +- Plan filenames use Kordant prefix +- Task directory names use kordant prefix +- All internal task references updated + +validation: +- Run `grep -rn "ShieldAI\|shieldai" README.md plans/` — expect zero +- Verify plan files compile/make sense with new name diff --git a/tasks/rebrand-to-kordant/14-verify-rebrand-completeness.md b/tasks/rebrand-to-kordant/14-verify-rebrand-completeness.md new file mode 100644 index 0000000..8659014 --- /dev/null +++ b/tasks/rebrand-to-kordant/14-verify-rebrand-completeness.md @@ -0,0 +1,63 @@ +# 14. Final Sweep and Verification of Rebrand Completeness + +meta: + id: rebrand-to-kordant-14 + feature: rebrand-to-kordant + priority: P0 + depends_on: [rebrand-to-kordant-13] + tags: [quality-assurance] + +objective: +- Perform a comprehensive final sweep of the entire codebase to verify zero "ShieldAI" or "shieldai" references remain in source code (excluding git history and third-party dependencies), and that all builds and tests pass. + +deliverables: +- Final verification report +- Any straggling references cleaned up + +steps: +1. Run comprehensive grep searches across the repo for "ShieldAI" and "shieldai", filtering out: + - `.git/` directory + - `node_modules/` + - `pnpm-lock.yaml`, `bun.lock` + - Any false positives (e.g., in git history references) +2. For each remaining reference, determine if it needs updating +3. Run `pnpm build` — verify all packages build +4. Run `pnpm test` — verify all tests pass +5. Verify web app renders correctly with "Kordant" branding +6. Verify browser extension loads with new name +7. Review all changed files for consistency (punctuation, casing, sentence flow) +8. Run a second sweep to catch any missed references + +tests: +- Build: `pnpm build` succeeds across all packages +- Unit: `pnpm test` passes across all packages +- Integration: Web app, extension, and mobile builds succeed + +acceptance_criteria: +- Zero `ShieldAI` or `shieldai` references remain in source code +- All packages build successfully +- All tests pass +- Domain `kordant.ai` is correctly referenced throughout + +validation: +```bash +# Final sweep commands +echo "=== ShieldAI (PascalCase) ===" +grep -rn "ShieldAI" --include="*.{ts,tsx,js,jsx,kt,swift,html,svg,yml,yaml,sh,env,toml,json}" . \ + --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=.turbo \ + | grep -v "pnpm-lock\|bun.lock\|\.next\|dist" + +echo "=== shieldai (lowercase) ===" +grep -rn "shieldai" --include="*.{ts,tsx,js,jsx,kt,swift,html,svg,yml,yaml,sh,env,toml,json}" . \ + --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=.turbo \ + | grep -v "pnpm-lock\|bun.lock\|\.next\|dist" + +echo "=== SHIELD (pattern check) ===" +grep -rn "SHIELD" --include="*.{ts,tsx,kt,swift}" . \ + --exclude-dir=.git --exclude-dir=node_modules | grep -v "nodemon" +``` + +notes: +- The `.git` directory contains the old name in commit history — that's expected and should NOT be changed +- Third-party dependencies in `node_modules/` may reference "shieldai" in cached packages — ignore these +- The `tasks/` directory contains historical task files with "shieldai" IDs — these are internal planning docs and may optionally be skipped diff --git a/tasks/rebrand-to-kordant/README.md b/tasks/rebrand-to-kordant/README.md new file mode 100644 index 0000000..a8a2d67 --- /dev/null +++ b/tasks/rebrand-to-kordant/README.md @@ -0,0 +1,42 @@ +# Rebrand ShieldAI → Kordant + +Objective: Complete rebrand of the ShieldAI platform to Kordant across all packages, apps, infrastructure, and assets. + +Status legend: [ ] todo, [~] in-progress, [x] done + +Tasks +- [x] 01 — Update monorepo foundation (root package, env, config) → `01-update-monorepo-foundation.md` +- [x] 02 — Update database connection strings and DB names → `02-update-database-connection-strings.md` +- [x] 03 — Update @shieldai/* package scopes and web imports → `03-update-web-package-scope-and-imports.md` +- [ ] 04 — Replace "ShieldAI" display text across web UI → `04-update-web-ui-brand-text.md` +- [ ] 05 — Update email templates, notification content, and queue names → `05-update-web-email-notification-templates.md` +- [ ] 06 — Update browser storage keys, theme keys, and user-agent headers → `06-update-web-storage-keys-and-constants.md` +- [ ] 07 — Update seed data, blog content, and landing page copy → `07-update-web-seed-data-and-blog-content.md` +- [ ] 08 — Update browser extension manifest, HTML, and storage keys → `08-update-browser-extension-branding.md` +- [ ] 09 — Update iOS bundle ID, Xcode project, and Swift source branding → `09-update-ios-app-branding.md` +- [ ] 10 — Update Android package names, Kotlin source, and resources → `10-update-android-app-branding.md` +- [ ] 11 — Update CI/CD workflows, Docker tags, and service names → `11-update-ci-cd-and-infrastructure.md` +- [ ] 12 — Update SVG ad creatives and marketing assets → `12-update-assets-and-ad-creatives.md` +- [ ] 13 — Update READMEs, plan documents, and task references → `13-update-documentation-and-plan-files.md` +- [ ] 14 — Final sweep and verification of rebrand completeness → `14-verify-rebrand-completeness.md` + +Dependencies +- 01 → 03 (package scope must be updated first before web imports) +- 03 → 04 (web scope must resolve before changing text) +- 01 → 05 (env changes needed for email domain) +- 01 → 08 (root package affects extension) +- 01 → 11 (env vars feed CI/CD) +- 03 → 06 (package resolution needed) +- 01 → 09 (root package naming) +- 01 → 10 (root package naming) +- 13 → 14 (docs update before final sweep) + +Exit criteria +- Zero occurrences of "ShieldAI" or "shieldai" remain in source code (excluding third-party dependencies and git history) +- All packages build successfully (`pnpm build`) +- All tests pass (`pnpm test`) +- iOS and Android projects open without errors in Xcode / Android Studio +- Browser extension loads successfully in Chrome +- Web app renders with "Kordant" branding throughout +- Domain `kordant.ai` is functional for all environments +- CI/CD pipelines reference kordant-* services and tags diff --git a/web/.env.example b/web/.env.example deleted file mode 100644 index b8f57fe..0000000 --- a/web/.env.example +++ /dev/null @@ -1,16 +0,0 @@ -DATABASE_URL="postgresql://postgres:postgres@localhost:5432/shieldai" - -## Clerk Authentication Configuration -# Get these from https://clerk.com -CLERK_SECRET_KEY=sk_test_your_clerk_secret_key -VITE_CLERK_PUBLISHABLE_KEY=pk_test_your_clerk_publishable_key - -DATABASE_URL=libsql://your-database-url.turso.io -DATABASE_AUTH_TOKEN=your-turso-auth-token - -# Stripe (get test keys from https://dashboard.stripe.com/test/apikeys) -STRIPE_SECRET_KEY="sk_test_..." -STRIPE_WEBHOOK_SECRET="whsec_..." -STRIPE_PRICE_BASIC="price_basic" -STRIPE_PRICE_PLUS="price_plus" -STRIPE_PRICE_PREMIUM="price_premium" diff --git a/web/drizzle.config.ts b/web/drizzle.config.ts index 6a5c938..2292b51 100644 --- a/web/drizzle.config.ts +++ b/web/drizzle.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ out: "./drizzle", dialect: "turso", dbCredentials: { - url: process.env.DATABASE_URL ?? "libsql://shieldai-dev-mikefreno.aws-us-east-1.turso.io", + url: process.env.DATABASE_URL ?? "libsql://kordant-dev-mikefreno.aws-us-east-1.turso.io", authToken: process.env.DATABASE_AUTH_TOKEN, }, }); diff --git a/web/src/server/db/index.ts b/web/src/server/db/index.ts index 753dc5f..6e5eabd 100644 --- a/web/src/server/db/index.ts +++ b/web/src/server/db/index.ts @@ -4,7 +4,7 @@ import { drizzle } from "drizzle-orm/libsql"; import * as schema from "./schema"; const client = createClient({ - url: process.env.DATABASE_URL ?? "libsql://shieldai-dev-mikefreno.aws-us-east-1.turso.io", + url: process.env.DATABASE_URL ?? "libsql://kordant-dev-mikefreno.aws-us-east-1.turso.io", authToken: process.env.DATABASE_AUTH_TOKEN, }); diff --git a/web/src/server/jobs/queue.ts b/web/src/server/jobs/queue.ts index 50c77c8..58e1ff9 100644 --- a/web/src/server/jobs/queue.ts +++ b/web/src/server/jobs/queue.ts @@ -134,7 +134,7 @@ function createRedisAdapter(): QueueAdapter { maxRetriesPerRequest: null, }); - const queue = new BullMQ.Queue("shieldai-jobs", { connection }); + const queue = new BullMQ.Queue("kordant-jobs", { connection }); let bullJobs = new Map<string, any>(); async function toJob(bullJob: any): Promise<Job> { diff --git a/web/src/server/services/alert.publisher.ts b/web/src/server/services/alert.publisher.ts index 7cdc829..18c1956 100644 --- a/web/src/server/services/alert.publisher.ts +++ b/web/src/server/services/alert.publisher.ts @@ -48,7 +48,7 @@ export async function publishAlert(userId: string, alert: PublishableAlert): Pro if (user?.email) { await sendEmail( user.email, - `[ShieldAI] ${alert.title}`, + `[Kordant] ${alert.title}`, `<p>${alert.message}</p>`, alert.message, ); diff --git a/web/src/server/services/email.templates.test.ts b/web/src/server/services/email.templates.test.ts index 6dc5091..0f19ba2 100644 --- a/web/src/server/services/email.templates.test.ts +++ b/web/src/server/services/email.templates.test.ts @@ -13,7 +13,7 @@ describe("welcomeEmail", () => { expect(result.subject).toContain("Welcome"); expect(result.html).toContain("Alice"); expect(result.text).toContain("Alice"); - expect(result.html).toContain("ShieldAI"); + expect(result.html).toContain("Kordant"); }); }); @@ -37,30 +37,30 @@ describe("alertNotificationEmail", () => { describe("passwordResetEmail", () => { it("includes the reset link", () => { - const result = passwordResetEmail("https://shieldai.app/reset/token123"); - expect(result.html).toContain("https://shieldai.app/reset/token123"); - expect(result.text).toContain("https://shieldai.app/reset/token123"); + const result = passwordResetEmail("https://kordant.ai/reset/token123"); + expect(result.html).toContain("https://kordant.ai/reset/token123"); + expect(result.text).toContain("https://kordant.ai/reset/token123"); expect(result.subject).toContain("Reset"); }); }); describe("familyInviteEmail", () => { it("includes inviter name and group name", () => { - const result = familyInviteEmail("Bob", "Smith Family", "https://shieldai.app/invite/abc"); + const result = familyInviteEmail("Bob", "Smith Family", "https://kordant.ai/invite/abc"); expect(result.html).toContain("Bob"); expect(result.html).toContain("Smith Family"); - expect(result.html).toContain("https://shieldai.app/invite/abc"); + expect(result.html).toContain("https://kordant.ai/invite/abc"); expect(result.subject).toContain("Bob"); }); }); describe("billingReceiptEmail", () => { it("includes payment details", () => { - const result = billingReceiptEmail("Premium Plan", "$19.99", "Mar 15, 2025", "https://shieldai.app/receipt/r1"); + const result = billingReceiptEmail("Premium Plan", "$19.99", "Mar 15, 2025", "https://kordant.ai/receipt/r1"); expect(result.html).toContain("Premium Plan"); expect(result.html).toContain("$19.99"); expect(result.html).toContain("Mar 15, 2025"); - expect(result.html).toContain("https://shieldai.app/receipt/r1"); + expect(result.html).toContain("https://kordant.ai/receipt/r1"); expect(result.subject).toContain("Premium Plan"); }); }); diff --git a/web/src/server/services/email.templates.ts b/web/src/server/services/email.templates.ts index ddb5f35..8df3d21 100644 --- a/web/src/server/services/email.templates.ts +++ b/web/src/server/services/email.templates.ts @@ -9,7 +9,7 @@ function brandedWrapper(title: string, body: string) { <table width="480" cellpadding="0" cellspacing="0" style="background-color:#ffffff;border-radius:8px;overflow:hidden"> <tr> <td style="background-color:#1a1a2e;padding:24px;text-align:center"> - <h1 style="color:#ffffff;margin:0;font-size:20px;font-weight:700">🛡️ ShieldAI</h1> + <h1 style="color:#ffffff;margin:0;font-size:20px;font-weight:700">🛡️ Kordant</h1> <p style="color:#94a3b8;margin:4px 0 0;font-size:13px">Intelligent Protection</p> </td> </tr> @@ -21,7 +21,7 @@ function brandedWrapper(title: string, body: string) { </tr> <tr> <td style="background-color:#f8fafc;padding:16px 24px;text-align:center;border-top:1px solid #e2e8f0"> - <p style="color:#64748b;margin:0;font-size:12px">ShieldAI — Your intelligent digital protection platform</p> + <p style="color:#64748b;margin:0;font-size:12px">Kordant — Your intelligent digital protection platform</p> </td> </tr> </table> @@ -33,7 +33,7 @@ function brandedWrapper(title: string, body: string) { } function brandedText(text: string) { - return `ShieldAI - Intelligent Protection\n\n${text}\n\n---\nShieldAI - Your intelligent digital protection platform`; + return `Kordant - Intelligent Protection\n\n${text}\n\n---\nKordant - Your intelligent digital protection platform`; } export interface EmailTemplate { @@ -44,16 +44,16 @@ export interface EmailTemplate { export function welcomeEmail(name: string): EmailTemplate { return { - subject: "Welcome to ShieldAI", + subject: "Welcome to Kordant", html: brandedWrapper( - "Welcome to ShieldAI!", + "Welcome to Kordant!", `<p style="color:#334155;margin:0 0 12px;line-height:1.6">Hi ${name},</p> -<p style="color:#334155;margin:0 0 12px;line-height:1.6">Thank you for joining ShieldAI. We're here to help you monitor and protect your digital identity.</p> +<p style="color:#334155;margin:0 0 12px;line-height:1.6">Thank you for joining Kordant. We're here to help you monitor and protect your digital identity.</p> <p style="color:#334155;margin:0 0 12px;line-height:1.6">Get started by adding your first watchlist item, and we'll alert you to any exposures or threats.</p> -<p style="color:#334155;margin:0;line-height:1.6">Stay safe,<br>The ShieldAI Team</p>`, +<p style="color:#334155;margin:0;line-height:1.6">Stay safe,<br>The Kordant Team</p>`, ), text: brandedText( - `Hi ${name},\n\nThank you for joining ShieldAI. We're here to help you monitor and protect your digital identity.\n\nGet started by adding your first watchlist item, and we'll alert you to any exposures or threats.\n\nStay safe,\nThe ShieldAI Team`, + `Hi ${name},\n\nThank you for joining Kordant. We're here to help you monitor and protect your digital identity.\n\nGet started by adding your first watchlist item, and we'll alert you to any exposures or threats.\n\nStay safe,\nThe Kordant Team`, ), }; } @@ -68,22 +68,22 @@ export function alertNotificationEmail( severity === "warning" ? "#d97706" : "#2563eb"; return { - subject: `[${severity.toUpperCase()}] ShieldAI Alert: ${alertTitle}`, + subject: `[${severity.toUpperCase()}] Kordant Alert: ${alertTitle}`, html: brandedWrapper( `Alert: ${alertTitle}`, `<div style="display:inline-block;padding:4px 10px;border-radius:4px;font-size:12px;font-weight:600;text-transform:uppercase;color:#ffffff;background-color:${severityColor};margin-bottom:16px">${severity}</div> <p style="color:#334155;margin:0 0 12px;line-height:1.6">${alertMessage}</p> -<p style="color:#64748b;margin:0;line-height:1.6;font-size:13px">Log in to ShieldAI for more details.</p>`, +<p style="color:#64748b;margin:0;line-height:1.6;font-size:13px">Log in to Kordant for more details.</p>`, ), text: brandedText( - `[${severity.toUpperCase()}] Alert: ${alertTitle}\n\n${alertMessage}\n\nLog in to ShieldAI for more details.`, + `[${severity.toUpperCase()}] Alert: ${alertTitle}\n\n${alertMessage}\n\nLog in to Kordant for more details.`, ), }; } export function passwordResetEmail(resetLink: string): EmailTemplate { return { - subject: "Reset your ShieldAI password", + subject: "Reset your Kordant password", html: brandedWrapper( "Password Reset", `<p style="color:#334155;margin:0 0 12px;line-height:1.6">You requested a password reset. Click the button below to set a new password.</p> @@ -108,10 +108,10 @@ export function familyInviteEmail( acceptLink: string, ): EmailTemplate { return { - subject: `${inviterName} invited you to ${groupName} on ShieldAI`, + subject: `${inviterName} invited you to ${groupName} on Kordant`, html: brandedWrapper( "Family Invitation", - `<p style="color:#334155;margin:0 0 12px;line-height:1.6"><strong>${inviterName}</strong> has invited you to join <strong>${groupName}</strong> on ShieldAI.</p> + `<p style="color:#334155;margin:0 0 12px;line-height:1.6"><strong>${inviterName}</strong> has invited you to join <strong>${groupName}</strong> on Kordant.</p> <p style="color:#334155;margin:0 0 12px;line-height:1.6">As a family member, you'll get shared protection and alerts for your digital identity.</p> <table cellpadding="0" cellspacing="0" style="margin:24px 0"> <tr> @@ -122,7 +122,7 @@ export function familyInviteEmail( </table>`, ), text: brandedText( - `Family Invitation\n\n${inviterName} has invited you to join ${groupName} on ShieldAI.\n\nAs a family member, you'll get shared protection and alerts for your digital identity.\n\nAccept the invitation: ${acceptLink}`, + `Family Invitation\n\n${inviterName} has invited you to join ${groupName} on Kordant.\n\nAs a family member, you'll get shared protection and alerts for your digital identity.\n\nAccept the invitation: ${acceptLink}`, ), }; } @@ -134,7 +134,7 @@ export function billingReceiptEmail( receiptUrl: string, ): EmailTemplate { return { - subject: `ShieldAI receipt — ${planName} (${date})`, + subject: `Kordant receipt — ${planName} (${date})`, html: brandedWrapper( "Payment Receipt", `<p style="color:#334155;margin:0 0 8px;line-height:1.6">Thank you for your payment.</p> diff --git a/web/src/server/services/notification.service.test.ts b/web/src/server/services/notification.service.test.ts index 0eeea17..2c6f158 100644 --- a/web/src/server/services/notification.service.test.ts +++ b/web/src/server/services/notification.service.test.ts @@ -43,7 +43,7 @@ describe("sendEmail", () => { const result = await sendEmail("test@example.com", "Subject", "<p>Body</p>", "Text body"); expect(mockResendSend).toHaveBeenCalledWith({ - from: "noreply@shieldai.app", + from: "noreply@kordant.ai", to: "test@example.com", subject: "Subject", html: "<p>Body</p>", diff --git a/web/src/server/services/notification.service.ts b/web/src/server/services/notification.service.ts index 7432a36..7bc9619 100644 --- a/web/src/server/services/notification.service.ts +++ b/web/src/server/services/notification.service.ts @@ -20,7 +20,7 @@ export async function sendEmail( try { const { data, error } = await resend.emails.send({ - from: process.env.RESEND_FROM_EMAIL ?? "noreply@shieldai.app", + from: process.env.RESEND_FROM_EMAIL ?? "noreply@kordant.ai", to, subject, html, diff --git a/web/src/server/services/reports.service.ts b/web/src/server/services/reports.service.ts index b74e551..9d13e51 100644 --- a/web/src/server/services/reports.service.ts +++ b/web/src/server/services/reports.service.ts @@ -93,7 +93,7 @@ export async function generateReport( const periodEnd = periodEndStr ? new Date(periodEndStr) : undefined; const reportLabel = getReportTypeLabel(reportType); - const title = `ShieldAI ${reportLabel} Security Report`; + const title = `Kordant ${reportLabel} Security Report`; const [report] = await db .insert(securityReports) diff --git a/web/src/server/services/reports/generator.ts b/web/src/server/services/reports/generator.ts index a59f2a6..c440a52 100644 --- a/web/src/server/services/reports/generator.ts +++ b/web/src/server/services/reports/generator.ts @@ -165,13 +165,13 @@ export async function compileData( day: "numeric", }); - const title = `ShieldAI ${reportType === "WEEKLY_DIGEST" ? "Weekly" : reportType === "MONTHLY_PLUS" ? "Monthly" : "Annual"} Security Report`; + const title = `Kordant ${reportType === "WEEKLY_DIGEST" ? "Weekly" : reportType === "MONTHLY_PLUS" ? "Monthly" : "Annual"} Security Report`; return { title, periodStart: ps.toLocaleDateString(), periodEnd: pe.toLocaleDateString(), - summary: `During this period, ShieldAI detected ${alertCount} security alerts, ${exposureCount} data exposures, ${voiceAnalysisCount} voice analysis events, ${spamDetectionCount} spam detections, and ${propTotal.count} property changes.`, + summary: `During this period, Kordant detected ${alertCount} security alerts, ${exposureCount} data exposures, ${voiceAnalysisCount} voice analysis events, ${spamDetectionCount} spam detections, and ${propTotal.count} property changes.`, threatScore, threatLevel, threatTrend, @@ -206,7 +206,7 @@ function compileRecommendations( } if (voiceAnalysisCount > 0) { items.push( - `<div class="recommendation">🟢 <strong>Voice Security:</strong> Monitor voice call activity regularly. ShieldAI flagged ${voiceAnalysisCount} analysis event(s) this period.</div>`, + `<div class="recommendation">🟢 <strong>Voice Security:</strong> Monitor voice call activity regularly. Kordant flagged ${voiceAnalysisCount} analysis event(s) this period.</div>`, ); } if (spamDetectionCount > 5) { @@ -215,7 +215,7 @@ function compileRecommendations( ); } items.push( - `<div class="recommendation">ℹ️ <strong>Stay Proactive:</strong> Regularly review your ShieldAI dashboard for real-time security updates and run DarkWatch scans weekly.</div>`, + `<div class="recommendation">ℹ️ <strong>Stay Proactive:</strong> Regularly review your Kordant dashboard for real-time security updates and run DarkWatch scans weekly.</div>`, ); return items.join("\n");