Remove the extensible theme catalog (config/themes.php + ThemeConfigGateway)
in favor of a two-entry const on SettingsAppGateway. appThemes() now returns
the list directly — no container lookup, no file include. Drop the
dark-green theme assets, narrow [data-theme^="dark"] selectors to
[data-theme="dark"], and tighten isDarkTheme() to an exact match. Ship an
idempotent migration that corrects any leftover 'dark-green' rows on users
and tenants to safe defaults (light / NULL).
Tenant scoping (default_theme, allow_user_theme) and per-user override stay
intact; only the catalog extensibility and the third theme are removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds tokenSelectForm() in core/Support/helpers/ui.php: an inline
typeahead combobox + flat alphabetical removable list, designed for
selections that outgrow the chip-header of the vendor MultiSelect
(roles, permissions, and similar admin pickers).
Contract:
- Hidden <select multiple name="<name>[]"> is the form submission source —
consumers read via $request->body() verbatim, no parsing
- Items are ['id' => int, <labelKey> => string, 'key' => string?]
where labelKeys default to ['description', 'label', 'name']; 'key' is
an invisible fuzzy-match hint
- $labelOverrides lets callers swap emptyState / removeTooltip /
noMatches / clearAll / countSuffix / errorMessage with domain copy
- $disabled renders pure-presentation list (no combobox, no remove)
Runtime:
- initTokenSelect in web/js/components/app-token-select.js is registered
as 'token-select' in app-init.js; destroy()/cleanupFns contract
- Syncs the hidden <select> on every mutation and dispatches 'change'
so dependent UI can react
Wired up: pages/admin/permissions/_form.phtml (assigned roles),
pages/admin/roles/_form.phtml (permissions + assignable roles).
Helper contract lives in tests/Support/Helpers/TokenSelectFormHelperTest.php;
runtime contract + entrypoint registration + host usage are enforced by
FrontendRuntime*ContractTest.php.
Coexists with multiSelectForm() — pick tokenSelectForm() when the
selected set can grow beyond ~10 items or typeahead is expected.
Docs in CLAUDE.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Simplifies the tenant Visibility tab to match the tenant-only appearance
model established by the previous commits:
- primary_color: now required. Removed the "No brand color / Use system
default (no brand accent)" toggle — every tenant carries a concrete
primary color. Legacy NULL rows render the neutral app default (#2fa4a4)
in the color picker and are converted to that explicit hex on save.
- default_theme: the select no longer offers a "Use system default" empty
option. Legacy NULL rows resolve to 'light' at render time; saves always
persist a valid theme via SettingsAppGateway::normalizeTheme().
- allow_user_theme: replaced the tri-state "Use system default (allowed) /
Force allow / Force disallow" select with a single boolean switch
("Users may choose their own theme"). Legacy NULL rows load as checked.
Saves persist 0/1 explicitly.
TenantService: sanitize no longer reads primary_color_use_default or
allow_user_theme_mode; it validates primary_color as a required hex and
treats allow_user_theme as a plain boolean. Both create and update paths
write concrete values only — no more NULL writes for these three fields.
DirectorySettingsGateway gains a normalizeTheme() delegate so TenantService
can route through the same gateway it uses for isAllowedTheme().
Removed now-unused app-color-default-toggle JS component + its runtime
registration + its architecture-test entry. i18n cleanup: "No brand
color", "Use system default (no brand accent)", "When enabled the tenant
renders without a brand accent color.", "Use system default (light)",
"Use system default (allowed)", "Force allow user theme", "Force disallow
user theme", "User theme policy is invalid" all removed. New copy: "Users
may choose their own theme" + helper text, plus a tightened tab blockquote.
Tests: TenantServiceTest validInput() updated to send concrete values;
settingsGateway mock gets normalizeTheme() + isAllowedTheme() defaults.
All 1985 tests pass; PHPStan level 5 clean; QG-006 clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates the tenant edit Visibility tab so its labels and helper text
reflect the new tenant-only appearance model. After the global appearance
settings were removed, legends like "Use default color" / "Inherit global
setting" and help texts like "applies the global system appearance" /
"uses its own default theme instead of the global setting" were wrong —
there is no global appearance to inherit from anymore. An empty tenant
field means "use the system default" (hardcoded light theme, no brand
accent, user-theme allowed).
- Info blockquote at the top rewritten.
- "Use default color" → "No brand color" / "Use system default (no brand
accent)" with updated helper text.
- Default theme placeholder "Inherit global setting" → "Use system default
(light)" + new helper text.
- User-theme policy "Inherit global setting" → "Use system default
(allowed)".
i18n: removed the now-orphaned keys ("Use default color", "Using the
default color applies the global system appearance.", "Be careful - this
resets the tenants color", "Inherit global setting", "If set, this tenant
uses its own default theme instead of the global setting."). Added the
new keys in de + en.
No behavior change — only copy. PHPStan, PHPUnit green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>