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>
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>
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>
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>
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>
Saving security levels appeared broken in both the domain detail view and
the domains grid. Root causes:
- Repository never unwrapped MintyPHP DB rows (nested by table name), so
reads always returned defaults even though writes succeeded.
- Batch queries bound tenant_id last while SQL expected it first, so
list/detail enrichment for the grid matched nothing.
- Detail page AJAX submit looked up CSRF via a non-existent data attribute
and dropped the note field entirely.
- Endpoint used a hand-rolled CSRF check instead of Session::checkCsrfToken().
Fixes:
- DomainSecurityLevelRepository: use RepositoryArrayHelper::unwrap(List),
swap param order in listLevelsByDomainNos / listDetailsByDomainNos,
centralize table name in a const.
- security-level-data endpoint: canonical Session::checkCsrfToken(),
unified JSON/PRG error path with proper HTTP status codes and flash.
- Detail page: drop broken AJAX hijack, rely on native form POST + PRG
(GR-CORE-012).
- Grid list: refetch via gridRef.grid.forceRender() after dialog save
instead of DOM-only mutation, so Grid.js internal state stays in sync.
- Add dedicated Note column next to the badge with ellipsis + title
tooltip.
- Replace console.* in the dialog with showAsyncFlash for user-visible
errors; add i18n keys (de/en) for all error messages.
- Drop unused postAction import and TENANT_ID_OTHER test constant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New Updates page in the Helpdesk module that fetches UPDATE/UPDATE-HF tickets
from Business Central (PBI_LV_Tickets) and allows assigning a domain and Gitea
link via a dialog. Ticket status (from BC) and assignment status (local) are
shown as separate columns with filters for both plus type and free-text search.
Assigned updates also appear on the domain detail page. Includes session-cached
BC fetch with refresh button, admin permissions, migration, and 16 unit tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New domain detail page showing customer info, contract data, linked handovers,
updates section, and related domains for the same customer. Data loaded async
with skeleton loading states. Includes DomainDetailService and tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds domain selection (cascaded from debitor) to handover creation and edit,
bulk delete with confirmation dialog, and various UI improvements to the
handover wizard and list page. Includes migration for domain columns,
domain-select AJAX endpoint, and updated tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Every save creates an immutable revision snapshot. The aside shows a
commit-graph-style timeline (newest first) with vertical line, circle
nodes, version numbers, change type, user, and date. Clicking a past
revision renders it readonly with inline git-diff-style highlights
(changed/added/removed with tinted backgrounds). Compare mode allows
diffing any two arbitrary revisions. MANAGE users can restore old
versions, which creates a new revision preserving full history.
New: HandoverRevisionService, HandoverRevisionRepository,
migration 006, 14 PHPUnit tests, DE/EN translations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- JOIN users table in findById for created/updated-by display names
- Fix row mapping to handle merged JOIN results
- Use product name instead of debitor name in page title
- Sidebar: collapsible customer/product sections, status dropdown,
audit partial with user attribution
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Require ABILITY_HANDOVERS_CREATE instead of generic ABILITY_ACCESS.
Replace manual header + echo with Router::json() for consistency.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Avoids the separate updateFields call after insert by accepting fieldValues
in HandoverService::create() and validating/encoding them before the insert.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement handover protocols as a new entity in the helpdesk module,
allowing users to create fillable protocol records from admin-defined
software product schemas.
Key additions:
- DB migration (helpdesk_handovers table) with tenant scope
- HandoverService with status workflow (draft/in_progress/completed/archived)
- Three-tier permissions (view/create/manage)
- Two-step creation wizard (Stripe-style assistant)
- Grid.js list page with search and status filter
- Edit/detail page with aside metadata and status controls
- Reusable core autocomplete lookup component (app-lookup-field)
- Debitor lookup data endpoint for autocomplete
- Dynamic form rendering from schema snapshots
- 11 PHPUnit tests for HandoverService
- DE+EN i18n translations (48 keys each)
Also includes: PHPStan fixes (dead code removal, stale baseline cleanup),
software product edit title improvement, fieldset simplification,
and Stripe-style hover for schema preview.
Workflow: HD-HANDOVERS-001 (.agents/runs/HD-HANDOVERS-001/)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Auto-generate field keys and option values from labels via slugify,
add live preview panel with hover-highlight, insert-between-fields
dividers, and various spacing/styling improvements.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a JSON-based schema editor as a new tab on the software product edit
page. Admins can define protocol fields (heading, paragraph, text,
textarea, number, date, checkbox, select) via a structured UI with
add/remove/reorder controls. The schema is stored as a versioned JSON
column on the product row.
Includes DB migration, server-side validation (type whitelist, key
uniqueness, key format, max 50 fields, select option checks), i18n
(DE+EN), CSS, and 10 PHPUnit tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Scheduler jobs run without an active session, causing SessionStore::all()
to fail. Wrap the call in try-catch to fall back to global config (tenant
ID 0) when no session is available.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code and BC description are already shown in the aside — no need to
duplicate them as readonly inputs in the form.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use badge-list wrapper, <p><small> pattern, <hr> separators, and dt()
for date formatting — matching the departments edit page convention.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use fully qualified \MintyPHP\Session in _form.phtml and remove
duplicate CSRF token call from edit template (already in form partial).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a new Software-Produkte feature to the helpdesk module that syncs
contract types (Create_SaaS_License=true) from BC OData into a local
database table with a nightly scheduler job, providing a Grid.js list
page and detail/edit page for managing custom product names.
- DB migration 003: helpdesk_software_products table (code UNIQUE key)
- BcODataGateway: FS_Contract_Types entity with SaaS filter + fallback
- SoftwareProductRepository: upsert, listPaged, softDelete, updateName
- SoftwareProductSyncService: fetch → upsert → soft-delete lifecycle
- SoftwareProductSyncJobHandler: daily at 02:00 via scheduler platform
- SoftwareProductService: web UI business logic with validation
- Permission: helpdesk.software-products.manage (nav-gated)
- List page: Grid.js with Code, BC Description, Name, Status columns
- Detail page: Code/BC Description read-only, Name editable, PRG pattern
- i18n: de + en translation keys
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a new "Domains" page to the helpdesk module that fetches domain data
from the FS_Contract_Domains OData endpoint and enriches each domain with
its contract type (PI_Header_Type) by joining with FS_Contract_Lines_Test
where Type = 'Domain'.
New files:
- domains/index().php, index(default).phtml, filter-schema.php — list page
- domains-data().php — data endpoint with PHP-side filtering/sorting
- helpdesk-domains-index.js — Grid.js via initStandardListPage()
Gateway additions:
- listDomains() — fetch all contract domains
- listDomainContractLines() — fetch domain-type contract lines for type lookup
Sidebar restructured into three collapsible groups (Lookup, Monitoring,
Administration) with per-group icons and color coding, matching the core
admin panel pattern.
Co-Authored-By: Claude Opus 4.6 (1M context) <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 11 tests for BcODataGateway covering getMeetingsForCustomer (empty,
whitespace, not-configured), OData injection rejection for all customerNo
methods (meetings, contract lines, escalation tickets, controlling), and
valid input acceptance. Also adds meetingsKey format and inclusion tests
for DebitorCacheControl.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces escapeODataTrustedLiteral with escapeODataStrictUserInput for all
customerNo parameters that originate from HTTP request input. The trusted
literal escape (quote-only) remains for BC-sourced values like customerName.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a meetings section to the debitor detail page, fetching meeting data from BC OData API with cache control support. Includes timeline UI for upcoming and past meetings.
Co-Authored-By: Claude Opus 4.6 <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>
Introduce data-icon-swap attribute with paired outline/fill <i> elements.
CSS toggles to filled icon on hover and active state. Module slots
auto-derive fill variant from icon name, with explicit icon_fill override
for icons without a fill counterpart (e.g. bi-headset). Add enforcement
script bin/icon-swap-check.sh.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use Stripe pattern: outline icons by default, filled for active state
(bookmarks bookmarked, notifications unread). Remove chevron arrows
from tenant/user dropdowns. Add max-width with ellipsis to breadcrumb
items to prevent overflow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
createForTenantAdminUsersFromMessage relies on listActiveUserIdsByTenantId
which returns empty results in CLI context (scheduler). Instead, iterate
admin users directly and call createFromMessage per user per tenant.
Verified end-to-end: notification created, dedup blocks duplicates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change DEDUPE_TYPES from bool to int|true — int value overrides the
default 30-min window. scheduler.job_failed uses 1440 min (24h) so
admins receive at most one notification per failed job per day.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
Introduce per-tenant override for helpdesk BC connection config.
New table helpdesk_tenant_settings stores tenant-specific credentials
with encrypted secrets (AES-256-GCM). EffectiveHelpdeskSettingsService
resolves global vs tenant config per request. Settings UI extended with
tenant override tab, toggle, and dual editing mode.
All runtime consumers (OData, SOAP, OAuth) read through the new
resolver. Token cache flushed on any config change. Default behavior
unchanged — tenants use global config until override explicitly enabled.
Also includes risk radar UI refinements: Stripe-style card redesign
with accent borders and score pills, removal of redundant KPI tiles
and search, and filtering of zero-score entries.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- margin-bottom: 0 on cards to override article shell default
- Level thresholds stricter: high >= 55 (was 70), medium >= 25 (was 40)
so fewer customers appear as 'low risk' and issues surface earlier
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove background circle, just show the score as a large colored
number. Lighter, more Stripe-like.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stripe-style layout: large colored score circle on the left, customer
name + risk level label on the right. One-line key facts below
separated by middot (e.g. '5 open · 2 critical · 1 SLA · ↑3').
Removed border-left color accent, pill list, and metric row.
Three visual levels: score (eye-catching), name+level (context),
facts (detail). Clean scannable layout.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each dimension shows 'points / max' (e.g. '18 / 35') making the
score composition immediately understandable. Total row sums to
the final 'score / 100'. Replaces abstract percentage column.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolution time measures the past, not current risk. The remaining
4 dimensions (open pressure 35%, trend 30%, SLA overdue 25%,
inactivity 10%) are all current and actionable. Removes resolution
aggregation, median calculation, and weight redistribution logic.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>