Phase 1 of the Stripe-style policy-cockpit redesign for the user-lifecycle
settings page. Pure server-rendering — no async, no JS components, no
sparklines yet (those land in later phases).
Adds a four-tile KPI row above the configuration form (Last run,
Deactivated/30d, Deleted/30d, Pending deletion/7d), populates the
previously empty aside with three quick actions (Run policy now,
Purge logs, Policy reference link), and surfaces a relative-time +
status hint under the existing Run-Now collapsible.
Module-isolation is preserved through a new read-side contract:
* core/Service/Audit/UserLifecycleAuditDashboardInterface — read-only
pendant to the existing write-side UserLifecycleAuditInterface.
Methods: lastRun(), summaryByAction(int days), countActionInWindow(...).
* core/Service/Audit/NullUserLifecycleAuditDashboard — fail-closed
default when the audit module is disabled. KPI tiles 1-3 then
render "—"; tile 4 (pending deletion) keeps working because it
lives in the core domain.
* modules/audit/.../Service/UserLifecycleAuditDashboardService — the
module's implementation; reads through the existing
UserLifecycleAuditRepository (extended with three new aggregation
queries: lastRun, countByActionStatusSinceTimestamp, countSinceTimestamp).
* AuditContainerRegistrar binds the interface to the module impl;
registerContainer.php registers the Null fallback before module
bindings, mirroring how the write-side audit interface is wired.
The new core service UserLifecyclePolicyDashboardService computes
the pending-deletion-window count from the users table directly
(no audit dependency) — defensive when both policy days are 0
(returns 0 rather than running an unbounded query).
New shared template partial templates/partials/app-kpi-row.phtml is
generic — accepts a $kpiTiles array of {label, count, icon, iconTone,
href, tooltip} and reuses the existing app-tile primitive. Other
settings pages can pick it up without ceremony.
Includes:
* PHPUnit tests for both new services (happy path + Null-fallback +
policy-disabled edge cases).
* AuditModuleIsolationContractTest allowlist extended for the new
interface and module service.
* 14 new translation keys in both default_de.json and default_en.json
(i18n parity verified).
All six quality gates green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reworks the auth flow from a single centered card into a two-pane
layout — form on the left, tenant brand on the right — and tightens the
multi-step login UX along the way. Major changes:
LAYOUT
- templates/login.phtml splits into a flex-column body so the footer
spans both panes at the bottom instead of getting clipped under main.
- New .login-form-pane and .login-brand-pane on a 1fr/1fr grid above
768px; mobile stacks brand on top as a slim band, form below.
- Brand pane carries a soft halo + dot grid + tinted base, all derived
from --app-primary so it tracks the tenant accent automatically.
- Login card gets a Stripe-style hairline border + soft shadow, no
Pico article > header sectioning band, h1 in semibold + tracking-tight.
- The "body > main" global padding is overridden for login so the brand
panel reaches the very top + bottom edges of its column.
OS THEME FALLBACK
- New appExplicitTheme() returns the user/tenant theme or empty string,
used by login.phtml + error.phtml to OMIT data-theme entirely when no
preference exists. CSS prefers-color-scheme media query then drives
the theme — DB stays the source of truth, no browser-side persistence.
MULTI-STEP UX
- Heading is stage-aware: "Login" / "Select tenant" / "Login to {tenant}".
Drops the redundant "Login credentials" subtitle.
- Stage 3 gets an identity pill (icon + email + compact "use different
email" button) replacing the old tenant-context block, so the user
always sees which account they're signing in with regardless of
multi-tenant status.
- Stage 2 tenant selection drops avatars + initials — just radio + name
with text-overflow ellipsis for long tenant names.
- Tightens primary CTA: full-width on every stage incl. <p>-wrapped
buttons. autofocus moves to the right input per stage (ldap_username
/ password).
NOTICE / HELP LINKS
- The placeholder "Problems logging in" link is gone (it used to point
at the imprint route — misleading). show_support wired through 6
auth pages and the partial removed; architecture tests adjusted.
- Help-links centered with bullet separators between items, hairline
border-top so they read as secondary navigation under the main CTA.
- The "Encrypted / HTTPS/TLS 1.2+" trust badge at the card bottom is
removed — modern users assume HTTPS, and the badge added noise.
DEAD CODE
- $authLogoHref, $selectedTenantAvatarUrl, $selectedTenantInitial,
$hasSelectedTenantAvatar, $canSwitchTenant — unused after the
identity-pill / brand-pane move, removed from all 6 auth pages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Notices used to be styled only inside the toast stack — every inline
.notice on the login page, auth pages and admin edit screens fell back
to browser defaults and looked unstyled. The Stripe-style toast redesign
(icon pill + neutral text + soft card surface) now lives on the base
.notice rule, and .app-toast-stack adds the slide-in animation, soft
shadow, dismiss button and progress bar on top.
Inline notices auto-render the variant icon via a ::before pseudo using
the Bootstrap Icons codepoints, so all 30+ existing call sites get the
new look without markup changes. Toasts opt out of ::before via
:has(> .notice-icon) and keep their explicit icon span.
The previous app-flash.phtml partial is folded into app-toast-stack.phtml
(both create the same .app-toast-stack container — having both mounted
made two stacks fight for the same fixed corner). default/login/page
templates now mount the unified partial; the architecture contract is
updated to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the detail-drawer move: the .app-toast-stack container is no
longer lazy-created in JS on first toast, instead it lives in
templates/partials/app-toast-stack.phtml and is mounted once in
default.phtml alongside the confirm dialog. The aria-live attribute now
sits in the SSR markup so screen readers register the live region from
page load, not from the first toast.
getToastStack() drops to a plain querySelector and warns via warnOnce
when the partial is missing instead of constructing a fallback container.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The drawer chrome used to be built in JS via a 50-line innerHTML string
and needed every caller to plumb through a labels object sourced from
per-page PHP config. That diverged from the codebase convention — the
confirm dialog, search dialog and session warning are all PHP partials
mounted once in templates/default.phtml. The drawer now follows the
same pattern: templates/partials/app-detail-drawer.phtml renders the
markup with t() labels, default.phtml mounts it inside the logged-in
shell, and the JS only attaches behavior via the [data-detail-drawer]
selector.
Knock-on cleanup: ensureDrawerElement and resolveLabels disappear from
the JS, all three initDetailDrawer callers (admin/users, address book,
helpdesk debitor) drop their labels parameter, and the matching dead
'drawerClose'/'drawerPrev'/… entries leave the per-page PHP grid configs.
The drawer also gains a clean fallback when the partial is missing
(console warn + null return), so logged-out edges can't crash.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an iconTone API (blue, violet, red, orange, amber, emerald, cyan,
pink, green, neutral) to appTile() that derives both icon background and
color from a single hue via color-mix(). Light mode keeps the pastel-pill
look; dark mode picks subtle dark-tinted backgrounds with bright accent
icons so tiles read clearly on the dark background.
The existing iconBg/iconColor escape hatch stays for callers that need a
custom hex (admin/stats); the nine settings tiles migrate to iconTone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New core partial templates/partials/app-input-copy.phtml renders a
standard label + input with a compact copy button overlaid on the
right edge of the input. Used for privacy/imprint URLs on the tenant
form; reusable for any field where a one-click copy is helpful.
- Extend the shared copy-field component (web/js/components/app-copy-field.js)
with data-copy-target support — the button now reads the target input's
.value at click-time, so users can edit a field and still copy the
current value. Static data-copy-value keeps working unchanged.
- New component CSS web/css/components/app-input-copy.css positions the
button absolute/inset + margin-block:auto (robust vertical centering
regardless of input height) and uses a descendant selector (0,2,0)
so the button wins over the global [data-tooltip] position:relative.
- Also register app-input-copy.css + app-tenant-logo.css in core.css
@import list — they were only in the shared asset group before, which
default-template admin pages don't load, so tenant-logo styles were
effectively missing on admin tenant edit (topbar size etc.).
- Document the Copy-to-Clipboard + Copyable Input standards in
docs/reference-frontend-javascript.md so future consumers don't
re-invent the markup/selectors.
- File-upload preview: pending-file block restructured to a compact
list-item row (small square thumb + filename/size stack + clear X)
and removed the transparency checker pattern on the current-image
preview in favour of a flat --app-preview-bg surface.
- Tenant edit page title + breadcrumb now show the tenant description
("Acme GmbH") instead of the generic "Mandant bearbeiten" when one
is present.
- i18n: add "Copy to clipboard" / "In Zwischenablage kopieren" across
both locales.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Server renders initial file-upload state (src, alt, label text,
has-current class) so there is no flash of broken-image icon while
JS boots. JS no longer duplicates that work — it only reacts to
user input (select, clear, drag-and-drop) and uses data-current-src
solely to restore the server file after a blob preview.
- Clicking the current preview image now opens the file dialog, mirrors
the Replace button, with hover affordance (cursor + primary border).
- Drop the transparency checker pattern in the preview — the admin UI
does not need Figma-style transparency semantics. Preview is now a
flat theme-aware surface (--app-preview-bg) that keeps white or
black logos visible against a neutral gray.
- Move tenant favicon out of the aside into the Master-data tab as its
own details block next to the Tenant-logos section. Uses the same
barrier-form pattern (HTML5 form attribute + app-file-upload.phtml)
for a consistent instant-upload UX.
- Tenant-logo slot label is a native <label> element — picks up the
global form-label typography and spacing, no custom muted-color
class needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the single tenant avatar with a pair of theme-scoped brand logos.
Render only the theme-matching <img> server-side and swap src on theme
toggle via a JS hook — no reload, no double request, no CSS tricks.
Tenant logos
- TenantLogoService (ImageUploadTrait) with theme whitelist and per-theme
storage storage/tenants/{uuid}/logo/{light|dark}/, SIZES 128/256/512
- Public serving endpoint auth/tenant-logo-file so login can show the
logo pre-auth; matching authenticated admin preview endpoint
- appTenantLogoUrl(?size, ?theme) with 4-step fallback cascade; PDF +
mail always request 'light'
- Admin tenant edit: avatar block replaced by "Tenant logos" details
block inside the Master-data tab, two side-by-side slots via Pico
.grid with the core app-file-upload partial
- Policy rename ABILITY_ADMIN_TENANTS_AVATAR_VIEW -> LOGO_VIEW, action
routes logo / logo-delete / logo-file with theme body/query param
- API endpoint path kept (backward compat), internals on new service
- CLI tenant:logo-migrate-avatars moves legacy avatar/ -> logo/light/
idempotently (--dry-run, --yes, --cleanup)
- i18n "Tenant image" removed, 12 new keys synced across de/en
File upload component
- Full-width preview + filename/actions below (3D stack layout)
- Fixed 16:9 aspect ratio with 1rem inner padding for consistent
preview size across any logo aspect
- Transparency checker pattern as background so black logos stay
visible on dark mode and white logos on light mode
- form="" + deleteFormId support so the partial works with barrier
forms inside another form
Buttons
- width:100% dropped from button[type="submit"]; scoped back via
.login-main for the auth-flow primary CTA
- .outline base rule now tints background via color-mix of --app-color
so secondary/primary/danger outlines all gain a subtle surface
- .outline.secondary restyled Stripe-style in both themes: solid white
chip with soft shadow in light, solid elevated dark chip with white
text in dark; neutral border replaces role-colored border
- .app-action-success/.app-action-danger outlines get color-mix bg +
theme-aware outline-text tokens for stronger contrast
- Filled .primary/.app-action-success/.app-action-danger get raised
box-shadow (inset highlight + drop) — opt-in via class so chrome
buttons stay flat
- Dropped the legacy .secondary utility that was clobbering the
custom-property cascade with a hardcoded muted color
Theme swap
- Logo img carries data-src-light + data-src-dark; theme-toggle JS
swaps src when data-theme changes, keeping the topbar/login logo in
sync without a page reload
Quality gates: PHPUnit (2045), PHPStan L5, CS-Fixer, docs link/drift,
codex skills sync — all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Strip the heavy animated multi-layer radial gradient from the login
page. Removes:
- ani-gradient keyframes and 40 @property declarations
- Light and dark theme gradient rules
- Empty #gradient div from login template
- prefers-reduced-motion override that only served the gradient
The login page now uses the plain app background color consistently.
Refactors the address-book detail partial (templates/partials/app-user-profile.phtml)
toward a Stripe/Linear aesthetic: the banner + avatar still carry identity,
but the header now also carries meta chips (primary tenant, department,
hire date) and primary CTAs (Send email, Call). Email/phone/mobile move
out of a tab into an always-visible contact stack directly under the
identity block, with icon-led rows and a hover-revealed copy button.
Tabs reduce to Address / About / Organization, with Organization only
appearing for multi-tenant users — single-tenant assignments are fully
communicated through chips. The detail aside is gone; main content is
single-column and more focused.
Introduces web/js/components/app-copy-field.js — a generic, server-
markup-driven copy-to-clipboard button component wired from app-init.js.
The markup is rendered by the PHP partial (role-compatible, SSR-safe);
the component only attaches the click handler and drives icon-swap
feedback via an is-copied class.
Address-book index page now loads the 'address-book' CSS group alongside
'address-book-index' so the detail drawer (which mounts the same profile
partial inline) gets the correct styles.
Contact rows use inline-block + vertical-align centering inside the
clickable link; the field-label tooltip lives on a <span> wrapper rather
than the <i> itself (Bootstrap icons render their glyph via ::before, and
the tooltip rule would otherwise cap it). Hover reveals the copy button
on pointer devices; @media (hover: none) shows it permanently on touch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both endpoints were declared as bare relative paths ("admin/users/theme",
"admin/users/switch-tenant") and handed to postForm(), which normalizes
through `new URL(value, window.location.href)`. On a deep route like
/de/admin/permissions that resolves to /de/admin/admin/users/theme —
404 → 302 to error page → HTML response → JSON.parse throws → the theme
toggle reverts visually. The tenant switcher had the same latent bug.
- Topbar renders data-theme-url and data-switch-tenant-url via lurl(), so
both carry the locale-aware root-anchored path.
- Tenant switcher reads data-switch-tenant-url from the switcher root
(matching the theme-menu convention) and passes it through to
switchTenant(); fail-fast warning if the attribute is missing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first pass had admin/users sharing the address book profile partial,
which surfaced Contact/Address/Organization/About tabs that ignored the
admin-specific context. Separates the admin quick-view so it mirrors the
edit-page tab structure while staying read-only.
- `UserProfileViewService::buildAdminProfile()` extends the base profile
with `admin_extras` (active, email_verified_at, last_login_at,
last_login_provider, created, modified).
- `templates/partials/app-admin-user-profile.phtml` is the admin-only
render path: Master data · Organization · Security · About. Status
badge under the name; timestamps locale-formatted.
- `pages/admin/users/view-fragment($id).php` / `(none).phtml` switched
to the admin service method + partial.
- Drawer CSS aligns the aside with the main section's inline padding so
the content strip does not visually step in/out.
Address book is unchanged — still uses `buildProfile()` + the base
partial. Two i18n keys added (`Login provider`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracts the user profile read-view into a core service + shared partial so
modules no longer duplicate the assignment/scope logic, and migrates the
admin/users list as the drawer's second consumer.
Consolidation:
- `core/Service/User/UserProfileViewService` owns the single read-path that
resolves tenants/departments/roles for a user UUID, enforces scope-aware
department visibility, and returns a consistent `{status, user}` shape.
- `templates/partials/app-user-profile.phtml` is the shared render template;
takes `$profileKey` for storage namespace + lightbox grouping.
- `AddressBookService::buildViewContext()` shrinks to a one-line delegation
and drops its `UserAssignmentService` dependency. The old
`modules/addressbook/templates/address-book-profile.phtml` is removed.
admin/users migration:
- `pages/admin/users/view-fragment($id).php` + `(none).phtml` use the core
service and enforce `ABILITY_ADMIN_USERS_VIEW`.
- `admin-users-index.js` replaces the grid-native `linkColumn` on the first
name with a formatter that emits `data-drawer-trigger`; `linkColumn` is
disabled so the drawer click handler wins. `fullUrl` points at the edit
form — drawer → edit is one click.
- Drawer labels wired via page config.
The new `DetailDrawerFragmentContractTest` validated the users fragment
endpoint automatically — no manual test additions were needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a reusable CSV-export building block to the core so any Grid.js list
page can gain a filter/sort-aware download with two clicks of glue.
Core primitive:
- core/Service/Export/CsvExportService: flavor-aware writer (plain CSV
or Excel-compatible UTF-8+BOM+semicolon), with OWASP-aligned formula-
injection escape (=, @, +, -, TAB, CR) applied to both rows *and*
headers. Value objects for columns (ExportColumn) carry an extractor
closure and an allowSignedNumeric flag for phone-number-shaped cells.
- core/Support/helpers/export.php: thin HTTP layer (exportSendCsv,
exportCapLimit, exportResolveFlavor, exportRequireGetRequest) reusing
the requestInput() contract for GR-CORE-003 consistency. Filename is
sanitized and length-clamped; callers must still enforce auth.
- templates/partials/app-list-export-dropdown.phtml: zero-config
<details class="dropdown"> with CSV + Excel triggers.
- web/js/core/app-list-export.js: initListExport({ gridConfig, exportUrl })
mirrors the current grid filters + sort onto the export URL and
navigates, preserving session cookies for download.
First consumer — Helpdesk domains:
- DomainListService extracts the shared filter/enrich/sort logic from
domains-data so the grid endpoint and the new domains/export endpoint
cannot drift.
- domains/export endpoint delegates to DomainListService, declares
ExportColumns (with translated level labels), and exits via
exportSendCsv.
- domains-data now ~20 lines, delegating to DomainListService.
Tests:
- CsvExportServiceTest (10 cases): both flavors, BOM/no-BOM, formula
escapes incl. TAB/CR, header escape, signed-numeric allowlist,
quoting, multiline, empty columns, generator-compatible iterable.
- ExportHelpersTest (5 cases): exportCapLimit bounds.
- DomainListServiceTest (8 cases): filter, search, "all" sentinel,
sort, paging, enrichment, BC failure, tenant-scoped security filter.
Gates: PHPUnit green, PHPStan clean on touched files, module:sync ok.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduce data-icon-swap attribute with paired outline/fill <i> elements.
CSS toggles to filled icon on hover and active state. Module slots
auto-derive fill variant from icon name, with explicit icon_fill override
for icons without a fill counterpart (e.g. bi-headset). Add enforcement
script bin/icon-swap-check.sh.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move topbar outside the grid container as a standalone sticky header
(Shopify/Stripe pattern). Sidebar and icon bar use position:sticky
relative to topbar height. Mobile uses drawer pattern with backdrop,
focus trap, ESC close, and scroll lock. Tenant branding moved from
sidebar header into the topbar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add architecture test ViewLayerOutputEscapingContractTest that flags
raw <?php echo in .phtml files. Legitimate uses (pre-built ARIA attrs
from navActive(), hardcoded role values, pre-rendered HTML fragments)
must carry an inline // raw-html-ok: reason marker.
Annotated all 11 existing raw echo sites with justification markers.
Added guard to catalog, enforcement map (automated), CLAUDE.md security
section, and never-do-this list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace bare <input type="file"> across all upload locations with a
card-based dropzone component via shared partial. Three visual states:
current server file (thumbnail + Replace/Delete), empty dropzone, and
pending file preview (local FileReader thumbnail + metadata). Delete
actions use data-confirm-message for confirmation dialog.
Centralized as templates/partials/app-file-upload.phtml to prevent
markup drift — documented as deliberate exception to plain-HTML inputs
convention in CLAUDE.md and developer checklist.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move breadcrumb rendering from individual page templates into the core
topbar. Each page now sets $breadcrumbs in its action .php file; the
topbar renders it automatically via the shared partial.
- Remove global back/forward buttons and app-nav-history.js component
- Remove Alt+Arrow keyboard shortcuts for history navigation
- Render breadcrumb in topbar-left section (replaces button area)
- Clean up breadcrumb CSS: context-neutral base (flex, no margin)
- Recalculate sticky titlebar offset in details container
- Migrate all 41 pages (core + helpdesk, audit, addressbook, api-docs)
- Add missing breadcrumbs to addressbook detail view
- Update architecture contract tests (nav-history references removed)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restructure Support dashboard into 2 KPI groups (Tickets + Contracts) with
section dividers, merge escalations and recommendations into "Attention needed".
Redesign Sales dashboard with clickable contract timeline and dialog.
Add CSS shimmer skeleton loading for all dashboard tabs and aside.
Unify vertical rhythm via * + * sibling combinator on tab content containers.
Remove ~265 lines of dead CSS (contract cards, escalation styles) and unused JS
functions. Refactor hydrateTicketCategoryFilter into generic hydrateSelectFilter.
Fix role="button" CSS bleed from shell and remove extra future year from timeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduce generic explorer.nav_item slot type in app-main-aside.phtml
so modules can contribute links to the explorer nav panel.
Switch addressbook module from aside.tab_panel to explorer.nav_item.
Remove aside department-filter panel, AddressBookLayoutProvider, and
AddressBookSessionProvider (only served the now-removed aside panel).
Fix grid Name column overflow into Email column: CSS selector
.grid-name-cell > span:last-child never matched the <a> name link;
changed to :last-child and scoped flex-shrink:0 to :first-child only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove Documentation and System Info nav items from the admin sidebar
System group — they now live in the help-center panel. The admin
System group retains only Settings. Removes unused $canViewSystemInfo
variable from the sidebar template.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move the Swagger UI viewer from core (pages/admin/api-docs/) into
modules/api-docs/ as a self-contained module. Create module manifest
with routes, permission, sidebar.admin_nav_item slot, asset group,
and authorization policy. Remove API docs ability from core
OperationsAuthorizationPolicy and UiCapabilityMap. Remove hardcoded
sidebar nav item — now contributed via module slot. Add
admin-automation group meta to sidebar template for module nav items.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Module-contributed sidebar.admin_nav_item slots are now rendered as
complete <details><summary> nav groups appended to the admin panel,
matching the same pattern as core groups (Organization, Roles, etc.).
Fixes undefined $moduleNavItemSlots by reading layoutNav early.
Merges mail log into the module-contributed Audit & logs group when
both exist.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The sidebar template now reads sidebar.admin_nav_item UI slots from
modules and merges them into the matching admin nav group (by group
key). Module permissions are resolved via layoutAuth which already
includes module UI abilities from ModuleRegistry::getModuleUiAbilities().
This restores audit nav items (API audit, System audit, Import logs,
User lifecycle logs) in the Logs group when the audit module is active.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace <div>+<i> structure with <nav aria-label>+<ol>+<li>
- Move separators from DOM to CSS pseudo-elements (li+li::before)
- Add hover/focus-visible states and color differentiation
- Replace hard-coded px values with design tokens
- Explicitly reset shell nav/ol/li rules for self-contained styling
- Add i18n key for breadcrumb nav landmark label
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New page at /admin/system-info with three tabs:
- Overview: PHP version, SAPI, environment, 6 health checks (DB, schema,
storage, RBAC baseline, admin role, scheduler heartbeat)
- Modules: table of active modules with version, dependencies, permission count
- Permissions: active/inactive counts and per-source breakdown
Gated behind new system_info.view permission assigned to Admin role.
No mutations — purely diagnostic/observability.
Includes SystemHealthService, SystemInfoService, SystemHealthRepository
with interface, DI registration, i18n keys (de+en), idempotent DB update
script, and 17 new PHPUnit tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two bugs in the toast redesign:
1. Variant backgrounds (success/warning/info/error) used rgba with 12-20%
opacity, making toasts semi-transparent over page content. Fixed by
layering the tint over a solid background via linear-gradient compositing.
2. Flash messages with scoped paths including query strings (e.g.
edit pages with ?return=...) were not matched by Flash::peek(Request::path())
since path() strips the query. Added fallback to pathWithQuery() matching.
Also moved flash partial before JS init scripts to ensure DOM is present
when the component runtime mounts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a module kernel (ModuleManifest, ModuleRegistry) that allows modules to
contribute routes, UI slots, search providers, layout context, session
lifecycle, and permissions. Modules are activated via config/modules.php or
APP_ENABLED_MODULES env variable. Conflicts (duplicate routes, permissions,
slot keys) cause a fail-fast with a clear error message.
Extract the address book from hardcoded Core integration points into the
first module (modules/addressbook/). The module provides:
- Aside icon-bar tab + People panel via UI slot system
- Global search resource via AddressBookSearchProvider
- Layout context data via AddressBookLayoutProvider
- Session lifecycle via AddressBookSessionProvider
Core cleanup removes address-book hardcodings from SearchSqlResourceProvider,
SearchUiMetaProvider, SearchItemMapperProvider, appBuildLayoutNavContext(),
and the aside templates. Permissions (ADDRESS_BOOK_VIEW) and business logic
(AddressBookService) remain in Core as they gate general user visibility.
Includes 38 new tests (894 total), PHPStan level 5 clean, and architecture
tests verifying zero hardcoded address-book references in search/templates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The backend returns system-audit results but the sidebar had no
matching <li data-search-key="system-audit"> element, causing a
console warning on every global search query.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DRY duplicated helpers (escapeAttr, belongsToForm, isSubmitControl,
isEditableTarget) into web/js/core/app-form-utils.js and update 7
consumers. Add aria-live="polite" to flash messages, async messages,
and search results for screen reader announcements. Add
prefers-reduced-motion support to CSS tooltips.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Session timeout redirected to /de/login?return_to=... but the remember-me
cookie was not cleared by logoutDueToSessionTimeout(). On the next request,
autoLoginFromCookie() silently re-logged the user in, causing the MintyPHP
router to resolve the login route to pages/index().php (dashboard) instead
of the login action — the intended URL redirect logic never executed.
Fix: intercept ?return_to= immediately after successful auto-login in
index.php and redirect to the sanitized route path. Also harden the login
flow with hidden return_to form fields and POST body fallback for cases
without remember-me.
Additional improvements in this commit:
- Convert stored REQUEST_URI to router-compatible path (strip leading
slash and locale prefix) via IntendedUrlService::toRoutePath()
- Consolidate duplicate CSRF data attributes into shared data-csrf-key
and data-csrf-token on <html> element
- Add tenant branding (logo) to auth pages via appAuthLogoUrl() helper
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>