@@ -360,496 +360,490 @@
< div class = "auth-logo-text" >< span class = "brand-oxi" > Oxi</ span > Cloud</ div >
</ div >
<!-- Form paints immediately alongside the logo — the onMount
<!-- Form paints immediately alongside the logo — the onMount
probes (OIDC code exchange, session probe, providers
lookup) run concurrently and either redirect the user
away or upgrade the visible affordances (OIDC button,
magic-link toggle) in place. Guarding the whole form
behind `booting` caused a "logo only, then form" flash
on first paint. -->
< h1 class = "auth-title" >
{ #if mode === 'login' }
{ t ( 'auth.sign_in' , 'Sign in' )}
{ :else if mode === 'register' }
{ t ( 'auth.register' , 'Create account' )}
{ : else }
{ t ( 'auth.setup_title' , 'Initial setup' )}
{ /if }
</ h1 >
{ #if sessionExpiredNotice }
< div
class = "auth-error auth-error--dismissible"
style = "display: flex"
role = "alert"
data-testid = "login-session-expired-notice"
>
< span > { t ( 'auth.session_expired' , 'Your session expired. Please sign in again.' )} </ span >
< button
type = "button"
class = "auth-notice-dismiss"
aria-label = { t ( 'common.dismiss' , 'Dismiss' )}
data-testid="login-session-expired-dismiss-btn"
onclick = {() => ( sessionExpiredNotice = false )} > × </button
>
</ div >
{ /if }
{ #if postRegisterNotice && mode === 'login' }
< div
class = "auth-success auth-error--dismissible"
style = "display: flex"
role = "status"
data-testid = "login-post-register-notice"
>
< span > { postRegisterNotice } </ span >
< button
type = "button"
class = "auth-notice-dismiss"
aria-label = { t ( 'common.dismiss' , 'Dismiss' )}
data-testid="login-post-register-dismiss-btn"
onclick = {() => ( postRegisterNotice = null )} > × </button
>
</ div >
{ /if }
< h1 class = "auth-title" >
{ #if mode === 'login' }
<!-- Unified login form. One identifier + one (optional)
{ t ( 'auth.sign_in' , 'Sign in' )}
{ :else if mode === 'register' }
{ t ( 'auth.register' , 'Create account' )}
{ : else }
{ t ( 'auth.setup_title' , 'Initial setup' )}
{ /if }
</ h1 >
{ #if sessionExpiredNotice }
< div
class = "auth-error auth-error--dismissible"
style = "display: flex"
role = "alert"
data-testid = "login-session-expired-notice"
>
< span > { t ( 'auth.session_expired' , 'Your session expired. Please sign in again.' )} </ span >
< button
type = "button"
class = "auth-notice-dismiss"
aria-label = { t ( 'common.dismiss' , 'Dismiss' )}
data-testid="login-session-expired-dismiss-btn"
onclick = {() => ( sessionExpiredNotice = false )} > × </button
>
</ div >
{ /if }
{ #if postRegisterNotice && mode === 'login' }
< div
class = "auth-success auth-error--dismissible"
style = "display: flex"
role = "status"
data-testid = "login-post-register-notice"
>
< span > { postRegisterNotice } </ span >
< button
type = "button"
class = "auth-notice-dismiss"
aria-label = { t ( 'common.dismiss' , 'Dismiss' )}
data-testid="login-post-register-dismiss-btn"
onclick = {() => ( postRegisterNotice = null )} > × </button
>
</ div >
{ /if }
{ #if mode === 'login' }
<!-- Unified login form. One identifier + one (optional)
password field drive both flows:
* password filled → POST /api/auth/login
* password empty → POST /api/auth/magic-link/send
* password-only server → password field is required, no hint
* magic-link-only server → password field hides entirely -->
{ #if passwordLoginEnabled || magicLinkLoginEnabled }
{ #if error }
< div
class = { emailNotVerified ? 'auth-success' : 'auth-error' }
style="display: block "
role = "alert"
>
{ error }
</ div >
{ /if }
{ #if magicStatus }
< div
class = { magicStatus . ok
? 'auth-status auth-status-success'
: 'auth-status auth-status-error' }
role= { magicStatus . ok ? 'status' : 'alert' }
>
{ magicStatus . text }
</ div >
{ /if }
< form class = "auth-form" data-testid = "login-form" onsubmit = { onLogin } novalidate >
< div class = "auth-input-group" >
< label class = "auth-label" for = "login-username" >
{ t ( 'auth.login_identifier' , 'Username or email' )}
</ label >
< div class = "auth-input-wrap auth-input-wrap--user" >
< input
id = "login-username"
class = "auth-input"
data-testid = "login-username-input"
type = "text"
bind:value = { username }
bind:this= { loginIdentifierInput }
autocomplete = "username"
placeholder = { t (
'auth.login_identifier_placeholder' ,
'Enter your username or email'
)}
required
disabled = { busy }
/ >
</ div >
</ div >
{ #if passwordLoginEnabled }
< div class = "auth-input-group" >
< label class = "auth-label" for = "login-password" >
{ #if magicLinkLoginEnabled }
{ t ( 'auth.password_or_link_hint' , 'Password (leave blank for a sign-in link)' )}
{ : else }
{ t ( 'auth.password' , 'Password' )}
{ /if }
</ label >
< div class = "auth-input-wrap auth-input-wrap--lock has-toggle" >
< input
id = "login-password"
class = "auth-input"
data-testid = "login-password-input"
type = { showPassword ? 'text' : 'password' }
bind:value= { password }
onkeydown = { onPwKey }
onkeyup= { onPwKey }
autocomplete = "current-password"
required = { ! magicLinkLoginEnabled }
disabled= { busy }
/>
< button
type = "button"
class = "auth-pw-toggle"
aria-pressed = { showPassword }
data-testid="login-password-toggle-btn"
aria-label = { t ( 'auth.toggle_password' , 'Show password' )}
onclick= {() => ( showPassword = ! showPassword )}
></ button >
</ div >
{ #if capsOn }
< div class = "auth-caps-warning" > { t ( 'auth.caps_lock' , 'Caps Lock is on' )} </ div >
{ /if }
</ div >
{ /if }
< button
class = "auth-button"
type = "submit"
data-testid = "login-submit-btn"
disabled = { busy }
aria-busy= { busy }
>
{ #if busy }
{ submitAsMagicLink
? t ( 'auth.sending' , 'Sending…' )
: t ( 'auth.signing_in' , 'Signing in…' )}
{ :else if submitAsMagicLink }
{ t ( 'auth.magicLinkSubmit' , 'Send sign-in link' )}
{ : else }
{ t ( 'auth.sign_in' , 'Sign in' )}
{ /if }
</ button >
</ form >
{ /if }
{ #if oidc . enabled }
{ #if passwordLoginEnabled }
< div class = "auth-divider" >< span > { t ( 'auth.or' , 'or' )} </ span ></ div >
{ /if }
<!-- Backend OIDC authorize endpoint (not a SvelteKit route). -->
< a
class = "auth-button auth-button-oidc"
data-testid = "login-oidc-btn"
href = { oidc . authorize_endpoint }
rel="external"
{ #if passwordLoginEnabled || magicLinkLoginEnabled }
{ #if error }
< div
class = { emailNotVerified ? 'auth-success' : 'auth-error' }
style="display: block "
role = "alert"
>
{ t (
'auth.sso_login_provider' ,
{ provider : oidc.provider_name ?? 'SSO' },
' Sign in with {{ provider }} '
)}
</a>
{/if}
{#if passwordLoginEnabled}
<div class="auth-toggle">
{t(' auth . no_account ', ' No account ? ' )}
< button
class = "auth-toggle-link"
data-testid = "login-to-register-btn"
onclick = {() => ( mode = 'register' )}
>
{ t ( 'auth.register' , 'Create one' )}
</ button >
{ error }
</ div >
{ /if }
{ #if setupAvailable }
< div class = "auth-toggle" >
{ t ( 'auth.admin_setup' , 'First time?' )}
< button
class = "auth-toggle-link"
data-testid = "login-to-setup-btn"
onclick = {() => ( mode = 'setup' ) }
>
{ t ( 'auth.setup' , 'Set up administrator' )}
</ button >
{ #if magicStatus }
< div
class = { magicStatus . ok
? 'auth-status auth-status-success'
: 'auth-status auth-status-error' }
role= { magicStatus . ok ? 'status' : 'alert' }
>
{ magicStatus . text }
</ div >
{ /if }
{ :else if mode === 'register' }
{ #if regError } < div class = "auth-error" style = "display: block" role = "alert" >
{ regError }
</ div > { /if }
< form class = "auth-form" data-testid = "login-register-form" onsubmit = { onRegister } novalidate >
<!-- Email is the only required identifier since PR 18 — the
backend accepts email-only signup and mints a welcome
magic-link. Username is optional at this stage; the user
can claim a handle later via profile settings. -->
< form class = "auth-form" data-testid = "login-form" onsubmit = { onLogin } novalidate >
< div class = "auth-input-group" >
< label class = "auth-label" for = "reg-email" > { t ( 'auth.email' , 'Email' )} </ label >
< input
id = "reg-email"
class = "auth-input"
data-testid = "login-register-email-input"
type = "email"
bind:value = { regEmail }
bind:this= { registerEmailInput }
autocomplete = "email"
required
disabled = { busy }
/ >
</ div >
< div class = "auth-input-group" >
< label class = "auth-label" for = "reg-username" >
{ t ( 'auth.username_optional' , 'Username (optional)' )}
< label class = "auth-label" for = "login-username" >
{ t ( 'auth.login_identifier' , 'Username or email' )}
</ label >
< input
id = "reg-username"
class = "auth-input "
data-testid = "login-register-username -input"
bind:value = { regUsername }
autocomplete="username "
disabled = { busy }
/ >
< div class = "auth-input-wrap auth-input-wrap--user" >
< input
id = "login-username "
class = "auth -input"
data-testid = "login-username-input"
type = "text "
bind:value = { username }
bind:this= { loginIdentifierInput }
autocomplete = "username"
placeholder = { t ( 'auth.login_identifier_placeholder' , 'Enter your username or email' )}
required
disabled = { busy }
/ >
</ div >
</ div >
<!-- Password fields hide entirely when policy forbids password
login — the whole form becomes email-only in that mode. -->
{ #if passwordLoginEnabled }
< div class = "auth-input-group" >
< label class = "auth-label" for = "reg -password" >
{ t (
'auth.password_optional ' ,
'Password (optional — leave blank for a sign-in link)'
)}
< label class = "auth-label" for = "login -password" >
{ #if magicLinkLoginEnabled }
{ t ( 'auth.password_or_link_hint' , 'Password (leave blank for a sign-in link) ')}
{ : else }
{ t ( 'auth.password' , 'Password' )}
{ /if }
</ label >
< div class = "auth-input-wrap auth-input-wrap--lock has-toggle" >
< input
id = "reg -password"
id = "login -password"
class = "auth-input"
data-testid = "login-register- password-input"
type = { regShowPassword ? 'text' : 'password' }
bind:value= { regPassword }
onkeydown = { onRegPwKey }
onkeyup= { onRegPwKey }
autocomplete = "new -password"
data-testid = "login-password-input"
type = { showPassword ? 'text' : 'password' }
bind:value= { password }
onkeydown = { onPwKey }
onkeyup= { onPwKey }
autocomplete = "current -password"
required = { ! magicLinkLoginEnabled }
disabled= { busy }
/>
< button
type = "button"
class = "auth-pw-toggle"
aria-pressed = { regShowPassword }
data-testid="login-register- password-toggle-btn"
aria-pressed = { showPassword }
data-testid="login-password-toggle-btn"
aria-label = { t ( 'auth.toggle_password' , 'Show password' )}
onclick= {() => ( regShowPassword = ! regShowPassword )}
onclick= {() => ( showPassword = ! showPassword )}
></ button >
</ div >
{ #if regCapsOn }
{ #if capsOn }
< div class = "auth-caps-warning" > { t ( 'auth.caps_lock' , 'Caps Lock is on' )} </ div >
{ /if }
</ div >
{ #if ! regEmailOnly }
< div class = "auth-input-group" >
< label class = "auth-label" for = "reg-confirm"
> { t ( 'auth.confirm_password' , 'Confirm password' )} </ label
>
< div class = "auth-input-wrap auth-input-wrap--lock has-toggle" >
< input
id = "reg-confirm"
class = "auth-input"
data-testid = "login-register-confirm-input"
type = { regShowConfirm ? 'text' : 'password' }
bind:value= { regConfirm }
onkeydown = { onRegPwKey }
onkeyup= { onRegPwKey }
autocomplete = "new-password"
required
disabled = { busy }
/ >
< button
type = "button"
class = "auth-pw-toggle"
aria-pressed = { regShowConfirm }
data-testid="login-register-confirm-toggle-btn"
aria-label = { t ( 'auth.toggle_password' , 'Show password' )}
onclick= {() => ( regShowConfirm = ! regShowConfirm )}
></ button >
</ div >
{ #if matchState }
< div
class = "auth-match show { matchState === 'ok'
? 'auth-match--ok'
: 'auth-match--bad' } "
>
{ matchState === 'ok'
? t ( 'auth.passwords_match' , 'Passwords match' )
: t ( 'auth.passwords_mismatch' , "Passwords don't match" )}
</ div >
{ /if }
</ div >
{ /if }
{ /if }
< button
class = "auth-button"
type = "submit"
data-testid = "login-register-submit-btn"
disabled = { busy }
aria-busy= { busy }
>
{ ! passwordLoginEnabled || regEmailOnly
? t ( 'auth.register_email_only' , 'Send me a sign-in link' )
: t ( 'auth.register' , 'Create account' )}
</ button >
</ form >
< div class = "auth-toggle" >
{ t ( 'auth.have_account' , 'Already have an account?' )}
< button
class = "auth-toggle-link"
data-testid = "login-register-to-login-btn"
onclick = {() => ( mode = 'login' )}
>
{ t ( 'auth.sign_in' , 'Sign in' )}
</ button >
</ div >
{ : else }
< div class = "setup-steps" >
< div class = "setup-step" >
< div class = "step-number active" > 1</ div >
< div class = "step-title active" > { t ( 'auth.setup_step1' , 'Admin' )} </ div >
</ div >
< div class = "setup-step" >
< div class = "step-number" > 2</ div >
< div class = "step-title" > { t ( 'auth.setup_step2' , 'System' )} </ div >
</ div >
< div class = "setup-step" >
< div class = "step-number" > 3</ div >
< div class = "step-title" > { t ( 'auth.setup_step3' , 'Completed' )} </ div >
</ div >
</ div >
{ #if setupError } < div class = "auth-error" style = "display: block" role = "alert" >
{ setupError }
</ div > { /if }
{ #if setupSuccess } < div class = "auth-success" style = "display: block" > { setupSuccess } </ div > { /if }
< form class = "auth-form" data-testid = "login-setup-form" onsubmit = { onSetup } novalidate >
< div class = "auth-input-group" >
< label class = "auth-label" for = "setup-username" >
{ t ( 'auth.admin_username' , 'Administrator username' )}
</ label >
< div class = "auth-input-wrap auth-input-wrap--user" >
< input
id = "setup-username"
class = "auth-input"
data-testid = "login-setup-username-input"
type = "text"
value = "admin"
readonly
/>
</ div >
</ div >
< div class = "auth-input-group" >
< label class = "auth-label" for = "setup-email" >
{ t ( 'auth.admin_email' , 'Administrator email' )}
</ label >
< div class = "auth-input-wrap auth-input-wrap--mail" >
< input
id = "setup-email"
class = "auth-input"
data-testid = "login-setup-email-input"
type = "email"
bind:value = { setupEmail }
bind:this= { setupEmailInput }
autocomplete = "email"
required
disabled = { busy }
/ >
</ div >
</ div >
< div class = "auth-input-group" >
< label class = "auth-label" for = "setup-password" >
{ t ( 'auth.admin_password' , 'Administrator password' )}
</ label >
< div class = "auth-input-wrap auth-input-wrap--lock has-toggle" >
< input
id = "setup-password"
class = "auth-input"
data-testid = "login-setup-password-input"
type = { setupShowPassword ? 'text' : 'password' }
bind:value= { setupPassword }
onkeydown = { onSetupPwKey }
onkeyup= { onSetupPwKey }
autocomplete = "new-password"
minlength = "8"
required
disabled = { busy }
/ >
< button
type = "button"
class = "auth-pw-toggle"
aria-pressed = { setupShowPassword }
data-testid="login-setup-password-toggle-btn"
aria-label = { t ( 'auth.toggle_password' , 'Show password' )}
onclick= {() => ( setupShowPassword = ! setupShowPassword )}
></ button >
</ div >
{ #if setupCapsOn }
< div class = "auth-caps-warning" > { t ( 'auth.caps_lock' , 'Caps Lock is on' )} </ div >
{ /if }
</ div >
< div class = "auth-input-group" >
< label class = "auth-label" for = "setup-confirm" >
{ t ( 'auth.confirm_password' , 'Confirm password' )}
</ label >
< div class = "auth-input-wrap auth-input-wrap--lock has-toggle" >
< input
id = "setup-confirm"
class = "auth-input"
data-testid = "login-setup-confirm-input"
type = { setupShowConfirm ? 'text' : 'password' }
bind:value= { setupConfirm }
onkeydown = { onSetupPwKey }
onkeyup= { onSetupPwKey }
autocomplete = "new-password"
required
disabled = { busy }
/ >
< button
type = "button"
class = "auth-pw-toggle"
aria-pressed = { setupShowConfirm }
data-testid="login-setup-confirm-toggle-btn"
aria-label = { t ( 'auth.toggle_password' , 'Show password' )}
onclick= {() => ( setupShowConfirm = ! setupShowConfirm )}
></ button >
</ div >
{ #if setupMatchState }
< div
class = "auth-match show { setupMatchState === 'ok'
? 'auth-match--ok'
: 'auth-match--bad' } "
>
{ setupMatchState === 'ok'
? t ( 'auth.passwords_match' , 'Passwords match' )
: t ( 'auth.passwords_mismatch' , "Passwords don't match" )}
</ div >
{ /if }
</ div >
< button
class = "auth-button"
type = "submit"
data-testid = "login-setup- submit-btn"
data-testid = "login-submit-btn"
disabled = { busy }
aria-busy= { busy }
>
{ t ( 'auth.create_admin' , 'Create administrator' ) }
{ #if busy }
{ submitAsMagicLink
? t ( 'auth.sending' , 'Sending…' )
: t ( 'auth.signing_in' , 'Signing in…' )}
{ :else if submitAsMagicLink }
{ t ( 'auth.magicLinkSubmit' , 'Send sign-in link' )}
{ : else }
{ t ( 'auth.sign_in' , 'Sign in' )}
{ /if }
</ button >
</ form >
{ /if }
{ #if oidc . enabled }
{ #if passwordLoginEnabled }
< div class = "auth-divider" >< span > { t ( 'auth.or' , 'or' )} </ span ></ div >
{ /if }
<!-- Backend OIDC authorize endpoint (not a SvelteKit route). -->
< a
class = "auth-button auth-button-oidc"
data-testid = "login-oidc-btn"
href = { oidc . authorize_endpoint }
rel="external"
>
{ t (
'auth.sso_login_provider' ,
{ provider : oidc.provider_name ?? 'SSO' },
' Sign in with {{ provider }} '
)}
</a>
{/if}
{#if passwordLoginEnabled}
<div class="auth-toggle">
{ t ( 'auth.back_to_login' , 'Already configured?' )}
{t(' auth . no_account ', ' No account ? ' )}
< button
class = "auth-toggle-link"
data-testid = "login-setup-to-login -btn"
onclick = {() => ( mode = 'login ' )}
data-testid = "login-to-register -btn"
onclick = {() => ( mode = 'register ' )}
>
{ t ( 'auth.sign_in' , 'Sign in ')}
{ t ( 'auth.register' , 'Create one ')}
</ button >
</ div >
{ /if }
{ #if setupAvailable }
< div class = "auth-toggle" >
{ t ( 'auth.admin_setup' , 'First time?' )}
< button
class = "auth-toggle-link"
data-testid = "login-to-setup-btn"
onclick = {() => ( mode = 'setup' )}
>
{ t ( 'auth.setup' , 'Set up administrator' )}
</ button >
</ div >
{ /if }
{ :else if mode === 'register' }
{ #if regError } < div class = "auth-error" style = "display: block" role = "alert" >
{ regError }
</ div > { /if }
< form class = "auth-form" data-testid = "login-register-form" onsubmit = { onRegister } novalidate >
<!-- Email is the only required identifier since PR 18 — the
backend accepts email-only signup and mints a welcome
magic-link. Username is optional at this stage; the user
can claim a handle later via profile settings. -->
< div class = "auth-input-group" >
< label class = "auth-label" for = "reg-email" > { t ( 'auth.email' , 'Email' )} </ label >
< input
id = "reg-email"
class = "auth-input"
data-testid = "login-register-email-input"
type = "email"
bind:value = { regEmail }
bind:this= { registerEmailInput }
autocomplete = "email"
required
disabled = { busy }
/ >
</ div >
< div class = "auth-input-group" >
< label class = "auth-label" for = "reg-username" >
{ t ( 'auth.username_optional' , 'Username (optional)' )}
</ label >
< input
id = "reg-username"
class = "auth-input"
data-testid = "login-register-username-input"
bind:value = { regUsername }
autocomplete="username"
disabled = { busy }
/ >
</ div >
<!-- Password fields hide entirely when policy forbids password
login — the whole form becomes email-only in that mode. -->
{ #if passwordLoginEnabled }
< div class = "auth-input-group" >
< label class = "auth-label" for = "reg-password" >
{ t ( 'auth.password_optional' , 'Password (optional — leave blank for a sign-in link)' )}
</ label >
< div class = "auth-input-wrap auth-input-wrap--lock has-toggle" >
< input
id = "reg-password"
class = "auth-input"
data-testid = "login-register-password-input"
type = { regShowPassword ? 'text' : 'password' }
bind:value= { regPassword }
onkeydown = { onRegPwKey }
onkeyup= { onRegPwKey }
autocomplete = "new-password"
disabled = { busy }
/ >
< button
type = "button"
class = "auth-pw-toggle"
aria-pressed = { regShowPassword }
data-testid="login-register-password-toggle-btn"
aria-label = { t ( 'auth.toggle_password' , 'Show password' )}
onclick= {() => ( regShowPassword = ! regShowPassword )}
></ button >
</ div >
{ #if regCapsOn }
< div class = "auth-caps-warning" > { t ( 'auth.caps_lock' , 'Caps Lock is on' )} </ div >
{ /if }
</ div >
{ #if ! regEmailOnly }
< div class = "auth-input-group" >
< label class = "auth-label" for = "reg-confirm"
> { t ( 'auth.confirm_password' , 'Confirm password' )} </ label
>
< div class = "auth-input-wrap auth-input-wrap--lock has-toggle" >
< input
id = "reg-confirm"
class = "auth-input"
data-testid = "login-register-confirm-input"
type = { regShowConfirm ? 'text' : 'password' }
bind:value= { regConfirm }
onkeydown = { onRegPwKey }
onkeyup= { onRegPwKey }
autocomplete = "new-password"
required
disabled = { busy }
/ >
< button
type = "button"
class = "auth-pw-toggle"
aria-pressed = { regShowConfirm }
data-testid="login-register-confirm-toggle-btn"
aria-label = { t ( 'auth.toggle_password' , 'Show password' )}
onclick= {() => ( regShowConfirm = ! regShowConfirm )}
></ button >
</ div >
{ #if matchState }
< div
class = "auth-match show { matchState === 'ok'
? 'auth-match--ok'
: 'auth-match--bad' } "
>
{ matchState === 'ok'
? t ( 'auth.passwords_match' , 'Passwords match' )
: t ( 'auth.passwords_mismatch' , "Passwords don't match" )}
</ div >
{ /if }
</ div >
{ /if }
{ /if }
< button
class = "auth-button"
type = "submit"
data-testid = "login-register-submit-btn"
disabled = { busy }
aria-busy= { busy }
>
{ ! passwordLoginEnabled || regEmailOnly
? t ( 'auth.register_email_only' , 'Send me a sign-in link' )
: t ( 'auth.register' , 'Create account' )}
</ button >
</ form >
< div class = "auth-toggle" >
{ t ( 'auth.have_account' , 'Already have an account?' )}
< button
class = "auth-toggle-link"
data-testid = "login-register-to-login-btn"
onclick = {() => ( mode = 'login' )}
>
{ t ( 'auth.sign_in' , 'Sign in' )}
</ button >
</ div >
{ : else }
< div class = "setup-steps" >
< div class = "setup-step" >
< div class = "step-number active" > 1</ div >
< div class = "step-title active" > { t ( 'auth.setup_step1' , 'Admin' )} </ div >
</ div >
< div class = "setup-step" >
< div class = "step-number" > 2</ div >
< div class = "step-title" > { t ( 'auth.setup_step2' , 'System' )} </ div >
</ div >
< div class = "setup-step" >
< div class = "step-number" > 3</ div >
< div class = "step-title" > { t ( 'auth.setup_step3' , 'Completed' )} </ div >
</ div >
</ div >
{ #if setupError } < div class = "auth-error" style = "display: block" role = "alert" >
{ setupError }
</ div > { /if }
{ #if setupSuccess } < div class = "auth-success" style = "display: block" > { setupSuccess } </ div > { /if }
< form class = "auth-form" data-testid = "login-setup-form" onsubmit = { onSetup } novalidate >
< div class = "auth-input-group" >
< label class = "auth-label" for = "setup-username" >
{ t ( 'auth.admin_username' , 'Administrator username' )}
</ label >
< div class = "auth-input-wrap auth-input-wrap--user" >
< input
id = "setup-username"
class = "auth-input"
data-testid = "login-setup-username-input"
type = "text"
value = "admin"
readonly
/>
</ div >
</ div >
< div class = "auth-input-group" >
< label class = "auth-label" for = "setup-email" >
{ t ( 'auth.admin_email' , 'Administrator email' )}
</ label >
< div class = "auth-input-wrap auth-input-wrap--mail" >
< input
id = "setup-email"
class = "auth-input"
data-testid = "login-setup-email-input"
type = "email"
bind:value = { setupEmail }
bind:this= { setupEmailInput }
autocomplete = "email"
required
disabled = { busy }
/ >
</ div >
</ div >
< div class = "auth-input-group" >
< label class = "auth-label" for = "setup-password" >
{ t ( 'auth.admin_password' , 'Administrator password' )}
</ label >
< div class = "auth-input-wrap auth-input-wrap--lock has-toggle" >
< input
id = "setup-password"
class = "auth-input"
data-testid = "login-setup-password-input"
type = { setupShowPassword ? 'text' : 'password' }
bind:value= { setupPassword }
onkeydown = { onSetupPwKey }
onkeyup= { onSetupPwKey }
autocomplete = "new-password"
minlength = "8"
required
disabled = { busy }
/ >
< button
type = "button"
class = "auth-pw-toggle"
aria-pressed = { setupShowPassword }
data-testid="login-setup-password-toggle-btn"
aria-label = { t ( 'auth.toggle_password' , 'Show password' )}
onclick= {() => ( setupShowPassword = ! setupShowPassword )}
></ button >
</ div >
{ #if setupCapsOn }
< div class = "auth-caps-warning" > { t ( 'auth.caps_lock' , 'Caps Lock is on' )} </ div >
{ /if }
</ div >
< div class = "auth-input-group" >
< label class = "auth-label" for = "setup-confirm" >
{ t ( 'auth.confirm_password' , 'Confirm password' )}
</ label >
< div class = "auth-input-wrap auth-input-wrap--lock has-toggle" >
< input
id = "setup-confirm"
class = "auth-input"
data-testid = "login-setup-confirm-input"
type = { setupShowConfirm ? 'text' : 'password' }
bind:value= { setupConfirm }
onkeydown = { onSetupPwKey }
onkeyup= { onSetupPwKey }
autocomplete = "new-password"
required
disabled = { busy }
/ >
< button
type = "button"
class = "auth-pw-toggle"
aria-pressed = { setupShowConfirm }
data-testid="login-setup-confirm-toggle-btn"
aria-label = { t ( 'auth.toggle_password' , 'Show password' )}
onclick= {() => ( setupShowConfirm = ! setupShowConfirm )}
></ button >
</ div >
{ #if setupMatchState }
< div
class = "auth-match show { setupMatchState === 'ok'
? 'auth-match--ok'
: 'auth-match--bad' } "
>
{ setupMatchState === 'ok'
? t ( 'auth.passwords_match' , 'Passwords match' )
: t ( 'auth.passwords_mismatch' , "Passwords don't match" )}
</ div >
{ /if }
</ div >
< button
class = "auth-button"
type = "submit"
data-testid = "login-setup-submit-btn"
disabled = { busy }
aria-busy= { busy }
>
{ t ( 'auth.create_admin' , 'Create administrator' )}
</ button >
</ form >
< div class = "auth-toggle" >
{ t ( 'auth.back_to_login' , 'Already configured?' )}
< button
class = "auth-toggle-link"
data-testid = "login-setup-to-login-btn"
onclick = {() => ( mode = 'login' )}
>
{ t ( 'auth.sign_in' , 'Sign in' )}
</ button >
</ div >
{ /if }
< div class = "auth-lang" >
< select
aria-label = { t ( 'settings.language' , 'Language' )}