Files
ecw-charge-validation/web/templates/signin.html
T
ManikandanandClaude Opus 5.5 ce9bdbd974 Sign-in page redesign and Portal roles Super Admin / Admin / Viewer
Sign-in: brand panel, show / hide password, Caps Lock warning, moving
background, tab icon. Roles: Super Admin manages Admins and Viewers,
Admin manages Viewers only (and can Start / Stop, see Sign-ins), Viewer
sees claims and Runs only. create_tables.sql moves an old table.
71 test files, 1220 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:45:44 +05:30

151 lines
8.1 KiB
HTML

{% extends "base.html" %}
{% block title %}Sign in{% endblock %}
{% block style %}
/* Operator, 2026-10-26: the Portal's front door — what it is on the
left, the form on the right; one column on a phone. */
/* A slow-moving navy-to-blue background, this page only. */
body { background: linear-gradient(120deg, #0f2a47, #12355b 35%,
#1d4f86 65%, #1565c0);
background-size: 220% 220%; animation: drift 18s ease infinite; }
@keyframes drift { 0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; } }
footer.foot { background: rgba(255, 255, 255, .92); }
.auth { display: grid; grid-template-columns: 5fr 6fr; max-width: 860px;
margin: 40px auto; background: var(--panel); border-radius: 16px;
overflow: hidden; box-shadow: 0 24px 60px rgba(4, 20, 40, .35);
animation: rise .5s ease both; }
.auth-brand { background: var(--navy); color: #dbe5f1; padding: 40px 34px;
display: flex; flex-direction: column; gap: 22px;
position: relative; overflow: hidden; }
/* Two soft circles drifting behind the brand. */
.auth-brand::before, .auth-brand::after { content: ""; position: absolute;
border-radius: 50%; background: rgba(255, 255, 255, .07); }
.auth-brand::before { width: 220px; height: 220px; right: -70px; top: -60px;
animation: float 9s ease-in-out infinite; }
.auth-brand::after { width: 150px; height: 150px; left: -50px; bottom: -40px;
background: rgba(21, 101, 192, .35);
animation: float 11s ease-in-out infinite reverse; }
@keyframes float { 0%, 100% { transform: translate(0, 0); }
50% { transform: translate(-18px, 22px); } }
.auth-brand > * { position: relative; z-index: 1; }
.auth-brand .big { display: inline-grid; place-items: center; width: 56px;
height: 56px; border-radius: 16px; background: #fff;
color: var(--navy); font-size: 19px; font-weight: 800;
animation: ring 2.8s ease-out infinite; }
@keyframes ring { 0% { box-shadow: 0 0 0 0 rgba(255, 255, 255, .45); }
70%, 100% { box-shadow: 0 0 0 14px rgba(255, 255, 255, 0); } }
.auth-brand h1 { color: #fff; font-size: 28px; margin: 0; line-height: 1.2; }
.auth-brand ul { list-style: none; margin: 0; padding: 0;
display: grid; gap: 14px; }
.auth-brand li { display: flex; gap: 12px; align-items: center;
animation: slide .5s ease both; }
.auth-brand li:nth-child(1) { animation-delay: .15s; }
.auth-brand li:nth-child(2) { animation-delay: .3s; }
.auth-brand li:nth-child(3) { animation-delay: .45s; }
@keyframes slide { from { transform: translateX(-16px); }
to { transform: none; } }
.auth-brand li .dot { flex: none; display: inline-grid; place-items: center;
width: 30px; height: 30px; border-radius: 9px;
background: rgba(255, 255, 255, .12); color: #fff;
font-size: 14px; }
.auth-brand li b { color: #fff; }
.auth-brand .foot-note { margin-top: auto; font-size: 12px; color: #8fa3bb; }
.auth-form { padding: 40px 38px; display: flex; flex-direction: column;
justify-content: center; }
.auth-form h2 { font-size: 24px; margin: 0 0 22px; }
.field { margin-bottom: 16px; }
.field label { display: block; font-weight: 600; margin-bottom: 6px;
font-size: 13px; }
.field .box { display: flex; align-items: center; border: 1px solid #c9d2de;
border-radius: 8px; background: var(--panel);
transition: border-color .15s, box-shadow .15s; }
.field .box:focus-within { border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(21, 101, 192, .15); }
.field .ic { padding: 0 4px 0 12px; color: var(--muted); font-size: 15px; }
.field input { flex: 1; border: 0; padding: 11px 10px; min-width: 0;
background: transparent; outline: none; }
.field button.eye { background: transparent; border: 0; color: var(--accent);
font-weight: 600; font-size: 12px; padding: 0 12px; }
.field button.eye:hover { background: transparent; text-decoration: underline; }
#caps { color: var(--review); font-size: 12px; margin-top: 6px; }
.auth-form button[type="submit"] { width: 100%; padding: 12px 14px;
font-size: 15px; border-radius: 8px;
margin-top: 4px; position: relative;
overflow: hidden;
transition: transform .15s, box-shadow .15s; }
.auth-form button[type="submit"]:hover { transform: translateY(-1px);
box-shadow: 0 8px 18px rgba(21, 101, 192, .35); }
/* A light sweep across the button on hover. */
.auth-form button[type="submit"]::after { content: ""; position: absolute;
top: 0; left: -60%; width: 40%; height: 100%;
background: linear-gradient(90deg, transparent,
rgba(255, 255, 255, .35), transparent);
transform: skewX(-20deg); }
.auth-form button[type="submit"]:hover::after { left: 120%;
transition: left .6s ease; }
.auth-form .rules { margin-top: 18px; padding-top: 14px;
border-top: 1px solid var(--line); }
@media (max-width: 760px) {
.auth { grid-template-columns: 1fr; margin: 8px 0; }
.auth-brand, .auth-form { padding: 24px 20px; }
.auth-brand ul { display: none; }
}
{% endblock %}
{% block content %}
<div class="auth">
<section class="auth-brand">
<span class="big" aria-hidden="true">CV</span>
<h1>Charge Validation</h1>
<ul>
<li><span class="dot" aria-hidden="true">✓</span><b>Charge conditions</b></li>
<li><span class="dot" aria-hidden="true">▦</span><b>Pass, Fail, Review</b></li>
<li><span class="dot" aria-hidden="true">◷</span><b>Every run recorded</b></li>
</ul>
<p class="foot-note">Ecare India · {{ mode }} mode</p>
</section>
<section class="auth-form">
<h2>Sign in</h2>
{% if error %}<div class="msg bad" role="alert">{{ error }}</div>{% endif %}
{% if no_users %}<div class="msg bad">No Portal Users yet. On this PC run
<code>python tools\add_portal_user.py</code> to add the first Admin.</div>{% endif %}
<form method="post" action="/signin" autocomplete="off">
<input type="hidden" name="token" value="{{ run_token }}">
<input type="hidden" name="next" value="{{ next_url }}">
<div class="field">
<label for="username">Username</label>
<div class="box"><span class="ic" aria-hidden="true">👤</span><input id="username" name="username" autocomplete="username" required autofocus></div>
</div>
<div class="field">
<label for="pw">Password</label>
<div class="box"><span class="ic" aria-hidden="true">🔒</span><input id="pw" name="password" type="password" autocomplete="current-password" required><button type="button" class="eye" id="pw-toggle" aria-controls="pw" aria-pressed="false">Show</button></div>
<div id="caps" role="status" hidden>Caps Lock is on</div>
</div>
<button type="submit">Sign in</button>
</form>
<p class="note rules">Signed out after {{ idle_minutes }} minutes idle.
{{ lock_after }} wrong passwords lock the account for
{{ lock_minutes }} minutes. Forgot it? An Admin can reset it.</p>
</section>
</div>
{% endblock %}
{% block scripts %}
<script>
(() => {
const pw = $('pw'), eye = $('pw-toggle'), caps = $('caps');
eye.addEventListener('click', () => {
const show = pw.type === 'password';
pw.type = show ? 'text' : 'password';
eye.textContent = show ? 'Hide' : 'Show';
eye.setAttribute('aria-pressed', String(show));
pw.focus();
});
const capsCheck = e => {
if (e.getModifierState) caps.hidden = !e.getModifierState('CapsLock');
};
pw.addEventListener('keydown', capsCheck);
pw.addEventListener('keyup', capsCheck);
pw.addEventListener('blur', () => { caps.hidden = true; });
})();
</script>
{% endblock %}