feat(compliance): P-01b Mobile/PWA-Zugriff auf Rechtstexte via Einstellungen
Some checks failed
Deploy Development / deploy (push) Successful in 34s
Test Suite / pytest-backend (push) Successful in 32s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 8s
Test Suite / playwright-tests (push) Failing after 49s

- SettingsLegalPage.jsx: neue Hub-Seite /settings/legal mit allen 4 Rechtstext-Links
- App.jsx: Route /settings/legal in ProtectedLayout registriert
- AccountSettingsPage.jsx: Link zu /settings/legal unterhalb System-Info
- 3 Playwright-Tests für P-01b (Einstellungen → Rechtliches → Links → Routen)
- Version: 0.8.69 → 0.8.70 (backend + frontend)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-05-10 10:51:20 +02:00
parent 75d6a40817
commit 8261fa4420
6 changed files with 135 additions and 2 deletions

View File

@ -1,6 +1,6 @@
# Shinkan Jinkendo Version Information
APP_VERSION = "0.8.69"
APP_VERSION = "0.8.70"
BUILD_DATE = "2026-05-10"
DB_SCHEMA_VERSION = "20260508049"
@ -29,6 +29,13 @@ MODULE_VERSIONS = {
}
CHANGELOG = [
{
"version": "0.8.70",
"date": "2026-05-10",
"changes": [
"Compliance P-01b: Einstellungen/Rechtliches (/settings/legal) fuer mobile/PWA-Darstellung; Hub mit Links zu Impressum, Datenschutz, Nutzungsbedingungen, Medienrichtlinie",
],
},
{
"version": "0.8.69",
"date": "2026-05-10",

View File

@ -38,6 +38,7 @@ import AdminHomeRedirect from './components/AdminHomeRedirect'
import PlatformAdminRoute from './components/PlatformAdminRoute'
import MediaLibraryPage from './pages/MediaLibraryPage'
import LegalPage from './pages/LegalPage'
import SettingsLegalPage from './pages/SettingsLegalPage'
import ActiveClubSwitcher from './components/ActiveClubSwitcher'
import InactiveMembershipBanner from './components/InactiveMembershipBanner'
import './app.css'
@ -183,6 +184,7 @@ function AppRoutes() {
<Route path="profile" element={<Navigate to="/settings" replace />} />
<Route path="settings" element={<AccountSettingsPage />} />
<Route path="settings/system" element={<SettingsSystemInfoPage />} />
<Route path="settings/legal" element={<SettingsLegalPage />} />
<Route path="media" element={<MediaLibraryPage />} />
<Route path="exercises">
<Route index element={<ExercisesListPage />} />

View File

@ -426,6 +426,10 @@ function AccountSettingsPage() {
<Link to="/settings/system">Technische Systeminformationen</Link>
{' — App-Version, Build, Umgebung, Datenbankschema'}
</p>
<p className="muted" style={{ marginTop: '0.6rem', fontSize: '0.875rem', lineHeight: 1.5 }}>
<Link to="/settings/legal">Rechtliches</Link>
{' — Impressum, Datenschutz, Nutzungsbedingungen, Medienrichtlinie'}
</p>
</div>
)
}

View File

@ -0,0 +1,66 @@
import { Link } from 'react-router-dom'
import { Scale } from 'lucide-react'
const LEGAL_LINKS = [
{ to: '/impressum', label: 'Impressum', description: 'Angaben zum Betreiber und Verantwortlichen' },
{ to: '/datenschutz', label: 'Datenschutzerklärung', description: 'Verarbeitung personenbezogener Daten' },
{ to: '/nutzungsbedingungen', label: 'Nutzungsbedingungen', description: 'Regeln für die Nutzung der Plattform' },
{ to: '/medienrichtlinie', label: 'Medienrichtlinie', description: 'Urheberrecht, Rechte am eigenen Bild, Sichtbarkeit' },
]
function SettingsLegalPage() {
return (
<div className="page-padding app-page" style={{ padding: '1rem' }}>
<p style={{ marginBottom: '0.75rem' }}>
<Link to="/settings" style={{ fontSize: '0.9rem' }}>
Zurück zu Einstellungen
</Link>
</p>
<h1 style={{ marginBottom: '0.35rem', fontSize: '1.5rem' }}>Rechtliches</h1>
<p
style={{
color: 'var(--text2)',
marginBottom: '1.25rem',
fontSize: '0.95rem',
lineHeight: 1.5,
maxWidth: '40rem',
}}
>
Rechtstexte und Richtlinien der Plattform.
Die Inhalte befinden sich noch in redaktioneller Prüfung.
</p>
<div className="card" style={{ padding: 0, overflow: 'hidden' }}>
{LEGAL_LINKS.map((item, idx) => (
<Link
key={item.to}
to={item.to}
style={{
display: 'flex',
alignItems: 'center',
gap: '0.85rem',
padding: '1rem 1.1rem',
borderBottom: idx < LEGAL_LINKS.length - 1 ? '1px solid var(--border)' : 'none',
textDecoration: 'none',
color: 'inherit',
}}
>
<Scale size={18} style={{ color: 'var(--text3)', flexShrink: 0 }} />
<div>
<div style={{ fontWeight: 500, color: 'var(--text1)', fontSize: '0.95rem' }}>
{item.label}
</div>
<div style={{ fontSize: '0.8rem', color: 'var(--text3)', marginTop: '0.15rem' }}>
{item.description}
</div>
</div>
<span style={{ marginLeft: 'auto', color: 'var(--text3)', fontSize: '1.1rem' }}></span>
</Link>
))}
</div>
</div>
)
}
export default SettingsLegalPage

View File

@ -1,11 +1,12 @@
// Shinkan Jinkendo Frontend Version
export const APP_VERSION = "0.8.69"
export const APP_VERSION = "0.8.70"
export const BUILD_DATE = "2026-05-10"
export const PAGE_VERSIONS = {
LoginPage: "1.0.2",
LegalPage: "1.0.0",
SettingsLegalPage: "1.0.0",
Dashboard: "1.0.0",
AccountSettingsPage: "1.0.1",
ExercisesPage: "1.5.0", // Fokus +/- Regeln, nur ohne Fokusbereich; Filterprefs

View File

@ -230,6 +230,59 @@ test('P-01: Login-Seite enthält Links zu allen vier Rechtstextseiten', async ({
console.log('✓ P-01: Login-Seite alle vier Rechtstext-Links vorhanden');
});
// P-01b: Rechtliches über Einstellungen (Mobile/PWA-Erreichbarkeit)
test('P-01b: Einstellungen enthält Link zu Rechtliches', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await login(page);
await page.waitForLoadState('networkidle');
await page.goto('/settings');
await page.waitForLoadState('networkidle');
const link = page.locator('a[href="/settings/legal"]');
await expect(link).toBeVisible();
console.log('✓ P-01b: Einstellungen enthält Link zu /settings/legal');
});
test('P-01b: /settings/legal enthält Links zu allen vier Rechtstextseiten', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await login(page);
await page.waitForLoadState('networkidle');
await page.goto('/settings/legal');
await page.waitForLoadState('networkidle');
await expect(page.getByRole('heading', { level: 1 })).toContainText('Rechtliches');
for (const route of LEGAL_ROUTES) {
const link = page.locator(`a[href="${route.path}"]`);
await expect(link).toBeVisible();
}
console.log('✓ P-01b: /settings/legal Überschrift + alle vier Rechtstext-Links vorhanden');
});
test('P-01b: Jeder Rechtstext-Link aus /settings/legal führt zur korrekten Route', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await login(page);
await page.waitForLoadState('networkidle');
for (const route of LEGAL_ROUTES) {
await page.goto('/settings/legal');
await page.waitForLoadState('networkidle');
await page.locator(`a[href="${route.path}"]`).click();
await page.waitForLoadState('networkidle');
expect(page.url()).toContain(route.path);
await expect(page.getByRole('heading', { level: 1 })).toContainText(route.label);
console.log(`✓ P-01b: ${route.label} Link aus /settings/legal korrekt`);
}
});
test('8. Keine kritischen Console-Fehler', async ({ page }) => {
const errors = [];
page.on('console', msg => {