From e3811226eba6ea6119be196bbf21831376223227 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Mon, 5 Oct 2026 13:41:09 +1100 Subject: [PATCH 1/3] fix(onboarding): preserve attribution and recovery --- app/pages/index.vue | 1 - app/utils/homepage-person-skills.ts | 8 ++- layers/identity/app/pages/login.vue | 1 + layers/identity/app/pages/me/index.vue | 2 + .../app/pages/onboarding/discover.vue | 35 +++++++------ .../identity/app/pages/onboarding/email.vue | 9 ++-- test/unit/me-watchlist.test.ts | 11 ++++ test/unit/onboarding-watch.test.ts | 50 +++++++++++++++++++ test/unit/skill-source-list.test.ts | 19 +++++++ 9 files changed, 116 insertions(+), 20 deletions(-) create mode 100644 test/unit/onboarding-watch.test.ts diff --git a/app/pages/index.vue b/app/pages/index.vue index 64051fc1..7064f8e3 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -193,7 +193,6 @@ const homepageTrendingSelection = computed(() => selectHomepageTrendingSkills( name: skill.name, displayName: skill.displayName, registryPath: skill.registryPath, - maintainerName: repo.evidence?.authorName ?? null, description: skill.description, context: trendingShareLabel(repo.authorCount), })), diff --git a/app/utils/homepage-person-skills.ts b/app/utils/homepage-person-skills.ts index 22da662e..7df88ba6 100644 --- a/app/utils/homepage-person-skills.ts +++ b/app/utils/homepage-person-skills.ts @@ -59,10 +59,14 @@ export function uniqueByOwner(items: readonly SkillSourceItem[]): SkillSourceIte * the caller then loads the live person feed instead. */ export function selectHomepageTrendingSkills( - trending: readonly SkillSourceItem[], + trending: readonly Omit[], fallbacks: readonly SkillSourceItem[], ): HomepageTrendingSelection { - const unique = uniqueByOwner(trending) + const namesByOwner = new Map(fallbacks.map(item => [item.owner, item.maintainerName ?? item.owner])) + const unique = uniqueByOwner(trending.map(item => ({ + ...item, + maintainerName: namesByOwner.get(item.owner) ?? item.owner, + }))) if (unique.length < HOMEPAGE_TRENDING_MINIMUM) return { _tag: 'fallback' } diff --git a/layers/identity/app/pages/login.vue b/layers/identity/app/pages/login.vue index 247230b9..7e69d379 100644 --- a/layers/identity/app/pages/login.vue +++ b/layers/identity/app/pages/login.vue @@ -43,6 +43,7 @@ useSeoMeta({ icon="i-lucide-github" trailing-icon="i-lucide-arrow-right" size="sm" + class="min-h-11" block /> diff --git a/layers/identity/app/pages/me/index.vue b/layers/identity/app/pages/me/index.vue index bb57dc9f..b94e1e08 100644 --- a/layers/identity/app/pages/me/index.vue +++ b/layers/identity/app/pages/me/index.vue @@ -424,6 +424,7 @@ async function deleteAccount() {
+import type { IdentityMutationResponse } from '../../../shared/contracts/account' import type { StarsSyncResponse } from '../../utils/sync-starred-repos' import { syncStarredRepos } from '../../utils/sync-starred-repos' @@ -82,22 +83,26 @@ onMounted(() => { const actionFailed = useActionFailure() -const submitting = ref(false) +const watchMutation = useNuxtMutation, IdentityMutationResponse>({ + mutation: repos => $fetch('/api/me/subscriptions', { + method: 'POST', + body: { source: 'star-import', repos }, + }), + invalidates: ['identity:subscriptions'], + onError: actionFailed('start watching those repos'), +}) +const submitting = watchMutation.pending async function watchSelected() { if (!selected.value.size) { await navigateTo('/onboarding/email') return } - submitting.value = true - const repos = [...selected.value].map((k) => { - const [owner, repo] = k.split('/') - return { owner, repo } - }) - await $fetch('/api/me/subscriptions', { - method: 'POST', - body: { source: 'star-import', repos }, - }).catch(actionFailed('start watching those repos')) - submitting.value = false + const repos = selectableItems.value + .filter(item => selected.value.has(`${item.owner}/${item.repo}`)) + .map(({ owner, repo }) => ({ owner, repo })) + const saved = await watchMutation.mutateSafe(repos) + if (saved._tag === 'err') + return await navigateTo('/onboarding/email') } @@ -123,6 +128,7 @@ useSeoMeta({ title: 'Discover skills', robots: 'noindex' }) None of your starred repos with "skill" in the name are in the registry yet. - + skip ahead .
@@ -183,7 +189,7 @@ useSeoMeta({ title: 'Discover skills', robots: 'noindex' }) type="checkbox" :checked="r.watching || selected.has(`${r.owner}/${r.repo}`)" :disabled="r.watching" - class="mt-1" + class="mt-1 size-4 accent-primary" @change="toggle(`${r.owner}/${r.repo}`)" >
@@ -210,7 +216,7 @@ useSeoMeta({ title: 'Discover skills', robots: 'noindex' })
- + Skip
diff --git a/layers/identity/app/pages/onboarding/email.vue b/layers/identity/app/pages/onboarding/email.vue index bb3e4923..b7403cf7 100644 --- a/layers/identity/app/pages/onboarding/email.vue +++ b/layers/identity/app/pages/onboarding/email.vue @@ -73,7 +73,8 @@ useSeoMeta({ title: 'Email opt-in', robots: 'noindex' }) type="email" :aria-invalid="missingAddress" :aria-describedby="missingAddress ? 'email-error' : undefined" - class="mt-1 w-full rounded border border-default bg-default px-2 py-1 font-mono text-sm" + autocomplete="email" + class="mt-1 min-h-11 w-full rounded border border-default bg-default px-3 py-2 font-mono text-sm" >

Add an email address, or turn off both emails. @@ -81,14 +82,14 @@ useSeoMeta({ title: 'Email opt-in', robots: 'noindex' })