mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-10 07:52:13 +00:00
Immich : publier l'origine du portrait dans le graphe, pas l'identifiant
- avatarFromImmich (fichier préfixé « immich- ») remplace immichPersonId côté front : le panneau ne propose le retrait que si le portrait vient d'Immich, et le bouton de la fiche retrouve son libellé distinctif - helper isImmichAvatarPath partagé entre le payload du graphe et la route unassign, qui testaient le préfixe chacun de son côté - tests : origine du portrait dans le payload (vrai/faux/null) Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
1 parent
8d36a08b11
commit
310f2fb597
10 files changed
+50
-12
No files matched your search
@@ -211,6 +211,12 @@ export function applyMediaResponseHeaders(reply: FastifyReply, storedMimeType: s
|
||||
reply.header('Content-Disposition', `attachment; filename="${safeName}"`)
|
||||
}
|
||||
|
||||
// Portrait venu d'une association Immich : son fichier porte le préfixe « immich- ». Le graphe
|
||||
// ne publie jamais l'identifiant Immich, seulement cette origine du portrait.
|
||||
export function isImmichAvatarPath(avatarPath: string | null | undefined): boolean {
|
||||
return avatarPath != null && path.basename(avatarPath).startsWith('immich-')
|
||||
}
|
||||
|
||||
// Efface un fichier du stockage, sans jamais sortir du dossier racine
|
||||
export async function removeStoredFile(storageRoot: string, relativePath: string | null | undefined): Promise<void> {
|
||||
if (!relativePath || /^https?:\/\//.test(relativePath)) return
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
type ImmichPersonSummary,
|
||||
} from '../lib/immich.js'
|
||||
import { encryptImmichApiKey } from '../lib/immich-secret.js'
|
||||
import { InvalidMediaError, removeStoredFile, sniffMimeType } from '../lib/media-files.js'
|
||||
import { InvalidMediaError, isImmichAvatarPath, removeStoredFile, sniffMimeType } from '../lib/media-files.js'
|
||||
import { exceedsStorageQuota, getStorageRoot, prepareUploadedFile, requireAdminMembership } from './media.js'
|
||||
|
||||
// Reconnaissance des visages : associer une personne de l'arbre à une personne Immich, dont la
|
||||
@@ -503,7 +503,7 @@ export const immichRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
// Le portrait vient d'Immich uniquement si son fichier porte notre préfixe. Un portrait
|
||||
// envoyé à la main reste en place : seule l'association est retirée.
|
||||
const avatarFromImmich = person.avatarPath !== null && path.basename(person.avatarPath).startsWith('immich-')
|
||||
const avatarFromImmich = isImmichAvatarPath(person.avatarPath)
|
||||
|
||||
const updateData: {
|
||||
immichPersonId: null
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Annotation, MediaItem, ParentChildLink, Person, Union } from '@prisma/client'
|
||||
|
||||
import { sanitizeAnnotationContentForRead } from '../lib/annotation-content.js'
|
||||
import { isImmichAvatarPath } from '../lib/media-files.js'
|
||||
|
||||
export type GraphPayload = {
|
||||
persons: Array<{
|
||||
@@ -17,6 +18,7 @@ export type GraphPayload = {
|
||||
avatarUrlPath: string | null
|
||||
sex: string | null
|
||||
notes: string | null
|
||||
avatarFromImmich: boolean
|
||||
}>
|
||||
unions: Array<{
|
||||
id: string
|
||||
@@ -90,6 +92,7 @@ export function buildGraphPayload(
|
||||
avatarUrlPath: person.avatarPath ? `/trees/${treeId}/persons/${person.id}/avatar` : null,
|
||||
sex: person.sex,
|
||||
notes: person.notes,
|
||||
avatarFromImmich: isImmichAvatarPath(person.avatarPath),
|
||||
})),
|
||||
unions: unions.map((union) => ({
|
||||
id: union.id,
|
||||
|
||||
@@ -42,6 +42,24 @@ describe('buildGraphPayload', () => {
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
id: 'p-3',
|
||||
treeId: 't-1',
|
||||
firstName: 'Paul',
|
||||
lastName: 'Durand',
|
||||
birthName: null,
|
||||
birthDate: null,
|
||||
deathDate: null,
|
||||
sex: 'male',
|
||||
notes: null,
|
||||
avatarPath: 't-1/p-3/immich-abc.jpg',
|
||||
immichPersonId: '123e4567-e89b-42d3-a456-426614174999',
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
},
|
||||
] as any,
|
||||
[
|
||||
{
|
||||
@@ -99,8 +117,13 @@ describe('buildGraphPayload', () => {
|
||||
expect(graph.persons[0].birthName).toBe('Durand')
|
||||
expect(graph.persons[0].avatarUrlPath).toBe('/trees/t-1/persons/p-2/avatar')
|
||||
// L'identifiant Immich ne part jamais dans le graphe : il ne sert qu'aux administrateurs.
|
||||
// Seule l'origine du portrait (fichier préfixé « immich- ») est publiée.
|
||||
expect(graph.persons[0]).not.toHaveProperty('immichPersonId')
|
||||
expect(graph.persons[1]).not.toHaveProperty('immichPersonId')
|
||||
expect(graph.persons[2]).not.toHaveProperty('immichPersonId')
|
||||
expect(graph.persons[0].avatarFromImmich).toBe(false)
|
||||
expect(graph.persons[1].avatarFromImmich).toBe(false)
|
||||
expect(graph.persons[2].avatarFromImmich).toBe(true)
|
||||
expect(graph.persons[1].avatarUrlPath).toBeNull()
|
||||
expect(graph.unions[0].id).toBe('u-1')
|
||||
expect(graph.filiations[0].viaUnionId).toBe('u-1')
|
||||
|
||||
@@ -113,7 +113,7 @@ const ImmichFacePanel = ({ person, treeId, token, onClose, onImmichChange }) =>
|
||||
}, [selected, actionLoading, treeId, person, token, onImmichChange, onClose])
|
||||
|
||||
const handleUnassign = useCallback(async () => {
|
||||
if (actionLoading) return
|
||||
if (!person.avatarFromImmich || actionLoading) return
|
||||
setActionLoading(true)
|
||||
setError('')
|
||||
try {
|
||||
@@ -137,13 +137,12 @@ const ImmichFacePanel = ({ person, treeId, token, onClose, onImmichChange }) =>
|
||||
}
|
||||
}
|
||||
|
||||
// Le retrait se propose toujours : le graphe ne dit plus qui est associé. Une association en
|
||||
// place se retire aussi bien sans connexion : changer ou retirer le serveur Immich ne laisse
|
||||
// jamais une ancienne image impossible à nettoyer manuellement. Sans association, le retrait
|
||||
// ne fait rien côté serveur.
|
||||
const unassignBlock = (
|
||||
// L'association en place se retire aussi bien sans connexion : changer ou retirer le serveur
|
||||
// Immich ne laisse jamais une ancienne image impossible à nettoyer manuellement. Le graphe
|
||||
// ne dit que l'origine du portrait (avatarFromImmich), jamais l'identifiant Immich.
|
||||
const unassignBlock = person.avatarFromImmich && (
|
||||
<div className="immich-current">
|
||||
<p>{t('{name} peut être détachée d’une personne Immich, même si Immich est injoignable.', { name: person.firstName || person.lastName })}</p>
|
||||
<p>{t('{name} est déjà associée à une personne Immich.', { name: person.firstName || person.lastName })}</p>
|
||||
<button type="button" className="pz-btn pz-btn--danger-ghost" onClick={handleUnassign} disabled={actionLoading}>
|
||||
<Unlink size={16} aria-hidden="true" /> {t('Retirer l’association')}
|
||||
</button>
|
||||
|
||||
@@ -378,7 +378,7 @@ const PersonCard = ({
|
||||
)}
|
||||
{treeId && onImmichChange && (
|
||||
<button type="button" className="pc-photo-btn" onClick={() => setShowImmichPanel(true)} disabled={avatarActionLoading}>
|
||||
<Sparkles size={14} aria-hidden="true" /> {t('Depuis Immich')}
|
||||
<Sparkles size={14} aria-hidden="true" /> {person.avatarFromImmich ? t('Visage Immich') : t('Depuis Immich')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -115,6 +115,7 @@ function mapRemotePersonToLocal(person, current = null) {
|
||||
profession: person?.profession || null,
|
||||
nationality: person?.nationality || null,
|
||||
photo: current?.photo || null,
|
||||
avatarFromImmich: person?.avatarFromImmich ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ describe('loadGraphData avatar mapping', () => {
|
||||
deathDate: null,
|
||||
notes: null,
|
||||
avatarUrlPath: '/trees/t-1/persons/p-1/avatar',
|
||||
avatarFromImmich: true,
|
||||
},
|
||||
{
|
||||
id: 'p-2',
|
||||
@@ -24,6 +25,7 @@ describe('loadGraphData avatar mapping', () => {
|
||||
deathDate: null,
|
||||
notes: null,
|
||||
avatarUrlPath: null,
|
||||
avatarFromImmich: false,
|
||||
},
|
||||
],
|
||||
unions: [],
|
||||
@@ -48,6 +50,9 @@ describe('loadGraphData avatar mapping', () => {
|
||||
expect(getPersonById('p-1')?.photo).toBe('/api/trees/t-1/persons/p-1/avatar?token=abc')
|
||||
expect(getPersonById('p-2')?.photo).toBeNull()
|
||||
expect(getPersonMedias('p-2')[0]?.source).toBe('Album familial')
|
||||
// Origine du portrait portée par la personne (faux quand il ne vient pas d'Immich)
|
||||
expect(getPersonById('p-1')?.avatarFromImmich).toBe(true)
|
||||
expect(getPersonById('p-2')?.avatarFromImmich).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -261,6 +261,7 @@ export default {
|
||||
'Inconnu': 'Unknown',
|
||||
// Visages venus d'Immich (PersonCard, ImmichFacePanel, immichApi)
|
||||
'Depuis Immich': 'From Immich',
|
||||
'Visage Immich': 'Immich face',
|
||||
'Reconnaissance des visages': 'Face recognition',
|
||||
'Immich n’est pas configuré pour cet arbre.': 'Immich is not configured for this tree.',
|
||||
'Immich n’est pas configuré pour cet arbre. Ses administrateurs le règlent dans Paramètres de l’arbre.': 'Immich is not configured for this tree. Its administrators set it up in Tree settings.',
|
||||
@@ -274,8 +275,7 @@ export default {
|
||||
'Cette adresse Immich n’est pas autorisée par l’opérateur du serveur.': 'This Immich address is not authorized by the server operator.',
|
||||
'Seuls les administrateurs de l’arbre peuvent associer un visage Immich.': 'Only tree administrators can link an Immich face.',
|
||||
'Erreur inattendue.': 'Unexpected error.',
|
||||
'{name} peut être détachée d’une personne Immich, même si Immich est injoignable.':
|
||||
'{name} can be detached from an Immich person, even when Immich cannot be reached.',
|
||||
'{name} est déjà associée à une personne Immich.': '{name} is already linked to an Immich person.',
|
||||
'Retirer l’association': 'Remove the link',
|
||||
'Personne à associer dans Immich': 'Immich person to link',
|
||||
'Rechercher par nom': 'Search by name',
|
||||
|
||||
@@ -57,6 +57,7 @@ export function adaptGraphForGalaxy(apiGraph, _treeId) {
|
||||
birthDate: p.birthDate,
|
||||
deathDate: p.deathDate,
|
||||
photo: p.avatarUrlPath ? `/api${p.avatarUrlPath}` : null,
|
||||
avatarFromImmich: Boolean(p.avatarFromImmich),
|
||||
frameType: 'round', // Default
|
||||
}))
|
||||
|
||||
|
||||
Reference in new issue
Block a user