Skip to content

fix(server): resolve shared-space birthdays on the asset faces endpoint (#808)#809

Open
Deeds67 wants to merge 1 commit into
mainfrom
fix/808-space-birthdate-info-panel
Open

fix(server): resolve shared-space birthdays on the asset faces endpoint (#808)#809
Deeds67 wants to merge 1 commit into
mainfrom
fix/808-space-birthdate-info-panel

Conversation

@Deeds67

@Deeds67 Deeds67 commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Fixes #808.

The bug

A name or birthday set inside a shared space is stored on shared_space_person and resolved at read time against the face identity — it is never written back to person. PersonService.getById already overlays that resolution, which is why the person's own overview page (/people/{id}) shows the age correctly.

The asset viewer Info panel does not read that endpoint. For the owner it renders faceManager.people, fed by GET /faces?id= (DetailPanelPeople.svelte:28), and PersonService.getFacesById mapped the raw person row straight through mapFaces. When the birthday only ever existed on a space profile — the common case when a non-owner sets it inside a Space — person.birthDate is NULL, so the age silently disappeared on every photo, exactly as reported.

The fix

Apply the same identity-wide resolution in getFacesById.

  • mapFaces already nulls people the caller does not own, so only owned faces are resolved — no cross-owner metadata is exposed.
  • Each distinct identity is resolved once regardless of how many faces map onto it.
  • The overlay is extracted to src/utils/person-identity.ts, and AssetService.applyResolvedPersonMetadata — the sibling read path the Info panel uses for a space member — now shares it, so the two surfaces cannot drift apart again. That refactor is behaviour-preserving; the existing asset.service.spec.ts identity tests cover both branches and still pass untouched.

Coverage

Written test-first; each test was watched failing before the fix landed.

Unit (person.service.spec.ts, 6 new) — resolution applied; person without an identity; resolver returns nothing; face owned by someone else (stays null, resolver never called); one identity shared by several faces resolved exactly once; birthday already on the base person preserved.

Medium / real database (test/medium/specs/services/person.service.spec.ts, 4 new) — reproduces the reported shape: person.birthDate NULL, birthday only on a shared_space_person profile reached via the shared identityId. Covers manual and inherited birthDateSource, the no-birthday-anywhere case, and a non-member outsider. These matter because the unit tests stub the resolver; these prove the real hydrateAccessiblePeople query actually returns the space birthday for this shape. Verified RED against a real DB (expected null to be '2014-02-14') before the fix.

Web (detail-panel.spec.ts, 3 new) — pins the user-visible symptom: age renders when the birthday arrives on the faces payload, and does not render with no birthday or a birthday after the photo was taken.

Deliberately not changed

AssetService.applySpacePeople still lets a space profile's NULL birthDate win in explicit space context, discarding the base person's value. That looks adjacent but is a member-visibility boundary (sharePersonMetadata), not the reported bug — the reporter's three space profiles all carry the birthday. Changing it would be an RBAC decision, so it is left alone and flagged here.

Verification

  • server: 5099 unit tests pass; tsc --noEmit clean; eslint --max-warnings 0 clean; prettier clean
  • server medium person.service.spec.ts: 25/25 pass. The repo has 28 pre-existing medium failures on this base (unbuilt plugin-core, exiftool) — baseline captured before and after; this branch adds none
  • web: 3657 tests pass; tsc --noEmit clean; lint 0 errors

…nt (#808)

A name or birthday set inside a shared space is stored on
`shared_space_person` and resolved at read time against the face
identity — it is never written back to `person`. `PersonService.getById`
already overlays that resolution, which is why a person's own overview
page shows the age.

The asset viewer Info panel does not read that endpoint. For the owner it
renders `faceManager.people`, fed by `GET /faces?id=`, and
`PersonService.getFacesById` mapped the raw `person` row straight through.
For anyone whose birthday only ever existed on a space profile — the
common case when a non-owner sets it inside a Space — `person.birthDate`
is NULL, so the age silently disappeared on every photo.

Apply the same identity-wide resolution in `getFacesById`. `mapFaces`
already nulls people the caller does not own, so only owned faces are
resolved and no cross-owner metadata is exposed. Each distinct identity is
resolved once regardless of how many faces map onto it.

The overlay is extracted to `src/utils/person-identity.ts` and
`AssetService.applyResolvedPersonMetadata` — the sibling read path the
Info panel uses for a space member — now shares it, so the two surfaces
cannot drift apart again.

Coverage: six unit tests on `getFacesById` (resolution, no identity,
resolver miss, non-owned face, identity deduped across faces, birthday
already on the base person) and four medium tests that reproduce the
reported shape against a real database, proving the real
`hydrateAccessiblePeople` query returns the space birthday. Three web
tests pin the Info panel's age rendering (present, absent, birthday after
the photo).

Not changed: `applySpacePeople` still lets a space profile's NULL
birthDate win in explicit space context. That is a member-visibility
boundary (`sharePersonMetadata`), not the reported bug — the reporter's
space profiles all carry the birthday.
@Deeds67
Deeds67 force-pushed the fix/808-space-birthdate-info-panel branch from 8b962a8 to 2da8bba Compare July 21, 2026 20:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] Person's age not shown in the photo Info panel when birthDate is only set at the Space level (not on the base person)

1 participant