Removes the global app_theme, app_theme_user and app_primary_color settings
from the admin/settings area and the underlying service/cache/API/i18n/docs
layers. The tenant columns (tenants.primary_color, default_theme,
allow_user_theme) become the single source of truth for appearance.
Branding helpers are tenant-only and fall back to a hardcoded 'light' theme
with no brand color when no tenant context is available (login, error,
pre-session pages). The APP_THEME env var is removed.
Scope:
- UI: Appearance tab gone from /admin/settings; tenant edit form drops the
global-fallback color preview.
- Service: SettingsAppGateway no longer exposes setting-backed accessors
for theme/user-theme/primary-color. Theme catalog utilities (listThemes,
isAllowedTheme, normalizeTheme, resolveDefaultTheme) stay and are used
across Tenant / Auth / User flows; resolveDefaultTheme now hardcodes
'light' with a catalog fallback (no global/env lookup).
- AdminSettingsService: buildPageData, sanitization, audit snapshot, apply
step and cache payload are all stripped of the three keys. Dead helper
applyAppPrimaryColor + normalizePrimaryColor removed.
- Cache: SettingCacheService HOT_PATH_KEYS drops the three keys.
- API: /settings (GET/PUT) and /settings/public (GET) no longer expose
appearance fields; OpenAPI schemas pruned accordingly.
- Audit redaction list shortened.
- DB: db/init/init.sql seed rows removed. No migration for existing
installs — legacy rows stay inert.
- i18n + docs: setting.app_theme, setting.app_theme_user,
setting.app_primary_color removed from de+en locales; reference and
explanation docs updated.
- Tests: covering tests for the removed methods pruned; ThemeResolutionTest
rewritten for tenant-only semantics. One unrelated PHP-CS-Fixer issue in
UserRegistrar.php cleaned up to keep QG-006 green.
Workflow: .agents/runs/SETTINGS-APPEARANCE-TENANT-ONLY/ (gitignored).
Gates: QG-001..003, QG-006, QG-008 all pass (1985 tests, 28764 assertions).
Reviews: code, security, acceptance all pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Loose ends surfaced by comparing the three drawer consumers side by side:
- Core i18n was missing `Open full page`, `Loading`, `Failed to load` —
admin/users showed English strings in the German UI because those labels
resolve from core, not module catalogs.
- Helpdesk tickets page config did not pass drawer labels; the JS fell back
to its hard-coded English defaults. Labels now wired via `t(...)`.
- CLAUDE.md notes the `fetchUrl` single-expression convention (required so
the architecture test can statically locate the fragment path) and
documents the `onContentLoaded` hook for module-specific runtime
(helpdesk's async communication feed uses it).
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>
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>
Audited all 1441 keys against 12 translation pipelines (direct t() calls,
flash messages, enum labelToken(), filter schemas, module manifests, theme
configs, etc.). Removed 180 keys unused by any pipeline from both language
files (1441 → 1261 keys, parity maintained).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add missing i18n key "LDAP attribute map could not be encoded" (de+en)
- Replace raw font-weight/line-height with design tokens in app-topbar.css
- Fix import ordering in SchedulerRunService.php (php-cs-fixer)
- Fix PHPStan mock chain errors in DebitorDetailServiceTest and TenantSsoServiceTest (PHPUnit 12 API)
- Simplify redundant comparison in NotificationService dedupe window
- Add PHPStan type hint for mock callback array in SchedulerJobFailedNotificationListenerTest
- Fix undefined variable and add flow-analysis suppression in login().php
- Increase dev PHP memory_limit to 1G for PHPStan (prod stays 256M)
All 6 quality gates now pass: QG-001 (1618 tests), QG-002 (0 errors),
QG-003 (6630 assertions), QG-006 (0 fixable), QG-008, QG-009.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fetches displayName/jobTitle from Graph API and syncs them via SSO profile sync. Also fixes LDAP enabled state in tenant form, adds LDAP save on tenant create, and hardens LDAP port validation precedence.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds deleteOrphanedJobs() to clean up stale job_keys during ensureSystemJobs, and prevents editing jobs that are no longer registered in the runtime registry.
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>
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>
- Change query_json column from TEXT to JSON, ip/user_agent to CHAR(64)
for PII redaction compliance
- Update repository, service, and views for hardened schema
- Add architecture contract test for security logging redaction
- Add search resource provider test coverage
- Include DB migration script for existing installs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use short i18n keys for audit nav labels (nav.audit.*) with translations
in both language files. Restyle admin sidebar group icons with colored
pill backgrounds per group. Adjust details summary line-height and
active-state border color to use group icon color.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the single "SSO" tab in tenant edit with two dedicated tabs:
"Microsoft SSO" and "LDAP". Each provider now has its own tab panel
with focused status tiles and config cards, reducing scroll and
cognitive load for admins configuring a single provider.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add LDAP as a third authentication option alongside local password and
Microsoft Entra ID SSO. Per-tenant configuration supports Active Directory,
OpenLDAP, and FreeIPA with configurable attribute mapping, search-then-bind
and direct-bind methods, encrypted bind credentials (AES-256-GCM),
profile sync on login, and user auto-provisioning via external identity
linking.
Includes admin UI for connection settings with test-connection button,
login page LDAP form, 25 new PHPUnit tests, and de/en translations.
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>
Titlebar, breadcrumbs, and page title now reflect the user's actual
permission level across all 6 edit pages (roles, users, departments,
tenants, permissions, scheduled jobs). Added i18n keys for de and en.
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>
Each module can now ship its own i18n/ directory with translation files
(default_de.json, default_en.json). At boot time, ModuleI18nLoader
preloads core translations then merges module translations on top via
Reflection into I18n::$strings — the t() helper stays unchanged.
- Add i18nPath property to ModuleManifest
- Add ModuleI18nLoader with Reflection-based preload
- Extract 59 module-exclusive keys from core i18n into module files
(notifications: 8, bookmarks: 41, addressbook: 10)
- Expand TranslationKeysTest to scan modules/ and validate against
merged core + module translations
- Add ModuleI18nContractTest for per-module de/en parity and completeness
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add Flash::dismissByKey() to clear flash messages by key+scope
- Dismiss 'session_timeout' flash after successful remember-me auto-login
so the message doesn't persist through manual logout
- Suppress PHPStan false positives for dynamically defined MINTY_ALLOW_OUTPUT
- Remove unnecessary ?? [] fallbacks on preg_match_all results in tests
- Add missing i18n key 'No active roles are configured yet.'
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a session expires, the user's current URL is now captured and restored
after re-authentication (via remember-me cookie or manual login), instead of
always redirecting to the dashboard. A JavaScript session warning dialog
appears ~2 minutes before idle timeout, allowing users to extend their session
with a single click. Includes open-redirect prevention via URL validation,
Microsoft SSO callback support, and 33 unit tests.
Refs: SESSION-REDIRECT-PRESERVE-001
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Session timeout: configurable idle (default 30min) and absolute (default 8h)
timeouts via DB settings, enforced in web/index.php on every request.
Timestamps set at login in action layer; graceful fallback for pre-existing
sessions.
Transaction wrapping: UserAccountService.createFromAdmin/register, roles
create/edit, and permissions edit now wrap multi-step DB operations in
transactions to guarantee atomicity.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks
- Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists
- Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services
- Add Microsoft OIDC SSO, API token management, and user lifecycle features
- Add swagger-ui vendor integration and OpenAPI spec
- Add production Docker setup and bin/ scripts
- Update composer dependencies, config, templates, and frontend assets throughout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>