Self-healing agent (supervisor, recovery, login fixes, operator mail) and Portal sage redesign
Snapshot of D:\Ecw Process a11c9ff (2026-10-30). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fafa5285eb
commit
d500f750da
26 files changed
+1704
-357
No files matched your search
+25
-3
@@ -283,6 +283,27 @@ def _reasons(rows, top=6) -> list:
|
||||
for (cat, text), n in ordered[:top]]
|
||||
|
||||
|
||||
def _runs_today(rows) -> list:
|
||||
"""The Dashboard's Runs today card (operator, 2026-10-30): one entry
|
||||
per run in the day's rows, newest first, with its counts."""
|
||||
runs = {}
|
||||
for r in rows:
|
||||
run = runs.setdefault(r.get("run_at"), {
|
||||
"run_at": r.get("run_at"), "run_mode": r.get("run_mode"),
|
||||
"claims": 0, "Pass": 0, "Fail": 0, "Review": 0})
|
||||
run["claims"] += 1
|
||||
if r.get("category") in CATEGORIES:
|
||||
run[r["category"]] += 1
|
||||
return sorted(runs.values(), key=lambda x: x["run_at"] or datetime.min,
|
||||
reverse=True)
|
||||
|
||||
|
||||
def _greeting(now=None) -> str:
|
||||
hour = (now or datetime.now()).hour
|
||||
return ("Good morning" if hour < 12 else
|
||||
"Good afternoon" if hour < 17 else "Good evening")
|
||||
|
||||
|
||||
def portal_settings(urls: str, server: bool) -> dict:
|
||||
"""create_app's host / origin / cookie / Start settings from
|
||||
PORTAL_URLS (comma separated, as people type them in the browser) and
|
||||
@@ -461,11 +482,12 @@ def create_app(repo, allowed_hosts=ALLOWED_HOSTS, runner=None,
|
||||
if today - timedelta(days=6) <= d["day"] <= today),
|
||||
key=lambda d: d["day"])
|
||||
# Only a day with claims costs the second query.
|
||||
reasons = (_reasons(_db(repo.day_results, today))
|
||||
if counts["claims"] else [])
|
||||
rows = _db(repo.day_results, today) if counts["claims"] else []
|
||||
return _page(request, "dashboard.html", [["Dashboard", None]],
|
||||
today=counts, has_runs=bool(days),
|
||||
alert=_last_run_alert(), week=week, reasons=reasons)
|
||||
alert=_last_run_alert(), week=week,
|
||||
reasons=_reasons(rows), runs_today=_runs_today(rows),
|
||||
greeting=_greeting())
|
||||
|
||||
@app.get("/days", response_class=HTMLResponse)
|
||||
def days_page(request: Request):
|
||||
|
||||
+120
-94
@@ -5,75 +5,99 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="run-token" content="{{ run_token }}">
|
||||
<title>{% block title %}Charge Validation{% endblock %} · eCW</title>
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%2312355b'/%3E%3Ctext x='16' y='21' font-family='Segoe UI,Arial' font-size='13' font-weight='800' text-anchor='middle' fill='%23fff'%3ECV%3C/text%3E%3C/svg%3E">
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%232e3f36'/%3E%3Ctext x='16' y='21' font-family='Segoe UI,Arial' font-size='13' font-weight='800' text-anchor='middle' fill='%23fff'%3ECV%3C/text%3E%3C/svg%3E">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@200;300;500;600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
{% block head %}{% endblock %}
|
||||
<style>
|
||||
/* Navy & blue (operator, 2026-10-19): navy header, white cards on light
|
||||
grey, blue buttons; Pass green, Fail red, Review amber everywhere. */
|
||||
/* Sage (operator, 2026-10-30, the approved mockup): a deep sage side
|
||||
menu, white rounded cards on soft sage-grey, a lime highlight; Pass
|
||||
green, Fail orange, Review blue everywhere (told apart by lightness
|
||||
too, not hue alone). The old names stay so every page follows. */
|
||||
:root {
|
||||
--navy: #12355b; --navy-2: #0f2a47; --bg: #f4f6fa; --panel: #ffffff;
|
||||
--text: #1f2a37; --muted: #6b7785; --line: #dde3ec; --head: #e8edf4;
|
||||
--accent: #1565c0; --accent-dark: #0d4f9e;
|
||||
--pass: #2e7d32; --pass-bg: #e8f5e9;
|
||||
--fail: #c62828; --fail-bg: #fdecea;
|
||||
--review: #b45309; --review-bg: #fff3e0; --review-bar: #ef6c00;
|
||||
--stripe: #f9fafc; --hilite: #eef4ff; --sel: #dbe7fb;
|
||||
--shadow: 0 1px 2px rgba(18, 53, 91, .06), 0 2px 8px rgba(18, 53, 91, .05);
|
||||
--navy: #2e3f36; --navy-2: #24332b; --bg: #f2f4f1; --panel: #ffffff;
|
||||
--text: #1b2420; --muted: #5b6661; --line: #e2e6e2; --head: #eef2ee;
|
||||
--accent: #4a6b58; --accent-dark: #3b5747; --lime: #d4e88a;
|
||||
--pass: #245c3a; --pass-bg: #e3f0e7; --pass-bar: #4a6b58;
|
||||
--fail: #9a4312; --fail-bg: #fbe9dd; --fail-bar: #d9772e;
|
||||
--review: #244f96; --review-bg: #e3ecfa; --review-bar: #3b6fc4;
|
||||
--stripe: #f8faf8; --hilite: #eef4ef; --sel: #dfe9e2;
|
||||
--shadow: 0 1px 2px rgba(27, 36, 32, .05), 0 2px 10px rgba(27, 36, 32, .04);
|
||||
--display: Outfit, "Segoe UI", system-ui, sans-serif;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: var(--bg); color: var(--text);
|
||||
font: 14px/1.45 "Segoe UI", system-ui, sans-serif;
|
||||
display: flex; flex-direction: column; min-height: 100vh; }
|
||||
font: 14px/1.45 Inter, "Segoe UI", system-ui, sans-serif;
|
||||
min-height: 100vh; }
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
/* Header, breadcrumb, page and footer share one column (1180px), so
|
||||
their edges line up (operator, 2026-10-26). */
|
||||
.wrap { max-width: 1180px; margin: 0 auto; padding: 0 24px; width: 100%; }
|
||||
header.top { background: var(--navy); color: #fff; }
|
||||
header.top .wrap { display: flex; align-items: center; gap: 14px;
|
||||
min-height: 58px; }
|
||||
header.top .brand { color: #fff; font-size: 17px; font-weight: 600;
|
||||
letter-spacing: .2px; display: flex; align-items: center; }
|
||||
header.top .brand:hover { text-decoration: none; }
|
||||
header.top .mark { display: inline-grid; place-items: center; width: 30px;
|
||||
height: 30px; border-radius: 8px; background: #fff;
|
||||
color: var(--navy); font-size: 12px; font-weight: 800;
|
||||
margin-right: 10px; letter-spacing: .5px; }
|
||||
.pill { border-radius: 12px; padding: 2px 10px; font-size: 11px;
|
||||
font-weight: 700; letter-spacing: .4px; background: var(--pass);
|
||||
color: #fff; }
|
||||
.pill.LIVE-WRITE { background: var(--fail); }
|
||||
header.top nav.links { margin-left: auto; display: flex; gap: 4px;
|
||||
align-items: center; }
|
||||
header.top nav.links a { color: #e3ecf7; font-weight: 600; padding: 6px 11px;
|
||||
border-radius: 6px; transition: background .15s; }
|
||||
header.top nav.links a:hover { background: rgba(255, 255, 255, .08);
|
||||
.shell { display: flex; flex-wrap: wrap; gap: 20px; padding: 20px;
|
||||
min-height: 100vh; align-items: stretch; }
|
||||
.page { flex: 999 1 560px; min-width: 0; display: flex;
|
||||
flex-direction: column; }
|
||||
aside.side { flex: 1 1 232px; max-width: 260px; min-width: 220px;
|
||||
background: var(--navy); color: #f2f4f1; border-radius: 24px;
|
||||
padding: 22px 14px; display: flex; flex-direction: column;
|
||||
gap: 24px; }
|
||||
aside.side .brand { color: #f2f4f1; display: flex; align-items: center;
|
||||
gap: 12px; padding: 0 8px; }
|
||||
aside.side .brand:hover { text-decoration: none; }
|
||||
aside.side .mark { display: inline-grid; place-items: center; width: 38px;
|
||||
height: 38px; border-radius: 12px; background: var(--lime);
|
||||
color: var(--navy); font-size: 13px; font-weight: 800;
|
||||
flex: none; }
|
||||
aside.side .brand b { display: block; font-family: var(--display);
|
||||
font-weight: 500; font-size: 17px; }
|
||||
aside.side .brand small { color: #b9c6be; font-size: 12px; }
|
||||
.pill { border-radius: 999px; padding: 3px 10px; font-size: 11px;
|
||||
font-weight: 700; letter-spacing: .4px; background: var(--lime);
|
||||
color: var(--navy); align-self: flex-start; margin-left: 8px; }
|
||||
.pill.LIVE-WRITE { background: var(--fail-bar); color: #fff; }
|
||||
aside.side nav.links { display: flex; flex-direction: column; gap: 4px; }
|
||||
aside.side nav.links a { color: #e3e9e5; font-weight: 500; height: 44px;
|
||||
padding: 0 14px; border-radius: 12px;
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
transition: background .15s; }
|
||||
aside.side nav.links a::before { content: ""; width: 18px; height: 18px;
|
||||
flex: none; background: currentColor;
|
||||
-webkit-mask: var(--ic) center / contain no-repeat;
|
||||
mask: var(--ic) center / contain no-repeat; }
|
||||
aside.side nav.links a:hover { background: rgba(242, 244, 241, .08);
|
||||
text-decoration: none; }
|
||||
header.top nav.links a.on { background: rgba(255, 255, 255, .16); color: #fff; }
|
||||
header.top .who { color: #b9c6d6; font-size: 13px; display: flex;
|
||||
align-items: center; gap: 7px; margin-left: 10px; }
|
||||
header.top .who b { color: #fff; }
|
||||
header.top .avatar { display: inline-grid; place-items: center; width: 26px;
|
||||
height: 26px; border-radius: 50%; background: var(--accent);
|
||||
color: #fff; font-size: 12px; font-weight: 700; }
|
||||
header.top form.out { margin: 0; }
|
||||
header.top form.out button { background: transparent; border-color: #6f86a3;
|
||||
font-weight: 400; padding: 4px 10px; }
|
||||
.msg { border-radius: 8px; padding: 10px 14px; margin-bottom: 14px; }
|
||||
aside.side nav.links a.on { background: var(--lime); color: var(--navy);
|
||||
font-weight: 700; }
|
||||
aside.side nav.links a[href="/"] { --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2'%3E%3Crect x='3' y='3' width='7' height='9' rx='1.5'/%3E%3Crect x='14' y='3' width='7' height='5' rx='1.5'/%3E%3Crect x='14' y='12' width='7' height='9' rx='1.5'/%3E%3Crect x='3' y='16' width='7' height='5' rx='1.5'/%3E%3C/svg%3E"); }
|
||||
aside.side nav.links a[href="/days"] { --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Crect x='4' y='3' width='16' height='18' rx='2'/%3E%3Cpath d='M8 8h8M8 12h8M8 16h5'/%3E%3C/svg%3E"); }
|
||||
aside.side nav.links a[href="/runs"] { --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 12h4l3 8 4-16 3 8h4'/%3E%3C/svg%3E"); }
|
||||
aside.side nav.links a[href$="/users"] { --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='9' cy='8' r='3.5'/%3E%3Cpath d='M2.5 20c.8-3.5 3.4-5.5 6.5-5.5s5.7 2 6.5 5.5M16 4.5a3.5 3.5 0 0 1 0 7M18 14.8c1.8.8 3 2.6 3.5 5.2'/%3E%3C/svg%3E"); }
|
||||
aside.side nav.links a[href$="/signins"] { --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 7v5l3 2'/%3E%3C/svg%3E"); }
|
||||
aside.side .who { margin-top: auto; display: flex; align-items: center;
|
||||
gap: 12px; padding: 12px; border-radius: 16px;
|
||||
background: rgba(242, 244, 241, .08); color: #f2f4f1; }
|
||||
aside.side .who:hover { text-decoration: none;
|
||||
background: rgba(242, 244, 241, .14); }
|
||||
aside.side .who b { display: block; font-size: 14px; }
|
||||
aside.side .who small { color: #b9c6be; font-size: 12px; }
|
||||
aside.side .avatar { display: inline-grid; place-items: center; width: 36px;
|
||||
height: 36px; border-radius: 50%; background: var(--accent);
|
||||
color: #fff; font-size: 13px; font-weight: 700; flex: none; }
|
||||
aside.side form.out { margin: 0; padding: 0 8px; }
|
||||
aside.side form.out button { width: 100%; background: transparent;
|
||||
border-color: rgba(242, 244, 241, .3);
|
||||
font-weight: 500; }
|
||||
aside.side form.out button:hover { background: rgba(242, 244, 241, .08); }
|
||||
.msg { border-radius: 12px; padding: 10px 14px; margin-bottom: 14px; }
|
||||
.msg.bad { background: var(--fail-bg); color: var(--fail); }
|
||||
.msg.good { background: var(--pass-bg); color: var(--pass); }
|
||||
nav.crumbs { background: var(--navy-2); color: #b9c6d6; padding: 7px 0;
|
||||
font-size: 13px; }
|
||||
nav.crumbs a { color: #e3ecf7; }
|
||||
nav.crumbs .sep { margin: 0 8px; color: #7d8ea3; }
|
||||
nav.crumbs .here { color: #fff; font-weight: 600; }
|
||||
main { flex: 1; width: 100%; max-width: 1180px; margin: 0 auto;
|
||||
padding: 22px 24px 40px; }
|
||||
footer.foot { background: var(--panel); border-top: 1px solid var(--line);
|
||||
color: var(--muted); font-size: 12px; }
|
||||
nav.crumbs { color: var(--muted); padding: 4px 4px 0; font-size: 13px; }
|
||||
nav.crumbs a { color: var(--muted); }
|
||||
nav.crumbs .sep { margin: 0 8px; color: #8a958f; }
|
||||
nav.crumbs .here { color: var(--text); font-weight: 600; }
|
||||
main { flex: 1; width: 100%; padding: 14px 4px 28px; }
|
||||
footer.foot { color: var(--muted); font-size: 12px; padding: 0 4px; }
|
||||
footer.foot .wrap { display: flex; gap: 16px; align-items: center;
|
||||
justify-content: space-between; flex-wrap: wrap;
|
||||
padding-top: 12px; padding-bottom: 12px; }
|
||||
padding: 12px 0; border-top: 1px solid var(--line); }
|
||||
footer.foot b { color: var(--text); font-weight: 600; }
|
||||
/* Motion: short, never in the way, off when Windows asks for less. */
|
||||
/* Movement only, never opacity: a tab the browser holds in the
|
||||
@@ -87,36 +111,37 @@
|
||||
a.tile, a.card { transition: transform .15s, box-shadow .15s,
|
||||
border-color .15s; }
|
||||
a.tile:hover, a.card:hover { transform: translateY(-2px);
|
||||
box-shadow: 0 6px 18px rgba(18, 53, 91, .12); }
|
||||
box-shadow: 0 6px 18px rgba(27, 36, 32, .10); }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { animation: none !important;
|
||||
transition: none !important; }
|
||||
}
|
||||
h1 { font-size: 20px; margin: 0 0 4px; }
|
||||
h1 { font-family: var(--display); font-weight: 300; font-size: 30px;
|
||||
letter-spacing: -.3px; margin: 0 0 4px; }
|
||||
.sub { color: var(--muted); margin: 0 0 16px; }
|
||||
.card { background: var(--panel); border: 1px solid var(--line);
|
||||
border-radius: 10px; padding: 16px 18px; box-shadow: var(--shadow);
|
||||
margin-bottom: 18px; }
|
||||
.card h2 { font-size: 15px; margin: 0 0 10px; }
|
||||
.card { background: var(--panel); border: 0; border-radius: 22px;
|
||||
padding: 20px 22px; box-shadow: var(--shadow); margin-bottom: 18px; }
|
||||
.card h2 { font-family: var(--display); font-weight: 500; font-size: 19px;
|
||||
margin: 0 0 10px; }
|
||||
.tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px; margin-bottom: 18px; }
|
||||
.tile { background: var(--panel); border: 1px solid var(--line);
|
||||
border-radius: 10px; padding: 12px 16px; box-shadow: var(--shadow);
|
||||
.tile { background: var(--panel); border: 2px solid transparent;
|
||||
border-radius: 20px; padding: 16px 18px; box-shadow: var(--shadow);
|
||||
color: var(--text); display: block; }
|
||||
.tile .n { font-size: 26px; font-weight: 700; display: block; }
|
||||
.tile .l { color: var(--muted); font-size: 12px; text-transform: uppercase;
|
||||
letter-spacing: .5px; }
|
||||
.tile .n { font-family: var(--display); font-size: 36px; font-weight: 500;
|
||||
line-height: 1.1; display: block; }
|
||||
.tile .l { color: var(--muted); font-size: 13px; }
|
||||
.tile.Pass .n { color: var(--pass); }
|
||||
.tile.Fail .n { color: var(--fail); }
|
||||
.tile.Review .n { color: var(--review); }
|
||||
a.tile:hover { text-decoration: none; border-color: var(--accent); }
|
||||
a.tile.on { border: 2px solid var(--accent); padding: 11px 15px; }
|
||||
select, input, button { font: inherit; padding: 7px 10px;
|
||||
border: 1px solid #c9d2de; border-radius: 6px;
|
||||
a.tile.on { border-color: var(--accent); }
|
||||
select, input, button { font: inherit; padding: 8px 12px;
|
||||
border: 1px solid #d5dbd6; border-radius: 10px;
|
||||
background: var(--panel); color: var(--text); }
|
||||
button, .btn { background: var(--accent); color: #fff;
|
||||
border: 1px solid var(--accent); border-radius: 6px;
|
||||
padding: 7px 14px; cursor: pointer; font-weight: 600;
|
||||
border: 1px solid var(--accent); border-radius: 10px;
|
||||
padding: 8px 16px; cursor: pointer; font-weight: 600;
|
||||
display: inline-block; }
|
||||
button:hover, .btn:hover { background: var(--accent-dark);
|
||||
text-decoration: none; }
|
||||
@@ -124,7 +149,7 @@
|
||||
border-color: #c9d2de; font-weight: 400; }
|
||||
button.plain:hover { background: var(--hilite); }
|
||||
button[disabled] { opacity: .5; cursor: default; }
|
||||
.cat { display: inline-block; padding: 2px 9px; border-radius: 10px;
|
||||
.cat { display: inline-block; padding: 2px 10px; border-radius: 999px;
|
||||
font-weight: 600; font-size: 12px; }
|
||||
.cat.Pass { background: var(--pass-bg); color: var(--pass); }
|
||||
.cat.Fail { background: var(--fail-bg); color: var(--fail); }
|
||||
@@ -135,15 +160,14 @@
|
||||
.note { color: var(--muted); font-size: 12px; }
|
||||
/* Tables: short, no sideways scroll; long text cut on one line. */
|
||||
table.list { width: 100%; border-collapse: separate; border-spacing: 0;
|
||||
background: var(--panel); border: 1px solid var(--line);
|
||||
border-radius: 10px; overflow: hidden; table-layout: fixed; }
|
||||
background: var(--panel); border: 0; box-shadow: var(--shadow);
|
||||
border-radius: 18px; overflow: hidden; table-layout: fixed; }
|
||||
table.list th, table.list td { padding: 9px 12px; text-align: left;
|
||||
border-bottom: 1px solid var(--line);
|
||||
overflow: hidden; text-overflow: ellipsis;
|
||||
white-space: nowrap; }
|
||||
table.list th { background: var(--head); font-size: 12px; color: #3b4757;
|
||||
font-weight: 600; text-transform: uppercase;
|
||||
letter-spacing: .4px; }
|
||||
table.list th { background: var(--head); font-size: 12px;
|
||||
color: var(--muted); font-weight: 600; }
|
||||
table.list tbody tr { cursor: pointer; }
|
||||
table.list tbody tr:nth-child(even) td { background: var(--stripe); }
|
||||
table.list tbody tr:hover td { background: var(--hilite); }
|
||||
@@ -167,7 +191,7 @@
|
||||
.drawer { position: fixed; top: 0; right: 0; height: 100vh;
|
||||
width: min(480px, 100vw); background: var(--panel);
|
||||
border-left: 1px solid var(--line); z-index: 20;
|
||||
box-shadow: -8px 0 24px rgba(18, 53, 91, .12);
|
||||
box-shadow: -8px 0 24px rgba(27, 36, 32, .12);
|
||||
padding: 0 0 30px; overflow-y: auto; }
|
||||
.drawer[hidden] { display: none; }
|
||||
.drawer .top { display: flex; align-items: center; gap: 10px;
|
||||
@@ -186,10 +210,10 @@
|
||||
white-space: pre-wrap; font-size: 12px; margin: 0; }
|
||||
@media (max-width: 760px) {
|
||||
.tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
main { padding: 16px; }
|
||||
.wrap { padding-left: 16px; padding-right: 16px; }
|
||||
header.top .wrap { flex-wrap: wrap; padding-top: 8px; padding-bottom: 8px; }
|
||||
header.top nav.links { margin-left: 0; flex-wrap: wrap; }
|
||||
.shell { padding: 12px; gap: 12px; }
|
||||
aside.side { max-width: none; flex-basis: 100%; padding: 14px; gap: 12px; }
|
||||
aside.side nav.links { flex-direction: row; flex-wrap: wrap; }
|
||||
aside.side .who { margin-top: 0; }
|
||||
}
|
||||
{% block style %}{% endblock %}
|
||||
</style>
|
||||
@@ -213,17 +237,17 @@
|
||||
</head>
|
||||
<body>
|
||||
{% set section = crumbs[1][0] if crumbs | length > 1 else crumbs[0][0] %}
|
||||
<header class="top">
|
||||
<div class="wrap">
|
||||
<a class="brand" href="/"><span class="mark" aria-hidden="true">CV</span>eCW Charge Validation</a>
|
||||
<div class="shell">
|
||||
{% if user %}<aside class="side">
|
||||
<a class="brand" href="/"><span class="mark" aria-hidden="true">CV</span><span><b>Charge Validation</b><small>eCW · Salud</small></span></a>
|
||||
<span class="pill {{ mode }}" title="SAVE_CORRECTIONS mode">{{ mode }}</span>
|
||||
{% if user %}<nav class="links">
|
||||
<nav class="links" aria-label="Main">
|
||||
{% for label, link, admin in [("Dashboard", "/", False), ("Claim status", "/days", False), ("Runs", "/runs", False), ("Users", "/admin/users", True), ("Sign-ins", "/admin/signins", True)] %}{% if not admin or is_admin %}{% if label == section %}<a href="{{ link }}" class="on" aria-current="page">{{ label }}</a>{% else %}<a href="{{ link }}">{{ label }}</a>{% endif %}{% endif %}{% endfor %}
|
||||
<a class="who" href="/account/password" title="Change password"><span class="avatar" aria-hidden="true">{{ (user.display_name or "?")[:1] | upper }}</span><b>{{ user.display_name }}</b> · {{ user.role }}</a>
|
||||
<form class="out" method="post" action="/signout"><input type="hidden" name="token" value="{{ run_token }}"><button type="submit">Sign out</button></form>
|
||||
</nav>{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
</nav>
|
||||
<a class="who" href="/account/password" title="Change password"><span class="avatar" aria-hidden="true">{{ (user.display_name or "?")[:1] | upper }}</span><span><b>{{ user.display_name }}</b><small>{{ user.role }}</small></span></a>
|
||||
<form class="out" method="post" action="/signout"><input type="hidden" name="token" value="{{ run_token }}"><button type="submit">Sign out</button></form>
|
||||
</aside>{% endif %}
|
||||
<div class="page">
|
||||
<nav class="crumbs">
|
||||
<div class="wrap">{% for label, link in crumbs %}{% if not loop.first %}<span class="sep">›</span>{% endif %}{% if link %}<a href="{{ link }}">{{ label }}</a>{% else %}<span class="here">{{ label }}</span>{% endif %}{% endfor %}</div>
|
||||
</nav>
|
||||
@@ -233,11 +257,13 @@
|
||||
<footer class="foot">
|
||||
<div class="wrap">
|
||||
<span><b>eCW Charge Validation</b> · Ecare India</span>
|
||||
<span class="phi">Local page — contains patient data; do not share
|
||||
screenshots outside the team.</span>
|
||||
<span class="phi">Contains patient data; do not share screenshots
|
||||
outside the team.</span>
|
||||
<span>Mode <b>{{ mode }}</b></span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
+136
-106
@@ -1,79 +1,98 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard{% endblock %}
|
||||
{% block style %}
|
||||
/* The approved mockup (operator, 2026-10-30): greeting and claim search
|
||||
on top, today's tiles, the lime Agent run card beside results by day,
|
||||
then today's reasons beside today's runs. */
|
||||
header.hello { display: flex; flex-wrap: wrap; gap: 14px;
|
||||
align-items: center; justify-content: space-between;
|
||||
margin-bottom: 18px; }
|
||||
header.hello h1 { font-size: 34px; }
|
||||
header.hello .sub { margin: 0; font-size: 15px; }
|
||||
form.find { display: flex; gap: 8px; }
|
||||
form.find input { width: 240px; height: 44px; background: var(--panel); }
|
||||
form.find button { height: 44px; }
|
||||
.tiles .tile .head { display: flex; justify-content: space-between;
|
||||
align-items: center; }
|
||||
.tile .pct { float: right; font-size: 12px; font-weight: 700;
|
||||
padding: 2px 9px; border-radius: 999px; margin-top: 2px; }
|
||||
.tile.Pass .pct { background: var(--pass-bg); color: var(--pass); }
|
||||
.tile.Fail .pct { background: var(--fail-bg); color: var(--fail); }
|
||||
.tile.Review .pct { background: var(--review-bg); color: var(--review); }
|
||||
.tile .rate { display: block; margin-top: 6px; font-size: 12px;
|
||||
color: var(--pass); font-weight: 600; }
|
||||
.row2 { display: flex; flex-wrap: wrap; gap: 18px; margin-bottom: 18px; }
|
||||
.row2 > .card { margin-bottom: 0; flex: 1 1 340px; min-width: 0; }
|
||||
.row2 > #week, .row2 > #runs-today { flex: 2 1 420px; }
|
||||
.card-h { display: flex; justify-content: space-between;
|
||||
align-items: baseline; margin-bottom: 14px; gap: 10px; }
|
||||
.card-h h2 { margin: 0; }
|
||||
|
||||
/* Agent run: the lime card. */
|
||||
.botrun { background: var(--lime); color: var(--text); }
|
||||
.botrun .row { display: flex; gap: 12px; align-items: center;
|
||||
flex-wrap: wrap; }
|
||||
.botrun .row + .row { margin-top: 10px; }
|
||||
.botrun .row + .row { margin-top: 12px; }
|
||||
.botrun h2 { margin: 0; }
|
||||
#br-message { color: var(--muted); }
|
||||
.botrun button.stop { background: var(--fail); border-color: var(--fail); }
|
||||
.botrun progress { width: 260px; height: 10px; accent-color: var(--accent); }
|
||||
.botrun pre { background: var(--head); padding: 10px; border-radius: 6px;
|
||||
max-height: 260px; overflow: auto; font-size: 12px;
|
||||
white-space: pre-wrap; }
|
||||
.botrun summary { cursor: pointer; color: var(--accent); margin-top: 10px; }
|
||||
.chip { padding: 2px 9px; border-radius: 10px; font-size: 12px;
|
||||
#br-message { color: var(--navy); font-weight: 600; }
|
||||
.botrun button { background: var(--text); border-color: var(--text); }
|
||||
.botrun button:hover { background: #000; }
|
||||
.botrun button.plain { background: rgba(255, 255, 255, .6);
|
||||
border-color: transparent; color: var(--text); }
|
||||
.botrun button.stop { background: var(--fail-bar); border-color: var(--fail-bar); }
|
||||
.botrun .note { color: var(--navy); }
|
||||
.botrun progress { width: 220px; height: 10px; accent-color: var(--navy); }
|
||||
.botrun pre { background: rgba(255, 255, 255, .6); padding: 10px;
|
||||
border-radius: 12px; max-height: 260px; overflow: auto;
|
||||
font-size: 12px; white-space: pre-wrap; }
|
||||
.botrun summary { cursor: pointer; color: var(--navy); margin-top: 10px;
|
||||
font-weight: 600; }
|
||||
.chip { padding: 2px 9px; border-radius: 999px; font-size: 12px;
|
||||
font-weight: 600; }
|
||||
/* `.row { display: flex }` beats the browser's [hidden] rule. */
|
||||
.botrun [hidden] { display: none; }
|
||||
.tile .rate { display: block; margin-top: 4px; font-size: 12px;
|
||||
color: var(--pass); font-weight: 600; }
|
||||
/* Two columns, then three equal cards: every row the same edges. */
|
||||
.split { display: grid; grid-template-columns: 3fr 2fr; gap: 18px;
|
||||
margin-bottom: 18px; }
|
||||
.split .card, .actions .card { margin-bottom: 0; }
|
||||
.card-h { display: flex; justify-content: space-between;
|
||||
align-items: baseline; margin-bottom: 12px; }
|
||||
.card-h h2 { margin: 0; }
|
||||
|
||||
/* Results by day. */
|
||||
.week { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
gap: 10px; align-items: end; height: 170px; }
|
||||
gap: 12px; align-items: end; height: 190px; }
|
||||
a.wd { display: flex; flex-direction: column; align-items: center;
|
||||
justify-content: flex-end; height: 100%; color: var(--text);
|
||||
text-decoration: none; }
|
||||
a.wd .col { display: flex; flex-direction: column; width: 70%;
|
||||
max-width: 42px; border-radius: 6px 6px 2px 2px;
|
||||
overflow: hidden; background: var(--head); min-height: 4px;
|
||||
height: 0; }
|
||||
max-width: 44px; border-radius: 8px; overflow: hidden;
|
||||
background: var(--head); min-height: 4px; height: 0; }
|
||||
a.wd:hover .col { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
a.wd .col i { display: block; }
|
||||
a.wd .col i.Pass { background: var(--pass); }
|
||||
a.wd .col i.Fail { background: var(--fail); }
|
||||
a.wd .col i.Pass { background: var(--pass-bar); }
|
||||
a.wd .col i.Fail { background: var(--fail-bar); }
|
||||
a.wd .col i.Review { background: var(--review-bar); }
|
||||
a.wd .cnt { font-size: 12px; font-weight: 700; margin-bottom: 4px; }
|
||||
a.wd .lbl { font-size: 12px; color: var(--muted); margin-top: 6px; }
|
||||
.legend { display: flex; gap: 14px; margin-top: 10px; font-size: 12px;
|
||||
color: var(--muted); }
|
||||
.legend span::before { content: ""; display: inline-block; width: 9px;
|
||||
height: 9px; border-radius: 2px; margin-right: 5px; }
|
||||
.legend .Pass::before { background: var(--pass); }
|
||||
.legend .Fail::before { background: var(--fail); }
|
||||
.legend { display: flex; gap: 14px; font-size: 13px; color: var(--muted); }
|
||||
.legend span::before { content: ""; display: inline-block; width: 10px;
|
||||
height: 10px; border-radius: 3px; margin-right: 6px; }
|
||||
.legend .Pass::before { background: var(--pass-bar); }
|
||||
.legend .Fail::before { background: var(--fail-bar); }
|
||||
.legend .Review::before { background: var(--review-bar); }
|
||||
ol.reasons { list-style: none; margin: 0; padding: 0; }
|
||||
ol.reasons li { display: flex; gap: 10px; align-items: baseline;
|
||||
padding: 7px 0; border-bottom: 1px solid var(--line); }
|
||||
ol.reasons li:last-child { border-bottom: 0; }
|
||||
ol.reasons b { min-width: 30px; text-align: center; border-radius: 10px;
|
||||
padding: 1px 8px; font-size: 12px; }
|
||||
|
||||
/* Today's reasons and runs. */
|
||||
ol.reasons, ol.runs { list-style: none; margin: 0; padding: 0; }
|
||||
ol.reasons li, ol.runs li { display: flex; gap: 12px; align-items: center;
|
||||
padding: 9px 0; border-bottom: 1px solid var(--line); }
|
||||
ol.reasons li:last-child, ol.runs li:last-child { border-bottom: 0; }
|
||||
ol.reasons b { min-width: 34px; text-align: center; border-radius: 999px;
|
||||
padding: 2px 9px; font-size: 12px; }
|
||||
ol.reasons b.Fail { background: var(--fail-bg); color: var(--fail); }
|
||||
ol.reasons b.Review { background: var(--review-bg); color: var(--review); }
|
||||
ol.reasons span { overflow: hidden; text-overflow: ellipsis;
|
||||
white-space: nowrap; }
|
||||
.actions { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 18px; align-items: stretch; }
|
||||
a.card.go { display: flex; align-items: center; gap: 14px; color: var(--text); }
|
||||
a.card.go:hover { text-decoration: none; border-color: var(--accent); }
|
||||
a.card.go b, form.search > b { display: block; font-size: 15px; }
|
||||
a.card.go .ic { display: grid; place-items: center; width: 40px; height: 40px;
|
||||
border-radius: 10px; background: var(--hilite);
|
||||
color: var(--accent); font-size: 20px; flex: none; }
|
||||
a.card.go .arrow { margin-left: auto; color: var(--accent); font-size: 18px; }
|
||||
a.card.go .note { display: block; }
|
||||
form.search.card { display: block; }
|
||||
form.search .find { display: flex; gap: 8px; margin-top: 8px; }
|
||||
form.search .find input { flex: 1; min-width: 0; }
|
||||
.today-h { display: flex; justify-content: space-between; align-items: baseline;
|
||||
margin: 4px 0 10px; }
|
||||
.today-h h2 { font-size: 15px; margin: 0; }
|
||||
ol.runs .t { font-weight: 600; width: 72px; }
|
||||
ol.runs .m { font-size: 11px; font-weight: 700; padding: 2px 9px;
|
||||
border-radius: 999px; background: var(--head); color: var(--navy); }
|
||||
ol.runs .c { margin-left: auto; color: var(--muted); }
|
||||
ol.runs .c b { color: var(--text); }
|
||||
ol.runs .cat { min-width: 40px; text-align: center; }
|
||||
|
||||
#live-summary { color: var(--muted); margin: 0 0 10px; }
|
||||
#live-list { list-style: none; margin: 0; padding: 0; }
|
||||
#live-list li { display: flex; gap: 10px; align-items: center;
|
||||
@@ -87,15 +106,25 @@
|
||||
#live-list li.st-checking .st { color: var(--accent); font-weight: 600; }
|
||||
#live-list li.st-idle .st { color: var(--muted); }
|
||||
@media (max-width: 760px) {
|
||||
.actions, .split { grid-template-columns: 1fr; }
|
||||
form.find, form.find input { width: 100%; }
|
||||
}
|
||||
/* The last Run ended badly (EEWA-33). */
|
||||
.alert { background: var(--fail-bg); color: var(--fail);
|
||||
border: 1px solid #f5c2bd; border-left: 5px solid var(--fail);
|
||||
border-radius: 10px; padding: 12px 16px; margin-bottom: 18px; }
|
||||
border-radius: 16px; padding: 12px 16px; margin-bottom: 18px; }
|
||||
.alert a { color: var(--fail); font-weight: 600; text-decoration: underline; }
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<header class="hello">
|
||||
<div>
|
||||
<h1>{{ greeting }}, {{ (user.display_name or "").split(" ")[0] }}</h1>
|
||||
<p class="sub">{{ today.day | longdate }} · Medcor Ready queue</p>
|
||||
</div>
|
||||
<form class="find" method="get" action="/claim">
|
||||
<input type="search" name="claim_no" placeholder="Search claim number"
|
||||
inputmode="numeric" aria-label="Claim number">
|
||||
<button type="submit">Find</button>
|
||||
</form>
|
||||
</header>
|
||||
{% if alert %}
|
||||
<div class="alert" id="last-run-alert" role="alert">
|
||||
<b>The last run {{ "ended in a Run Error" if alert.status == "Run Error" else "ended without a record (killed or crashed)" }}</b>
|
||||
@@ -103,10 +132,20 @@
|
||||
<a href="/runs/{{ alert.run_id }}">See the run</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="tiles">
|
||||
<a class="tile" href="/day/{{ today.day.isoformat() }}"><span class="n">{{ today.claims }}</span><span class="l">Checked today</span>{% if today.claims %}<span class="rate">{{ (100 * today.pass_count / today.claims) | round | int }}% pass</span>{% endif %}</a>
|
||||
<a class="tile Pass" href="/day/{{ today.day.isoformat() }}?category=Pass"><span class="n">{{ today.pass_count }}</span><span class="l">Pass</span>{% if today.claims %}<span class="pct">{{ (100 * today.pass_count / today.claims) | round | int }}%</span>{% endif %}</a>
|
||||
<a class="tile Fail" href="/day/{{ today.day.isoformat() }}?category=Fail"><span class="n">{{ today.fail_count }}</span><span class="l">Fail</span>{% if today.claims %}<span class="pct">{{ (100 * today.fail_count / today.claims) | round | int }}%</span>{% endif %}</a>
|
||||
<a class="tile Review" href="/day/{{ today.day.isoformat() }}?category=Review"><span class="n">{{ today.review_count }}</span><span class="l">Review</span>{% if today.claims %}<span class="pct">{{ (100 * today.review_count / today.claims) | round | int }}%</span>{% endif %}</a>
|
||||
</div>
|
||||
|
||||
<div class="row2">
|
||||
<section class="card botrun" id="botrun">
|
||||
<div class="row">
|
||||
<h2>Bot run</h2>
|
||||
<h2>Agent run</h2>
|
||||
<span id="br-message">…</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button type="button" id="br-start"{% if not can_run %} hidden{% endif %}>▶ Start run</button>
|
||||
<button type="button" id="br-stop" class="stop" hidden>Stop</button>
|
||||
<span id="br-terminal" hidden>Run started in the terminal — stop it
|
||||
@@ -128,59 +167,50 @@
|
||||
<pre id="br-log"></pre></details>
|
||||
</section>
|
||||
|
||||
<div class="today-h">
|
||||
<h2>Today · {{ today.day | longdate }}</h2>
|
||||
{% if today.claims %}<a href="/day/{{ today.day.isoformat() }}">Open today's claims →</a>{% endif %}
|
||||
</div>
|
||||
<div class="tiles">
|
||||
<a class="tile" href="/day/{{ today.day.isoformat() }}"><span class="n">{{ today.claims }}</span><span class="l">Checked today</span>{% if today.claims %}<span class="rate">{{ (100 * today.pass_count / today.claims) | round | int }}% pass</span>{% endif %}</a>
|
||||
<a class="tile Pass" href="/day/{{ today.day.isoformat() }}?category=Pass"><span class="n">{{ today.pass_count }}</span><span class="l">Pass</span></a>
|
||||
<a class="tile Fail" href="/day/{{ today.day.isoformat() }}?category=Fail"><span class="n">{{ today.fail_count }}</span><span class="l">Fail</span></a>
|
||||
<a class="tile Review" href="/day/{{ today.day.isoformat() }}?category=Review"><span class="n">{{ today.review_count }}</span><span class="l">Review</span></a>
|
||||
</div>
|
||||
|
||||
<div class="split">
|
||||
<section class="card" id="week">
|
||||
<div class="card-h"><h2>Last 7 days</h2><a href="/days">All days →</a></div>
|
||||
{% if week %}
|
||||
<div class="week">
|
||||
{% for d in week %}
|
||||
<a class="wd" href="/day/{{ d.day.isoformat() }}" data-day="{{ d.day.isoformat() }}" title="{{ d.day | longdate }}: {{ d.pass_count }} Pass, {{ d.fail_count }} Fail, {{ d.review_count }} Review">
|
||||
<span class="cnt">{{ d.claims }}</span>
|
||||
<span class="col" data-n="{{ d.claims }}"><i class="Review" data-n="{{ d.review_count }}"></i><i class="Fail" data-n="{{ d.fail_count }}"></i><i class="Pass" data-n="{{ d.pass_count }}"></i></span>
|
||||
<span class="lbl">{{ d.day.strftime("%a %d") }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<section class="card" id="week">
|
||||
<div class="card-h"><h2>Results by day</h2>
|
||||
<div class="legend"><span class="Pass">Pass</span><span class="Fail">Fail</span><span class="Review">Review</span></div>
|
||||
{% else %}
|
||||
<p class="note">No claims checked in the last 7 days.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
<section class="card" id="reasons">
|
||||
<div class="card-h"><h2>Today's Fail and Review reasons</h2>{% if today.claims %}<a href="/day/{{ today.day.isoformat() }}?category=Fail">Open →</a>{% endif %}</div>
|
||||
{% if reasons %}
|
||||
<ol class="reasons">
|
||||
{% for r in reasons %}<li><b class="{{ r.category }}">{{ r.count }}</b><span>{{ r.comment }}</span></li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% else %}
|
||||
<p class="note">No Fail or Review today.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
<a href="/days" id="claim-status">All days →</a></div>
|
||||
{% if week %}
|
||||
<div class="week">
|
||||
{% for d in week %}
|
||||
<a class="wd" href="/day/{{ d.day.isoformat() }}" data-day="{{ d.day.isoformat() }}" title="{{ d.day | longdate }}: {{ d.pass_count }} Pass, {{ d.fail_count }} Fail, {{ d.review_count }} Review">
|
||||
<span class="cnt">{{ d.claims }}</span>
|
||||
<span class="col" data-n="{{ d.claims }}"><i class="Review" data-n="{{ d.review_count }}"></i><i class="Fail" data-n="{{ d.fail_count }}"></i><i class="Pass" data-n="{{ d.pass_count }}"></i></span>
|
||||
<span class="lbl">{{ d.day.strftime("%a %d") }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="note">No claims checked in the last 7 days.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<a class="card go" href="/days" id="claim-status"><span class="ic" aria-hidden="true">▦</span><span><b>Claim status</b><span class="note">Every day's claims, by result</span></span><span class="arrow">→</span></a>
|
||||
<a class="card go" href="/runs"><span class="ic" aria-hidden="true">◷</span><span><b>Runs</b><span class="note">Logins, timings, errors</span></span><span class="arrow">→</span></a>
|
||||
<form class="search card" method="get" action="/claim">
|
||||
<b>Find a claim</b>
|
||||
<div class="find">
|
||||
<input type="search" name="claim_no" placeholder="Claim #"
|
||||
inputmode="numeric" aria-label="Claim number">
|
||||
<button type="submit">Find</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="row2">
|
||||
<section class="card" id="reasons">
|
||||
<div class="card-h"><h2>Top reasons today</h2>{% if today.claims %}<a href="/day/{{ today.day.isoformat() }}?category=Fail">Open →</a>{% endif %}</div>
|
||||
{% if reasons %}
|
||||
<ol class="reasons">
|
||||
{% for r in reasons %}<li><b class="{{ r.category }}">{{ r.count }}</b><span>{{ r.comment }}</span></li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% else %}
|
||||
<p class="note">No Fail or Review today.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="card" id="runs-today">
|
||||
<div class="card-h"><h2>Runs today</h2><a href="/runs">All runs →</a></div>
|
||||
{% if runs_today %}
|
||||
<ol class="runs">
|
||||
{% for r in runs_today %}<li><span class="t">{{ r.run_at | clock }}</span><span class="m">{{ r.run_mode }}</span><span class="c"><b>{{ r.claims }}</b> claims</span><span class="cat Pass">{{ r.Pass }}</span><span class="cat Fail">{{ r.Fail }}</span><span class="cat Review">{{ r.Review }}</span></li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% else %}
|
||||
<p class="note">No runs today yet.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
{% if not has_runs %}
|
||||
<p class="note">No results saved yet. Run the bot with <code>RESULTS_DB=1</code>
|
||||
|
||||
+82
-93
@@ -1,110 +1,98 @@
|
||||
{% 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);
|
||||
/* The approved mockup (operator, 2026-10-30): a deep sage panel with
|
||||
what the Portal is, the form in a white card; one column on a phone. */
|
||||
footer.foot { display: none; }
|
||||
main { padding: 0; }
|
||||
nav.crumbs { display: none; }
|
||||
.auth { display: flex; flex-wrap: wrap; gap: 20px;
|
||||
min-height: calc(100vh - 40px); }
|
||||
.auth-brand { flex: 1 1 520px; background: var(--navy); color: #f2f4f1;
|
||||
border-radius: 28px; padding: 44px 52px; display: flex;
|
||||
flex-direction: column; justify-content: space-between;
|
||||
gap: 32px; }
|
||||
.auth-brand .logo { display: flex; align-items: center; gap: 12px;
|
||||
font-family: var(--display); font-weight: 500;
|
||||
font-size: 18px; }
|
||||
.auth-brand .big { display: inline-grid; place-items: center; width: 42px;
|
||||
height: 42px; border-radius: 12px; background: var(--lime);
|
||||
color: var(--navy); font-size: 14px; font-weight: 800; }
|
||||
.auth-brand h1 { font-family: var(--display); font-weight: 200;
|
||||
font-size: clamp(52px, 7vw, 88px); line-height: .95;
|
||||
letter-spacing: -2px; margin: 0 0 18px; color: #f2f4f1; }
|
||||
.auth-brand h1 span { color: var(--lime); font-weight: 300; }
|
||||
.auth-brand p.lead { margin: 0; font-size: 17px; line-height: 1.55;
|
||||
color: #c9d4cd; max-width: 520px; }
|
||||
.auth-brand ul { list-style: none; margin: 0; padding: 0; display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 14px; }
|
||||
.auth-brand li { background: rgba(242, 244, 241, .08); border-radius: 18px;
|
||||
padding: 16px; display: flex; flex-direction: column;
|
||||
gap: 8px; animation: rise .5s ease both; }
|
||||
.auth-brand li:nth-child(2) { animation-delay: .1s; }
|
||||
.auth-brand li:nth-child(3) { animation-delay: .2s; }
|
||||
.auth-brand li svg { color: var(--lime); }
|
||||
.auth-brand li b { color: #f2f4f1; font-size: 15px; }
|
||||
.auth-brand li small { color: #c9d4cd; font-size: 13px; line-height: 1.45; }
|
||||
.auth-side { flex: 999 1 400px; display: flex; align-items: center;
|
||||
justify-content: center; padding: 24px 8px; }
|
||||
.auth-form { width: 100%; max-width: 420px; background: var(--panel);
|
||||
border-radius: 28px; padding: 40px; box-shadow: var(--shadow);
|
||||
animation: rise .5s ease both; }
|
||||
.auth-form h2 { font-family: var(--display); font-weight: 500;
|
||||
font-size: 32px; letter-spacing: -.5px; margin: 0 0 6px; }
|
||||
.auth-form .intro { color: var(--muted); margin: 0 0 22px; font-size: 15px; }
|
||||
.field { margin-bottom: 18px; }
|
||||
.field label { display: block; font-weight: 500; margin-bottom: 8px;
|
||||
font-size: 14px; }
|
||||
.field .box { display: flex; align-items: center; height: 48px;
|
||||
border: 1px solid #d5dbd6; border-radius: 12px;
|
||||
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; }
|
||||
box-shadow: 0 0 0 3px rgba(74, 107, 88, .18); }
|
||||
.field .ic { padding: 0 2px 0 14px; color: var(--muted); display: flex; }
|
||||
.field input { flex: 1; border: 0; padding: 0 10px; height: 100%;
|
||||
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; }
|
||||
font-weight: 600; font-size: 13px; padding: 0 14px; }
|
||||
.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;
|
||||
.auth-form button[type="submit"] { width: 100%; height: 52px;
|
||||
font-size: 16px; border-radius: 14px;
|
||||
margin-top: 4px;
|
||||
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); }
|
||||
box-shadow: 0 8px 18px rgba(74, 107, 88, .3); }
|
||||
.auth-form .rules { margin: 20px 0 0; padding: 12px 14px;
|
||||
background: var(--bg); border-radius: 12px;
|
||||
line-height: 1.5; }
|
||||
@media (max-width: 760px) {
|
||||
.auth { grid-template-columns: 1fr; margin: 8px 0; }
|
||||
.auth-brand, .auth-form { padding: 24px 20px; }
|
||||
.auth-brand { padding: 28px 24px; }
|
||||
.auth-brand ul { display: none; }
|
||||
.auth-form { padding: 28px 22px; }
|
||||
}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="auth">
|
||||
<section class="auth-brand">
|
||||
<span class="big" aria-hidden="true">CV</span>
|
||||
<h1>Charge Validation</h1>
|
||||
<div class="logo"><span class="big" aria-hidden="true">CV</span>Ecare OTS</div>
|
||||
<div>
|
||||
<h1>Charge<br><span>Validation</span></h1>
|
||||
<p class="lead">Agent run monitoring for the eCW Medcor Ready queue:
|
||||
every claim checked against the Charge Condition Document.</p>
|
||||
</div>
|
||||
<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>
|
||||
<li><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12h4l3 8 4-16 3 8h4"></path></svg><b>Daily run status</b><small>Live progress of the agent and each run.</small></li>
|
||||
<li><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"></rect><path d="M3 10h18M9 4v16"></path></svg><b>Claim results</b><small>Pass, Fail and Review with the reason.</small></li>
|
||||
<li><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7z"></path></svg><b>{{ mode }} mode</b><small>{% if mode == "VERIFY" %}Nothing is saved to eCW claims.{% else %}Corrections are saved after a person confirms.{% endif %}</small></li>
|
||||
</ul>
|
||||
<p class="foot-note">Ecare India · {{ mode }} mode</p>
|
||||
</section>
|
||||
<section class="auth-form">
|
||||
<h2>Sign in</h2>
|
||||
<section class="auth-side">
|
||||
<div class="auth-form">
|
||||
<h2>Welcome back</h2>
|
||||
<p class="intro">Sign in to the Charge Validation portal.</p>
|
||||
{% if ecare_on %}<p class="note">Ecare staff: your Ecare office username and password.</p>{% endif %}
|
||||
{% 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
|
||||
@@ -114,19 +102,20 @@
|
||||
<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 class="box"><span class="ic" aria-hidden="true"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="8" r="4"></circle><path d="M4 21c1-4 4-6 8-6s7 2 8 6"></path></svg></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 class="box"><span class="ic" aria-hidden="true"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="4" y="11" width="16" height="10" rx="2"></rect><path d="M8 11V7a4 4 0 0 1 8 0v4"></path></svg></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 a Portal password? An Admin can
|
||||
reset it.</p>
|
||||
<p class="note rules">Ecare network or VPN only. Signed out after
|
||||
{{ idle_minutes }} minutes idle. {{ lock_after }} wrong passwords lock
|
||||
the account for {{ lock_minutes }} minutes. Forgot a Portal password?
|
||||
An Admin can reset it.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Reference in new issue
Block a user