fix(review) : resout findings 3e passe review (HIGH frontend + MEDIUMs backend/frontend/E2E)

Backend :
- AuditLogWriter::stripSensitive rendu reellement recursif (matche doc).
- Tests GET /api/permissions/{id} non-admin pour chaque branche OR (gap Codex).
- Gardes non-regression UserRbacProcessor : PATCH /rbac sans clef sites ne
  doit ni auto-selectionner currentSite ni exiger sites.manage.

Frontend :
- useAuditLog : renomme export trompeur fetchLogs -> fetchLogsCached, le
  nom reflete desormais le comportement (cache pollue sinon).
- RoleDrawer / UserRbacDrawer : catch explicite + message d'erreur +
  bouton save disabled si le chargement des referentiels a echoue (evite
  un ecrasement silencieux des droits).
- AuditTimeline / AuditLogDetail : `oui`/`non` passent par common.yes/no.
- AuditTimeline : Intl.RelativeTimeFormat et toLocaleString suivent la
  locale i18n courante (plus de hardcode 'fr').

E2E :
- sidebar-visibility.spec : remplace waitForLoadState('networkidle')
  fragile par attente semantique sur accountDashboardLink (stable en CI).

Tests : 237/237 green, eslint clean, php-cs-fixer clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-04-23 10:31:03 +02:00
parent 25cd6a1ecc
commit b1255bb57a
12 changed files with 271 additions and 48 deletions

View File

@@ -91,7 +91,9 @@ const collectionDiff = computed<Record<string, { added: unknown[]; removed: unkn
function formatValue(value: unknown): string {
if (value === null || value === undefined) return '∅'
if (typeof value === 'boolean') return value ? 'oui' : 'non'
// Passe par i18n plutot qu'un hardcode FR : si une autre locale est
// ajoutee, le rendu s'adapte sans nouvelle passe sur ce composant.
if (typeof value === 'boolean') return value ? t('common.yes') : t('common.no')
if (typeof value === 'object') return JSON.stringify(value)
return String(value)
}

View File

@@ -100,7 +100,7 @@ const props = defineProps<{
const { entityType, entityId } = toRefs(props)
const { t } = useI18n()
const { t, locale } = useI18n()
const { can } = usePermissions()
const { fetchEntityLogs } = useAuditLog()
@@ -162,24 +162,28 @@ function dotClass(action: string): string {
}
}
// Relativise une date en francais via Intl.RelativeTimeFormat. On selectionne
// l'unite la plus grossiere possible (minutes < heures < jours < semaines).
const rtf = new Intl.RelativeTimeFormat('fr', { numeric: 'auto' })
// Relativise une date via Intl.RelativeTimeFormat. On selectionne l'unite
// la plus grossiere possible (minutes < heures < jours < semaines). La
// locale suit dynamiquement celle de l'app pour qu'un switch de langue
// prenne effet sans nouveau mount (recomputed = cache par-locale).
const rtf = computed(() => new Intl.RelativeTimeFormat(locale.value, { numeric: 'auto' }))
function relativeDate(iso: string): string {
const diffMs = Date.now() - new Date(iso).getTime()
const diffSec = Math.round(diffMs / 1000)
const absSec = Math.abs(diffSec)
const fmt = rtf.value
if (absSec < 60) return rtf.format(-Math.sign(diffSec) * Math.abs(diffSec), 'second')
if (absSec < 3600) return rtf.format(-Math.sign(diffSec) * Math.round(absSec / 60), 'minute')
if (absSec < 86400) return rtf.format(-Math.sign(diffSec) * Math.round(absSec / 3600), 'hour')
if (absSec < 604800) return rtf.format(-Math.sign(diffSec) * Math.round(absSec / 86400), 'day')
return rtf.format(-Math.sign(diffSec) * Math.round(absSec / 604800), 'week')
if (absSec < 60) return fmt.format(-Math.sign(diffSec) * Math.abs(diffSec), 'second')
if (absSec < 3600) return fmt.format(-Math.sign(diffSec) * Math.round(absSec / 60), 'minute')
if (absSec < 86400) return fmt.format(-Math.sign(diffSec) * Math.round(absSec / 3600), 'hour')
if (absSec < 604800) return fmt.format(-Math.sign(diffSec) * Math.round(absSec / 86400), 'day')
return fmt.format(-Math.sign(diffSec) * Math.round(absSec / 604800), 'week')
}
function absoluteDate(iso: string): string {
return new Date(iso).toLocaleString('fr-FR', {
// Meme logique : la locale de formatage suit celle de l'app.
return new Date(iso).toLocaleString(locale.value, {
dateStyle: 'medium',
timeStyle: 'short',
})
@@ -223,7 +227,9 @@ function snapshotSummary(entry: AuditLogEntry): string {
function formatValue(value: unknown): string {
if (value === null || value === undefined) return '∅'
if (typeof value === 'boolean') return value ? 'oui' : 'non'
// Passe par i18n plutot qu'un hardcode FR : si une autre locale est
// ajoutee, le rendu s'adapte sans nouvelle passe sur ce composant.
if (typeof value === 'boolean') return value ? t('common.yes') : t('common.no')
if (typeof value === 'object') return JSON.stringify(value)
return String(value)
}