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:
ManikandanandClaude Opus 5.5 committed 2026-10-30 10:58:35 +05:30
1 parent fafa5285eb
commit d500f750da
26 files changed
+1704 -357

No files matched your search

+136 -106
View File
@@ -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>