Skip to content

chore(deps): bump actions/upload-artifact from 4 to 7#1

Closed
dependabot[bot] wants to merge 1 commit into
developfrom
dependabot/github_actions/actions/upload-artifact-7
Closed

chore(deps): bump actions/upload-artifact from 4 to 7#1
dependabot[bot] wants to merge 1 commit into
developfrom
dependabot/github_actions/actions/upload-artifact-7

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github May 26, 2026

Copy link
Copy Markdown

Bumps actions/upload-artifact from 4 to 7.

Release notes

Sourced from actions/upload-artifact's releases.

v7.0.0

v7 What's new

Direct Uploads

Adds support for uploading single files directly (unzipped). Callers can set the new archive parameter to false to skip zipping the file during upload. Right now, we only support single files. The action will fail if the glob passed resolves to multiple files. The name parameter is also ignored with this setting. Instead, the name of the artifact will be the name of the uploaded file.

ESM

To support new versions of the @actions/* packages, we've upgraded the package to ESM.

What's Changed

New Contributors

Full Changelog: actions/upload-artifact@v6...v7.0.0

v6.0.0

v6 - What's new

[!IMPORTANT] actions/upload-artifact@v6 now runs on Node.js 24 (runs.using: node24) and requires a minimum Actions Runner version of 2.327.1. If you are using self-hosted runners, ensure they are updated before upgrading.

Node.js 24

This release updates the runtime to Node.js 24. v5 had preliminary support for Node.js 24, however this action was by default still running on Node.js 20. Now this action by default will run on Node.js 24.

What's Changed

Full Changelog: actions/upload-artifact@v5.0.0...v6.0.0

v5.0.0

What's Changed

BREAKING CHANGE: this update supports Node v24.x. This is not a breaking change per-se but we're treating it as such.

... (truncated)

Commits
  • 043fb46 Merge pull request #797 from actions/yacaovsnc/update-dependency
  • 634250c Include changes in typespec/ts-http-runtime 0.3.5
  • e454baa Readme: bump all the example versions to v7 (#796)
  • 74fad66 Update the readme with direct upload details (#795)
  • bbbca2d Support direct file uploads (#764)
  • 589182c Upgrade the module to ESM and bump dependencies (#762)
  • 47309c9 Merge pull request #754 from actions/Link-/add-proxy-integration-tests
  • 02a8460 Add proxy integration test
  • b7c566a Merge pull request #745 from actions/upload-artifact-v6-release
  • e516bc8 docs: correct description of Node.js 24 support in README
  • Additional commits viewable in compare view

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot @github

dependabot Bot commented on behalf of github May 26, 2026

Copy link
Copy Markdown
Author

Labels

The following labels could not be found: ci, dependencies. Please create them before Dependabot can add them to a pull request.

Please fix the above issues or remove invalid values from dependabot.yml.

@windischb

Copy link
Copy Markdown
Contributor

Closing — bundling all current Dependabot-detected updates into a single locally-tested PR to keep CI Actions usage focused. Next Dependabot run will confirm the bumps landed.

@windischb windischb closed this May 26, 2026
@dependabot @github

dependabot Bot commented on behalf of github May 26, 2026

Copy link
Copy Markdown
Author

OK, I won't notify you again about this release, but will get in touch when a new version is available. If you'd rather skip all updates until the next major or minor version, let me know by commenting @dependabot ignore this major version or @dependabot ignore this minor version. You can also ignore all major, minor, or patch releases for a dependency by adding an ignore condition with the desired update_types to your config file.

If you change your mind, just re-open this PR and I'll resolve any conflicts on it.

@dependabot dependabot Bot deleted the dependabot/github_actions/actions/upload-artifact-7 branch May 26, 2026 15:16
windischb added a commit that referenced this pull request Jun 4, 2026
… + in-app per-realm error feed (#51)

* docs(logging): design doc — split AuthLog into durable audit + centralized operational logging

Captures the redesign plan: replace the "Auth:"-magic-prefix Serilog sink with
(A) a typed, durable, GDPR-erasable per-realm AUDIT trail (event-sourced stream
+ Wolverine outbox + RealmSettings retention), and (B) a centralized OPERATIONAL
logging track (OTel Logs → OTLP + a slim in-app platform live-tail). Grounded in
existing codebase conventions (outbox, GdprService masking, Inbox slice,
RealmSettings). Includes the 6 verified smells (incl. the GDPR false-promise),
shared principles, 7 open decisions, and a 6-phase plan.

dev-docs only (repo-only, not deployed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(logging): register the redesign doc in the dev-docs navigation

The new logging-audit-redesign.md was an orphan page (file only). Per the
"Adding to this section" convention: register it in .vitepress/config.ts sidebar
and link it from future-features/index.md. dev-docs build verified (no dead links).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(logging): rewrite redesign to the converged projection-over-streams model

Replace the first-draft "new event-sourced audit store" with the converged
design: the tenant audit is a projection over the user/config event streams we
already keep (GDPR masking inherited at source), the streamless remainder gets a
short-retention security store under Art-6(1)(f), and operational logs move to
OTel Logs -> OTel Collector (redaction guarantee) -> OpenObserve.

Grounded in a code-fact verification pass + an adversarial review:
- login telemetry isn't reliably event-sourced today (password/magic-link append
  no event; UserLoginFailedEvent never appended) -> Phase 1 closes the gap with a
  minimal UserLoggedInEvent(userId, method) marker, IP stays in Sessions
- known-user failures: aggregated UserLoginFailuresObservedEvent on the user
  stream (erasable + boundary-conformant), not the streamless store
- AuthAuditView is a per-tenant-DB MultiStreamProjection; erase must scrub the
  projected rows (masking rewrites event bytes, not already-projected rows)
- streamless records ARE personal data, lawful under legitimate interest, not
  "outside GDPR"; "retention" is a visibility window, not a deletion

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(logging): lock the erase-scrub mechanism — DeleteWhere, not re-project

Buildability assessment verified Marten 9.3.5 has no targeted single-user
rebuild (RebuildProjectionAsync is projection-wide only). Resolve the §A.4.2
caveat: the erase-scrub is session.DeleteWhere<AuthAuditView>(x => x.UserId ==
userId) after mask+archive — correct because the source streams are archived in
the same flow (no re-emit), and the established GdprService read-model-scrub
pattern (GdprService.cs:247-268, AuthLogEndpoints.cs:54-64).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(logging): make Track B explicitly opt-in / off by default

Operational logging (OTel Logs -> collector -> OpenObserve) is external infra
many deployments won't have (single-instance, dev). Add B.0: Track B is
toggleable and the IdP — including all of Track A audit — runs fully with it
off. Export reuses the existing Observability__Otlp__Enabled gate (already off
by default, ObservabilitySettings.cs:58); the in-app per-realm error feed is
local-only and independently toggleable. Track A must never hard-depend on
Track B (Principle 3 extended).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(audit): Phase 0 — AuditEvents taxonomy + AuthAuditView EventProjection

First implementation step of the logging/audit redesign (no behavior change).

- AuditEvents / AuditCategories: stable event-type + category vocabulary in
  Modgud.Authentication.Audit (sibling-in-spirit of DcrAuditEvents; lives in
  Authentication because that's where the consuming projection lives).
- AuthAuditView: flat per-event read doc ([DocumentAlias("auth_audit_view")]),
  metadata only — no PII payload.
- AuthAuditViewProjection: an EventProjection (one row per event), NOT a
  Single/MultiStream aggregation — an audit log is a list of occurrences, not a
  per-aggregate snapshot. Folds user-aggregate (auth/lifecycle/federation) +
  login-provider config streams; metadata from the IEvent envelope (Id, Timestamp,
  TenantId->Realm, StreamId->UserId). Registered async.
- Integration test (Modgud.Api.Tests/Audit): appends events, rebuilds the
  projection on the system realm DB, asserts flat typed rows with realm tag + IP.

OAuth application/scope/API config events are the next mechanical addition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(logging): correct projection type to EventProjection; mark Phase 0 shipped

Building Phase 0 proved AuthAuditView is an EventProjection (one row per event),
not the MultiStreamProjection the design + both review rounds assumed — an
aggregation collapses a stream into one snapshot per identity; an audit log is a
list of occurrences. Also: AuditEvents lives in Modgud.Authentication (not
Application — Authentication doesn't reference it), and there is no EventVersion
machinery (events evolve via tolerant STJ + MapEventType aliases).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(audit): Phase 1 — login-success markers + aggregated known-user failures

First behavior-change phase of the logging/audit redesign (events that the audit
projection most needs were not reliably event-sourced).

- UserLoggedInEvent gains a non-PII `method` code; it now appends on password +
  magic-link + external login (was: external only, with null IP). Password +
  magic-link previously logged only via Serilog. Marker carries no IP — IP/device
  live in the Sessions feature. The password-path append is best-effort (try-catch)
  so a marker-write failure can never 500 an already-authenticated login.
- New UserLoginFailuresObservedEvent: ONE aggregated event per resolved known-user
  failure streak (Decision (b)), emitted from EventSourcedUserStore when the
  access-failed counter resets >0 -> 0. Not one event per attempt — avoids stream
  spam + the amplification vector; stays on the user stream (erasable).
- AuthAuditView gains Method + Count columns; projection maps both + the new event.
- Masking rule passes `method` through (non-PII); MapEventType alias registered.
- Tests: projection mapping of method/count/new event; emission test driving the
  real EventSourcedUserStore failure-streak path via UserManager.

Reviewed adversarially (auth-flow safety, emission correctness, GDPR/schema,
regression) — 6 findings folded: best-effort marker (#1), emission test (#2),
documented magic-link/external streak-resolution limitation (#3/#4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(logging): mark Phase 1 shipped + note magic-link/external streak limitation

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(audit): Phase 2 (GDPR slice) — mask-and-keep erased audit, durable via IncludeArchivedEvents

A GDPR-erased user is masked, not deleted (the event stream is masked in place +
archived; the ApplicationUser becomes a deleted-{guid} tombstone). So the tenant
audit must be retained de-identified (Art-17(3)), not deleted — this replaces the
earlier "DeleteWhere the rows" plan.

- AuthAuditViewProjection.IncludeArchivedEvents = true: because the masked events
  are archived (kept), a full rebuild regenerates the erased user's rows FROM the
  masked events (Ip already null). The masked archived events ARE the durable
  de-identified record — no separate store, no duplication.
- GdprService.PerformPermanentEraseAsync: after mask + archive, null Ip on the
  user's AuthAuditView rows (live freshness — masking appends no new event, so the
  already-projected rows don't auto-refresh). Live view then matches an
  archived-inclusive rebuild. Ip is the only PII column (UserName null, UserId is a
  pseudonymous tombstone key).
- Test: an erased user's audit rows SURVIVE de-identified (Ip null) AND survive a
  full projection rebuild. Regression: 21 GDPR/erase/lifecycle/rebuild tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(logging): §A.4.2 erase = mask-and-keep, durable via IncludeArchivedEvents

Supersedes the committed "DeleteWhere the rows / separate durable store" design:
a GDPR-erased user is masked (not deleted), so the masked archived events are the
durable de-identified record. Set IncludeArchivedEvents on the projection (rebuild
regenerates erased rows from masked events) + null Ip in the erase call (live
freshness). Reconciled the lifecycle / phasing / provenance references.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(audit): Phase 2 — tenant audit read endpoint (GET /api/admin/audit)

Per-realm GDPR-audit read surface over AuthAuditView. A tenant-scoped IDocumentSession
returns only the caller's realm by physical isolation (no WHERE Realm = filter, unlike
the cross-realm AuthLog) — a filter bug can't leak cross-realm. category/eventType
filters, Timestamp-desc, capped limit, gated on auth-log:read. Control-plane
cross-realm fan-out deferred (platform-wide surface = the Phase-3 streamless store).

HTTP integration test: realm-admin client gets the realm's rows + the category filter
narrows correctly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(logging): Phase 2 read endpoint shipped; AuditSettings/fan-out/UI pending

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(audit): Phase 2 — per-realm audit visibility window (AuditSettings)

Adds AuditSettings.VisibilityWindowDays (default 90) as a nullable JSONB sub-record
on RealmSettings, wired through GET/PATCH /admin/realm-settings (DTO + service patch
with >=1 validation), mirroring DeletionSettings. The audit read endpoint applies it
as a Timestamp cutoff.

Named a *visibility* window, NOT "retention": it bounds what the read surface shows,
it does not delete history (the source events live with the aggregate, masked on
erase) — so a realm-admin can't mistake it for a deletion guarantee (§A.6).

Test: a 100-day-old row is hidden under the default 90-day window; a recent row shows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(logging): Phase 2 visibility window shipped; only SPA UI + deferred fan-out left

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(audit): Phase 2 — SPA AuditLogView (tenant audit grid + category chips)

New /admin/audit view (sidebar under System, gated on auth-log:read) consuming
GET /api/admin/audit, mirroring AuthLogView: a CoarDataGrid over the per-realm
AuthAuditView with Time/Category/Event/Method/IP/Level/Realm columns and
data-derived category filter chips. de.json admin.auditLog block added.

Verified live with Chrome DevTools: a real admin password login produced an
auth.login_succeeded/password row end-to-end (Phase 1 emission -> EventProjection
-> endpoint -> view); category-chip filter narrows correctly; all
GET /api/admin/audit calls 200. Frontend type-check green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(logging): Phase 2 SPA AuditLogView shipped + DevTools-verified

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(audit): show the actor — resolve user identity at read time (erasure-safe)

The audit grid showed no "who". Resolve it: the read endpoint joins AuthAuditView.UserId
to the ApplicationUser doc and returns a User field (new AuditLogEntryDto); the SPA
gains a Benutzer/User column.

GDPR-safe by construction: we join ApplicationUser (NOT the UserView projection)
because GdprService masks the ApplicationUser doc IN PLACE on erase (UserName ->
"deleted-{guid}", name/email nulled) — so an erased user shows as deleted-{guid},
de-identified, whereas UserView keeps the stale real name until a rebuild and would
leak it. Config-stream rows (UserId null) show no actor.

Verified live with Chrome DevTools: user-stream rows show "admin"; the config
admin.login_provider_added row's user cell is empty. Test asserts a resolved User.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(logging): read-time actor resolution from erasure-masked ApplicationUser (§A.3)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(audit): Phase 3a — streamless security/ops store scaffold

Foundation for Track A streamless half (logging/audit redesign A.5): a typed,
best-effort, system-DB store that replaces the personal-data-bearing-but-
streamless portion of the legacy "Auth:"-prefix AuthLog.

- Move AuditEvents/AuditCategories taxonomy from Modgud.Authentication.Audit
  down to Modgud.Infrastructure.Audit so lower-layer call sites (e.g.
  RealmProvisioningService in Infrastructure) can reference event codes without
  magic strings. Extend with the streamless security.* / ops.* / audit.*
  vocabulary + CategoryOf/IsPlatformOnly routing helpers.
- SecurityAuditEntry: flat typed Marten doc in the system DB (Realm, Category,
  EventType, Level, PlatformOnly, Actor, Ip, Status, Reason, Message), indexed
  on Timestamp/Realm/EventType.
- ISecurityAuditLog + SecurityAuditLog (bounded drop-on-full channel; realm
  captured from TenantContext.Current at emit) + SecurityAuditWriter (batched
  background drain to the system DB). Never blocks/throws on the auth path;
  bounded where the legacy channel was unbounded.

Additive only (strangler): the new store runs alongside the legacy sink; nothing
writes to it until 3c, nothing reads it until 3b. Full solution builds; Phase-0/2
audit tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(audit): Phase 3c — migrate streamless "Auth:" sites to typed emit

Replace the "Auth:"-message-prefix logging convention at every call site with
typed ISecurityAuditLog.Record(...) into the Phase-3 streamless security/ops
store. Audit-worthy streamless events (unknown-actor login attempts, magic-link
probes, external-login rejections incl. the SAML protocol + required-signature
tamper gate, identity-hijack/JIT-conflict, privilege-escalation blocked, rate
limits, DCR register/reject/GC/first-use, key/cert rotation, recovery-CLI, realm
provision/adopt/CP-transfer, lifecycle sweep, bootstrap invite) now emit a typed
record; known-user/on-stream and pure-diagnostic sites drop the prefix to plain
logs (Track B later). No behavior change — logging/audit emission only.

Notable:
- Layering: LoginTimeMembershipDeriver lives in Modgud.Authorization, which
  cannot reference Infrastructure (cycle). It now surfaces DroppedRealmAdminCount
  and ExternalLoginProcessor (which can reach the store) emits the
  security.privilege_escalation_blocked record.
- RecoveryCli / STARTUP_COMMAND never start the host, so the background writer
  never drains — added SecurityAuditLog.FlushAsync, called in the recover path,
  so break-glass forensic records aren't lost on process exit.
- SecurityAuditRecord gains an explicit Realm override for realm-iterating
  background jobs (run in the system session); request-path sites keep ambient.
- Adversarial review caught two missed files (first sweep had not found them):
  SamlLoginFlow (6 SAML rejection sites + new security.saml_signature_rejected
  tamper code) and DcrLastUsedTrackerHandler (new ops.dcr_client_first_used).
- Fixes: mask the bootstrap-invite-consumed email (PII rule); drop now-dead
  ILogger params (DcrGcJob, DcrLastUsedTrackerHandler).

Ground-truth grep: zero remaining "Auth:" log call sites (only legacy-sink
internals, deleted in 3f, + doc comments). Build green; 1050 unit + 254
integration tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(audit): Phase 3b — Security-log read surface + audit-of-the-audit + perm split

Rewire the carried-forward /api/admin/auth-log surface onto the typed streamless
SecurityAuditEntry store (was the flat AuthLogDocument):

- GET scopes by the caller realm via ScopeToCallerRealm carried forward from PR #50,
  extended with the visibility split: a tenant realm-admin sees only their realm's
  tenant-visible rows (!PlatformOnly); the control-plane realm sees the full
  cross-realm log incl. control-plane-only operational rows. Optional category /
  eventType filters for the taxonomy chips. Carry-forward DTO keeps the legacy grid
  columns (Actor -> UserName) and adds EventType/Category/Status/Reason.
- DELETE (clear) is now audited (audit-of-the-audit): after the wipe it emits a typed
  audit.log_cleared record naming the operator + scope, so the destructive action
  leaves its own forensic trail.

Permission split (Open Decision #8): the GDPR-audit (/api/admin/audit) moves to a new
audit-log:read; the streamless security store keeps auth-log:read. Registered in the
runtime resource catalog, the (evolving) per-realm seeder catalog, and the seeded
User Manager bootstrap role — so existing realms gain audit-log:read on next boot.

Ported AuthLogAttributionTests (scoping now over SecurityAuditEntry + new PlatformOnly
cases) and AuthLogTenantVisibilityTests (seeds SecurityAuditEntry; control-plane sees
PlatformOnly rows). Build green; 9 unit + 91 integration tests pass across audit /
auth-log / bootstrap / provisioning / permission.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(audit): Phase 3d — Quartz prune job for the streamless security store

SecurityAuditPruneJob hard-deletes SecurityAuditEntry rows older than a fixed 7-day
window, replacing the legacy AuthLogPersistenceService cleanup loop with a Quartz job
(visible/re-cronnable/triggerable from /admin/jobs). The fixed short retention IS the
GDPR proportionality control for this legitimate-interest store (no per-subject erase);
deliberately not per-realm configurable, unlike the per-realm GDPR-audit visibility
window. Single indexed delete over the cross-realm system-DB store. Registered in the
Quartz scheduler (02:00 UTC daily). Build green; host boots with the job registered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(audit): Phase 3e — repurpose AuthLogView as the tenant Security view

Rewire the carried-forward /admin/auth-log SPA view onto the streamless security
store (was the legacy free-text AuthLog):

- Relabel to "Security" (Sicherheit) with a shield-alert icon; new typed row shape
  (Timestamp/Realm/Category/EventType/Level/UserName/Ip/Status/Reason/Message).
- Replace the DCR message-prefix substring filter with proper category chips
  derived from the rows (mirrors the GDPR AuditLogView). Columns: Time, Category,
  Event (EventType), Detail (Message), Actor, IP, Level, Realm. Clear action kept
  (now server-audited as audit.log_cleared).
- Permission split (Open Decision #8): the GDPR audit (/admin/audit) sidebar item +
  router ADMIN_PERMS move to audit-log:read; the Security view keeps auth-log:read.
- securityLog i18n block (English fallbacks inline + German in public/i18n/de.json).
  Full LF rewrite of AuthLogView.vue (was CRLF under eol=lf .gitattributes).

DevTools-verified end-to-end on a live stack: a failed login on an unknown user
surfaced a security.login_failed_unknown_user row (actor + IP + realm); the GDPR
audit view loaded under the new audit-log:read permission; and Clear produced an
audit.log_cleared row naming the operator (audit-of-the-audit). Frontend type-check
(build mode) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(audit): Phase 3f — delete legacy AuthLog, LIA, tests, docs

Retire the "Auth:" magic-prefix audit mechanism now that the typed streamless
store (3a–3e) carries every record:

- DELETE AuthLogSink + AuthLogPersistenceService (AuthLogService.cs), AuthLogDocument,
  its Marten schema registration, the Program.cs sink/hosted-service registration +
  Serilog WriteTo.Sink + warmup query, and the now-orphaned DcrAuditEvents vocabulary.
  RealmLogEnricher is KEPT (it still tags operational logs for Console/File + the
  Phase-4 OTel export) with its doc-comment updated.
- Tests: SecurityAuditStoreTests — (1) a streamless record SURVIVES a user's
  permanent-erase (the §A.5 boundary; Open Decision #4 = time-expiry only), and
  (2) clearing the log emits a typed audit.log_cleared naming the operator
  (audit-of-the-audit). Ported AuthLogAttributionTests: dropped the sink tests, kept
  the enricher + the SecurityAuditEntry scoping/visibility tests.
- Legitimate-Interest Assessment for the streamless store (dev-docs/compliance/):
  Art. 6(1)(f) three-part test — purpose, necessity of raw IP vs hash/geo,
  proportionality (fixed short retention), safeguards, and Art-15/17/21 stance.
- Design doc: Phases 0–3 marked shipped; §A.7 taxonomy-placement corrected
  (Infrastructure.Audit, not Authentication); Open Decisions #4 + #8 resolved.

Full solution builds; 1048 unit + 256 integration tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(audit): Phase 4a — Serilog → OTLP log export sink (off by default)

Wire application log export to OTLP behind the existing
Observability__Otlp__Enabled gate (OtlpSettings.Endpoint/Protocol, no new
config block). Implemented as a Serilog sink (Serilog.Sinks.OpenTelemetry
4.2.0), not OTel .WithLogs(): AddSerilog runs with writeToProviders:false, so
an OTel ILoggerProvider would never see the Serilog enrichers — in particular
the RealmLogEnricher tag that §B.1 requires. The sink emits every Serilog
property (incl. Realm) as a log-record attribute and reads Activity.Current
for trace/span correlation. The redaction guarantee lives at the collector
(Phase 4b/4c), not here; LogPiiMasking stays as belt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(audit): Phase 4b/4c — collector redaction-ruleset v1 + end-to-end test

The PII redaction GUARANTEE (§B.2) now lives in a versioned OTel Collector
config (docker/otel-collector/otel-collector-config.yaml): a transform/OTTL
processor strips emails, JWTs, Bearer/Basic credentials and IPv4/IPv6 from the
log body AND every string attribute value, before any exporter. Resource
attributes (service.version etc.) are deliberately untouched so a version like
"1.0.0.0" is not mistaken for an IP.

Proven end-to-end, not by config unit test: OtelLogsRedactionTests emits a log
carrying PII through the real Serilog -> OTLP sink into a real collector
(Testcontainers, otel-collector-contrib) running the SAME redaction block, and
asserts the exported record is scrubbed while non-PII survives (HH:MM:SS
timestamp, the realm tag, service.version). A second test pins the test
collector's redaction block byte-for-byte against the shipped one, so the rules
under test can never drift from the ones that ship.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(audit): Phase 4d — log export/redaction docs + local observability stack

Document the third OTel signal in docs/operate/observability.md: the shared
Otlp.Enabled gate, realm-tagging + trace correlation, and — the §B.2 deliverable
— the failure modes (collector down -> app unaffected; redaction processor
removed -> silent PII leak, guarded by the ruleset version + e2e test; missing
OPENOBSERVE_* -> fail fast; realm=system for background work).

Add docker/docker-compose.observability.yml (Collector + OpenObserve) as a dev
convenience for watching redacted logs land. Verified end-to-end against the real
stack: an OTLP log with PII arrives in OpenObserve scrubbed and realm-filterable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(audit): Phase 4e — mark Phase 4 shipped in the redesign design doc

Record the deliberate ".WithLogs() -> Serilog.Sinks.OpenTelemetry" deviation in
§B.1 (writeToProviders:false would drop the Realm enricher) and flip the Phase 4
phasing entry to shipped with the redaction-ruleset v1 + e2e-test summary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(audit): Phase 4f — adversarial-review fixes (redaction-ruleset v1 -> v2)

Acted on the confirmed findings of an adversarial diff review of the Phase 4
changes:

- HIGH: usernames bypassed redaction (no value shape). v2 drops the UserName/
  Actor carrier attributes (delete_key) and masks the "User=" body form. Prose
  usernames in other forms remain a source-side concern (see deferred follow-up
  in the design doc).
- IPv6 leading-"::" forms (e.g. ::1) leaked — added a leading-:: pattern to v2.
- Test-validity: added ShippedPipeline_WiresRedactionBeforeExport so dropping the
  processor from the shipped pipeline can't pass silently (the byte-equal block
  check only pinned the definition, not the wiring); extended the e2e test with
  username, ::1, and attribute-credential cases + service.instance.id.
- Docs: corrected the HttpProtobuf endpoint guidance (the sink derives/trims the
  path — a bare base host:port is right), softened the unbacked "fails fast on
  unset env" claim to the real "starts, redacts, export fails/drops", fixed the
  dead #logs-export-redaction anchor, and documented the v2 known-limits.
- Hardening: bound the dev compose OTLP ports to 127.0.0.1 (unauthenticated
  receiver, dev-only); documented error_mode: ignore as a deliberate best-effort
  choice (propagate would drop whole payloads).

Re-verified against a real collector (v2 rules) + 3 green tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(audit): username source-belt — log user.Id, mask attempted identifiers

The collector redaction (v2) covers usernames only by attribute-key delete +
the "User=" body form, because a username has no value shape. This closes the
gap at the source so no raw login identifier reaches a log or the streamless
security store in the first place — the collector rules stay as belt.

- Operational logs now log user.Id (a GUID that erasure tombstones) instead of
  user.UserName across ~27 sites: Account/Profile/AdminGrace/passkey/magic-link/
  email-verification/2FA-enforcement/external-unlink/bootstrap surfaces.
- Unidentified actors are masked via a new email-aware LogPiiMasking.MaskUsername:
  the unknown-user failed-login Actor/Message (which persists to the system-DB
  security store — that sink does NOT pass through the collector, so masking is
  the only control), the bootstrap-invite issue/consume records, and the six
  Recovery-CLI break-glass emits (the CLI console output stays human-readable).

Verified by two adversarial review passes (correctness + completeness — the
latter surfaced the RecoveryCli/PendingAdminInvite sites that the first sweep
missed). Build + 1048 unit + 259 integration green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(observability): append /v1/<signal> for HttpProtobuf OTLP metrics/traces

ConfigureOtlp set OtlpExporterOptions.Endpoint explicitly, which disables the
SDK's AppendSignalPathToEndpoint — so under HttpProtobuf the metrics/traces
exporters POSTed to the bare host (404) instead of /v1/metrics and /v1/traces.
Make ConfigureOtlp signal-aware and append the path for HttpProtobuf (gRPC
ignores the path, so the bare endpoint stays correct there). The path is
verified: a manual OTLP POST to the produced /v1/traces returns 200.

Note: an app-boot smoke surfaced a SEPARATE, pre-existing problem — the OTel SDK
OTLP exporter for metrics/traces does not deliver to the collector under either
protocol (the HTTP export is canceled/times out), while log export (the separate
Serilog.Sinks.OpenTelemetry client) works fine. That is independent of this path
fix and of Phase 4 (logs); tracked for separate investigation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(observability): make plaintext OTLP metrics/traces export actually work

Completes the metrics/traces export fix (with the prior /v1/<signal> path fix).
An app-boot smoke showed the OTel SDK exporters hung and timed out (10s) against
a plaintext local collector under BOTH protocols, while log export was fine. Two
pre-existing causes, both at the transport layer (the log sink uses its own
HTTP/1.1 client, which is why it was unaffected):

- HTTP/2 cleartext (h2c): gRPC always, and HttpProtobuf negotiates HTTP/2 — over
  a plaintext http:// endpoint that needs h2c, which .NET disables by default.
  Enable Http2UnencryptedSupport in AddModgudObservability when OTLP is on and the
  endpoint is http:// (a TLS endpoint negotiates HTTP/2 natively).
- localhost → IPv6 ::1: the SDK exporter resolved localhost to ::1 and hung
  against the IPv4-only Docker port map. Default the endpoint to 127.0.0.1 (config
  + settings) and document the gotcha; production sets its own (TLS) endpoint.

Verified end-to-end against a real collector: gRPC+127.0.0.1 → 166 spans / 585
metrics, HttpProtobuf+127.0.0.1 → 179 spans / 629 metrics, 0 export errors, plus
realm-tagged redacted logs. (Corrects the note on the prior commit, which was
written before this root-cause was found.) Build + 1048 unit + 259 integration green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(observability): Phase 5 — in-app per-realm live error feed

Logging/audit redesign §B.3/§B.4 — the last open phase. A local-only,
bounded live error feed for the admin observability view.

- RealmErrorBuffer (Modgud.Infrastructure.Observability): an independently
  capped ring PER realm (keyed by slug), NOT the global ring of
  ObservabilityActivityBuffer — a noisy realm can never evict a quiet realm's
  errors (the §B.3 guarantee). Holds only display-safe rendered strings.
- ErrorFeedSink (Modgud.Authentication/AuthLog, beside RealmLogEnricher since
  Infrastructure has no Serilog ref): captures Error+ from Modgud.* loggers
  only (Open Decision #7), reading the realm from the enricher-stamped Realm
  property. Behind its own Observability__ErrorFeed__Enabled flag (default on,
  independent of the OTLP export gate — §B.0); MinimumLevel/SourcePrefix/
  CapacityPerRealm configurable.
- ObservabilityHub.LogsSubscribe() + GET /api/admin/observability/errors +
  an error panel in AdminObservabilityView.vue (i18n errorFeed*).
- Carried-forward hardening: per-method observability:read auth on both hub
  stream methods — SignalARR has no per-method authorisation attribute, so it
  is checked imperatively via IPermissionService.

Adversarial-review fix (2 HIGH): TenantContext.Current is NOT set during
SignalARR hub dispatch (it unwinds after negotiate → falls back to "system"),
so the realm filter AND the permission query now read the caller's realm from
HttpContext.Items (like the sibling hubs), and the permission check runs inside
TenantContext.Enter(realm) on a fresh DI scope so the tenant-scoped IQuerySession
binds to the right realm DB. The original used TenantContext.Current for both —
which would have wrongly denied non-system realm-admins and leaked system-realm
errors to every tenant admin (masked in single-realm dev). The same latent bug
in the pre-existing metrics Subscribe() is fixed too. LOW: settings docs now note
the effective floor is max(this, Serilog's global+namespace pipeline floors).

Tests: 17 new unit (per-realm eviction isolation, sink level/source filter,
realm read/fallback). build + 1065 unit + 259 integration green. DevTools-verified
end-to-end (panel renders, /errors 200, SignalR streams subscribe clean, a live
login event pushed to the feed via the shared async-Observable.Create helper).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(admin): combine Audit + Security into one tabbed /admin/logs view

The logging/audit redesign left two separate tenant-admin log surfaces as two
sidebar items under "System": Audit (GDPR audit trail, audit-log:read) and
Security (streamless security/ops store, auth-log:read). Consolidate them into a
single "Protokolle" entry that opens a tabbed view.

- New AdminLogsView.vue wraps both grids under a CoarTabGroup (the established
  RealmSettings tab pattern). Owns the page header; each tab is gated by its own
  permission (canAudit / canSecurity), so a user with only one of the two sees
  only that tab. The active tab syncs to the ?tab= query (bookmarkable).
- AuditLogView/AuthLogView stripped of their own useUI header block — they are
  now pure grid content embedded as the two tabs (lazy-mounted via v-if, so only
  the active tab polls).
- Router: new /admin/logs; the old /admin/audit and /admin/auth-log routes
  redirect onto the matching tab (back-compat for bookmarks/links).
- Sidebar: the two "System" items replaced by one "Protokolle" item gated on
  either permission.
- i18n: admin.logs (Protokolle / Audit / Sicherheit) in de.json + inline EN.

The platform operational error feed stays separate under Observability —
different audience (operator) and permission (observability:read).

type-check clean. DevTools-verified: tabs render + switch (URL syncs), old-route
redirects land on the right tab, single sidebar item, each tab shows its own grid
(Audit: Benutzer/Methode; Security: Detail/Akteur + Clear).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(audit): Phase 2 — AuditSettings visibility window is shipped, not pending

Stale parenthetical: the VisibilityWindowDays window is fully wired (domain
record + DTOs + PATCH validation + applied at read), the body already said so.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(architecture): ADR — persistence model, event sourcing vs. flat documents

Records the hybrid-by-design decision: default to a flat Marten document, reach
for an event-sourced aggregate only when a feature needs history, a real state
machine, or temporal/rebuildable read models. No rebuild of existing aggregates.
Captures the five safety rules that keep the hybrid coherent (tombstone what is
referenced, hard-delete only leaves, read-time tolerant joins, self-contained
events, one-aggregate-one-consistency-boundary), grounded in the GDPR erase.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(codeql): triage Bucket 3 — masked-PII exposure FPs (PR #51)

Three cs/exposure-of-sensitive-information alerts dismissed: #42 (masked via
LogPiiMasking.MaskEmail, false positive) and #40/#41 (OTel-redaction test fixtures,
used in tests). Records the verified finding that this query hardcodes its
Sanitizer and ignores Models-as-Data — so a data-extension sanitizer model is
impossible, and a custom-query fork would re-flag ~19 already-dismissed alerts
(rule-id-keyed dismissals). Hence dismissal, with standing guidance for future
masked-PII log sites.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
windischb added a commit that referenced this pull request Jun 9, 2026
…ize contract + overflow fix (waves 0–6) (#63)

* feat(admin-ui): create-flow defect fixes + German i18n sweep (UI/UX wave 0+1)

Wave 0 — three defects surfaced and live-verified by the UI/UX audit:
- OAuth client create no longer dead-ends on a bare "HTTP 400 Bad Request":
  surface the server's actual message (e.g. "client_credentials must be linked
  to a ServiceAccount ...") in a top-of-modal error banner. Read the response
  body's `error`/`detail` field instead of the never-populated `Message`.
- Roles: the Permissions tab + catalog picker now render in the *create* modal
  (previously edit-only), so an admin can assign permissions while creating a
  role. Backend/DTO already accepted PermissionIds on create — this only ungates
  the three isCreate template guards.
- User create/update: reject malformed emails (e.g. "notanemail") client-side
  (inline error + disabled submit) and server-side (new DomainErrors.User.
  EmailInvalid + format guard in CreateUser/UpdateUser handlers).

Wave 1 — German i18n sweep (de.json was missing ~190 used keys; their English
fallbacks leaked into the German UI):
- New useGridLocale() composable → German search placeholder ("Suchen…") and
  empty overlay ("Keine Einträge vorhanden") applied to all 10 list grids.
- ~190 missing keys translated (OAuth client/scope/api modals + dual-list
  labels, roles, groups, apps, realm-settings DCR, profile, consent, login,
  bootstrap, logout, change-requests, …).
- Modal-title verb order fixed ("Erstelle X" → "X erstellen") for user/role/group.
- Dropped the "Keycloak-style" competitor reference from user-facing copy.
- Reworded "(eine pro Zeile)" labels that actually sit over add-row grids.

Verified live against ghcr.io/cocoar-dev/modgud:beta (local cold-start) through
the dev server — all five changes confirmed in the browser. FE type-check and
BE build both green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(admin-ui): shared grid pass — empty-states, row-open cue, truncation tooltips (UI/UX wave 2)

Wave 2 of the admin-UI UX remediation (REPORT.md §7 item 7): one shared
change across all 14 admin list/log/queue grids instead of per-view fixes.

useGridLocale() is extended into the single shared grid layer:
- applyListGridDefaults(builder, { openable }) wires the German locale
  overlay, a shared defaultColDef, and (opt-in) the row-open affordance.
- sharedDefaultColDef carries only tooltipValueGetter (full value on hover
  for truncated cells). flex/minWidth are deliberately NOT defaulted: in
  AG-Grid an inherited flex overrides explicit column widths and an inherited
  minWidth clamps narrow columns up, which would break the fixed/pinned
  identifier and icon columns. Flex priority is set per-column instead.
- Icon columns opt out of the tooltip (() => null) so they never surface the
  raw lucide name ('check').
- Row-open cue is a pure-CSS pointer + hover class (admin-grid-row--openable),
  no behaviour change — keeps cell-double-click, selection and context menus.

New GridEmptyState.vue renders an onboarding empty-state (icon + one-line
concept definition + optional CTA) as a sibling to the grid, gated on the
store's readiness flag AND zero *source* rows — so a search/filter-empty grid
keeps the grid and its localized "Keine Einträge vorhanden" overlay rather than
wrongly telling the user to create the first record. Log/queue views use the
no-CTA variant.

ServiceAccountsView + AuthLogView + AuditLogView + ChangeRequestsView adopt
useGridLocale net-new, which also fixes their untranslated "Search..." /
"No Rows To Show" overlays (finding 36). ~14 German emptyHint strings added to
de.json with English fallbacks inline.

Live-verified against the :beta container: empty-state + CTA→create modal,
truncation tooltip, row-hover cue, icon cell shows no raw-token tooltip,
German chrome on the net-new adopters. pnpm type-check green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(admin-ui): modal-size contract + form-layout pass — kill the dead half (UI/UX wave 3)

Wave 3 of the admin-UI UX remediation (REPORT.md Phase 3) — the owner's core
complaint: cramped / unpredictable modals and stretched field arrangement.

Modal-size contract (router/index.ts). Replace the per-modal one-off size
constants with a named system (MODAL_MD / MODAL_LG / MODAL_FULL) using two
height strategies:
- cap-to-content (height:auto + maxHeight) for single forms, so the panel
  sizes to its content instead of forcing 90vh with a dead lower half. Proven
  by the old SERVICE_ACCOUNT size; drive the family toward ScopeDetails.
- stable tall frame (height==minHeight==maxHeight) for tabbed / grid / editor
  modals whose flex:1 dual-listbox / AG-Grid / Monaco children need a definite
  ancestor height. Big sizes keep vw width + maxWidth cap and NO minWidth rem
  floor (the documented viewport-overflow gotcha).
Remap: Scope/Realm/Role/ServiceAccount → MD; API → MD with a minHeight floor so
selecting an App fills reserved space instead of jumping the frame; User → a
cap-to-content fluid size (compact create, no dead half) with the Groups
dual-listbox carrying its own explicit height so edit still works; Group keeps a
tall frame (Monaco + two dual-listboxes); IdpClaims/LoginProvider/ScheduledJob/
ConsistencyCheck → LG; App/Client → FULL.

Form layout:
- UserDetails: the email-verified toggle moves OUT of the Email field to the
  form's end, so it no longer injects between Email and Username (the create
  layout-jump, #10). General form wrapped in the new max-width column.
- main.css: app-level .modal-form-col (~720px form column) + per-field width
  caps (.field-name/.field-email/.field-enum/.field-num). Controls obey their
  container, so capping the CoarFormField parent suffices — no vue-ui fork.
  Applied to UserDetails and the LoginProvider General tab (which must stay wide
  for its Monaco / dual ClaimMapEditor tabs but should not stretch the form).
- New ColorField.vue: hex/CSS-color text input + a swatch that doubles as a live
  preview and OS color-picker trigger + inline validity (#11). Replaces the raw
  hex inputs in BrandingView (PrimaryColor) and LoginProvider (Button-Farbe).

Live-verified against the :beta container at 1440×900: User create is now
cap-to-content (dead half gone) while edit's Groups dual-listbox renders at 50vh;
Scope/API/Role compact; Client (FULL) and LoginProvider (LG) stable; confirm-email
appears at the form end without shifting fields; ColorField preview updates live.
pnpm type-check green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(admin-ui): polish pass — AA primary contrast, header model, maintenance + dashboard a11y (UI/UX wave 4)

Wave 4 of the admin-UI UX remediation (REPORT.md Phase 4 polish). Honest
corrections to the brief's framing after re-verifying each claim:

#12 Primary-button token / login contrast. Login and admin do NOT diverge —
the login submit button omits `variant`, so it already renders the SAME default
`coar-button--primary` as admin primaries. The real defect is the shared
`--coar-accent` (#1183CD) giving only ~4.08:1 white-on-blue, below WCAG AA. One
root override in main.css — `--coar-accent: #1077be` — lifts every primary
button (login submit, admin variant=primary, the modal footer confirm) to
4.77:1 at once, keeping the hue and staying above the ramp's fixed hover/active
lightness so buttons still darken on hover. The realm-create CTA and
modal-confirm-as-footer-anchor were already satisfied by the wave-3 ModalLayout.

#13 Header model. Converge the two breadcrumb-array outliers onto the app-wide
string-subtitle model: ScheduledJobList (its leading "Administration" crumb only
duplicated the title); PageEditorView keeps its hierarchy as text ("Pages ·
<name>"). The dashboard stays title-only (a landing page, deliberately exempt).

#15 Maintenance + observability. The destructive "Rebuild Projections" fired
immediately — now it sits behind a CoarPopconfirm and uses the danger variant so
it no longer reads as a benign action (mirrors RealmSettings' rotate-key twin).
The observability sparkline drew a blank chart on an empty window — now shows an
empty-state message, like the activity/error feeds already do.

#14 Dashboard. Measured a11y/affordance polish, NOT a redesign (tiles already
navigate on click): tiles get role=button + tabindex + Enter/Space keyboard
activation + a focus ring + a drill-down chevron revealed on hover/focus; the
bad/warn KPI values pair their colour with an alert icon + an sr-only "Achtung"
so status isn't colour-only; the Login-Provider rows pair the status dot with an
"Aktiviert/Deaktiviert" text tag. (The subjective personal-vs-ops reorder was
deliberately left out.)

Live-verified against the :beta container: primary contrast 4.77:1 with correct
hover darkening (resolved oklch ramp checked in-page); ScheduledJobs string
subtitle; rebuild danger button + confirm popover; dashboard chevron on hover,
role=button, "Achtung" status, provider text tag. pnpm type-check green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(admin-ui): wave 5 — MD form width caps, group-modal cap-to-content, dashboard sections

Closes the three deferred follow-ups from the UI/UX remediation (waves 0–4).

#1 MD form per-field width caps (Realm/Scope/API/Role/ServiceAccount): tag the
full-width single-line fields with the existing .field-* caps — .field-enum (18rem)
on the Application selects, .field-name (24rem) on Description / Name / AccountName /
Purpose. No new CSS, no .modal-form-col (a no-op at 42rem). Multi-line list editors
(RealmDomains, EditableStringList, permission checklist) stay full-width. Also drop
two dead ModalLayout width= props (Scope 40rem, ServiceAccount 48rem) — modal size is
route-owned and the prop is ignored.

#2 Group modal — kill the create "dead half": move GROUP_MODAL_SIZE off the fixed
82vh LG frame to cap-to-content (60vw / 52rem, minHeight 30rem floor) so the
create-landing General tab collapses to its content. The heavy edit tabs carry their
OWN explicit section height (.editor-section 50vh for Members/Roles/Script,
.effective-section 32vh for the read-only Effective lists) so they survive
cap-to-content instead of collapsing — .flex-section drops flex:1. Cap the General
fields with .modal-form-col + .field-*. Drop the dead width=44rem prop.

#3 Dashboard — labelled personal/ops sections: split the single KPI list into
personalKpiTiles + opsKpiTiles, rendered under two labelled bands ("Mein Konto" /
"Realm-Betrieb"). The ops band collapses entirely for viewers without ops perms.
Extract the KPI card into KpiCard.vue (+ kpiTile.ts types) so both grids share it.
The ops grid stays count-aware (centered for <=2 tiles) so a lone tile doesn't float
alone in a 6-col row.

Live-verified @1440x900 against the :beta backend; vue-tsc --build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(admin-ui): wave 6 — universal modal-overflow fix + form-layout redesign

Modal overflow (owner blocker): cap-to-content modals (height:auto +
max-height) could not internal-scroll once content exceeded the cap. The
overlay panel's explicit height:100% resolved to auto against the
auto-height host, escaping the max-height clamp — so tall forms laid out
at full content height and overflowed the viewport, leaving the
footer/Save button unreachable. Dropping height:100% lets the host's
flex align-items:stretch size the panel to the *clamped* cross-size:
short forms still cap to content (no dead space), tall forms scroll
internally with the footer pinned. Definite-height modals (LG/FULL) are
unaffected — align-stretch fills the definite height just as height:100%
did, so Monaco / dual-listbox / AG-Grid keep a definite ancestor.

Form-layout redesign (8 detail modals): one shared grid contract
(.modal-form*) replaces content-arbitrary rem-width fields — labelled
sections, paired short fields, full-width prose/lists, and visible
inline field-hints (vue-ui's :hint is a hover popover only in 2.5.2).
Applied to Group, Role, Scope, API, Realm, ServiceAccount, User
(General) and LoginProvider (Allgemein); raw checkboxes → CoarCheckbox;
German microcopy de-jargoned in de.json.

Tab-switch resize fix: tabbed modals keep one fixed size across tabs
(Role 33rem, Group 80vh, User-edit pins .user-edit-frame at 60vh).

Verified live across all 11 modal types: footer reachable, correct
scroll/cap behaviour, no inner-widget collapse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant