Performance: Code-Splitting pro Seite, KI-Chat lazy, Bilder als WebP (Einstieg 1071->285 kB)

This commit is contained in:
2026-10-04 21:49:22 +02:00
parent 844657e622
commit 38e681dce7
6 changed files with 47 additions and 22 deletions
+7
View File
@@ -826,3 +826,10 @@ Zustellung an axel@hocks.eu status=sent, Test-Ticket bereinigt.
- User kommuniziert auf Deutsch; alle UI-Texte und Antworten auf Deutsch. - User kommuniziert auf Deutsch; alle UI-Texte und Antworten auf Deutsch.
- Dateien außerhalb des Workspaces immer erst in den Workspace kopieren. - Dateien außerhalb des Workspaces immer erst in den Workspace kopieren.
- Vorsicht mit destruktiven Aktionen (Explizite Bestätigung einholen). - Vorsicht mit destruktiven Aktionen (Explizite Bestätigung einholen).
## 2026-10-04 — Fix: Navigation springt bei Seitenwechsel
- Ursache: Inhalts-Spalte im AppLayout hatte min-width:auto und wurde von breitem Tabellen-Inhalt auf ~1645px gedrückt (Viewport 878px) → Header/Navigation wirkten gesprungen.
- Fix: min-w-0 auf Inhalts-Spalte + main (AppLayout.tsx); Tabellen-Wrapper in Zaehler/Bestand/MieterVerwaltung von overflow-hidden auf overflow-x-auto + min-w-[...] umgestellt.
- Verifiziert live: /zaehler und /kosten jeweils scrollWidth == clientWidth == viewport.
- Deploy: Build → pscp → /opt/mieterportal/dist (direktes Kopieren, kein mv). Commit 844657e.
+20 -15
View File
@@ -1,22 +1,25 @@
import { Routes, Route, Navigate } from 'react-router' import { Routes, Route, Navigate } from 'react-router'
import { Component, type ReactNode } from 'react' import { Component, Suspense, lazy, type ReactNode } from 'react'
import { StoreProvider, useStore } from './store/store' import { StoreProvider, useStore } from './store/store'
import AppLayout from './components/AppLayout' import AppLayout from './components/AppLayout'
import Login from './pages/Login' import Login from './pages/Login'
import Dashboard from './pages/Dashboard'
import Payments from './pages/Payments' // Seiten werden beim ersten Aufruf nachgeladen (Code-Splitting) –
import Tickets from './pages/Tickets' // Verwaltungsmodule landen nicht im Bundle der Mieter.
import Documents from './pages/Documents' const Dashboard = lazy(() => import('./pages/Dashboard'))
import Messages from './pages/Messages' const Payments = lazy(() => import('./pages/Payments'))
import Zaehler from './pages/Zaehler' const Tickets = lazy(() => import('./pages/Tickets'))
import Kosten from './pages/Kosten' const Documents = lazy(() => import('./pages/Documents'))
import Bestand from './pages/Bestand' const Messages = lazy(() => import('./pages/Messages'))
import Mietvertraege from './pages/Mietvertraege' const Zaehler = lazy(() => import('./pages/Zaehler'))
import MieterVerwaltung from './pages/MieterVerwaltung' const Kosten = lazy(() => import('./pages/Kosten'))
import Assistent from './pages/Assistent' const Bestand = lazy(() => import('./pages/Bestand'))
import BenutzerVerwaltung from './pages/BenutzerVerwaltung' const Mietvertraege = lazy(() => import('./pages/Mietvertraege'))
import Profil from './pages/Profil' const MieterVerwaltung = lazy(() => import('./pages/MieterVerwaltung'))
import Anleitung from './pages/Anleitung' const Assistent = lazy(() => import('./pages/Assistent'))
const BenutzerVerwaltung = lazy(() => import('./pages/BenutzerVerwaltung'))
const Profil = lazy(() => import('./pages/Profil'))
const Anleitung = lazy(() => import('./pages/Anleitung'))
/** Fängt Render-Fehler ab – statt schwarzem Bildschirm: Hinweis + Reload. */ /** Fängt Render-Fehler ab – statt schwarzem Bildschirm: Hinweis + Reload. */
class FehlerGrenze extends Component<{ children: ReactNode }, { fehler: boolean }> { class FehlerGrenze extends Component<{ children: ReactNode }, { fehler: boolean }> {
@@ -76,6 +79,7 @@ export default function App() {
return ( return (
<FehlerGrenze> <FehlerGrenze>
<StoreProvider> <StoreProvider>
<Suspense fallback={<Ladebildschirm />}>
<Routes> <Routes>
<Route path="/login" element={<LoginGate />} /> <Route path="/login" element={<LoginGate />} />
<Route <Route
@@ -102,6 +106,7 @@ export default function App() {
</Route> </Route>
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>
</Suspense>
</StoreProvider> </StoreProvider>
</FehlerGrenze> </FehlerGrenze>
) )
Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+16 -4
View File
@@ -1,9 +1,11 @@
import { useEffect, useState } from 'react' import { Suspense, lazy, useEffect, useState } from 'react'
import { useLocation } from 'react-router' import { useLocation } from 'react-router'
import { Sparkles, X } from 'lucide-react' import { Sparkles, X } from 'lucide-react'
import AssistChat from './AssistChat'
import { useStore } from '../../store/store' import { useStore } from '../../store/store'
// Chat-Code erst laden, wenn die Verwaltung den Assistenten das erste Mal öffnet
const AssistChat = lazy(() => import('./AssistChat'))
/** KI-Assistent als einblendbares Widget (v1.38): /** KI-Assistent als einblendbares Widget (v1.38):
* Vertikale Lasche am rechten Rand öffnet ein Chat-Panel über der aktuellen Seite. * Vertikale Lasche am rechten Rand öffnet ein Chat-Panel über der aktuellen Seite.
* Nur für die Verwaltung (role admin); auf der Assistenten-Seite selbst ausgeblendet. */ * Nur für die Verwaltung (role admin); auf der Assistenten-Seite selbst ausgeblendet. */
@@ -11,6 +13,9 @@ export default function AssistWidget() {
const { role } = useStore() const { role } = useStore()
const location = useLocation() const location = useLocation()
const [offen, setOffen] = useState(false) const [offen, setOffen] = useState(false)
// Chat bleibt nach dem ersten Öffnen gemountet (Verlauf erhalten), wird aber
// vorher nicht geladen – der Chunk kommt erst bei Bedarf über die Leitung.
const [warOffen, setWarOffen] = useState(false)
// Bei Seitenwechsel Panel schließen (außer manuell wieder geöffnet) // Bei Seitenwechsel Panel schließen (außer manuell wieder geöffnet)
useEffect(() => { useEffect(() => {
@@ -34,7 +39,10 @@ export default function AssistWidget() {
{/* Lasche am rechten Rand */} {/* Lasche am rechten Rand */}
<button <button
type="button" type="button"
onClick={() => setOffen(true)} onClick={() => {
setWarOffen(true)
setOffen(true)
}}
aria-label="KI-Assistent öffnen" aria-label="KI-Assistent öffnen"
className={`fixed right-0 top-1/2 z-40 flex -translate-y-1/2 flex-col items-center gap-2 rounded-l-xl bg-teal-600 py-4 pl-2.5 pr-2 text-white shadow-lg transition-all hover:bg-teal-500 hover:pr-3 ${ className={`fixed right-0 top-1/2 z-40 flex -translate-y-1/2 flex-col items-center gap-2 rounded-l-xl bg-teal-600 py-4 pl-2.5 pr-2 text-white shadow-lg transition-all hover:bg-teal-500 hover:pr-3 ${
offen ? 'pointer-events-none translate-x-full opacity-0' : 'translate-x-0 opacity-100' offen ? 'pointer-events-none translate-x-full opacity-0' : 'translate-x-0 opacity-100'
@@ -76,7 +84,11 @@ export default function AssistWidget() {
</header> </header>
<div className="flex min-h-0 flex-1 flex-col p-4"> <div className="flex min-h-0 flex-1 flex-col p-4">
<AssistChat /> {warOffen ? (
<Suspense fallback={<div className="py-8 text-center text-sm text-slate-400">Assistent wird geladen …</div>}>
<AssistChat />
</Suspense>
) : null}
<p className="mt-2 text-[11px] leading-snug text-slate-400 dark:text-slate-500"> <p className="mt-2 text-[11px] leading-snug text-slate-400 dark:text-slate-500">
Antworten können fehlerhaft sein – kritische Daten bitte prüfen. Antworten können fehlerhaft sein – kritische Daten bitte prüfen.
</p> </p>
+4 -3
View File
@@ -2,8 +2,8 @@ import { useState } from 'react'
import { useNavigate } from 'react-router' import { useNavigate } from 'react-router'
import { KeyRound, MailPlus, Sparkles, UserCheck } from 'lucide-react' import { KeyRound, MailPlus, Sparkles, UserCheck } from 'lucide-react'
import logo from '../assets/hinterstein/logo.png' import logo from '../assets/hinterstein/logo.png'
import hero from '../assets/hero.jpg' import hero from '../assets/hero.webp'
import joseph from '../assets/hinterstein/joseph-hinterstein.jpg' import joseph from '../assets/hinterstein/joseph-hinterstein.webp'
import kontakt from '../assets/hinterstein/kontakt.jpg' import kontakt from '../assets/hinterstein/kontakt.jpg'
import { useStore, DEMO_PASSWORT } from '../store/store' import { useStore, DEMO_PASSWORT } from '../store/store'
@@ -91,6 +91,7 @@ export default function Login() {
<img <img
src={joseph} src={joseph}
alt="Joseph Hinterstein, Ihr Ansprechpartner" alt="Joseph Hinterstein, Ihr Ansprechpartner"
loading="lazy"
className="h-32 w-24 rounded-2xl object-cover object-top" className="h-32 w-24 rounded-2xl object-cover object-top"
/> />
<div className="text-sm text-slate-100"> <div className="text-sm text-slate-100">
@@ -103,7 +104,7 @@ export default function Login() {
</div> </div>
</div> </div>
<img src={kontakt} alt="Kontakt: joseph@hinterstein-immo.de" className="w-full rounded-2xl shadow-lg" /> <img src={kontakt} alt="Kontakt: joseph@hinterstein-immo.de" loading="lazy" className="w-full rounded-2xl shadow-lg" />
</div> </div>
{/* Rechte Spalte: Login-/Registrier-Karte */} {/* Rechte Spalte: Login-/Registrier-Karte */}