feat(custom-modules): besuchte eigene Module offen halten (Keep-Alive, hoechstens 5)
Tessera CI/CD / Lint & Type Check (push) Successful in 53s
Tessera CI/CD / Tests (push) Successful in 1m37s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 20s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m12s

iframes liegen dauerhaft in einem Behaelter im Portal-Rahmen und werden per
position: fixed deckungsgleich ueber den Platzhalter der Modulseite gelegt;
beim Wegnavigieren nur versteckt (visibility), nie neu eingehaengt. Name/Adresse
je Modul im Sitzungsspeicher, Nachladen im Hintergrund, 404 verwirft. Abmelden
und Loeschen leeren. Im Browser nachgewiesen: gleiches iframe-Element und kein
neuer Seitenabruf nach Dashboard -> Modul B -> Modul A; folgt Seitenleiste
ein-/ausgeklappt und Handybreite.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 12:02:50 +02:00
parent f78b422abf
commit 7e70fc4d32
10 changed files with 553 additions and 73 deletions
+11 -6
View File
@@ -1,12 +1,13 @@
'use client';
import { useEffect, useState } from 'react';
import { useSidebarStore } from '@/lib/stores/sidebar-store';
import { installErrorBuffer } from '@/lib/error-buffer';
import { Header } from '@/components/layout/header';
import { Sidebar } from '@/components/layout/sidebar';
import { CustomModuleFrameHost } from '@/components/modules/custom-module-frame-host';
import { ReleaseNoticeHost } from '@/components/release-notice/release-notice-host';
import { ReminderNotifier } from '@/components/reminders/reminder-notifier';
import { installErrorBuffer } from '@/lib/error-buffer';
import { useSidebarStore } from '@/lib/stores/sidebar-store';
export function AppShell({ children }: { children: React.ReactNode }) {
const isCollapsed = useSidebarStore((s) => s.isCollapsed);
@@ -24,9 +25,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
// Desktop: sidebar pushes content via margin-left
// Mobile: sidebar overlays, no margin needed (handled by app-shell-main class in globals.css)
const sidebarWidth = isCollapsed
? 'var(--sidebar-width-collapsed)'
: 'var(--sidebar-width)';
const sidebarWidth = isCollapsed ? 'var(--sidebar-width-collapsed)' : 'var(--sidebar-width)';
return (
<div className="min-h-screen bg-background">
@@ -35,7 +34,9 @@ export function AppShell({ children }: { children: React.ReactNode }) {
{/* Seitenrahmen seit quick-260916-bwo halbiert: p-3 (12 px) statt vorher 24 px (Stufe 6), gilt fuer alle Seiten. */}
<main
className="app-shell-main min-h-[calc(100vh-var(--header-height))] transition-all duration-200 p-3"
style={mounted ? { '--current-sidebar-width': sidebarWidth } as React.CSSProperties : undefined}
style={
mounted ? ({ '--current-sidebar-width': sidebarWidth } as React.CSSProperties) : undefined
}
>
{children}
</main>
@@ -46,6 +47,10 @@ export function AppShell({ children }: { children: React.ReactNode }) {
Benachrichtigung auf jeder Portalseite ausloest — nicht nur, wenn die
Kachel auf dem Dashboard sichtbar ist. Rendert nichts. */}
<ReminderNotifier />
{/* Eigene Module offen halten (30.09.2026): die <iframe>s leben hier im
Rahmen und ueberstehen so das Wegnavigieren; die Modulseite zeigt nur
einen Platzhalter, ueber den der Behaelter das aktive Modul legt. */}
<CustomModuleFrameHost />
</div>
);
}
+28 -27
View File
@@ -1,18 +1,19 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { useEffect, useRef, useState } from 'react';
import { TesseraLogo } from '@/components/brand/tessera-logo';
import { useSidebarStore } from '@/lib/stores/sidebar-store';
import { useAuthStore } from '@/lib/stores/auth-store';
import { fetchSessionState, logout } from '@/lib/auth-actions';
import { buildNextParam } from '@/lib/safe-next';
import { ThemeToggle } from '@/components/theme-toggle';
import { BugReportButton } from '@/components/bug-report/bug-report-button';
import { HEADER_ACTIONS_SLOT_ID, HEADER_CENTER_SLOT_ID } from '@/components/layout/header-slot';
import { usePathname } from 'next/navigation';
import { ThemeToggle } from '@/components/theme-toggle';
import { fetchSessionState, logout } from '@/lib/auth-actions';
import { buildNextParam } from '@/lib/safe-next';
import { useAuthStore } from '@/lib/stores/auth-store';
import { useCustomModuleCacheStore } from '@/lib/stores/custom-module-cache-store';
import { resolvePageTitle, useNavStore } from '@/lib/stores/nav-store';
import { useSidebarStore } from '@/lib/stores/sidebar-store';
export function Header() {
const t = useTranslations();
@@ -60,13 +61,8 @@ export function Header() {
return;
}
redirectedRef.current = true;
const next = buildNextParam(
window.location.pathname,
window.location.search,
);
window.location.href = next
? `/login?next=${encodeURIComponent(next)}`
: '/login';
const next = buildNextParam(window.location.pathname, window.location.search);
window.location.href = next ? `/login?next=${encodeURIComponent(next)}` : '/login';
}
// 'unavailable' (T-gyd-02): kein Redirect, bisheriges stilles
// Verhalten -- ein API-Ausfall darf keine Abmelde-Schleife ausloesen.
@@ -76,10 +72,7 @@ export function Header() {
// Close dropdown on outside click
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setDropdownOpen(false);
}
}
@@ -91,9 +84,7 @@ export function Header() {
const [avatarError, setAvatarError] = useState(false);
const userInitial = user
? (user.displayName ?? user.username).charAt(0).toUpperCase()
: '?';
const userInitial = user ? (user.displayName ?? user.username).charAt(0).toUpperCase() : '?';
const roleLabel = user ? tHeader(`role.${user.role}`) : '';
@@ -101,11 +92,7 @@ export function Header() {
// der Name der aktuellen Seite bzw. des Moduls.
const navModules = useNavStore((s) => s.modules);
const pageTitle = resolvePageTitle(pathname, navModules);
const pageTitleText = pageTitle
? 'key' in pageTitle
? t(pageTitle.key)
: pageTitle.text
: null;
const pageTitleText = pageTitle ? ('key' in pageTitle ? t(pageTitle.key) : pageTitle.text) : null;
return (
<header className="sticky top-0 z-50 flex h-[var(--header-height)] items-center bg-appbar px-2 text-appbar-foreground sm:px-3 dark:shadow-[inset_0_-1px_0_oklch(1_0_0/0.07)]">
@@ -258,7 +245,18 @@ export function Header() {
: 'text-foreground hover:bg-muted'
}`}
>
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
</svg>
{tHeader('admin.label')}
@@ -273,6 +271,9 @@ export function Header() {
type="button"
onClick={() => {
setDropdownOpen(false);
// Offen gehaltene eigene Module und ihr Sitzungsspeicher
// gehoeren zur Sitzung — beim Abmelden alles verwerfen.
useCustomModuleCacheStore.getState().clear();
logout();
}}
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-foreground hover:bg-muted transition-colors"