Commit Graph

522 Commits

Author SHA1 Message Date
aminovfariz
c8cf2892c9 ci: trigger pipeline after runner fix with node on host
Some checks failed
deploy / QA gates (push) Failing after 6s
deploy / Build & deploy (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 12:09:46 +02:00
aminovfariz
1ae3d5d984 ci: trigger pipeline after runner workspace mount fix
Some checks failed
deploy / QA gates (push) Failing after 7s
deploy / Build & deploy (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 12:01:13 +02:00
aminovfariz
2e316b2901 ci: trigger pipeline after switching runner to host mode
Some checks failed
deploy / QA gates (push) Failing after 1s
deploy / Build & deploy (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:59:33 +02:00
aminovfariz
925f8ec87e fix(ci): debug mounts via hostname inspect
Some checks failed
deploy / QA gates (push) Failing after 23s
deploy / Build & deploy (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:54:12 +02:00
aminovfariz
0486ebf2a2 fix(ci): debug runner container mounts to find host workspace path
Some checks failed
deploy / QA gates (push) Failing after 23s
deploy / Build & deploy (push) Has been skipped
qa-required / qa-required (push) Failing after 24s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:52:22 +02:00
aminovfariz
1d1f8479b6 fix(ci): set COMPOSE_PROJECT_NAME=breadcrumb-ci at job level for stable container names
Some checks failed
deploy / QA gates (push) Failing after 23s
deploy / Build & deploy (push) Has been skipped
qa-required / qa-required (push) Failing after 24s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:50:01 +02:00
aminovfariz
70d431cff5 fix(ci): install composer directly in runner container, not via docker run
Some checks failed
deploy / QA gates (push) Failing after 6s
deploy / Build & deploy (push) Has been skipped
qa-required / qa-required (push) Failing after 6s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:48:26 +02:00
aminovfariz
07cd2dd756 fix(ci): add path debug before composer install
Some checks failed
deploy / QA gates (push) Failing after 8s
deploy / Build & deploy (push) Has been skipped
qa-required / qa-required (push) Failing after 6s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:46:58 +02:00
aminovfariz
9668a23797 fix(ci): use GITHUB_WORKSPACE instead of pwd for composer docker run volume
Some checks failed
deploy / QA gates (push) Failing after 6s
deploy / Build & deploy (push) Has been skipped
qa-required / qa-required (push) Failing after 6s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:46:19 +02:00
aminovfariz
17ae333a39 fix(ci): install composer deps via docker run before compose up, force-recreate containers
Some checks failed
deploy / QA gates (push) Failing after 9s
deploy / Build & deploy (push) Has been skipped
qa-required / qa-required (push) Failing after 7s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:45:31 +02:00
aminovfariz
5df8abceaf fix(ci): force recreate containers with docker compose down before up
Some checks failed
deploy / QA gates (push) Failing after 29s
deploy / Build & deploy (push) Has been skipped
qa-required / qa-required (push) Failing after 18s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:44:08 +02:00
aminovfariz
b0355791bb fix(ci): add container state diagnostics before composer install
Some checks failed
deploy / QA gates (push) Failing after 16s
deploy / Build & deploy (push) Has been skipped
qa-required / qa-required (push) Failing after 17s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:43:16 +02:00
aminovfariz
a2ea9f385d fix(ci): debug - ls /var/www to find why composer.json not found
Some checks failed
deploy / QA gates (push) Failing after 17s
deploy / Build & deploy (push) Has been skipped
qa-required / qa-required (push) Failing after 16s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:42:23 +02:00
aminovfariz
03cdfa7702 fix(ci): wait for php container and use explicit workdir for composer install
Some checks failed
deploy / QA gates (push) Failing after 16s
deploy / Build & deploy (push) Has been skipped
qa-required / qa-required (push) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:41:51 +02:00
aminovfariz
3dca38d1c2 fix(ci): run composer install inside php container before QA gates
Some checks failed
deploy / QA gates (push) Failing after 6s
deploy / Build & deploy (push) Has been skipped
qa-required / qa-required (push) Failing after 6s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:40:28 +02:00
aminovfariz
5e8273b952 fix(ci): start only php+db+memcached in CI, skip nginx (file mount fails in runner)
Some checks failed
deploy / QA gates (push) Failing after 6s
deploy / Build & deploy (push) Has been skipped
qa-required / qa-required (push) Failing after 7s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:39:45 +02:00
aminovfariz
d47e45e9f8 fix(ci): create .env from .env.example before docker compose up in CI
Some checks failed
deploy / QA gates (push) Failing after 2m8s
deploy / Build & deploy (push) Has been skipped
qa-required / qa-required (push) Failing after 6s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:32:59 +02:00
aminovfariz
0e70e50abf feat(ci): add Gitea Actions deploy pipeline and fix prod image module assets
Some checks failed
deploy / QA gates (push) Failing after 1m9s
deploy / Build & deploy (push) Has been skipped
qa-required / qa-required (push) Failing after 6s
- Add .gitea/workflows/deploy.yaml: QA → build prod image → SCP to server → docker load + compose up + module:sync
- Fix Dockerfile prod stage: copy module web assets directly from modules/*/web/ instead of resolving symlinks (symlinks are excluded via .dockerignore and break on Windows/CI)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 11:22:47 +02:00
aminovfariz
9caa771a73 feat(helpdesk): add delete button on edit page, fix flash notifications
Some checks failed
qa-required / qa-required (push) Has been cancelled
- Edit page: managers see 'Delete handover' button in aside with confirm dialog
- Delete handler: POST action=delete → deleteByIds → redirect to list
- Flash scope set to null (global) so toast shows reliably after redirect
  regardless of locale prefix or query string in URL
- i18n: added Delete handover / Delete this handover? / Handover deleted (de+en)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 15:51:36 +02:00
aminovfariz
1caa198286 feat(helpdesk): rework handover tabs and assign flow
Tabs: replace open/closed with active (draft+in_progress+under_review)
and done (completed+archived) — records now appear in the correct tab

Create wizard assign mode: redirect to list with flash message after
assigning instead of opening edit page

i18n: add In progress / Done tab labels and wizard strings (de + en)

Nikita Soldatov note: user does not exist in DB — needs to be created
via admin user management

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 15:23:31 +02:00
aminovfariz
47a26d0ca6 test(helpdesk): add tests for assign, submitForReview, resendNotification and HandoverNotificationService
HandoverServiceTest: 30 new cases covering:
- assign(): happy path, draft→in_progress transition, permission check,
  zero assignedTo, not found, email sent, due date passed to repo
- submitForReview(): assignee and manager paths, non-assignee denied,
  wrong status (under_review/completed), revision created, email sent
- resendNotification(): happy path, permission denied, no assignee
- statusLabel/Variant for under_review

HandoverNotificationServiceTest: 9 new cases covering:
- notifyAssigned: sends correct template+vars, skips on empty/null email,
  fallback dash for empty debitor and due_date
- notifyReviewRequested: sends to manager, skips on no email,
  fallback dash for empty assignee name

48 tests total, all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 14:59:10 +02:00
aminovfariz
88bd7caae5 feat(helpdesk): rework handover create wizard with mode selection
- Managers now see step 1: choose 'Create myself' or 'Assign to employee'
- Assign mode: select customer+product+employee → creates record immediately,
  sends invitation email, skips protocol step
- Self mode: existing 2-step flow unchanged
- Fix open/closed tab: pass view param in dataUrl so grid filters correctly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 14:50:30 +02:00
aminovfariz
1c1e82b352 fix(helpdesk): handle nested table key from DB::select in user dropdowns
MintyPHP DB returns rows as $row[$table][$field], so tenant users
come back as $u['u']['id'] not $u['id']. Added fallback for both.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 14:41:48 +02:00
aminovfariz
3f56f3f279 fix(helpdesk): replace Guard::hasAbility() with AuthorizationService in create wizard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 14:38:02 +02:00
aminovfariz
c32a0f8c31 feat(helpdesk): Übergabe assignment workflow
- New statuses: under_review
- New DB fields: assigned_to, assigned_by, assigned_at, due_date (migration 012)
- HandoverNotificationService: emails on assign and review-request
- HandoverService: assign(), submitForReview(), resendNotification()
- Assign page: manager selects employee + due date, resend notification
- Create wizard: manager can assign during creation (step 1)
- Edit page: "Submit for review" button for assignee, assign link for manager
- List page: open/closed tabs, non-managers see only their assigned handovers
- Email templates: handover_assigned + handover_review_requested (de/en)
- i18n keys added for de and en

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 14:35:03 +02:00
aminovfariz
4f38911fce fix(docker): copy module web assets as real files in prod image
web_init uses cp -rp which copies symlinks as symlinks.
Since the nginx container doesn't have /var/www/modules/,
symlinks in web/modules/ break. Replace them with actual copies at build time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 11:50:45 +02:00
aminovfariz
0d078f233d fix: suppress open_basedir warning in favicon check, disable open_basedir in prod
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 19:07:47 +02:00
aminovfariz
ec596d35d4 fix(auth): extend stream-based HTTP to POST requests in OidcHttpGateway
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 13:14:19 +02:00
aminovfariz
4e51d9e882 fix(auth): use file_get_contents for OIDC GET requests to work around libcurl 8.14/OpenSSL 3.5 TLS fingerprint rejection by Microsoft
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 11:45:52 +02:00
aminovfariz
293807b92a chore: add .dockerignore for production image builds
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 09:31:36 +02:00
aminovfariz
1b615a5e1c fix: update local docker and php config 2026-05-26 11:04:57 +02:00
aminovfariz
cb2f429e0e feat(helpdesk): add empty Dashboard page with sidebar nav item
Adds helpdesk/dashboard route, empty page/view, Overview nav group in the
sidebar, and Dashboard i18n keys (de + en). No content yet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:01:35 +02:00
fs
873b75465e docs: important name change 2026-04-29 10:00:36 +02:00
fs
0a86d1b44b docs: Readme changes 2026-04-29 09:59:16 +02:00
fs
545bcc789b docs: replace ASCII umlaut fallbacks with proper äöüß across all .md
Most German docs were written with `ue`/`ae`/`oe`/`ss` ASCII fallbacks
(`fuer`, `aenderung`, `koennen`, `gemaess`) — relic from older toolchain
constraints. Replaced 237 occurrences across 38 .md files with proper
umlauts and ß so the docs read naturally.

Substitutions are constrained to plain prose: fenced code blocks
(```...```), inline code spans (`...`), markdown link targets `[..](..)`,
and HTML comments are preserved verbatim. Path references like
`docs/howto-erste-aenderung.md` keep their original (umlaut-replaced)
filenames — only the surrounding prose is normalised.

Tool: bin/fix-md-umlauts.py (idempotent — no-op on already-clean files).
Re-runnable if ASCII fallbacks creep back in via copy-paste.

Doc-link-check + doc-drift-check stay green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 09:57:22 +02:00
fs
2e73cb98b4 feat(console): add db:migrate CLI for idempotent core schema updates
Existing systems used to have no automatic mechanism for the
db/updates/*.sql idempotent updates after `git pull` — devs had to
remember which files were already applied and run new ones manually
with `mariadb < ...`. Mirror module:migrate to fix this:

- New `bin/console db:migrate` walks db/updates/*.sql in alphabetical
  order, skipping anything already recorded in the new core_migrations
  tracking table. Each file runs in its own transaction; failure
  rolls back so the file is retried on the next run.
- New `db:migrate --status` lists applied vs. pending files without
  running anything. Useful for ops debugging "schema seems stale"
  symptoms.
- module:sync now runs db:migrate as its first step, so the standard
  post-pull command stays a single invocation. README's 3-step setup
  is unchanged — module:sync now also covers core schema updates.

CoreMigrationRepository owns its own mysqli connection (using the
same DB credentials as MintyPHP\DB) and runs raw `$mysqli->query()`
instead of going through DB::query's prepared statement path —
MariaDB rejects some DDL (e.g. ALTER TABLE … ADD CONSTRAINT CHECK)
in the prepared protocol, which the existing 18 db/updates files
trip on. ModuleMigrationRepository is left untouched (no module
migration uses such DDL today and changing the vendor pattern is
out of scope).

End-to-end verified: against an existing DB the first run applies
all 19 idempotent files as no-ops and records them, second run
reports "all updates already applied". Against a freshly init'd
DB the same thing happens — no double work, no surprises. All
encrypted seed secrets keep decrypting because APP_CRYPTO_KEY
is unchanged.

Tests: tests/Console/CoreCommandsTest covers success/failure/status
output shapes against a FakeModuleRunner (mirror of the existing
ModuleCommandsTest pattern). 5 stale baseline entries in
phpstan-baseline removed (covered by the new @api annotation on
the test fixture class).

Docs: CLAUDE.md and docs/reference-cli-commands.md document the
new command + --status flag; docs/howto-fehlerbehebung.md gains
a "schema seems stale after git pull" section pointing at
db:migrate --status.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 08:59:57 +02:00
fs
0c78dc4355 chore(setup): track config.php directly, slim README to setup checklist
config/config.php was gitignored and devs had to copy it from
config/config.php.example on every fresh clone. Since the bootstrap
config reads everything from .env via $envString() / $envInt() /
$envBool() defaults, there is nothing developer-specific in the file —
the cp step was dead ceremony.

- Remove config/config.php from .gitignore and track it directly
- Delete config/config.php.example
- Drop the example-existence + per-doc reference checks from
  bin/docs-drift-check.sh; add a legacy-pattern check that flags
  any new mention of the removed example file
- Update ConfigContractsTest to require config.php and forbid the
  example
- Clean stale references in CLAUDE.md, README.md, six docs files,
  and the core-guardrails skill

Same commit slims README down from 294 to 47 lines: one setup
checklist (now 3 commands instead of 4), the seeded login table,
the three quality-gate commands, and pointers to CLAUDE.md and
docs/index.md for everything else.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 08:37:45 +02:00
fs
f36b169f67 chore(seed): bake live test connections into init.sql + helpdesk migration
Fresh installs now come up with working dev test connections for
Microsoft SSO and Business Central without manual configuration:

- init.sql appends the live breadcrumb + icoreon tenants (idempotent
  by uuid), Felix as a 6th user with all five roles, the live
  departments, plus tenant_auth_microsoft (SSO enabled for
  breadcrumb), tenant_auth_ldap stubs, and the global Microsoft
  shared-app credentials. The MusterMandant demo seed is preserved
  as tenant 1.
- A new helpdesk migration 011 seeds the per-tenant BC connection
  rows in helpdesk_tenant_settings and the helpdesk.bc_* fallback
  settings — module-owned data stays in module migrations.
- .env.example sets APP_CRYPTO_KEY so the AES-256-GCM blobs in the
  seed (Microsoft client secret, BC basic auth passwords) actually
  decrypt out-of-the-box. Verified by decrypting all four blobs
  end-to-end through Crypto::decryptString.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 08:27:47 +02:00
fs
a5213977ca fix(helpdesk): drop stale require of removed app-flash partial
Commit 4890a28 folded templates/partials/app-flash.phtml into
app-toast-stack.phtml, which the base layout now mounts itself. 14
helpdesk views still required the deleted partial and crashed every
helpdesk page with a 500 (Failed opening required app-flash.phtml).
Remove the redundant requires; the unified toast stack continues to
render flash messages via the layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 07:59:51 +02:00
fs
6146b0bd00 test(architecture): catch missing container bindings statically
Codifies the rule that every class referenced via `$c->get(X::class)`
inside a container registrar must itself be bound via
`$container->set(X::class, ...)` somewhere — in the same registrar,
in core/App/registerContainer.php, or in a module container
registrar.

Catches the bug pattern from cd2c9c5: a registrar's factory closure
pulled UserSettingsGateway via $c->get() but no matching ->set() ever
ran. Unit tests with mocked dependencies passed; the failure only
surfaced at runtime ("Service not bound: ...") when the affected
admin/settings/user-lifecycle page tried to construct the dashboard
service against the live container.

Implementation is purely static — token / regex parsing of registrar
files plus class_exists() / interface_exists() gating to avoid false
positives on non-class identifiers. No factory invocation, so the
test does not require DB / session / HTTP and is fast.

Verified by reverting the cd2c9c5 binding line in the working tree
and running the test — it produces the exact "UserSettingsGateway
referenced in core/App/Container/Registrars/UserRegistrar.php"
finding. Restored after the dry-run.

Allowlist starts with two framework-bootstrap classes (AppContainer,
ModuleRegistry) that are bound outside the registrar mechanism.
Each future addition needs an inline justification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 08:54:21 +02:00
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
cd2c9c5741 fix(user-lifecycle): bind UserSettingsGateway in container
Previous commit 06118c1 wired UserLifecyclePolicyDashboardService to
$c->get(UserSettingsGateway::class) without ensuring the gateway
itself was a container service. The gateway is built by
UserServicesFactory::createUserSettingsGateway() and was previously
only obtained transitively as a constructor argument of other
factory-built services — never resolved through the container
directly.

Result at runtime: 500 with "Service not bound:
MintyPHP\\Service\\User\\UserSettingsGateway" the first time the
user-lifecycle settings page tried to construct the dashboard
service.

Adds the missing binding in UserRegistrar — same factory delegation
the file uses for UserAccountService, UserAssignmentService, etc.

Unit tests didn't catch this because they construct the dashboard
service with mocked dependencies; only the live page touched the
real container path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:30:34 +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
97d09fbd94 fix(user-lifecycle): aside actions use normal size + repair policy ref link
Two small follow-ups to the cockpit phase-1 commit cc2cf3a:

* Drop the 'small' class from the three aside Quick Actions. The
  aside has plenty of room and the small variant felt cramped next
  to the page title — normal-size buttons match the visual weight
  of the form's Save button above.
* Repair the Policy reference link. It pointed at a raw markdown
  path (docs/reference-benutzer-lifecycle-policy.md) which 404s
  because the static-file route never existed. The codebase has a
  Markdown viewer at admin/docs/<slug> backed by DocsCatalogService;
  the correct slug is 'reference-benutzer-lifecycle-policy'. Link
  is now also gated by ABILITY_ADMIN_DOCS_VIEW so users without
  docs permission don't see (and 403 on) it. target=_blank dropped
  because it's an in-app route now, not an external file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:43:28 +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