Commit Graph

20 Commits

Author SHA1 Message Date
fs
d7b73fcefe refactor(actions): complete admin request/csrf guard centralization 2026-04-24 19:00:13 +02:00
fs
da7e7c593e refactor(actions): centralize wave-2 post/csrf guards 2026-04-24 18:31:17 +02:00
fs
cfd867bcd7 refactor(actions): centralize POST/CSRF guard flow 2026-04-24 17:59:57 +02:00
fs
87652e55da refactor(system-info): reduce to Stripe-style status page
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>
2026-04-24 16:32:46 +02:00
fs
d9ed4ab5b4 refactor(ui): collapse Tile static class into appTile() helper
Align dashboard-tile primitive with the established helper-function +
partial convention (like tokenSelectForm / multiSelectForm). Keeps the
reusable substance (app-tile.phtml, .app-tile CSS) and makes the
primitive discoverable for modules via core/Support/helpers/ui.php.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:23:05 +02:00
fs
0002c07755 refactor(theme): drop dark-green and collapse theme registry to light/dark
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>
2026-04-24 16:15:10 +02:00
fs
b2982bdac7 feat(ui): token-select primitive for large multi-selects
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>
2026-04-23 23:52:32 +02:00
fs
68453fd6ed refactor(tenants): visibility form requires explicit appearance values
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>
2026-04-22 23:10:44 +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
6b05bbfe9c refactor(customfield): interface-based DI for repositories
Align CustomField-Schicht mit core/Repository/Access-Muster. Vier
Repositories (Tenant-Definition, Tenant-Option, User-Value, User-Value-Option)
bekommen je ein Interface, werden von static auf instance umgestellt und
ueber CustomFieldRepositoryFactory injiziert.

TenantCustomFieldService und UserCustomFieldValueService injizieren die
Repository-Interfaces statt statisch aufzurufen. CustomFieldServicesFactory
reicht Instanzen durch. AddressBookService (Modul) bekommt
TenantCustomFieldOptionRepositoryInterface per Container — Interface-Kopplung
verbessert Modul-Isolation gegenueber dem vorherigen statischen Aufruf.

Kein Verhaltenswechsel, keine SQL-Aenderung, keine Service-Signatur-Aenderung.
Alle tenant_id-Parameter erhalten (GR-SEC-009, Security-Review SR-002).

Oeffnet den Weg fuer Cluster B1 (TenantCustomFieldService-Tests) und B2
(UserCustomFieldValueService-Tests), die nun mit createMock() moeglich sind.

Nebenbei zwei PHPStan-Folge-Findings korrigiert:
- Veralteter Ignore-Eintrag fuer findById aus phpstan-baseline.neon entfernt
- Redundanter is_array-Check in TenantCustomFieldOptionRepository entfernt
  (durch typisierte Interface-Signatur abgedeckt)

Gates: QG-001 (1945 Tests, 28581 Assertions) / QG-002 / QG-003 / QG-006 pass.

Workflow: .agents/runs/CUSTOMFIELD-DI-REFACTOR-001/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:32:14 +02:00
fs
9a78a81af8 feat(admin-users): dedicated admin profile drawer with own tabs
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>
2026-04-22 16:30:03 +02:00
fs
c1cf9f94bb feat(core): UserProfileViewService + admin/users detail drawer
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>
2026-04-22 15:49:10 +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
12356e2266 feat(core): generic CSV export primitive + helpdesk domains consumer
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>
2026-04-21 21:57:19 +02:00
fs
57b7920098 fix(flash): make flash messages one-shot per request 2026-04-15 18:45:45 +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
94b746ddf8 refactor: remove 13 dead code artifacts across core/
Dead methods (9):
- Command::bootstrapModuleApp() — unused by all subclasses
- SearchConfig::providerCoverageGaps() — diagnostic method, never called
- RequestInput::queryArray() — no callers
- LocaleResolver::getAvailableLocales() / getDefaultLocale() — unused getters
- EmailVerificationService::getExpiryMinutes() — constant wrapper, never called
- AuthService::loginAndRedirect() — convenience wrapper, never called
- DepartmentService::listForUserAssignments() — no callers
- UserTenantContextService::getAvailableDepartmentsByTenant() — no callers

Dead functions (3):
- navActivePublicPages(), appNormalizeStringList(), appNormalizePositiveIntList()

Dead constant (1):
- PermissionService::API_DOCS_VIEW — duplicated via other paths

Orphaned file (1):
- bin/cli-bootstrap.php — never included anywhere

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 08:48:29 +02:00
fs
b1a907b79c feat(modules): harden module platform with event constants, validation checks, and CLI improvements
- Add ModuleEvents constants class with all 8 event strings; update dispatch call sites
- ValidateCommand: check AuthorizationPolicy container registration (check 20)
- ValidateCommand: warn on unknown event_listeners keys (check 21)
- make:module --simple flag for minimal manifests
- module:deactivate reverse-dependency warning
- 12 new PHPUnit tests covering all improvements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 08:23:04 +02:00
fs
143d887a5c fix: patch remaining lib/ references missed in core/ rename
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>
2026-04-14 07:54:02 +02:00
fs
0e86925464 refactor: rename lib/ to core/ for clearer core-module separation
Rename the top-level lib/ directory to core/ so the project root
immediately communicates which code is core platform and which lives
in modules/. PHP namespaces (MintyPHP\*) are unchanged — only the
Composer PSR-4 path mapping moves from lib/ to core/.

Module-internal lib/ directories (modules/*/lib/) are untouched.

Updated across the full stack:
- composer.json PSR-4 mapping
- bootstrap entry points (web/index.php, bin/*, tests/bootstrap.php)
- tooling configs (phpstan.neon, phpunit.xml, php-cs-fixer)
- 26 architecture contract tests
- enforcement-policy, guard-catalog, quality-gates
- all documentation (CLAUDE.md, README, 25 docs/, .agents/skills/)
- bin/qa-extended.sh search paths
- .claude/settings.local.json permission paths

Workflow: .agents/runs/CORE-LIB-RENAME-001/ (Analyst → Planner →
Executor → Code Review (4 findings fixed) → Security Review →
Acceptance Test → Finalizer)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 23:20:42 +02:00