Reduce admin/system-info to a single focused status view: overall
status banner, components list from existing health checks, and an
environment meta table. Drops the Modules and Permissions tabs —
those were dev-diagnostics already available via CLI.
- Remove SystemInfoService + its test; wire the action directly to
SystemHealthService and build meta inline
- Extend SystemHealthService with per-check label + description so
the UI speaks in customer terms while the CLI doctor path stays
compatible
- Rebuild the view on existing app-stats-table + app-empty-state
primitives; add a small app-status-banner component for the
headline signal
- Align help-center nav label and i18n keys (de/en)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
Cluster 1 der Testabdeckungs-Initiative (.agents/runs/TEST-SEC-COVERAGE-001).
+38 Tests / +233 Assertions, nur tests/** veraendert.
- SettingsAuthorizationPolicyTest: Allow-Pfade fuer UPDATE/BRANDING_UPDATE/
TOKENS_REVOKE/USER_LIFECYCLE_RUN plus Unknown-Ability — schliesst die
Halb-Test-Luecke, die zuvor nur Deny-Pfade fuer 4 von 5 Abilities pruefte.
- AssignableRoleServiceTest: neu (GR-TEST-001, vorher nur transitiv via
UserAssignmentServiceTest gedeckt).
- AuthCryptoGatewayTest: neu (Roundtrip + Fehlerpfad, GR-SEC-005).
- AuthSettingsGatewayTest: neu (Delegation zu 5 Settings-Sub-Gateways).
- UserTenantContextServiceTest: neu, 18 Tests, Tenant-Scope-Logik
(GR-SEC-009) — current/primary/fallback, inactive-filter, assign-checks.
LdapConnectionGateway und OidcHttpGateway bleiben bewusst ohne Direkttests:
beide sind architektonische Test-Seams (dokumentiert im Docblock bzw. reiner
statischer Curl-Delegator). Ihre Logik ist ueber Konsumenten-Tests
(LdapAuthServiceTest, MicrosoftOidcServiceTest) bereits gedeckt.
Gates: QG-001 (1945 Tests) / QG-002 (PHPStan L5) / QG-003 (Architecture) /
QG-006 (php-cs-fixer) pass.
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>
Encodes two bugs that recently cost debugging time as architecture tests so
they cannot recur.
PageTemplateNamingTest
MintyPHP's Router splits page filenames on parens. `view(default).phtml` →
view="view", template="default". A nested group like `view($id)(none).phtml`
produces an empty view name, resolves to the wrong file, and the response
silently becomes an empty body. URL parameters belong only in the .php
action filename. This test flags any .phtml with more than one paren group.
FilterSchemaConsistencyTest
Every query key declared in a list's filter-schema.php must also appear in
the toolbar section (standard pagination/sort keys exempted). If a filter
param is not in the toolbar, Grid.js's URL-sync drops it on every refetch —
the filter works on initial page load and breaks after any user interaction.
This test caught one dormant violation (system-audit's target_type), which
is now declared as a hidden toolbar field for future use.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces a reusable core detail-drawer primitive that slides in from the
right and loads any view via a `*-fragment(none).phtml` endpoint. Bundles the
address book list overhaul that is its first consumer.
Core additions:
- `app-detail-drawer.js` — generic drawer with stepper, focus trap, body
scroll-lock, URL-hash deep-linking, session-expiry detection
- `app-fragment-init.js` — auto-wires tabs/lookups/confirm/file-upload/
fslightbox inside injected HTML; consumers do not re-initialize components
- `app-focus-trap.js` — shared focus-trap + refcounted scroll-lock, used by
both filter-drawer and detail-drawer
- `getHtml()` in `app-http.js` + `SessionExpiredError`; drawer reloads the
page on auth redirect instead of rendering the login form in the panel
- `DetailDrawerFragmentContractTest` enforces that every `initDetailDrawer`
consumer ships matching `*-fragment($id).php` + `*-fragment(none).phtml`
Address book list:
- Grid collapses from 9 columns to 4 (identity / context / phone / actions)
with a two-line identity cell (avatar + name + email)
- Tenant register tabs above the grid using the `app-list-tabs` partial;
tenant filter wired via hidden toolbar field so grid.js forwards it on
every data fetch
- Profile body extracted to a shared partial so the full-page view and the
new drawer fragment share the same markup
- New i18n keys for the drawer/list labels
Also refactors `app-filter-drawer` to reuse the shared focus-trap and
scroll-lock instead of maintaining its own copy, and documents the
detail-drawer convention in CLAUDE.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Make the core-primitive-end-to-end rule for Grid.js list exports binding
instead of advisory — so future consumers (and reviewers) cannot quietly
drift back into inline fputcsv / hand-rolled headers / bespoke click
listeners / lurl() for query-carrying URLs.
- CLAUDE.md: new UI Patterns bullet + two "Never Do This" entries
spelling out the server/view/client contract in one place.
- .agents/checks/guard-catalog.json: new GR-UI-EXPORT entry describing
the end-to-end requirement (exportRequireGetRequest + exportCapLimit +
exportSendCsv + ExportColumn + shared filter-schema + dropdown partial
+ endpointUrl() + initListExport).
- .agents/checks/guard-enforcement-map.json: wire the new guard to
tests/Architecture/ListExportContractTest.php as the automated
evidence source.
- .agents/checks/guard-checklist.md & .agents/prompts/reviewer-code.md:
add GR-UI-EXPORT so the Code Reviewer prompt and the checklist flag
violations alongside GR-UI-LIST.
- tests/Architecture/ListExportContractFiles: registry of every list
export (currently helpdesk-domains, admin/users, audit/system-audit).
Adding a new list = one entry, contract test covers the rest.
- tests/Architecture/ListExportContractTest: six checks per registered
entry — endpoint uses the primitive, no hand-rolled output, data and
export share the same filter-schema, template includes the dropdown
partial and uses endpointUrl(), page module imports and invokes
initListExport, and the shared dropdown partial stays zero-config.
- pages/admin/users/export().php: align with the new contract by
switching from ad-hoc requestInput()->queryAll() reads to
gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'), same
as the data endpoint — eliminates the last place Grid and export
could disagree on what "the current filter" means.
Gates: PHPUnit 1900 OK (+6 contract checks), PHPStan 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduce a helper that returns the registered route TARGET for a given
source path, so URLs built for endpoints that carry query parameters do
not rely on MintyPHP's applyRoutes() rewrite layer — that layer compares
the full request URI (including `?query`) against source paths and
silently misses modules where path ≠ target.
- core/Support/helpers/app.php: endpointUrl($path) consults
ModuleRegistry::getRoutes(), returns lurl(target) when the path is a
registered module source path, otherwise falls through to lurl($path).
Safe fallback when the container or registry is unavailable.
- modules/audit/pages/audit/system-audit/index(default).phtml: replace
the ad-hoc target-path workaround with endpointUrl('admin/system-audit/
export'). Callers can now write the natural source path without
knowing about the rewrite trap.
- Apply the same helper to modules/helpdesk/.../domains/index and
pages/admin/users/index for consistency — a no-op where path already
equals target, but establishes the convention: every export/data
endpoint URL in page configs goes through endpointUrl().
- tests/Support/Helpers/EndpointUrlTest: 5 cases covering source→target
resolution, idempotence when path == target, fall-through for
unregistered paths, leading-slash normalization, and graceful
degradation when the registry is missing. Uses
AppContainerIsolationTrait per the contract test in
tests/Architecture/AppContainerIsolationContractTest.
Gates: PHPUnit 1894 OK, PHPStan 0 errors, module:sync ok.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the row formatting that was duplicated between system-audit's
data() and export() endpoints into a single presenter. Both endpoints
now call presentAll() on the same object, so enum label translation,
actor resolution, and timestamp formatting cannot drift between the
Grid.js UI and the CSV download.
- SystemAuditRowPresenter::present()/presentAll(): canonical row shape
with outcome + outcome_label + outcome_badge, channel + channel_label,
actor_user_label with display-name-then-email fallback, and safe
defaults for every missing field.
- data().php shrinks from ~45 to ~15 lines; export().php drops its
inline outcome/channel/actor resolvers and reads the already-
resolved fields from the presenter output.
- AuditContainerRegistrar registers the presenter.
- tests/Module/Audit/Service/SystemAuditRowPresenterTest: 9 cases
covering enum normalization, unknown-value fallback, actor label
precedence (display name → email → "-"), whitespace trimming, safe
defaults for missing keys, and iterable input.
- StatusTaxonomyContractFiles: the taxonomy data contract now points
at the presenter (the single source of truth for badge/label
resolution) instead of the thin data endpoint, and the presenter is
added to the literal-guard file list.
Gates: PHPUnit 1889 OK, PHPStan 0 errors, module:sync ok.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two pre-existing findings surfaced during the export-feature test run.
Both are trivial housekeeping, not related to the feature:
- phpstan-baseline.neon: drop the stale ignore for
RequestInput::wantsJson — the method is now called from the
helpdesk security-level endpoint and the new export flavor helper,
so PHPStan no longer matched the pattern and failed with
`ignore.unmatched` (non-ignorable).
- tests/Service/Auth/SsoUserLinkServiceTest.php: replace the deprecated
isType('array') / isType('string') assertions with the
PHPUnit 13-compatible isArray() / isString() equivalents.
Gates after cleanup: PHPUnit 1880 OK (0 deprecations), PHPStan 0 errors.
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>
CliAppBootstrap and ModuleCliRuntime still required from the old
lib/ path, which would break all CLI commands. Also fixes stale
lib/ references in docs, test assertion messages, and a PHPDoc
comment.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cover Settings gateways (Session, LoginPersistence, SystemAudit, Smtp,
FrontendTelemetry, Metadata, App), Auth gateways (ExternalIdentity,
Tenant), DirectorySettings, UserSettings, and HotkeyService.
Gateway test coverage rises from 25% to 52%, overall service/gateway
coverage from 58.6% to 70.7%.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move HelpdeskTenantSettingsRepository and HelpdeskTokenRepository from
Service/ to Repository/ directory (GR-SEC-003: SQL only in repositories)
- Make AccessControl constructor require IntendedUrlService explicitly
instead of falling back to `new IntendedUrlService()` (GR-TEST-002:
no service instantiation outside factories)
- Update all imports and tests accordingly
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Core code (lib/) no longer imports any MintyPHP\Module\Audit\* classes directly.
New ApiAuditServiceInterface and ApiSystemAuditReporterInterface follow the
existing pattern (interface + null implementation in core, concrete binding
from module registrar). DoctorRunner and helpers/app.php now resolve through
DI interfaces, ensuring the audit module remains fully optional.
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>
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>
Users who exist in Microsoft Entra ID or LDAP but have no local account
were blocked at the email-first login step because the system required a
local user record before showing SSO options. This adds a domain-based
fallback: when the email is unknown locally, the system checks tenant SSO
configurations for matching allowed_domains and presents SSO-only login
options, enabling the existing JIT provisioning to trigger on callback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restructure helpdesk tenant settings into per-connection config with
improved token handling. Update risk radar data endpoint and UI.
Clean up ImageUploadTrait and update architecture contract tests.
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>
- 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>
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>
Add support/sales/controlling dashboards with KPIs, trend charts and
risk indicators. Add debitor communication timeline, contact filters,
system recommendations engine, and configurable controlling risk rules.
Rename search→index to fix query-string preservation on back navigation.
Remove fake escalation rate metric, add KPI info tooltips, and switch
trend chart colors to red (created) / green (closed) for clarity.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- add helpdesk module pages, services, settings and tests
- standardize debtor list on drawer/grid contracts and robust filter drawer behavior
- add helpdesk aside panel navigation and settings visibility provider
- switch primary list slug to helpdesk/debitor and remove helpdesk/search compatibility
- include required core contract updates for list contracts and detail/drawer integration
- remove legacy bin/module-*.php and bin/doctor.php entry scripts
- move module/doctor execution into class-based runners under lib/Console
- add stable JSON output for doctor and module:sync
- introduce bin/dev for init/up/down/logs/console/qa workflow
- update DI wiring, phpstan scan config, tests, and docs to new CLI contract
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 AuditRepositoryFactory and AuditServicesFactory — two-layer
factory chain replaced by direct DI container wiring. Delete 4
single-consumer repository interfaces. Register all 4 repositories
and SystemAuditRedactionService directly in container. Update all
service constructors to type-hint concrete classes. Fix architecture
test and documentation references to deleted factories.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix 5 broken FQCN references (runtime errors) where core pages referenced
non-existent MintyPHP\Service\Audit\AuditMetadataEnricher and SystemAuditService.
Add AuditMetadataEnricherInterface with NullAuditMetadataEnricher fallback so
deactivating the audit module no longer crashes core edit pages.
Move audit search resources from core Search*Provider files into the module
via a new AuditSearchResourceProvider implementing the existing
SearchResourceProvider contract. Add module-specific purge permissions
(audit.api.purge, audit.imports.purge) replacing core ABILITY_ADMIN_SETTINGS_UPDATE.
Also fixes: session key prefix convention, hardcoded English string, $_SERVER
superglobal fallback, and manifest schema drift (i18n_path, group in ui_slots).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Module pages must use a unique top-level directory (audit/) instead of
nesting under admin/ which collides with core's pages/admin/ during
module:build symlink creation. Routes still map admin/* URLs to the
audit/ page directory via manifest route targets.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>