Skip to content

Commit 09d329c

Browse files
committed
fix: address PR review feedback round 7
1 parent 6574ba7 commit 09d329c

6 files changed

Lines changed: 73 additions & 69 deletions

File tree

client/src/components/ancestry-tree/views/MigrationMapView.tsx

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,17 @@ import { useEffect, useRef, useState, useMemo, useCallback } from 'react';
1818
import { MapContainer, TileLayer, Marker, Polyline, Popup, useMap } from 'react-leaflet';
1919
import type { MapPerson, MapData } from '@fsf/shared';
2020
import { GeocodeProgressBar } from '../../map/GeocodeProgressBar';
21+
import { Link } from 'react-router-dom';
2122
import {
2223
createPersonMarker,
23-
buildPopupHtml,
2424
getMigrationLineStyle,
2525
buildMigrationLines,
2626
calculateBounds,
2727
} from '../../map/mapUtils';
2828

2929
import 'leaflet/dist/leaflet.css';
3030

31-
// Fix Leaflet default icon issue with Vite
3231
import L from 'leaflet';
33-
// @ts-expect-error Leaflet icon path fix for bundlers
34-
delete L.Icon.Default.prototype._getIconUrl;
35-
L.Icon.Default.mergeOptions({
36-
iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
37-
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
38-
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
39-
});
4032

4133
interface MigrationMapViewProps {
4234
mapData: MapData | null;
@@ -67,6 +59,43 @@ function FitBounds({ persons }: { persons: MapPerson[] }) {
6759
return null;
6860
}
6961

62+
function PersonPopupContent({ person, dbId }: { person: MapPerson; dbId: string }) {
63+
const genderIcon = person.gender === 'male' ? '\u2642' : person.gender === 'female' ? '\u2640' : '';
64+
const lineageLabel = person.lineage === 'paternal' ? 'Paternal' : person.lineage === 'maternal' ? 'Maternal' : '';
65+
66+
return (
67+
<div style={{ minWidth: 180, maxWidth: 280 }}>
68+
{person.photoUrl && (
69+
<img
70+
src={person.photoUrl}
71+
alt={person.name}
72+
style={{ width: 48, height: 48, borderRadius: '50%', objectFit: 'cover', marginRight: 8, float: 'left' }}
73+
/>
74+
)}
75+
<div>
76+
<Link
77+
to={`/person/${encodeURIComponent(dbId)}/${encodeURIComponent(person.id)}`}
78+
style={{ color: '#4A90D9', fontWeight: 600, fontSize: 14, textDecoration: 'none' }}
79+
>
80+
{person.name}{person.isFavorite && ' \u2B50'}
81+
</Link>
82+
<div style={{ color: '#888', fontSize: 12, marginTop: 2 }}>
83+
{genderIcon} {person.lifespan}
84+
{lineageLabel && ` \u00B7 ${lineageLabel}`}
85+
{person.generation > 0 && ` \u00B7 Gen ${person.generation}`}
86+
</div>
87+
{(person.birthPlace || person.deathPlace) && (
88+
<div style={{ color: '#aaa', fontSize: 11, marginTop: 4 }}>
89+
{person.birthPlace && <div>Born: {person.birthPlace}</div>}
90+
{person.deathPlace && <div>Died: {person.deathPlace}</div>}
91+
</div>
92+
)}
93+
</div>
94+
<div style={{ clear: 'both' }} />
95+
</div>
96+
);
97+
}
98+
7099
export function MigrationMapView({ mapData, dbId, loading, onReload }: MigrationMapViewProps) {
71100
const [timeRange, setTimeRange] = useState<[number, number]>([0, 2100]);
72101
const [showPaternal, setShowPaternal] = useState(true);
@@ -277,7 +306,7 @@ export function MigrationMapView({ mapData, dbId, loading, onReload }: Migration
277306
icon={createPersonMarker(person)}
278307
>
279308
<Popup>
280-
<div dangerouslySetInnerHTML={{ __html: buildPopupHtml(person, dbId) }} />
309+
<PersonPopupContent person={person} dbId={dbId} />
281310
</Popup>
282311
</Marker>
283312
);

client/src/components/map/mapUtils.ts

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,6 @@ import { PATERNAL_COLORS, MATERNAL_COLORS } from '../ancestry-tree/utils/lineage
1212
// Self color (for root person)
1313
const SELF_COLOR = '#A37FDB'; // Purple
1414

15-
/**
16-
* Escape HTML special characters to prevent XSS
17-
*/
18-
function escapeHtml(str: string): string {
19-
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;');
20-
}
21-
2215
/**
2316
* Get the lineage color for a person
2417
*/
@@ -58,42 +51,6 @@ export function createPersonMarker(person: MapPerson): L.DivIcon {
5851
});
5952
}
6053

61-
/**
62-
* Build popup HTML for a person marker
63-
*/
64-
export function buildPopupHtml(person: MapPerson, dbId: string): string {
65-
const name = escapeHtml(person.name);
66-
const photoUrl = person.photoUrl ? escapeHtml(person.photoUrl) : '';
67-
const photoHtml = photoUrl
68-
? `<img src="${photoUrl}" alt="${name}" style="width:48px;height:48px;border-radius:50%;object-fit:cover;margin-right:8px;float:left;" />`
69-
: '';
70-
71-
const genderIcon = person.gender === 'male' ? '\u2642' : person.gender === 'female' ? '\u2640' : '';
72-
const lineageLabel = person.lineage === 'paternal' ? 'Paternal' : person.lineage === 'maternal' ? 'Maternal' : '';
73-
const favoriteLabel = person.isFavorite ? ' &#11088;' : '';
74-
const lifespan = escapeHtml(person.lifespan);
75-
76-
const places: string[] = [];
77-
if (person.birthPlace) places.push(`Born: ${escapeHtml(person.birthPlace)}`);
78-
if (person.deathPlace) places.push(`Died: ${escapeHtml(person.deathPlace)}`);
79-
80-
return `
81-
<div style="min-width:180px;max-width:280px;">
82-
${photoHtml}
83-
<div>
84-
<a href="/person/${encodeURIComponent(dbId)}/${encodeURIComponent(person.id)}" style="color:#4A90D9;font-weight:600;font-size:14px;text-decoration:none;">
85-
${name}${favoriteLabel}
86-
</a>
87-
<div style="color:#888;font-size:12px;margin-top:2px;">
88-
${genderIcon} ${lifespan}${lineageLabel ? ` &middot; ${lineageLabel}` : ''}${person.generation > 0 ? ` &middot; Gen ${person.generation}` : ''}
89-
</div>
90-
${places.length > 0 ? `<div style="color:#aaa;font-size:11px;margin-top:4px;">${places.join('<br/>')}</div>` : ''}
91-
</div>
92-
<div style="clear:both;"></div>
93-
</div>
94-
`;
95-
}
96-
9754
/**
9855
* Migration line style based on lineage
9956
*/

client/src/services/api.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -722,23 +722,39 @@ export const api = {
722722
body: JSON.stringify({ personId, whyInteresting, tags })
723723
}),
724724

725-
applyDiscoveryBatch: (dbId: string, candidates: DiscoveryCandidate[]) =>
726-
fetchJson<{ applied: number }>(`/ai-discovery/${dbId}/apply-batch`, {
727-
method: 'POST',
728-
body: JSON.stringify({ candidates })
729-
}),
725+
applyDiscoveryBatch: async (dbId: string, candidates: DiscoveryCandidate[]): Promise<{ applied: number }> => {
726+
const CHUNK_SIZE = 1000;
727+
let totalApplied = 0;
728+
for (let i = 0; i < candidates.length; i += CHUNK_SIZE) {
729+
const chunk = candidates.slice(i, i + CHUNK_SIZE);
730+
const result = await fetchJson<{ applied: number }>(`/ai-discovery/${dbId}/apply-batch`, {
731+
method: 'POST',
732+
body: JSON.stringify({ candidates: chunk })
733+
});
734+
totalApplied += result.applied;
735+
}
736+
return { applied: totalApplied };
737+
},
730738

731739
dismissDiscoveryCandidate: (dbId: string, personId: string, whyInteresting?: string, suggestedTags?: string[]) =>
732740
fetchJson<{ success: boolean }>(`/ai-discovery/${dbId}/dismiss`, {
733741
method: 'POST',
734742
body: JSON.stringify({ personId, whyInteresting, suggestedTags })
735743
}),
736744

737-
dismissDiscoveryBatch: (dbId: string, candidates: Array<{ personId: string; whyInteresting?: string; suggestedTags?: string[] }>) =>
738-
fetchJson<{ dismissed: number }>(`/ai-discovery/${dbId}/dismiss-batch`, {
739-
method: 'POST',
740-
body: JSON.stringify({ candidates })
741-
}),
745+
dismissDiscoveryBatch: async (dbId: string, candidates: Array<{ personId: string; whyInteresting?: string; suggestedTags?: string[] }>): Promise<{ dismissed: number }> => {
746+
const CHUNK_SIZE = 1000;
747+
let totalDismissed = 0;
748+
for (let i = 0; i < candidates.length; i += CHUNK_SIZE) {
749+
const chunk = candidates.slice(i, i + CHUNK_SIZE);
750+
const result = await fetchJson<{ dismissed: number }>(`/ai-discovery/${dbId}/dismiss-batch`, {
751+
method: 'POST',
752+
body: JSON.stringify({ candidates: chunk })
753+
});
754+
totalDismissed += result.dismissed;
755+
}
756+
return { dismissed: totalDismissed };
757+
},
742758

743759
getDismissedCandidates: (dbId: string) =>
744760
fetchJson<{ dismissed: DismissedCandidate[]; count: number }>(`/ai-discovery/${dbId}/dismissed`),

server/src/routes/ai-discovery.routes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ router.delete('/:dbId/dismissed', async (req: Request, res: Response) => {
207207
* Debug endpoints - gated behind ENABLE_AI_DEBUG env var or non-production mode.
208208
* These expose AI run metadata, prompts, and outputs.
209209
*/
210-
const debugEnabled = process.env.ENABLE_AI_DEBUG === '1' || process.env.NODE_ENV !== 'production';
210+
const debugEnabled = process.env.ENABLE_AI_DEBUG === '1';
211211

212212
/**
213213
* Get recent AI run logs for debugging

server/src/routes/map.routes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ mapRouter.post('/geocode/reset-not-found', (_req: Request, res: Response) => {
4141
* Use POST /geocode/reset-not-found first to retry previously failed places.
4242
*/
4343
mapRouter.get('/geocode/stream', async (req: Request, res: Response) => {
44-
const dbId = req.query.dbId as string;
44+
const dbId = typeof req.query.dbId === 'string' ? req.query.dbId : '';
4545

4646
if (!dbId) {
4747
res.status(400).json({ success: false, error: 'dbId query param required' });

server/src/services/ai-discovery.service.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -550,10 +550,12 @@ export const aiDiscoveryService = {
550550
candidates: Array<{ personId: string; whyInteresting?: string; suggestedTags?: string[] }>
551551
): { dismissed: number } {
552552
let dismissed = 0;
553-
for (const candidate of candidates) {
554-
this.dismissCandidate(dbId, candidate.personId, candidate.whyInteresting, candidate.suggestedTags);
555-
dismissed++;
556-
}
553+
sqliteService.transaction(() => {
554+
for (const candidate of candidates) {
555+
this.dismissCandidate(dbId, candidate.personId, candidate.whyInteresting, candidate.suggestedTags);
556+
dismissed++;
557+
}
558+
});
557559
return { dismissed };
558560
},
559561

0 commit comments

Comments
 (0)