Skip to content

Commit 2afc92b

Browse files
committed
fix: "Set as Primary" photo not updating in UI
Multiple issues prevented the photo from updating after clicking "Set as Primary": 1. getPhotoPath() only checked provider-suffixed photos, not the primary photo - Now checks {personId}.jpg first, then falls back to provider photos 2. Photo priority was backwards - showed provider photos before primary - PersonDetail header now shows primary photo first - SparseTree row always shows primary photo (source of truth) 3. Browser caching "exists" endpoint responses (304 Not Modified) - Added Cache-Control: no-cache headers to all photo exists endpoints 4. No FamilySearch-specific photo endpoint - Added GET /augment/:personId/familysearch-photo endpoint - Added getFsPhotoUrl() and hasFsPhotoProvider() client API methods 5. No cache busting for photo URLs after changes - Added photoVersion state that increments on photo change - Photo URLs now include ?v={version} parameter Files changed: - client/src/components/person/PersonDetail.tsx - client/src/components/person/ProviderDataTable.tsx - client/src/services/api.ts - server/src/routes/augmentation.routes.ts - server/src/routes/browser.routes.ts - server/src/services/scraper.service.ts
1 parent 6432158 commit 2afc92b

7 files changed

Lines changed: 81 additions & 24 deletions

File tree

‎.changelog/v0.6.x.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ This release normalizes FamilySearch as a downstream provider (equal to Ancestry
1919
- Provider cache (`data/provider-cache/`) is for comparison only, not a fallback data source
2020
- All data reads come from SQLite; provider cache shows what providers have vs what SparseTree has
2121

22+
## Bug Fixes
23+
24+
### "Set as Primary" Photo Not Updating (v0.6.x)
25+
- Fixed photo not updating after clicking "Set as Primary" on provider photos
26+
- Root causes fixed:
27+
- `getPhotoPath()` now checks for primary photo (`{personId}.jpg`) before provider-suffixed photos
28+
- Photo priority in PersonDetail header and SparseTree row now shows primary photo first
29+
- Added `Cache-Control: no-cache` headers to all photo "exists" endpoints to prevent stale responses
30+
- Added FamilySearch-specific photo endpoint: `GET /augment/:personId/familysearch-photo`
31+
- Added client API methods: `getFsPhotoUrl()`, `hasFsPhotoProvider()`
32+
- Added `photoVersion` state for cache-busting photo URLs after changes
33+
2234
## Improvements
2335

2436
### Legacy Code Removal (v0.6.x)

‎client/src/components/person/PersonDetail.tsx‎

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export function PersonDetail() {
113113
const [hasFsPhoto, setHasFsPhoto] = useState(false);
114114
const [hasWikiPhoto, setHasWikiPhoto] = useState(false);
115115
const [hasAncestryPhoto, setHasAncestryPhoto] = useState(false);
116+
const [photoVersion, setPhotoVersion] = useState(0); // For cache busting
116117
const [loading, setLoading] = useState(true);
117118
const [lineageLoading, setLineageLoading] = useState(false);
118119
const [scrapeLoading, setScrapeLoading] = useState(false);
@@ -425,6 +426,8 @@ export function PersonDetail() {
425426
setHasAncestryPhoto(ancestryPhotoCheck?.exists ?? false);
426427
setHasWikiTreePhoto(wikiTreePhotoCheck?.exists ?? false);
427428
setHasLinkedInPhoto(linkedInPhotoCheck?.exists ?? false);
429+
// Increment photo version to bust browser cache
430+
setPhotoVersion(v => v + 1);
428431
}, [personId]);
429432

430433
const handleSavePersonField = useCallback(async (fieldName: string, value: string, originalValue: string | null) => {
@@ -550,17 +553,22 @@ export function PersonDetail() {
550553
const isRoot = database?.rootId === personId;
551554
const generations = lineage ? lineage.path.length - 1 : 0;
552555
const relationship = isRoot ? 'Root Person (You)' : lineage ? getRelationshipLabel(generations) : null;
553-
// Photo priority: Ancestry > WikiTree > LinkedIn > Wiki > FamilySearch scraped
554-
const photoUrl = hasAncestryPhoto
555-
? api.getAncestryPhotoUrl(personId!)
556-
: hasWikiTreePhoto
557-
? api.getWikiTreePhotoUrl(personId!)
558-
: hasLinkedInPhoto
559-
? api.getLinkedInPhotoUrl(personId!)
560-
: hasWikiPhoto
561-
? api.getWikiPhotoUrl(personId!)
562-
: hasPhoto
563-
? api.getPhotoUrl(personId!)
556+
// Photo priority: Primary (user-selected) > Ancestry > WikiTree > LinkedIn > Wiki > FamilySearch
557+
// Add cache-busting version parameter to force refresh after changing primary photo
558+
const cacheBust = photoVersion > 0 ? `?v=${photoVersion}` : '';
559+
// Primary photo is at api.getPhotoUrl() - check if it exists by seeing if hasPhoto is true
560+
// but we need to distinguish between "has primary" vs "has any photo"
561+
// For now, always show the primary photo URL if hasPhoto is true (getPhotoPath returns primary first)
562+
const photoUrl = hasPhoto
563+
? api.getPhotoUrl(personId!) + cacheBust
564+
: hasAncestryPhoto
565+
? api.getAncestryPhotoUrl(personId!) + cacheBust
566+
: hasWikiTreePhoto
567+
? api.getWikiTreePhotoUrl(personId!) + cacheBust
568+
: hasLinkedInPhoto
569+
? api.getLinkedInPhotoUrl(personId!) + cacheBust
570+
: hasWikiPhoto
571+
? api.getWikiPhotoUrl(personId!) + cacheBust
564572
: null;
565573

566574
// Get primary description from augmentation

‎client/src/components/person/ProviderDataTable.tsx‎

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -356,17 +356,10 @@ export function ProviderDataTable({
356356
};
357357

358358
// Provider photo URLs
359-
const sparseTreePhotoUrl = hasAncestryPhoto
360-
? api.getAncestryPhotoUrl(personId)
361-
: hasWikiTreePhoto
362-
? api.getWikiTreePhotoUrl(personId)
363-
: hasWikiPhoto
364-
? api.getWikiPhotoUrl(personId)
365-
: hasPhoto
366-
? api.getPhotoUrl(personId)
367-
: null;
368-
369-
const fsPhotoUrl = hasFsPhoto ? api.getPhotoUrl(personId) : null;
359+
// SparseTree row always shows the primary (user-selected) photo - this is our source of truth
360+
const sparseTreePhotoUrl = hasPhoto ? api.getPhotoUrl(personId) : null;
361+
362+
const fsPhotoUrl = hasFsPhoto ? api.getFsPhotoUrl(personId) : null;
370363
const ancestryPhotoUrl = hasAncestryPhoto ? api.getAncestryPhotoUrl(personId) : null;
371364
const wikiTreePhotoUrl = hasWikiTreePhoto ? api.getWikiTreePhotoUrl(personId) : null;
372365
const wikiPhotoUrl = hasWikiPhoto ? api.getWikiPhotoUrl(personId) : null;

‎client/src/services/api.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,12 @@ export const api = {
327327

328328
getWikiTreePhotoUrl: (personId: string) => `${BASE_URL}/augment/${personId}/wikitree-photo`,
329329

330+
// FamilySearch photo (provider-specific, not primary)
331+
hasFsPhotoProvider: (personId: string) =>
332+
fetchJson<{ exists: boolean }>(`/augment/${personId}/familysearch-photo/exists`),
333+
334+
getFsPhotoUrl: (personId: string) => `${BASE_URL}/augment/${personId}/familysearch-photo`,
335+
330336
// LinkedIn linking
331337
linkLinkedIn: (personId: string, url: string) =>
332338
fetchJson<PersonAugmentation>(`/augment/${personId}/linkedin`, {

‎server/src/routes/augmentation.routes.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ router.get('/:personId/wiki-photo', async (req: Request, res: Response) => {
9393
router.get('/:personId/wiki-photo/exists', async (req: Request, res: Response) => {
9494
const { personId } = req.params;
9595
const exists = augmentationService.hasWikiPhoto(personId);
96+
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
9697
res.json({ success: true, data: { exists } });
9798
});
9899

@@ -144,6 +145,7 @@ router.get('/:personId/ancestry-photo', async (req: Request, res: Response) => {
144145
router.get('/:personId/ancestry-photo/exists', async (req: Request, res: Response) => {
145146
const { personId } = req.params;
146147
const exists = augmentationService.hasAncestryPhoto(personId);
148+
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
147149
res.json({ success: true, data: { exists } });
148150
});
149151

@@ -195,6 +197,33 @@ router.get('/:personId/wikitree-photo', async (req: Request, res: Response) => {
195197
router.get('/:personId/wikitree-photo/exists', async (req: Request, res: Response) => {
196198
const { personId } = req.params;
197199
const exists = augmentationService.hasWikiTreePhoto(personId);
200+
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
201+
res.json({ success: true, data: { exists } });
202+
});
203+
204+
// Serve FamilySearch photo
205+
router.get('/:personId/familysearch-photo', async (req: Request, res: Response) => {
206+
const { personId } = req.params;
207+
const photoPath = augmentationService.getFamilySearchPhotoPath(personId);
208+
209+
if (!photoPath || !fs.existsSync(photoPath)) {
210+
res.status(404).json({ success: false, error: 'FamilySearch photo not found' });
211+
return;
212+
}
213+
214+
const ext = path.extname(photoPath).toLowerCase();
215+
const contentType = ext === '.png' ? 'image/png' : 'image/jpeg';
216+
217+
res.setHeader('Content-Type', contentType);
218+
res.setHeader('Cache-Control', 'public, max-age=86400');
219+
fs.createReadStream(photoPath).pipe(res);
220+
});
221+
222+
// Check if FamilySearch photo exists
223+
router.get('/:personId/familysearch-photo/exists', async (req: Request, res: Response) => {
224+
const { personId } = req.params;
225+
const exists = augmentationService.hasFamilySearchPhoto(personId);
226+
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
198227
res.json({ success: true, data: { exists } });
199228
});
200229

@@ -246,6 +275,7 @@ router.get('/:personId/linkedin-photo', async (req: Request, res: Response) => {
246275
router.get('/:personId/linkedin-photo/exists', async (req: Request, res: Response) => {
247276
const { personId } = req.params;
248277
const exists = augmentationService.hasLinkedInPhoto(personId);
278+
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
249279
res.json({ success: true, data: { exists } });
250280
});
251281

‎server/src/routes/browser.routes.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ router.get('/photos/:personId/exists', async (req: Request, res: Response) => {
211211
const { personId } = req.params;
212212
const exists = scraperService.hasPhoto(personId);
213213
const fsExists = scraperService.hasFsPhoto(personId);
214+
// Disable caching so changes are detected immediately
215+
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
214216
res.json({ success: true, data: { exists, fsExists } });
215217
});
216218

‎server/src/services/scraper.service.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,16 @@ export const scraperService = {
9090
},
9191

9292
getPhotoPath(personId: string): string | null {
93-
// Photo patterns: all providers use suffixed naming
94-
const suffixes = ['-familysearch', '-wiki', '-ancestry', '-wikitree', '-linkedin'];
9593
const extensions = ['.jpg', '.png'];
9694

95+
// First check for primary photo (user-selected, no suffix)
96+
for (const ext of extensions) {
97+
const primaryPath = path.join(PHOTOS_DIR, `${personId}${ext}`);
98+
if (fs.existsSync(primaryPath)) return primaryPath;
99+
}
100+
101+
// Then check provider-specific photos
102+
const suffixes = ['-familysearch', '-wiki', '-ancestry', '-wikitree', '-linkedin'];
97103
for (const suffix of suffixes) {
98104
for (const ext of extensions) {
99105
const filePath = path.join(PHOTOS_DIR, `${personId}${suffix}${ext}`);

0 commit comments

Comments
 (0)