Commit Graph

32 Commits

Author SHA1 Message Date
fs
ca13fcd4a3 chore(i18n): drop user-lifecycle translation keys orphaned by cockpit cleanup
Seven translation keys were orphaned by the recent user-lifecycle
cockpit work (commits 144d841, cc2cf3a, 157eb18) and now have zero
callers in pages/, modules/, or core/:

* "Run lifecycle now" — was the <details>-summary title
* "Run user lifecycle now" — was the <details>-button label
* "This runs the lifecycle policy immediately and may deactivate or
  delete users." — was the <details>-blockquote warning
* "User lifecycle logs" — was the embedded panel's title
* "Purge user lifecycle logs" — was the panel's purge-button label
* "Purge entries older than 365 days?" — was the panel's purge confirm
* "Lifecycle audit entry not found" — was the deleted view($id).php
  flash key

The aside-actions partial uses different, still-active keys
("Run policy now", "Purge logs", "Run user lifecycle now?") so no
visible string lost. Removed from all four i18n files
(core + audit-module, de + en) so translation parity stays balanced
and the architecture-test "i18n parity" check stays green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 08:44:17 +02:00
fs
06118c1b26 fix(user-lifecycle): track last-run state in core, not in audit log
The "Last run" KPI tile stayed empty after a manual policy run, even
though the run completed successfully. Two distinct bugs were
involved:

1. The dashboard read latestSystemRun() from the audit log filtered by
   trigger_type='system'. UserLifecycleService::run() never sets that
   value — it uses 'manual' for actor-triggered runs and 'cron' for
   scheduled ones. The query never matched anything.

2. Even with the right trigger_type, the audit log only writes per-user
   entries (logDeactivate / logDelete / logDeleteFailure). A run that
   processes zero users — including most cron ticks on a healthy
   tenant — leaves no trace, so the tile would still show "—" after a
   correct execution.

Both bugs share one root cause: run-trigger state was being inferred
from audit-log details, but those are two semantically different
things. Audit log answers "what did the run do?". A "last run" tile
answers "did the run happen?".

This commit moves run-trigger state to the core settings table and
keeps the audit log strictly for per-user events:

* Two new keys in core/Service/Settings/SettingKeys —
  USER_LIFECYCLE_LAST_RUN_AT_KEY and USER_LIFECYCLE_LAST_RUN_STATUS_KEY.
* SettingsUserLifecycleGateway gains recordLastRun() and getLastRun().
  UserSettingsGateway exposes them as recordLifecycleLastRun() /
  getLifecycleLastRun() so UserLifecycleService can call through its
  existing dependency without growing its constructor.
* UserLifecycleService::run() writes both keys in finally — every time
  the lock was acquired, regardless of whether any user was processed
  and regardless of whether the run finished cleanly. Status reflects
  $result['ok'] ('success' / 'failed').
* UserLifecyclePolicyDashboardService gains a lastRun() reader. Action
  page now sources the KPI tile from this core service instead of the
  audit interface — so the tile works even when the audit module is
  disabled.
* The audit-side lastRun() / latestSystemRun() / their tests are
  removed (YAGNI). Phase 4 (activity feed) can rebuild from the audit
  filter grid without a special method.

Behaviorally: a no-op run now records "Last run: just now · ✓ Success"
in the cockpit, exactly as expected.

All six quality gates green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:07:57 +02:00
fs
157eb18610 refactor(user-lifecycle): drop redundant titles and duplicate run-now button
Cleans up two leftover redundancies after the Phase-1 cockpit
foundation landed in cc2cf3a:

* The audit-list panel embedded inside the settings page rendered
  its own "User lifecycle logs" titlebar — a redundant heading
  below the page-level titlebar that already says exactly that.
  The embedded panel now renders the filter toolbar + grid
  directly. Page-level title carries the context.
* The Run-Now action existed twice — inline in a <details>-card
  inside the form and again in the aside Quick-Actions list. The
  inline version is gone; aside is the single discoverable home
  for policy-level danger actions, consistent with how Phase 1
  introduced Purge logs there too.
* The orphan $lastRunSummary string in the action and view stays
  removed accordingly. KPI tile "Last run" still carries the
  relative-time + status hint, so no information is lost — just
  surfaced once instead of twice.

Three architecture-test lists updated to match the panel's new
shape, each with an inline comment so future readers see why the
panel is intentionally absent:
* DetailActionPolicyContractFiles.migratedConfirmFiles drops the
  user-lifecycle settings view (its danger action delegates to the
  aside-actions partial, already in this list).
* ListUiSharedPartialsContractTest.purgeTitlebarTemplateFiles drops
  the panel (no titlebar of its own anymore).
* ListTitlebarContractFiles.titlebarTemplateFiles drops the panel
  for the same reason.

All six quality gates green; behaviour-identical to a user with the
required permission (purge + run-now both still available, just
sourced from the aside).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:38:15 +02:00
fs
cc2cf3a254 feat(user-lifecycle): cockpit foundation — KPI row + aside quick actions
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>
2026-04-26 20:36:28 +02:00
fs
004c25ac4b refactor(user-lifecycle-audit): drop unused filterOptions / listFilterOptions
Follow-up to commits 144d841 (Embed user lifecycle audit into settings
page) + 8c07c2c (Revert "fix(user-lifecycle-panel): restore
filter-options population").

The audit page move turned the user-lifecycle list-page into a slot
template included by pages/admin/settings/user-lifecycle. Slot
templates run during the view phase, where MintyPHP forbids DB calls
("Database can only be used in MintyPHP action", DB.php). The
populated actor / action / status / trigger filter dropdowns —
previously fed by UserLifecycleAuditService::filterOptions() during
the action phase of the now-deleted index().php — therefore had to
go away and were not replaced. The service method and its
underlying UserLifecycleAuditRepository::listFilterOptions() became
dead code in the same commit but were kept; this cleans them up.

Removed:
* UserLifecycleAuditService::filterOptions(int) — single caller was
  the deleted index() action.
* UserLifecycleAuditRepository::listFilterOptions(int) — single
  caller was the service method above.
* Stale phpstan-baseline.neon entry for the service method.
* Now-unused RepositoryArrayHelper import in the repository.

Sister services (Api / System / Import) still use their own
filterOptions() — their list-pages remain action-driven and can
populate dropdowns from the DB. The constant FILTER_OPTIONS_LIMIT_MAX
in UserLifecycleAuditRepository is kept; listPaged() reuses it as a
generic upper bound for multi-value filter inputs.

The reduced-fidelity actor filter in the settings panel (URL-pinned
IDs only, no DB-side enumeration) is the architecturally correct
trade-off for the slot-based isolation. A future provider mechanism
on the slot manifest could restore richer filter population without
breaking module isolation, but that is out of scope here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:28:12 +02:00
fs
8c07c2c93a Revert "fix(user-lifecycle-panel): restore filter-options population"
This reverts commit a1c6d62d44.
2026-04-26 18:25:34 +02:00
fs
a1c6d62d44 fix(user-lifecycle-panel): restore filter-options population
Follow-up to commit 144d841 (Embed user lifecycle audit into settings
page). The original move from a standalone audit page to a settings
panel silently dropped UserLifecycleAuditService::filterOptions()
from the panel template. The service method was kept (still defined
in UserLifecycleAuditService.php) but had zero callers — dead code,
and three concrete UX/data regressions:

* Actor filter dropdown is empty: previously populated with
  display_name + email + (deleted)-marker for every actor that
  appeared in lifecycle events; after the move only actor IDs
  already pinned in the URL via ?actor_user_ids= are listed, with
  bare "User #N" labels instead of human-readable names.
* DB-only enum values are no longer surfaced: action/status/trigger
  filter dropdowns are populated only from the PHP enum cases.
  Migration drift values present in the DB but absent from the enum
  silently disappear from the filter UI.
* Active-actor-fallback lost the (deleted) suffix. An actor ID in
  the URL with no matching DB row used to be labeled "User #N
  (deleted)" — now just "User #N", losing the lifecycle hint.

Restores the three filter-options merge loops verbatim from the
pre-144d841 implementation: defensive enum-merge for actions /
statuses / triggers, full actor enumeration from filterOptions['actors']
with display_name/email/exists handling, and the (deleted) suffix
fallback. UserLifecycleAuditService::filterOptions() is once again
consumed; sister services (Api/System/Import) keep their existing
usage unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:21:27 +02:00
fs
144d8410a4 Embed user lifecycle audit into settings page 2026-04-26 18:03:42 +02:00
fs
4b9ce4bbad Consolidate audit permissions to audit namespace 2026-04-24 14:46:38 +02:00
fs
e9f210decb refactor(audit): system-audit detail view on app-details-card pattern
Migrates the event-details section to the .app-details-card summary +
container pattern with outcome badge in the summary meta slot, and
truncates request ID / IP hash / user-agent hash for readable inline
display. Same content, consistent with the rest of the detail-page
design language.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 23:52:38 +02:00
fs
149d4515de refactor(settings): remove global appearance settings — tenant is sole source
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>
2026-04-22 22:40:15 +02:00
fs
21c331cd06 test(architecture): lock down two routing/filter silent-failure modes
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>
2026-04-22 15:37:26 +02:00
fs
0e82e00495 feat(core): endpointUrl() helper — query-safe URLs for module routes
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>
2026-04-21 22:39:36 +02:00
fs
7c67d273aa fix(audit): reach system-audit export via target path, not route path
The export dropdown built URLs via lurl('admin/system-audit/export') which
hit a framework quirk: MintyPHP Router::applyRoutes() compares the full
request URI (including ?query) against registered source paths. When
a browser fetched /de/admin/system-audit/export?format=csv the rewrite
to audit/system-audit/export silently missed, the router fell back to
file-based resolution, found no such file under admin/, and redirected
back to the list page — the user saw the grid "reload" and nothing
downloaded.

Only audit is affected because it is the only module where the route
path (admin/…) differs from the page target path (audit/…); helpdesk
and admin/users route paths equal their targets so file-based routing
catches the export URL even when applyRoutes misses.

Fix: use lurl('audit/system-audit/export') in the page config — the
target path — which resolves directly via file-based routing regardless
of query string. This matches the convention the same JS already uses
for `dataUrl: 'audit/system-audit/data'`. Short inline comment records
why.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:31:42 +02:00
fs
3b6896266e refactor(audit): extract SystemAuditRowPresenter shared by data + export
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>
2026-04-21 22:20:39 +02:00
fs
0386ee08a6 feat(audit): CSV/Excel export for system-audit via core primitive
Third consumer of the generic export primitive. Adds a
GET /admin/system-audit/export endpoint, the reusable export dropdown
to the list titlebar, and wires initListExport() into the page JS.

- pages/audit/system-audit/export().php: guard + ABILITY_SYSTEM_AUDIT_VIEW,
  reuses the existing filter-schema.php via gridParseFiltersFromSchemaFile
  so exported rows match the grid under the same filters, caps the limit
  at 5000, and declares 11 ExportColumns (ID, created, event, status,
  channel, actor, actor email, target type/uuid, request id, error code)
  with translated enum labels for outcome/channel.
- module.php: new admin/system-audit/export route.
- index(default).phtml: app-list-export-dropdown partial placed above the
  existing purge action; exportUrl added to pageConfig via
  lurl('admin/system-audit/export').
- admin-system-audit-index.js: imports initListExport and invokes it
  whenever config.exportUrl is present.
- i18n de/en: new "Actor email" key (required by TranslationKeysTest).

Gates: PHPUnit 1880 OK, PHPStan 0 errors, module:sync ok.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:15:43 +02:00
fs
5219bbaff2 refactor(js): migrate audit and addressbook list pages to list factory 2026-04-20 22:45:54 +02:00
fs
3f0db68b27 refactor: fix all 105 PHPStan 2 strict type findings across core and modules
Resolve all non-false-positive PHPStan 2 findings, reducing the baseline
from 486 to 382 entries (only unused-public false positives remain).

Categories fixed:
- Remove 39 redundant type checks (is_string, is_array, is_object, method_exists)
- Remove 12 no-op array_values() on already-sequential lists
- Simplify 13 null-coalesce on guaranteed offsets
- Remove 8 always-true instanceof checks
- Replace 12 redundant test assertions (assertTrue(true) → addToAssertionCount)
- Simplify 6 unnecessary nullsafe operators (?-> → ->)
- Fix 6 always-true !== '' comparisons
- Fix remaining: unset.offset, return.unusedType, staticMethod narrowing

53 files changed across core/, modules/, pages/, and tests/.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 12:54:20 +02:00
fs
a17c2c7cea refactor(audit): decouple API audit classes from core via interfaces
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>
2026-04-13 19:19:17 +02:00
fs
97ff3ce135 feat(scheduler): dispatch event on job failure, notify admins via notifications module
Add ?ModuleEventDispatcher to SchedulerRunService (fail-open, nullable).
Dispatch scheduler.job_failed event when a job fails with payload:
job_id, job_key, label, error_code, error_message, trigger_type.

New SchedulerJobFailedNotificationListener in notifications module
creates in-app admin notifications for users with JOBS_MANAGE
permission, scoped per tenant, with 30-min dedup per job_key.

Also fixes audit module manifest: system_audit_purge job_key was
mismatched (audit_system_purge vs system_audit_purge in DB).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 12:26:54 +02:00
fs
c6c5d06936 feat(audit): harden API audit log redaction and schema
- 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>
2026-04-05 23:20:55 +02:00
fs
b749b5d192 feat: centralize breadcrumbs in topbar and remove history navigation
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>
2026-04-05 17:17:06 +02:00
fs
5d07236758 feat(search): refresh global search UI and align qa/docs updates 2026-04-02 10:49:35 +02:00
fs
a4eb1c6967 refactor(ui): polish admin sidebar nav groups and audit labels
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>
2026-03-26 15:09:55 +01:00
fs
c0843b1fca refactor(audit): extract shared page helpers for purge and view actions
Create AuditPageHelper with requirePurgePost() and findOrRedirect()
to eliminate duplicated POST/CSRF guard boilerplate across 4 purge
pages and find-or-redirect boilerplate across 4 view pages. Each page
retains its stream-specific logic (system-audit event recording,
user-lifecycle snapshot/restore).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:57:54 +01:00
fs
570e3923b7 refactor(audit): remove factory chain and repository interfaces
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>
2026-03-26 12:33:59 +01:00
fs
7a222f0b1f fix(audit): harden module isolation and fix post-extraction drift
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>
2026-03-26 10:03:27 +01:00
fs
292865b0f7 fix(audit): update routes from admin/ to audit/ and fix module namespaces
Routes in nav items, redirects, breadcrumbs, and JS data URLs all pointed
to the old admin/ prefix from before the module was restructured. Also
corrects use-statements still referencing core Domain\Taxonomy namespace
instead of Module\Audit\Domain.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:14:25 +01:00
fs
a93788acf1 fix(audit): use absolute import paths in module JS files
Module JS files were using relative imports (../core/) which resolved
incorrectly when served from modules/audit/js/pages/ instead of the
original web/js/pages/. Changed to absolute paths (/js/core/, /js/pages/)
so they correctly resolve to the core JS utilities.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 08:26:29 +01:00
fs
cd4055894d fix(sidebar): render audit module nav group as details/summary section
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>
2026-03-26 08:22:04 +01:00
fs
e10b1215da fix(audit): restructure module pages to avoid admin/ path collision
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>
2026-03-26 08:13:37 +01:00
fs
0c351f6aff refactor(audit): extract audit domain into self-contained module
Move the entire audit subsystem (system audit, API audit, import audit,
user lifecycle audit, frontend telemetry) from core into modules/audit/.

Core decoupling via interface-based injection:
- AuditRecorderInterface replaces SystemAuditService in 10+ core services
- UserLifecycleAuditInterface / ImportAuditInterface for specialized flows
- NullAuditRecorder fallback when audit module is disabled
- ApiBootstrap/ApiResponse use null-safe callable resolvers

Module structure (modules/audit/):
- Manifest with routes, permissions, scheduler jobs, authorization policy
- 9 services, 8 repositories, 6 domain enums, 4 job handlers
- 33 page files, 4 JS files, 8 test files, migration scripts, i18n

Core cleanup:
- OperationsAuthorizationPolicy, UiCapabilityMap, PermissionService
  surgically cleaned of audit-specific constants
- Sidebar template cleared of hardcoded audit navigation
- AuditModuleIsolationContractTest ensures no future core→module coupling

All quality gates pass: 1346 tests (19,276 assertions), PHPStan level 5
clean, architecture contracts verified.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:12:49 +01:00