Skip to content

Commit 2b30a45

Browse files
markgohoclaude
andcommitted
feat: add draft profile banner and improve profile 404 debugging
Show an info banner on the edit profile page when a profile is in draft mode so users know their profile isn't yet visible on the public directory. Add diagnostic logging to both the backend (ownerUid, requestingUid in denied logs) and frontend (console.error on fetch failures) to help debug intermittent 404s for draft profiles. Introduce loadProfile() gating so the profile resource only fetches when explicitly requested by a component. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d8fd14e commit 2b30a45

10 files changed

Lines changed: 71 additions & 20 deletions

File tree

bun.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

functions/package-lock.json

Lines changed: 13 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

functions/src/profiles-api/routes/read-profile-by-slug.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ export async function readProfileBySlugLogic({
6666
logger.info("Draft profile access denied", {
6767
slug,
6868
hasAuth: Boolean(userToken),
69+
requestingUid: userToken?.uid,
70+
profileOwnerUid: profileData.ownerUid,
71+
isAdmin: userToken?.["admin"] === true,
6972
});
7073
set.status = 404;
7174
return { error: "Profile not found" };

members/src/app/edit-profile-image/edit-profile-image.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ async function setup({ profileData, uploadError, deleteError }: SetupOptions = {
280280
const hasCustomImage = profileData?.image !== undefined;
281281

282282
const mockProfileService = {
283+
loadProfile: vi.fn(),
283284
profileResource: {
284285
isLoading: signal(false),
285286
hasValue: signal(profileData !== undefined),

members/src/app/edit-profile-image/edit-profile-image.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export class EditProfileImage {
2323
protected readonly successMessage = signal<string | undefined>(undefined);
2424

2525
constructor() {
26+
this.profileService.loadProfile();
27+
2628
// Cleanup preview URL when it changes or component is destroyed
2729
effect((onCleanup) => {
2830
const url = this.previewUrl();

members/src/app/edit-profile/edit-profile.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ <h2>Profile Setup Required</h2>
1818
<p>Once your profile is created, you'll be able to edit it here.</p>
1919
</div>
2020
} @else if (profile()) {
21+
@if (profile()?.draft === true && !isMembershipInactive()) {
22+
<app-alert-banner variant="info">
23+
<strong>Profile in Draft Mode</strong> — Your profile is not yet visible on the public doula
24+
directory. An administrator will review and publish your profile.
25+
</app-alert-banner>
26+
}
2127
@if (isMembershipInactive()) {
2228
<app-alert-banner variant="warning">
2329
<h2>Membership Inactive</h2>

members/src/app/edit-profile/edit-profile.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ async function setup({
385385

386386
const mockProfileService = {
387387
profile: signal(profileSignalValue),
388+
loadProfile: vi.fn(),
388389
profileImageUrl: signal(
389390
hasProfile && userHasSlug
390391
? 'https://ik.imagekit.io/doulacoop/tr:w-300,h-300,fo-face,z-0.5,di-default-profile.png/doulas/jane-doe/jane-doe-profile'

members/src/app/edit-profile/edit-profile.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export class EditProfile {
4343
});
4444

4545
constructor() {
46+
this.profileService.loadProfile();
47+
4648
effect(() => {
4749
const profile = this.profile();
4850
if (profile && !this.profileForm.dirty) {

members/src/app/services/profile.service.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ export class ProfileService {
3434

3535
private readonly imageCacheBust = signal(Date.now());
3636

37+
/**
38+
* Whether profile loading has been requested.
39+
* Pages that need the profile (edit-profile, edit-profile-image) call loadProfile().
40+
* Pages that don't (create-profile) skip calling it, avoiding a spurious 404.
41+
*/
42+
private readonly loadRequested = signal(false);
43+
3744
constructor() {
3845
// TODO(2026-06-01): Remove this localStorage cleanup once all users have loaded the app at least once.
3946
// Clean up any leftover optimistic state from the old implementation
@@ -57,9 +64,19 @@ export class ProfileService {
5764
});
5865
}
5966

60-
// Resource automatically loads profile based on membership status
67+
/**
68+
* Request profile loading. Call this from components that need the profile data.
69+
* The resource won't fetch until this is called, preventing spurious 404s
70+
* on pages like /profile/create where the profile doesn't exist yet.
71+
*/
72+
loadProfile(): void {
73+
this.loadRequested.set(true);
74+
}
75+
76+
// Resource loads profile only after loadProfile() is called
6177
readonly profileResource = resource({
6278
params: () => {
79+
if (!this.loadRequested()) return undefined;
6380
const user = this.membershipService.userDocument();
6481
// Only load if user has active membership and a slug
6582
return user?.membershipActive && user?.slug ? { slug: user.slug } : undefined;
@@ -215,7 +232,27 @@ export class ProfileService {
215232
}
216233

217234
private async fetchProfileFromServer(slug: string): Promise<ProfileData> {
218-
return firstValueFrom(this.http.get<ProfileData>(`/api/profiles/${slug}`));
235+
try {
236+
return await firstValueFrom(this.http.get<ProfileData>(`/api/profiles/${slug}`));
237+
} catch (error: unknown) {
238+
if (error instanceof HttpErrorResponse && error.status === 404) {
239+
console.error('Profile not found (404):', {
240+
slug,
241+
status: error.status,
242+
errorBody: error.error,
243+
hint: 'If the profile exists but is draft, this may be a draft access control issue. Check that the auth token is being sent and that ownerUid matches.',
244+
});
245+
} else {
246+
console.error('Profile fetch failed:', {
247+
slug,
248+
error:
249+
error instanceof HttpErrorResponse
250+
? { status: error.status, body: error.error }
251+
: String(error),
252+
});
253+
}
254+
throw error;
255+
}
219256
}
220257

221258
getTagUrl(tag: string): string {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
},
2626
"devDependencies": {
2727
"@imagekit/nodejs": "^7.3.0",
28-
"@types/bun": "^1.3.9",
28+
"@types/bun": "^1.3.10",
2929
"eslint": "^9.39.3",
3030
"eslint-plugin-unicorn": "^63.0.0",
3131
"firebase-admin": "^13.4.0",

0 commit comments

Comments
 (0)