Commit Graph

21 Commits

Author SHA1 Message Date
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