Files
MEK-Charge-Reconciliation/templates/login.html
T
VasanthandClaude Opus 5.5 6616c91b7f Branding, Excel watermark, live run progress, and uv
- Login and Reports pages: eCare logo, Waystar and Aquilaehr logos (light
  versions for the dark pages), eCare favicon.
- Excel reports: faded eCare/Waystar/Aquila watermark as the sheet
  background on both sheets. Report writer moves to xlsxwriter, the only
  engine that can set a sheet background.
- Reports page: animated run progress (both download lanes piped into the
  Excel sheet) and a plain-language activity feed built from the run log;
  the raw log stays under "Technical log". /status now returns the
  location count.
- Dependencies move from requirements.txt to pyproject.toml + uv.lock;
  Dockerfile installs uv from PyPI and runs uv sync --frozen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 14:43:31 +05:30

437 lines
18 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sign in · MEK Charge Reconciliation</title>
<link rel="icon" href="/static/ecare-logo.png">
<style>
:root {
--night: #040b26;
--panel: #0c1638;
--panel-line: #26335e;
--field: #050c28;
--field-line: #56689c;
--pill-line: #c9d5f2;
--ink: #eef2ff;
--ink-2: #a9b6d8;
--owl: #ec7d33;
--owl-soft: #f5a25a;
--error: #ff9a9a;
--owl-w: 190px;
--ui: "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, sans-serif;
color-scheme: dark;
scrollbar-color: var(--panel-line) var(--night);
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
background: var(--night);
color: var(--ink);
font: 15px/1.5 var(--ui);
caret-color: var(--owl);
}
::selection { background: var(--owl); color: var(--night); }
.sky { position: fixed; inset: 0; overflow: hidden; pointer-events: none; }
.sky span {
position: absolute;
width: var(--s);
height: var(--s);
border-radius: 50%;
background: var(--c);
opacity: var(--o);
}
.sky .tw { animation: twinkle 4s ease-in-out infinite; animation-delay: var(--d); }
@keyframes twinkle { 50% { opacity: .15; } }
.brand {
position: absolute;
top: 20px;
left: 24px;
z-index: 1;
}
.brand img { display: block; width: 72px; height: auto; background: #000; border-radius: 16px; box-shadow: 0 6px 16px rgba(0, 3, 15, .45); }
.stage {
position: relative;
min-height: 100%;
display: flex;
flex-direction: column;
align-items: center;
/* Anchored near the middle rather than centered, so an error line grows the card downward instead of moving the owl. */
padding: max(32px, calc(50vh - 230px)) 16px 48px;
text-align: center;
}
h1 {
margin: 0;
font-size: clamp(20px, 5vw, 23px);
font-weight: 650;
line-height: 1.2;
letter-spacing: -.01em;
text-wrap: balance;
text-align: center;
}
.sub { margin: 4px 0 22px; color: var(--ink-2); text-align: center; }
.card {
position: relative;
width: min(360px, 100%);
margin-top: calc(var(--owl-w) * 50 / 48 + 12px);
padding: 28px 24px 24px;
background: var(--panel);
border: 1px solid var(--panel-line);
border-radius: 18px;
box-shadow: 0 18px 40px rgba(0, 3, 15, .5);
text-align: left;
}
.owl {
position: absolute;
left: 50%;
bottom: calc(100% - 7px);
width: var(--owl-w);
height: auto;
margin-left: calc(var(--owl-w) / -2);
overflow: visible;
}
.sources {
display: flex;
align-items: center;
gap: 20px;
margin-top: 28px;
}
.sources img { display: block; height: auto; }
.sources .waystar { width: 132px; }
.sources .sep { width: 1px; height: 26px; background: var(--panel-line); }
.aquila { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 16px; color: #dfe6f7; letter-spacing: .01em; }
.aquila img { width: 30px; }
label { display: block; font-weight: 600; margin-bottom: 8px; }
.field { position: relative; }
input {
width: 100%;
height: 46px;
padding: 0 52px 0 20px;
background: var(--field);
color: var(--ink);
border: 1px solid var(--field-line);
border-radius: 999px;
font: 16px var(--ui);
letter-spacing: .04em;
}
input:focus-visible { outline: 2px solid var(--owl); outline-offset: 2px; border-color: var(--owl); }
input[aria-invalid="true"] { border-color: var(--error); outline-color: var(--error); }
.reveal {
position: absolute;
top: 4px;
right: 4px;
width: 38px;
height: 38px;
display: grid;
place-items: center;
padding: 0;
background: none;
border: 0;
border-radius: 50%;
color: var(--ink-2);
cursor: pointer;
}
.reveal:hover { color: var(--ink); background: rgba(201, 213, 242, .1); }
.reveal:focus-visible { outline: 2px solid var(--owl); outline-offset: 0; color: var(--ink); }
.reveal svg {
width: 20px;
height: 20px;
fill: none;
stroke: currentColor;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
.note, .error { margin: 8px 0 0; font-size: 14px; }
.note { color: var(--owl-soft); }
.error { color: var(--error); }
.error:empty { margin: 0; }
.pill {
width: 100%;
height: 46px;
margin-top: 20px;
background: #0a1435;
color: #fff;
border: 1.5px solid var(--pill-line);
border-radius: 999px;
font: 600 15px var(--ui);
letter-spacing: .02em;
cursor: pointer;
transition: border-color .15s ease-out, background-color .15s ease-out;
}
.pill:hover { border-color: var(--owl); background: #101d45; }
.pill:active { transform: translateY(1px); }
.pill:focus-visible { outline: 2px solid var(--owl); outline-offset: 3px; }
.done h2 { margin: 0; font-size: 20px; }
.done h2:focus { outline: none; }
.done p { margin: 6px 0 0; color: var(--ink-2); }
/* Owl. Origins are in the SVG's own units (viewBox 0 0 48 50). */
#bob, #head, #glasses, #wing-l, #wing-r { transform-box: view-box; }
#bob { transform-origin: 24px 30px; }
#head { transform-origin: 24px 30px; }
#glasses { transform-origin: 24px 24px; }
#wing-l { transform-origin: 11px 25px; }
#wing-r { transform-origin: 37px 25px; }
.iris { transition: transform .14s ease-out; }
.lid { transform-box: fill-box; transform-origin: 50% 0; transform: scaleY(0); transition: transform .08s ease-out; }
.owl.blink .lid { transform: scaleY(1); }
.smile { transform-box: fill-box; transform-origin: 50% 100%; transform: scaleY(0); transition: transform .12s ease-out; }
.owl.happy .smile { transform: none; }
.owl.idle #bob { animation: breathe 3.2s ease-in-out infinite; }
@keyframes breathe { 50% { transform: translateY(.4px); } }
.owl.sad #head { animation: droop 1.4s ease-in-out; }
@keyframes droop { 20%, 75% { transform: translateY(1.2px) rotate(-6deg); } }
.owl.sad .lid { animation: slow-blink 1.4s ease-in-out; }
@keyframes slow-blink { 30%, 55% { transform: scaleY(1); } }
.eye-rim { transition: stroke .15s ease-out, filter .15s ease-out; }
.owl.peek .eye-rim { stroke: #ffc400; stroke-width: 1.5; filter: drop-shadow(0 0 2px #ffd84d) drop-shadow(0 0 5px #ffb300); }
#glasses rect { transition: fill .15s ease-out, fill-opacity .15s ease-out, filter .15s ease-out; }
.owl.peek #glasses rect { fill: #ffe066; fill-opacity: .45; filter: drop-shadow(0 0 3px #ffc400); }
.owl.tilt #head { animation: tilt .8s ease-in-out; }
@keyframes tilt { 25%, 70% { transform: rotate(-10deg); } }
.owl { cursor: pointer; }
.owl.happy, .owl.jump { animation: hop .75s cubic-bezier(.3, .6, .35, 1); }
@keyframes hop { 40% { transform: translateY(-14px); } 70% { transform: none; } 82% { transform: translateY(-3px); } }
.owl.happy #wing-l, .owl.jump #wing-l { animation: flap-l .75s ease-in-out; }
.owl.happy #wing-r, .owl.jump #wing-r { animation: flap-r .75s ease-in-out; }
@keyframes flap-l { 20%, 60% { transform: rotate(32deg); } 40%, 80% { transform: rotate(8deg); } }
@keyframes flap-r { 20%, 60% { transform: rotate(-32deg); } 40%, 80% { transform: rotate(-8deg); } }
@media (max-width: 420px) {
:root { --owl-w: 160px; }
.brand { top: 16px; left: 16px; }
.brand img { width: 52px; border-radius: 12px; }
}
@media (prefers-reduced-motion: reduce) {
.sky .tw, .owl, .owl * { animation: none !important; transition: none !important; }
.pill { transition: none; }
}
</style>
</head>
<body>
<div class="sky" id="sky" aria-hidden="true"></div>
<header class="brand"><img src="/static/ecare-logo.png" alt="eCare Services" width="206" height="199"></header>
<main class="stage">
<section class="card">
<svg class="owl idle" id="owl" viewBox="0 0 48 50" aria-hidden="true">
<defs>
<radialGradient id="coat" cx="42%" cy="28%" r="78%">
<stop stop-color="#ff990e"/><stop offset=".6" stop-color="#f58200"/><stop offset="1" stop-color="#bd4b00"/>
</radialGradient>
<linearGradient id="wing" x2=".8" y2="1">
<stop stop-color="#ed7b00"/><stop offset="1" stop-color="#a73e00"/>
</linearGradient>
<radialGradient id="glow">
<stop offset="0" stop-color="#ffaa5a" stop-opacity=".35"/><stop offset="1" stop-color="#ffaa5a" stop-opacity="0"/>
</radialGradient>
<clipPath id="eye-l"><ellipse cx="17" cy="20" rx="4" ry="5"/></clipPath>
<clipPath id="eye-r"><ellipse cx="31" cy="20" rx="4" ry="5"/></clipPath>
</defs>
<ellipse cx="24" cy="48.5" rx="17" ry="2.6" fill="url(#glow)"/>
<g stroke="#38200b" stroke-width="1.2" stroke-linejoin="round">
<path id="wing-l" fill="url(#wing)" d="M10 24 Q5 24 4 32 Q3 38 8 39 L12 34Z"/>
<path id="wing-r" fill="url(#wing)" d="M38 24 Q43 24 44 32 Q45 38 40 39 L36 34Z"/>
<path fill="url(#coat)" d="M8 27 C8 18 7 11 13 6 Q17 2 24 2 Q31 2 35 6 C41 11 40 18 40 27 Q42 38 35 43 Q30 47 24 47 Q17 47 13 43 Q6 38 8 27Z"/>
<path fill="none" stroke="#ffbe42" stroke-width="1.25" stroke-linecap="round"
d="M13 33 l1.5 2 l1.5 -2 M19 33 l1.5 2 l1.5 -2 M25 33 l1.5 2 l1.5 -2 M31 33 l1.5 2 l1.5 -2 M16 38 l1.5 2 l1.5 -2 M22 38 l1.5 2 l1.5 -2 M28 38 l1.5 2 l1.5 -2 M20 43 l1.5 1 l1.5 -1 M26 43 l1.5 1 l1.5 -1"/>
<path fill="none" stroke="#ac4904" stroke-width="1" d="M12 8 Q18 6 24 14 Q21 6 18 4 M24 13 Q25 6 30 4 M26 13 Q31 7 36 9"/>
<g id="bob"><g id="head">
<path fill="#ffe8b3" stroke-width="1" d="M24 14 C19 8 11 11 11 18 V26 Q11 30 17 30 Q21 30 24 28 Q27 30 31 30 Q37 30 37 26 V18 C37 11 29 8 24 14Z"/>
<g clip-path="url(#eye-l)">
<ellipse cx="17" cy="20" rx="4" ry="5" fill="#fffdf4" stroke="none"/>
<g class="iris"><ellipse cx="17" cy="21" rx="2.7" ry="3.3" fill="#6b3518" stroke="none"/><ellipse cx="17" cy="21.5" rx="1.8" ry="2.5" fill="#170e08" stroke="none"/><circle cx="16.3" cy="19.5" r="1" fill="#fff" stroke="none"/></g>
<rect class="lid" x="13" y="15" width="8" height="10" fill="#ffe8b3" stroke="none"/>
<g class="smile"><path fill="#ffe8b3" stroke="none" d="M13 21Q17 16.5 21 21L21 25L13 25Z"/><path fill="none" stroke="#6b3518" stroke-width=".7" stroke-linecap="round" d="M13.6 20.6Q17 17 20.4 20.6"/></g>
</g>
<ellipse class="eye-rim" cx="17" cy="20" rx="4" ry="5" fill="none" stroke-width="1"/>
<g clip-path="url(#eye-r)">
<ellipse cx="31" cy="20" rx="4" ry="5" fill="#fffdf4" stroke="none"/>
<g class="iris"><ellipse cx="31" cy="21" rx="2.7" ry="3.3" fill="#6b3518" stroke="none"/><ellipse cx="31" cy="21.5" rx="1.8" ry="2.5" fill="#170e08" stroke="none"/><circle cx="30.3" cy="19.5" r="1" fill="#fff" stroke="none"/></g>
<rect class="lid" x="27" y="15" width="8" height="10" fill="#ffe8b3" stroke="none"/>
<g class="smile"><path fill="#ffe8b3" stroke="none" d="M27 21Q31 16.5 35 21L35 25L27 25Z"/><path fill="none" stroke="#6b3518" stroke-width=".7" stroke-linecap="round" d="M27.6 20.6Q31 17 34.4 20.6"/></g>
</g>
<ellipse class="eye-rim" cx="31" cy="20" rx="4" ry="5" fill="none" stroke-width="1"/>
<g id="glasses" fill="none" stroke="#2a180c" stroke-width="1">
<rect x="12" y="22" width="10" height="6" rx="1.5"/>
<rect x="26" y="22" width="10" height="6" rx="1.5"/>
<path d="M22 23 Q24 21 26 23 M10 21 L12 23 M36 23 L38 21"/>
</g>
<path fill="#eb870a" stroke-width=".8" d="M22 26 Q24 24 26 26 L24 30Z"/>
</g></g>
<path fill="#ed900f" stroke-width="1" d="M16 45 L14 48 Q17 49 20 48 L19 46Z M29 46 L28 48 Q31 49 34 48 L32 45Z"/>
</g>
</svg>
<h1>MEK Charge Reconciliation</h1>
<p class="sub" id="sub">Sign in</p>
<form id="login" method="post" action="/login" novalidate>
<label for="password">Password</label>
<div class="field">
<input id="password" name="password" type="password" autocomplete="current-password" autofocus
aria-describedby="password-msg caps" required>
<button type="button" class="reveal" id="reveal" aria-label="Show password" aria-pressed="false"></button>
</div>
<p class="note" id="caps" aria-live="polite" hidden>Caps Lock is on</p>
<p class="error" id="password-msg" role="alert"></p>
<button type="submit" class="pill">Sign in</button>
</form>
</section>
<footer class="sources">
<img src="/static/waystar-logo-light.png" alt="Waystar" width="362" height="61" class="waystar">
<span class="sep" aria-hidden="true"></span>
<span class="aquila"><img src="/static/aquilaehr-logo-light.png" alt="" width="86" height="75">Aquilaehr</span>
</footer>
</main>
<script>
// Password is checked on the server (APP_PASSWORD); nothing secret lives in this page.
const EYE = '<path d="M2.5 12s3.5-6.5 9.5-6.5 9.5 6.5 9.5 6.5-3.5 6.5-9.5 6.5S2.5 12 2.5 12z"/><circle cx="12" cy="12" r="2.8"/>';
const EYE_OFF = '<path d="M4 4l16 16"/><path d="M9.9 5.7A10 10 0 0 1 12 5.5c6 0 9.5 6.5 9.5 6.5a16 16 0 0 1-2.6 3.4M6.7 7.2C4 9 2.5 12 2.5 12s3.5 6.5 9.5 6.5a9.6 9.6 0 0 0 4.7-1.2"/><path d="M10 10.1a2.8 2.8 0 0 0 3.9 3.9"/>';
const owl = document.getElementById("owl");
const form = document.getElementById("login");
const pw = document.getElementById("password");
const reveal = document.getElementById("reveal");
const caps = document.getElementById("caps");
const msg = document.getElementById("password-msg");
const irises = owl.querySelectorAll(".iris");
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
// Seeded so the stars stay put; none behind the text, owl or card.
function drawSky() {
let seed = 7;
const rand = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
const colors = ["#6ea8ff", "#4fd1c5", "#f29c4b", "#dfe7ff"];
const keepOut = [...document.querySelectorAll(".card, .owl, .brand, .sources")].map(e => e.getBoundingClientRect());
let html = "";
for (let i = 0; i < 60; i++) {
const tw = rand() < .35, x = rand() * innerWidth, y = rand() * innerHeight;
const size = (1.5 + rand() * 1.5).toFixed(1), color = colors[Math.floor(rand() * 4)];
const alpha = (.45 + rand() * .45).toFixed(2), delay = (rand() * 4).toFixed(2);
if (keepOut.some(r => x > r.left - 10 && x < r.right + 10 && y > r.top - 10 && y < r.bottom + 10)) continue;
html += `<span class="${tw ? "tw" : ""}" style="left:${x.toFixed(0)}px;top:${y.toFixed(0)}px;` +
`--s:${size}px;--c:${color};--o:${alpha};--d:${delay}s"></span>`;
}
document.getElementById("sky").innerHTML = html;
}
drawSky();
addEventListener("resize", drawSky);
function look(dx, dy) {
irises.forEach(g => { g.style.transform = `translate(${dx}px, ${dy}px)`; });
}
// Pupils slide from the left end of the field to the right as the typed text grows.
const measure = document.createElement("canvas").getContext("2d");
function followCaret() {
if (document.activeElement !== pw) return look(0, 0);
const cs = getComputedStyle(pw);
measure.font = cs.font;
measure.letterSpacing = cs.letterSpacing;
const caret = pw.selectionStart ?? pw.value.length;
const shown = pw.type === "password" ? "•".repeat(caret) : pw.value.slice(0, caret);
const room = pw.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
const ratio = Math.min(measure.measureText(shown).width / room, 1);
look(-1.2 + 2.4 * ratio, 1.2);
}
const timers = {};
function play(name, ms, then) {
clearTimeout(timers[name]);
owl.classList.remove(name);
void owl.getBoundingClientRect();
owl.classList.add(name);
timers[name] = setTimeout(() => { owl.classList.remove(name); then?.(); }, ms);
}
// Cleared first so a repeated identical message is announced again.
function showError(text) {
msg.textContent = "";
pw.setAttribute("aria-invalid", text ? "true" : "false");
if (text) setTimeout(() => { msg.textContent = text; }, 50);
}
function setReveal(show) {
pw.type = show ? "text" : "password";
reveal.setAttribute("aria-pressed", String(show));
reveal.setAttribute("aria-label", show ? "Hide password" : "Show password");
owl.classList.toggle("peek", show);
reveal.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true">${show ? EYE_OFF : EYE}</svg>`;
}
setReveal(false);
for (const ev of ["focus", "blur", "input", "keyup", "click", "select"]) pw.addEventListener(ev, followCaret);
pw.addEventListener("input", () => { if (msg.textContent) showError(""); });
for (const ev of ["keydown", "keyup"]) {
pw.addEventListener(ev, e => { caps.hidden = !e.getModifierState?.("CapsLock"); });
}
pw.addEventListener("blur", () => { caps.hidden = true; });
// Keep focus (and the owl's gaze) in the field when the eye button is clicked.
reveal.addEventListener("mousedown", e => e.preventDefault());
reveal.addEventListener("click", () => { setReveal(pw.type === "password"); followCaret(); });
form.addEventListener("submit", async e => {
e.preventDefault();
if (!pw.value) {
showError("Enter your password.");
play("tilt", 800);
pw.focus();
return;
}
let res;
try {
res = await fetch("/login", { method: "POST", body: new FormData(form) });
} catch {
showError("Can't reach the server. Try again.");
return;
}
if (!res.ok) {
showError(res.status === 401 ? "That password isn't right. Try again." : "Sign-in failed. Try again.");
pw.focus();
pw.select();
play("sad", 1400, followCaret);
return;
}
showError("");
pw.value = "";
setReveal(false);
const enter = () => { location.href = "/"; };
if (reducedMotion.matches) enter();
else play("happy", 750, enter);
});
// Tap the owl and it jumps.
owl.addEventListener("click", () => { if (!reducedMotion.matches) play("jump", 750); });
(function blink() {
setTimeout(() => {
if (!reducedMotion.matches && !owl.classList.contains("happy") && !owl.classList.contains("sad")) play("blink", 160);
blink();
}, 2500 + Math.random() * 3500);
})();
</script>
</body>
</html>