further things around login

This commit is contained in:
2026-03-07 21:21:47 +01:00
parent fb6f87374f
commit 9168730a8a
18 changed files with 408 additions and 42 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,109 @@
{
"task_id": "AUTH-LOGIN-A11Y-REGRESSION-001",
"plan_ref": "agent-system/runs/AUTH-LOGIN-A11Y-REGRESSION-001/plan.json",
"status": "done",
"changed_files": [
{
"path": "pages/auth/login(login).phtml",
"summary": "In der choose_tenant Stage wurde eine semantische legend mit Klasse login-tenant-select-label ergaenzt, um die Fieldset-Gruppenbezeichnung und den erwarteten A11y-Marker wiederherzustellen."
},
{
"path": "agent-system/runs/AUTH-LOGIN-A11Y-REGRESSION-001/plan.json",
"summary": "Neuer isolierter Run-Plan fuer den Auth-Login-A11y-Regression-Fix angelegt."
}
],
"guard_evidence": [
{
"guard_id": "GR-UI-009",
"status": "pass",
"evidence": "Der fehlende Marker fuer die Tenant-Auswahl wurde im bestehenden Fieldset-Kontext wiederhergestellt; AuthLoginAccessibilityContractTest ist gruen."
},
{
"guard_id": "GR-UI-013",
"status": "pass",
"evidence": "Die Wiederherstellung erfolgte semantisch als legend innerhalb des Fieldsets, ohne ARIA-Hacks oder nicht-semantische Ersatzstrukturen."
},
{
"guard_id": "GR-UI-010",
"status": "pass",
"evidence": "Der eingefuegte Label-Text nutzt t('Select tenant') und fuegt keine hartkodierte UI-Zeichenkette ein."
},
{
"guard_id": "GR-TEST-001",
"status": "pass",
"evidence": "Der betroffene Contract-Test und die komplette Test-Suite wurden ausgefuehrt; der vorherige Failure ist behoben und QG-001 ist wieder gruen."
}
],
"commands": [
{
"cmd": "docker compose exec php vendor/bin/phpunit tests/Architecture/AuthLoginAccessibilityContractTest.php --no-progress",
"result": "pass"
},
{
"cmd": "docker compose exec php vendor/bin/phpunit",
"result": "pass"
},
{
"cmd": "docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress",
"result": "pass"
},
{
"cmd": "docker compose exec php vendor/bin/phpunit tests/Architecture/CoreStarterkitContractTest.php",
"result": "pass"
},
{
"cmd": "docker compose exec php composer cs:check",
"result": "pass"
}
],
"quality_gate_results": [
{
"gate_id": "QG-001",
"result": "pass",
"notes": "PHPUnit full: 454 tests, 10879 assertions, warnings/deprecations vorhanden aber keine Failures."
},
{
"gate_id": "QG-002",
"result": "pass",
"notes": "PHPStan: [OK] No errors."
},
{
"gate_id": "QG-003",
"result": "pass",
"notes": "CoreStarterkitContractTest: OK (11 tests, 6137 assertions)."
},
{
"gate_id": "QG-006",
"result": "pass",
"notes": "composer cs:check: 0 von 514 Dateien fixbar."
}
],
"test_results": [
{
"name": "AuthLoginAccessibilityContractTest",
"result": "pass",
"notes": "OK (4 tests, 85 assertions)."
},
{
"name": "PHPUnit Full",
"result": "pass",
"notes": "OK, but there were issues: Warnings 1, Deprecations 1."
},
{
"name": "PHPStan",
"result": "pass",
"notes": "No errors."
},
{
"name": "CoreStarterkitContractTest",
"result": "pass",
"notes": "OK (11 tests, 6137 assertions)."
},
{
"name": "PHP CS Check",
"result": "pass",
"notes": "0/514 files fixable."
}
],
"open_items": []
}

View File

@@ -0,0 +1,128 @@
{
"task_id": "AUTH-LOGIN-A11Y-REGRESSION-001",
"summary": "Isolierter Fix fuer den QG-001 Blocker im Auth-Login-Flow: fehlender Tenant-Label-Marker login-tenant-select-label in der choose_tenant-Stage wiederherstellen, Accessibility-Semantik intakt halten und Full PHPUnit wieder gruen bekommen.",
"assumptions": [
"Der Fehler ist eine Regression im Markup von pages/auth/login(login).phtml und nicht in Service- oder Auth-Logik.",
"Der bestehende Contract-Test AuthLoginAccessibilityContractTest bleibt bestehen und wird nicht abgeschwaecht.",
"Es sind keine DB- oder Backend-Flow-Aenderungen erforderlich."
],
"scope": {
"in": [
"Tenant-Auswahl-Markup in pages/auth/login(login).phtml (choose_tenant stage)",
"Bei Bedarf minimale, additive CSS-Korrektur in web/css/pages/app-login.css fuer legend/label Darstellung",
"Verifikation ueber bestehenden Architecture-Contract-Test und Full PHPUnit"
],
"out": [
"Theme-Flow oder Tenant-Switch Logik ausserhalb Auth-Login",
"Aenderungen an Auth-Service-Businesslogik in lib/Service/Auth",
"Neues UI-Redesign oder Umbau des gesamten Auth-Login-Bereichs"
]
},
"guardrails": {
"required_guard_ids": [
"GR-UI-009",
"GR-UI-013",
"GR-UI-010",
"GR-TEST-001"
],
"required_quality_gate_ids": [
"QG-001",
"QG-002",
"QG-003",
"QG-006"
]
},
"success_criteria": [
{
"id": "SC-001",
"criterion": "Die choose_tenant-Stage enthaelt wieder eine klare textuelle Gruppenbezeichnung mit dem Marker login-tenant-select-label im Fieldset-Kontext."
},
{
"id": "SC-002",
"criterion": "AuthLoginAccessibilityContractTest ist gruen ohne Abschwaechung der Assertions."
},
{
"id": "SC-003",
"criterion": "QG-001 (Full PHPUnit) laeuft wieder erfolgreich ohne diesen Fail."
},
{
"id": "SC-004",
"criterion": "Keine Regression bei PHPStan/CoreStarterkitContract/CS-Check."
}
],
"implementation_steps": [
{
"id": "S1",
"title": "Regression reproduzieren und Ursache fixieren",
"description": "Aktuellen Failure in AuthLoginAccessibilityContractTest bestaetigen und die fehlende Marker-Stelle im choose_tenant Fieldset lokalisieren.",
"guard_refs": [
"GR-TEST-001",
"GR-UI-009"
]
},
{
"id": "S2",
"title": "Tenant-Label semantisch wiederherstellen",
"description": "In pages/auth/login(login).phtml innerhalb von fieldset.login-tenant-fieldset eine textuelle Gruppenbezeichnung mit Klasse login-tenant-select-label einfuegen (bevorzugt als legend), i18n-konform mit t().",
"guard_refs": [
"GR-UI-013",
"GR-UI-010"
]
},
{
"id": "S3",
"title": "Darstellung stabilisieren",
"description": "Nur falls notwendig minimale additive CSS-Anpassung fuer legend/label vornehmen, ohne bestehende Tastatur- oder Screenreader-Semantik zu verschlechtern.",
"guard_refs": [
"GR-UI-009",
"GR-UI-013"
]
},
{
"id": "S4",
"title": "Gates ausfuehren und evidenzbasiert abschliessen",
"description": "QG-001, QG-002, QG-003 und QG-006 ausfuehren und in execution-report dokumentieren.",
"guard_refs": [
"GR-TEST-001",
"GR-UI-009"
]
}
],
"tests": [
"docker compose exec php vendor/bin/phpunit tests/Architecture/AuthLoginAccessibilityContractTest.php --no-progress",
"docker compose exec php vendor/bin/phpunit",
"docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress",
"docker compose exec php vendor/bin/phpunit tests/Architecture/CoreStarterkitContractTest.php",
"docker compose exec php composer cs:check"
],
"acceptance_checks": [
"SC-001: login-tenant-select-label ist in pages/auth/login(login).phtml im choose_tenant-Fieldset vorhanden und textuell befuellt.",
"SC-002: AuthLoginAccessibilityContractTest meldet PASS.",
"SC-003: Full PHPUnit (QG-001) meldet PASS ohne den bisherigen Tenant-Label-Fehler.",
"SC-004: QG-002, QG-003 und QG-006 melden PASS."
],
"ux_notes": {
"affected_patterns": [
"Auth tenant selection fieldset in login choose_tenant stage"
],
"ui_states_required": [
"error",
"success"
],
"a11y_touchpoints": [
"Fieldset/legend semantics for tenant radio group",
"Text label visibility for tenant selection group",
"Keyboard-operable radio selection unchanged"
]
},
"risks": [
{
"risk": "Visuelle Nebenwirkung durch legend-Einfuehrung im bestehenden Login-Layout.",
"mitigation": "Nur minimale additive CSS-Anpassung, keine strukturellen Layout-Umbauten."
},
{
"risk": "Scheinbarer Fix durch Test-Anpassung statt Markup-Korrektur.",
"mitigation": "Contract-Test nicht abschwaechen; Marker semantisch im Template wiederherstellen."
}
]
}

View File

@@ -0,0 +1,17 @@
{
"task_id": "AUTH-LOGIN-A11Y-REGRESSION-001",
"verdict": "pass",
"checked_guard_ids": [
"GR-UI-009",
"GR-UI-013",
"GR-UI-010",
"GR-TEST-001"
],
"checked_quality_gate_ids": [
"QG-001",
"QG-002",
"QG-003",
"QG-006"
],
"findings": []
}

View File

@@ -21,7 +21,7 @@ Options:
Examples:
bin/agent-prompts.sh --task AUTH-LOGIN-REVIEW-001
bin/agent-prompts.sh --task AUTH-LOGIN-REVIEW-001 --role planner --goal "Auth/Login review"
bin/agent-prompts.sh --task REVIEW-USER-THEME-001 --role planner --goal "Auth/Login review"
bin/agent-prompts.sh --task TASK-42 --short
USAGE
}

View File

@@ -1,5 +1,6 @@
{
"Login": "Login",
"Login credentials": "Mit Zugangsdaten oder SSO anmelden",
"Login options": "Anmeldeoptionen",
"Register": "Registrieren",
"Password": "Passwort",
@@ -47,7 +48,7 @@
"Logout": "Logout",
"Account": "Konto",
"Please enter your credentials": "Bitte geben Sie Ihre Zugangsdaten ein",
"Enter your email to continue": "E-Mail eingeben und weiter",
"Enter your email to continue": "E-Mail eingeben um fortzufahren",
"Select your tenant to continue": "Mandant auswählen und weiter",
"Choose how you want to sign in": "Anmeldeart auswählen",
"Continue": "Weiter",
@@ -1200,5 +1201,6 @@
"Restore actions (7d)": "Wiederherstellungsaktionen (7d)",
"Trend vs previous 7d": "Trend vs. vorherige 7d",
"Top lifecycle reason codes (7d)": "Top-Lifecycle-Fehlercodes (7d)",
"Recent lifecycle risks (7d)": "Aktuelle Lifecycle-Risiken (7d)"
"Recent lifecycle risks (7d)": "Aktuelle Lifecycle-Risiken (7d)",
"Login progress": "Anmeldefortschritt"
}

View File

@@ -1,5 +1,6 @@
{
"Login": "Login",
"Login credentials": "Login credentials",
"Login options": "Login options",
"Register": "Register",
"Password": "Password",
@@ -1200,5 +1201,6 @@
"Restore actions (7d)": "Restore actions (7d)",
"Trend vs previous 7d": "Trend vs previous 7d",
"Top lifecycle reason codes (7d)": "Top lifecycle reason codes (7d)",
"Recent lifecycle risks (7d)": "Recent lifecycle risks (7d)"
"Recent lifecycle risks (7d)": "Recent lifecycle risks (7d)",
"Login progress": "Login progress"
}

View File

@@ -16,7 +16,8 @@ $authHelpContext = [
];
?>
<article>
<article class="login-card">
<?php require templatePath('partials/auth-logo.phtml'); ?>
<header>
<hgroup>
<h1><?php e(t('Forgot password')); ?></h1>

View File

@@ -36,24 +36,18 @@ if ($selectedTenantInitial === '') {
$errors = is_array($errors ?? null) ? $errors : [];
$errorNoticeId = $errors ? 'auth-login-error-notice' : '';
$loginHeading = t('Login');
$loginSubheading = t('Please enter your credentials');
$loginSubheading = t('Login credentials');
$authHelpContext = ['show_support' => true];
if ($stage === 'resolve_email') {
$loginHeading = t('Login');
$loginSubheading = t('Enter your email to continue');
$authHelpContext = [
'show_register' => true,
'show_support' => true,
];
} elseif ($stage === 'choose_tenant') {
$loginHeading = t('Select tenant');
$loginSubheading = t('Select your tenant to continue');
$authHelpContext = [
'show_support' => true,
];
} else {
$loginHeading = t('Login options');
$loginSubheading = t('Choose how you want to sign in');
$authHelpContext = [
'show_forgot' => !empty($selectedTenantMethods['local']),
'show_support' => true,
@@ -63,6 +57,7 @@ if ($stage === 'resolve_email') {
?>
<article class="login-card">
<?php require templatePath('partials/auth-logo.phtml'); ?>
<header>
<hgroup>
<h1><?php e($loginHeading); ?></h1>
@@ -70,6 +65,7 @@ if ($stage === 'resolve_email') {
</hgroup>
</header>
<?php if ($errors): ?>
<div id="<?php e($errorNoticeId); ?>" class="notice" data-variant="error" role="alert" aria-live="assertive" tabindex="-1">
<ul>
@@ -99,6 +95,7 @@ if ($stage === 'resolve_email') {
<input type="hidden" name="email" value="<?php e($email); ?>">
<input type="hidden" name="tenant_hint" value="<?php e($tenantHint); ?>">
<fieldset class="login-tenant-fieldset">
<legend class="login-tenant-select-label"><?php e(t('Select tenant')); ?></legend>
<div class="login-tenant-choice-grid">
<?php foreach ($tenantCandidates as $tenantCandidate): ?>
<?php

View File

@@ -22,7 +22,8 @@ $authHelpContext = [
'show_support' => true,
];
?>
<article>
<article class="login-card">
<?php require templatePath('partials/auth-logo.phtml'); ?>
<header>
<hgroup>
<h1><?php e(t('Register')); ?></h1>

View File

@@ -18,7 +18,8 @@ $authHelpContext = [
];
?>
<article>
<article class="login-card">
<?php require templatePath('partials/auth-logo.phtml'); ?>
<header>
<hgroup>
<h1><?php e(t('Reset password')); ?></h1>

View File

@@ -17,7 +17,8 @@ $authHelpContext = [
];
?>
<article>
<article class="login-card">
<?php require templatePath('partials/auth-logo.phtml'); ?>
<header>
<hgroup>
<h1><?php e(t('Verify code')); ?></h1>

View File

@@ -17,7 +17,8 @@ $authHelpContext = [
];
?>
<article>
<article class="login-card">
<?php require templatePath('partials/auth-logo.phtml'); ?>
<header>
<hgroup>
<h1><?php e(t('Verify email')); ?></h1>

View File

@@ -0,0 +1,10 @@
<?php
$authLogoUrl = appLogoUrl();
if ($authLogoUrl === '') {
return;
}
?>
<div class="login-logo">
<img src="<?php e($authLogoUrl); ?>" alt="<?php e(appTitle()); ?>" class="login-logo-img">
</div>

View File

@@ -0,0 +1,21 @@
<?php
/**
* @var int $authStepCurrent 1-based current step (1, 2, or 3)
* @var int $authStepTotal total number of steps (2 or 3)
*/
$authStepCurrent = (int) ($authStepCurrent ?? 0);
$authStepTotal = (int) ($authStepTotal ?? 0);
if ($authStepCurrent < 1 || $authStepTotal < 2) {
return;
}
?>
<div class="login-step-indicator" aria-hidden="true">
<?php for ($i = 1; $i <= $authStepTotal; $i++): ?>
<span class="login-step-indicator-dot<?php if ($i === $authStepCurrent): ?> is-active<?php elseif ($i < $authStepCurrent): ?> is-completed<?php endif; ?>"></span>
<?php if ($i < $authStepTotal): ?>
<span class="login-step-indicator-line<?php if ($i < $authStepCurrent): ?> is-completed<?php endif; ?>"></span>
<?php endif; ?>
<?php endfor; ?>
</div>

View File

@@ -550,6 +550,59 @@
background: transparent;
}
/* ── Logo ── */
.login-logo {
margin-bottom: 1.25rem;
}
.login-logo-img {
max-height: 2.5rem;
max-width: 10rem;
object-fit: contain;
}
/* ── Password toggle ── */
.login-password-wrapper {
position: relative;
display: block;
}
.login-password-wrapper > input {
padding-right: 2.75rem;
}
.login-password-toggle {
position: absolute;
right: 0.5rem;
top: calc(50% - 8px);
transform: translateY(-50%);
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
padding: 0;
margin: 0;
background: transparent;
border: 0;
color: var(--app-muted-color);
cursor: pointer;
font-size: 1rem;
border-radius: var(--app-border-radius);
transition: color 0.15s ease;
}
.login-password-toggle:hover {
color: var(--app-contrast);
}
.login-password-toggle:focus-visible {
outline: 2px solid var(--app-primary-focus);
outline-offset: 1px;
}
main.login-main {
min-height: 100dvh;
display: grid;
@@ -693,7 +746,7 @@
display: inline-block;
padding: 0 0.75rem;
font-size: 0.875rem;
color: var(--app-muted);
color: var(--app-muted-color);
background: var(--app-card-background-color);
}
@@ -754,14 +807,6 @@
overflow-wrap: anywhere;
}
.login-tenant-context-title {
margin: 0;
font-size: 0.78rem;
letter-spacing: 0.02em;
text-transform: uppercase;
color: var(--app-muted);
}
.login-tenant-context-name {
margin: 0;
font-size: 11px;
@@ -788,6 +833,8 @@
list-style: none;
margin: 0;
padding: 0;
gap: 10px;
font-size: 0.72rem;
}
@@ -798,20 +845,10 @@
padding: 0;
}
.login-help-links-item + .login-help-links-item::before {
content: "";
display: inline-block;
width: 1px;
height: 0.85em;
margin-right: 0.8rem;
background: color-mix(in srgb, var(--app-muted) 55%, transparent);
transform: translateY(1px);
}
.login-security-note {
font-size: 0.72rem;
margin-bottom: 1px;
color: color-mix(in srgb, var(--app-contrast) 78%, var(--app-muted));
color: color-mix(in srgb, var(--app-contrast) 78%, var(--app-muted-color));
}
.login-security-note i {
@@ -823,7 +860,7 @@
width: 100%;
display: block;
font-size: 9px;
color: color-mix(in srgb, var(--app-muted) 75%, transparent);
color: color-mix(in srgb, var(--app-muted-color) 75%, transparent);
}
form[aria-busy="true"] button[type="submit"],
@@ -832,10 +869,6 @@
cursor: wait;
}
.login-secondary-action {
margin-top: 1rem;
}
@media (prefers-reduced-motion: reduce) {
html[data-theme="dark"] #gradient {
animation: none;

View File

@@ -1,6 +1,7 @@
import './core/app-telemetry.js';
import './components/app-flash-auto-dismiss.js';
import './components/app-password-hints.js';
import './components/app-password-toggle.js';
const focusPrimaryErrorNotice = () => {
const errorNotice = document.querySelector(
@@ -29,6 +30,7 @@ const lockLoginSubmitButtons = () => {
return;
}
submitButton.disabled = true;
submitButton.setAttribute('aria-busy', 'true');
submitButton.dataset.submitState = 'loading';
});
});

View File

@@ -0,0 +1,40 @@
export function initPasswordToggles() {
const inputs = document.querySelectorAll('input[type="password"]');
if (!inputs.length) {return;}
inputs.forEach((input) => {
if (!(input instanceof HTMLInputElement)) {return;}
const label = input.closest('label');
if (!label) {return;}
const wrapper = document.createElement('div');
wrapper.className = 'login-password-wrapper';
const toggle = document.createElement('button');
toggle.type = 'button';
toggle.className = 'login-password-toggle';
toggle.setAttribute('aria-label', 'Show password');
toggle.tabIndex = -1;
toggle.innerHTML = '<i class="bi bi-eye" aria-hidden="true"></i>';
input.replaceWith(wrapper);
wrapper.appendChild(input);
wrapper.appendChild(toggle);
toggle.addEventListener('click', () => {
const isPassword = input.type === 'password';
input.type = isPassword ? 'text' : 'password';
toggle.innerHTML = isPassword
? '<i class="bi bi-eye-slash" aria-hidden="true"></i>'
: '<i class="bi bi-eye" aria-hidden="true"></i>';
toggle.setAttribute('aria-label', isPassword ? 'Hide password' : 'Show password');
input.focus();
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initPasswordToggles);
} else {
initPasswordToggles();
}