From c3cfc4b016037e35601d67047f62a7cd330f2709 Mon Sep 17 00:00:00 2001 From: Wolfgang Kozian Date: Sun, 6 Sep 2026 03:56:50 +0200 Subject: [PATCH 1/3] feat(ui): RouteFallback spinner for lazy routes Deviates from the plan, which specified a hardcoded aria-label="Loading". The #15 i18n rollout left the scanner allowlist empty and gated in CI, and the plan's version trips it: src/components/RouteFallback.tsx:6 [attr:aria-label] Loading Uses the existing common:loading key instead (present in both en and de). Co-Authored-By: Claude Opus 5 --- frontend/src/components/RouteFallback.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 frontend/src/components/RouteFallback.tsx diff --git a/frontend/src/components/RouteFallback.tsx b/frontend/src/components/RouteFallback.tsx new file mode 100644 index 0000000..1d0833a --- /dev/null +++ b/frontend/src/components/RouteFallback.tsx @@ -0,0 +1,11 @@ +import { Loader2 } from 'lucide-react' +import { useTranslation } from 'react-i18next' + +export function RouteFallback() { + const { t } = useTranslation() + return ( +
+ +
+ ) +} From f1242a0ff550767356663d6bf79cb18816b944dd Mon Sep 17 00:00:00 2001 From: Wolfgang Kozian Date: Sun, 6 Sep 2026 03:57:47 +0200 Subject: [PATCH 2/3] perf(router): lazy-load leaf pages via React.lazy Co-Authored-By: Claude Opus 5 --- frontend/src/app/router.tsx | 83 ++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/frontend/src/app/router.tsx b/frontend/src/app/router.tsx index 6daecd3..b028bbb 100644 --- a/frontend/src/app/router.tsx +++ b/frontend/src/app/router.tsx @@ -1,45 +1,52 @@ +import { lazy } from 'react' import { createBrowserRouter, Navigate } from 'react-router' import { AuthLayout } from '@/layouts/AuthLayout' import { AppLayout } from '@/layouts/AppLayout' -import { LoginPage } from '@/pages/auth/LoginPage' -import { RegisterPage } from '@/pages/auth/RegisterPage' -import { DashboardPage } from '@/pages/dashboard/DashboardPage' -import { ProjectListPage } from '@/pages/projects/ProjectListPage' -import { ProjectCreatePage } from '@/pages/projects/ProjectCreatePage' -import { IssueListPage } from '@/pages/issues/IssueListPage' -import { IssueDetailPage } from '@/pages/issues/IssueDetailPage' -import { BoardPage } from '@/pages/board/BoardPage' -import { BacklogPage } from '@/pages/backlog/BacklogPage' -import { SprintsPage } from '@/pages/sprints/SprintsPage' -import { ProjectDashboardPage } from '@/pages/project-dashboard/ProjectDashboardPage' -import { ReportsPage } from '@/pages/reports/ReportsPage' -import { NotificationsPage } from '@/pages/notifications/NotificationsPage' -import { WorkflowEditorPage } from '@/pages/settings/WorkflowEditorPage' -import { AutomationPage } from '@/pages/automation/AutomationPage' -import { AutomationRuleEditorPage } from '@/pages/automation/AutomationRuleEditorPage' -import { AdminAutomationPage } from '@/pages/admin/AdminAutomationPage' -import AuditLogPage from '@/pages/admin/AuditLogPage' -import { SsoSettingsPage } from '@/pages/admin/SsoSettingsPage' -import { ApiKeysPage } from '@/pages/settings/ApiKeysPage' -import { AccessTokensPage } from '@/pages/settings/AccessTokensPage' -import { AccountSettingsPage } from '@/pages/settings/AccountSettingsPage' import { SettingsLayout } from '@/layouts/SettingsLayout' -import { ProfilePage } from '@/pages/settings/ProfilePage' -import { SecurityPage } from '@/pages/settings/SecurityPage' -import { NotificationSettingsPage } from '@/pages/settings/NotificationSettingsPage' -import { AdminUsersPage } from '@/pages/admin/AdminUsersPage' -import { WebhooksPage } from '@/pages/settings/WebhooksPage' -import { IntegrationsPage } from '@/pages/settings/IntegrationsPage' -import ProjectAuditPage from '@/pages/projects/settings/ProjectAuditPage' -import { LabelsPage } from '@/pages/projects/settings/LabelsPage' -import { VersionsPage } from '@/pages/projects/settings/VersionsPage' -import { CustomFieldsPage } from '@/pages/projects/settings/CustomFieldsPage' -import { MembersPage } from '@/pages/projects/settings/MembersPage' -import { OrganizationSettingsPage } from '@/pages/projects/settings/OrganizationSettingsPage' -import { OrgsPage } from '@/pages/orgs/OrgsPage' -import { OrgSettingsPage } from '@/pages/orgs/OrgSettingsPage' -import ServiceDeskPage from '@/pages/projects/servicedesk/ServiceDeskPage' -import IncidentDashboardPage from '@/pages/projects/servicedesk/IncidentDashboardPage' + +// Lazy-Helper: kapselt named- und default-Exports einheitlich als React.lazy. +const lazyPage = (imp: () => Promise, name: string) => + lazy(() => imp().then((m) => ({ default: m[name] }))) + +// Leaf-Pages (lazy). default-Export-Pages nutzen name = 'default'. +const LoginPage = lazyPage(() => import('@/pages/auth/LoginPage'), 'LoginPage') +const RegisterPage = lazyPage(() => import('@/pages/auth/RegisterPage'), 'RegisterPage') +const DashboardPage = lazyPage(() => import('@/pages/dashboard/DashboardPage'), 'DashboardPage') +const ProjectListPage = lazyPage(() => import('@/pages/projects/ProjectListPage'), 'ProjectListPage') +const ProjectCreatePage = lazyPage(() => import('@/pages/projects/ProjectCreatePage'), 'ProjectCreatePage') +const IssueListPage = lazyPage(() => import('@/pages/issues/IssueListPage'), 'IssueListPage') +const IssueDetailPage = lazyPage(() => import('@/pages/issues/IssueDetailPage'), 'IssueDetailPage') +const BoardPage = lazyPage(() => import('@/pages/board/BoardPage'), 'BoardPage') +const BacklogPage = lazyPage(() => import('@/pages/backlog/BacklogPage'), 'BacklogPage') +const SprintsPage = lazyPage(() => import('@/pages/sprints/SprintsPage'), 'SprintsPage') +const ProjectDashboardPage = lazyPage(() => import('@/pages/project-dashboard/ProjectDashboardPage'), 'ProjectDashboardPage') +const ReportsPage = lazyPage(() => import('@/pages/reports/ReportsPage'), 'ReportsPage') +const NotificationsPage = lazyPage(() => import('@/pages/notifications/NotificationsPage'), 'NotificationsPage') +const WorkflowEditorPage = lazyPage(() => import('@/pages/settings/WorkflowEditorPage'), 'WorkflowEditorPage') +const AutomationPage = lazyPage(() => import('@/pages/automation/AutomationPage'), 'AutomationPage') +const AutomationRuleEditorPage = lazyPage(() => import('@/pages/automation/AutomationRuleEditorPage'), 'AutomationRuleEditorPage') +const AdminAutomationPage = lazyPage(() => import('@/pages/admin/AdminAutomationPage'), 'AdminAutomationPage') +const AuditLogPage = lazyPage(() => import('@/pages/admin/AuditLogPage'), 'default') +const SsoSettingsPage = lazyPage(() => import('@/pages/admin/SsoSettingsPage'), 'SsoSettingsPage') +const ApiKeysPage = lazyPage(() => import('@/pages/settings/ApiKeysPage'), 'ApiKeysPage') +const AccessTokensPage = lazyPage(() => import('@/pages/settings/AccessTokensPage'), 'AccessTokensPage') +const AccountSettingsPage = lazyPage(() => import('@/pages/settings/AccountSettingsPage'), 'AccountSettingsPage') +const ProfilePage = lazyPage(() => import('@/pages/settings/ProfilePage'), 'ProfilePage') +const SecurityPage = lazyPage(() => import('@/pages/settings/SecurityPage'), 'SecurityPage') +const NotificationSettingsPage = lazyPage(() => import('@/pages/settings/NotificationSettingsPage'), 'NotificationSettingsPage') +const AdminUsersPage = lazyPage(() => import('@/pages/admin/AdminUsersPage'), 'AdminUsersPage') +const WebhooksPage = lazyPage(() => import('@/pages/settings/WebhooksPage'), 'WebhooksPage') +const IntegrationsPage = lazyPage(() => import('@/pages/settings/IntegrationsPage'), 'IntegrationsPage') +const ProjectAuditPage = lazyPage(() => import('@/pages/projects/settings/ProjectAuditPage'), 'default') +const LabelsPage = lazyPage(() => import('@/pages/projects/settings/LabelsPage'), 'LabelsPage') +const VersionsPage = lazyPage(() => import('@/pages/projects/settings/VersionsPage'), 'VersionsPage') +const CustomFieldsPage = lazyPage(() => import('@/pages/projects/settings/CustomFieldsPage'), 'CustomFieldsPage') +const MembersPage = lazyPage(() => import('@/pages/projects/settings/MembersPage'), 'MembersPage') +const OrganizationSettingsPage = lazyPage(() => import('@/pages/projects/settings/OrganizationSettingsPage'), 'OrganizationSettingsPage') +const OrgsPage = lazyPage(() => import('@/pages/orgs/OrgsPage'), 'OrgsPage') +const OrgSettingsPage = lazyPage(() => import('@/pages/orgs/OrgSettingsPage'), 'OrgSettingsPage') +const ServiceDeskPage = lazyPage(() => import('@/pages/projects/servicedesk/ServiceDeskPage'), 'default') +const IncidentDashboardPage = lazyPage(() => import('@/pages/projects/servicedesk/IncidentDashboardPage'), 'default') const isAuthenticated = () => !!localStorage.getItem('accessToken') From 58706083c8e7020e2b2e616dab578b05915ef226 Mon Sep 17 00:00:00 2001 From: Wolfgang Kozian Date: Sun, 6 Sep 2026 04:00:19 +0200 Subject: [PATCH 3/3] feat(router): Suspense fallback around each layout Outlet Initial bundle drops from 1,692.35 kB (gzip 501.25) to 832.55 kB (gzip 260.60); dist/assets goes from 1 JS chunk to 56, with per-page chunks for BoardPage, IssueDetailPage, ReportsPage and the rest. Co-Authored-By: Claude Opus 5 --- frontend/src/layouts/AppLayout.tsx | 6 +++++- frontend/src/layouts/AuthLayout.tsx | 6 +++++- frontend/src/layouts/SettingsLayout.tsx | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/frontend/src/layouts/AppLayout.tsx b/frontend/src/layouts/AppLayout.tsx index b2f0b37..b74f99f 100644 --- a/frontend/src/layouts/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout.tsx @@ -1,4 +1,6 @@ +import { Suspense } from 'react' import { Outlet, Link, useNavigate, useMatch } from 'react-router' +import { RouteFallback } from '@/components/RouteFallback' import { useQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { @@ -146,7 +148,9 @@ export function AppLayout() {
- + }> + + {projectKey && }
diff --git a/frontend/src/layouts/AuthLayout.tsx b/frontend/src/layouts/AuthLayout.tsx index 79a2d57..f195891 100644 --- a/frontend/src/layouts/AuthLayout.tsx +++ b/frontend/src/layouts/AuthLayout.tsx @@ -1,4 +1,6 @@ +import { Suspense } from 'react' import { Outlet } from 'react-router' +import { RouteFallback } from '@/components/RouteFallback' import { VersionTag } from '@/components/VersionTag' export function AuthLayout() { @@ -8,7 +10,9 @@ export function AuthLayout() { {/* i18n-ignore: brand name, not translated */}

🐺 TaskWolf

- + }> + + ) diff --git a/frontend/src/layouts/SettingsLayout.tsx b/frontend/src/layouts/SettingsLayout.tsx index 15d8e90..4a7b7dc 100644 --- a/frontend/src/layouts/SettingsLayout.tsx +++ b/frontend/src/layouts/SettingsLayout.tsx @@ -1,4 +1,6 @@ +import { Suspense } from 'react' import { Outlet, NavLink } from 'react-router' +import { RouteFallback } from '@/components/RouteFallback' import { useTranslation } from 'react-i18next' import { User, Shield, Bell, KeyRound, UserX } from 'lucide-react' @@ -32,7 +34,9 @@ export function SettingsLayout() { ))}
- + }> + +
)