fix(settings): remove the duplicated section heading on 11 tabs

A generic shell heading stacked on top of each tab component's own internal
heading. The report named five tabs "at least"; auditing all 28 found 11:
downloads, sso, apiTokens, webhooks, businessProfile, crm, accounting,
whatsapp, slideshow, moderation, styling. On the first eight the two headings
resolve to the identical string -- sso and businessProfile literally render
the same key twice. The other three were near-identical stacked titles
("Moderation"/"Word Filters", "Custom CSS"/"Custom CSS Templates",
"CRM behaviour"/"CRM settings").

Clean, and left alone: general, events, categories, thumbnails, security, seo,
imageSecurity, status, analytics (its first heading is a genuine sub-section),
plus the eight already in TABS_WITH_OWN_HEADER.

Removed the component side and kept the shell heading: the shell heading is
the consistent one (icon + label + divider on ~20 tabs) and always matches the
nav item the admin clicked, and none of these components are mounted outside
SettingsPage, so nothing loses a title. Subtitles and intro copy preserved
throughout; orphaned icon imports removed.

The guard test was checked against the pre-fix blobs and does fail on them.

Refs testplan REPORT.md, "duplicate H2 section heading" warning.
This commit is contained in:
Paul Nothaft
2026-09-02 09:43:10 +02:00
parent 22cada9082
commit 3acb452090
12 changed files with 177 additions and 71 deletions
@@ -95,10 +95,12 @@ export const AccountingTab: React.FC = () => {
return (
<div className="space-y-6">
<div>
<h2 className="text-xl font-bold text-neutral-900 dark:text-neutral-100">{t('settings.accounting.title', 'Accounting')}</h2>
<p className="text-neutral-600 dark:text-neutral-400 mt-1">{t('settings.accounting.subtitle', 'Default rates for internal expenses and the proof requirement.')}</p>
</div>
{/* No tab title here — the Settings shell renders the section
heading (icon + label + divider) for every tab that isn't in
SettingsPage's TABS_WITH_OWN_HEADER, and it reads from the same
`settings.accounting.title` key, so repeating it stacked two
identical H2s on top of each other (QA warning). */}
<p className="text-neutral-600 dark:text-neutral-400">{t('settings.accounting.subtitle', 'Default rates for internal expenses and the proof requirement.')}</p>
<Card><CardContent className="p-5 space-y-4">
<div>
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'react-toastify';
import { KeyRound, Trash2, Copy, AlertTriangle } from 'lucide-react';
import { Trash2, Copy, AlertTriangle } from 'lucide-react';
import { Button, Card, Input, Loading } from '../../../components/common';
import { api } from '../../../config/api';
import { useLocalizedDate } from '../../../hooks/useLocalizedDate';
@@ -83,10 +83,10 @@ export const ApiTokensTab: React.FC = () => {
return (
<div className="space-y-6">
<Card padding="md">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-2 flex items-center gap-2">
<KeyRound className="w-5 h-5" />
{t('settings.apiTokens.title', 'API Tokens')}
</h2>
{/* No tab title here — the Settings shell renders the section
heading (icon + label + divider) for every tab that isn't in
SettingsPage's TABS_WITH_OWN_HEADER, and repeating it stacked
two identical H2s on top of each other (QA warning). */}
<p className="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
{t('settings.apiTokens.subtitle', 'Long-lived bearer tokens for the public /api/v1 surface — n8n integrations, custom apps, scripts. Tokens act as the admin user that minted them, intersected with the chosen scopes.')}
</p>
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Save, Download, Plus, Trash2 } from 'lucide-react';
import { Save, Plus, Trash2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'react-toastify';
@@ -107,12 +107,10 @@ export const DownloadsTab: React.FC = () => {
return (
<div className="space-y-6">
<Card>
<div className="flex items-center gap-2 mb-1">
<Download className="w-5 h-5 text-neutral-500" />
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{t('settings.downloads.title', 'Download resolutions')}
</h2>
</div>
{/* No tab title here — the Settings shell renders the section
heading (icon + label + divider) for every tab that isn't in
SettingsPage's TABS_WITH_OWN_HEADER, and repeating it stacked
two identical H2s on top of each other (QA warning). */}
<p className="text-sm text-neutral-600 dark:text-neutral-400 mb-5">
{t('settings.downloads.intro',
'The standard size is what every gallery hands out by default. Individual galleries can override this.')}
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { toast } from 'react-toastify';
import { Save, KeyRound, PlugZap, Copy, Check, UserCog, ShieldAlert, Plus, Trash2 } from 'lucide-react';
import { Save, PlugZap, Copy, Check, UserCog, ShieldAlert, Plus, Trash2 } from 'lucide-react';
import type { AxiosError } from 'axios';
import { Button, Card, Input, Loading } from '../../../components/common';
@@ -127,12 +127,11 @@ export const SsoTab: React.FC = () => {
<div className="space-y-6">
<Card>
<div className="p-6 space-y-4">
<div className="flex items-center gap-2">
<KeyRound className="w-5 h-5 text-neutral-500" />
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{t('settings.sso.title', 'Single Sign-On (OIDC)')}
</h2>
</div>
{/* No tab title here — the Settings shell renders the section
heading (icon + label + divider) for every tab that isn't in
SettingsPage's TABS_WITH_OWN_HEADER, and it reads from the
same `settings.sso.title` key, so repeating it stacked two
identical H2s on top of each other (QA warning). */}
<p className="text-sm text-neutral-600 dark:text-neutral-400">
{t('settings.sso.intro', 'Let admins sign in through your identity provider (Keycloak, Authentik, Pocket ID, or any OIDC-compliant IdP). Local email/password login stays available as a fallback.')}
</p>
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'react-toastify';
import { Webhook as WebhookIcon, Trash2, Copy, AlertTriangle, Activity, CheckCircle2, XCircle } from 'lucide-react';
import { Trash2, Copy, AlertTriangle, Activity, CheckCircle2, XCircle } from 'lucide-react';
import { Button, Card, Input, Loading } from '../../../components/common';
import { api } from '../../../config/api';
import { useLocalizedDate } from '../../../hooks/useLocalizedDate';
@@ -124,10 +124,10 @@ export const WebhooksTab: React.FC = () => {
return (
<div className="space-y-6">
<Card padding="md">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-2 flex items-center gap-2">
<WebhookIcon className="w-5 h-5" />
{t('settings.webhooks.title', 'Webhooks')}
</h2>
{/* No tab title here — the Settings shell renders the section
heading (icon + label + divider) for every tab that isn't in
SettingsPage's TABS_WITH_OWN_HEADER, and repeating it stacked
two identical H2s on top of each other (QA warning). */}
<p className="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
{t('settings.webhooks.subtitle', 'POST event notifications to your URL the moment something happens — gallery published, photo uploaded, event archived, etc. Signed with HMAC-SHA256 in the X-PicPeak-Signature header.')}
</p>
@@ -119,17 +119,17 @@ export const WhatsAppTab: React.FC = () => {
return (
<div className="space-y-6">
<div>
<h2 className="text-xl font-bold text-neutral-900 dark:text-neutral-100">
{t('settings.whatsapp.title', 'WhatsApp')}
</h2>
<p className="text-neutral-600 dark:text-neutral-400 mt-1">
{t(
'settings.whatsapp.subtitle',
'Configure Meta Business credentials to deliver the gallery-ready notification via WhatsApp alongside email.',
)}
</p>
</div>
{/* No tab title here — the Settings shell renders the section
heading (icon + label + divider) for every tab that isn't in
SettingsPage's TABS_WITH_OWN_HEADER, and it reads from the same
`settings.whatsapp.title` key, so repeating it stacked two
identical H2s on top of each other (QA warning). */}
<p className="text-neutral-600 dark:text-neutral-400">
{t(
'settings.whatsapp.subtitle',
'Configure Meta Business credentials to deliver the gallery-ready notification via WhatsApp alongside email.',
)}
</p>
<Card>
<CardContent className="p-5 space-y-4">