MEK Charge Reconciliation: Waystar + Aquilaehr download, per-location reconciliation, web app

- mek_recon.py: Playwright download for both sites in parallel (headless on server)
- consolidate.py: Patient Name + DOS match, per-location and All Locations Excel
- app.py: sign-in (server-side password), Run button, per-file and ZIP downloads
- Dockerfile for Dokploy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
VasanthandClaude Opus 5.5 committed 2026-10-27 17:38:27 +05:30
commit fd480fa874
10 files changed
+1335

No files matched your search

+151
View File
@@ -0,0 +1,151 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MEK Charge Reconciliation</title>
<style>
/* same night palette as the sign-in page */
:root {
--night: #040b26;
--panel: #0c1638;
--panel-line: #26335e;
--field: #050c28;
--pill-line: #c9d5f2;
--ink: #eef2ff;
--ink-2: #a9b6d8;
--owl: #ec7d33;
--ok: #7fe0a8;
--error: #ff9a9a;
--ui: "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, sans-serif;
color-scheme: dark;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--night); color: var(--ink); font: 15px/1.5 var(--ui); }
main { width: min(860px, 100%); margin: 0 auto; padding: 32px 16px 48px; }
header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 20px; }
h1 { margin: 0; font-size: clamp(20px, 5vw, 24px); font-weight: 650; letter-spacing: -.01em; }
h2 { margin: 0 0 12px; font-size: 17px; font-weight: 650; }
.card { background: var(--panel); border: 1px solid var(--panel-line); border-radius: 18px; padding: 22px 22px 20px; margin-bottom: 16px; }
.muted { color: var(--ink-2); }
.pill {
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
height: 44px; padding: 0 22px; background: #0a1435; color: #fff; text-decoration: none;
border: 1.5px solid var(--pill-line); border-radius: 999px; font: 600 15px var(--ui); cursor: pointer;
}
.pill:hover { border-color: var(--owl); background: #101d45; }
.pill:focus-visible, a:focus-visible { outline: 2px solid var(--owl); outline-offset: 3px; }
.pill.primary { border-color: var(--owl); }
.pill[disabled] { opacity: .5; cursor: not-allowed; }
.pill.small { height: 34px; padding: 0 14px; font-size: 13px; }
.row { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
.status { font-weight: 600; }
.status.running { color: var(--owl); }
.status.ok { color: var(--ok); }
.status.fail { color: var(--error); }
pre {
margin: 14px 0 0; max-height: 280px; overflow: auto; padding: 12px 14px;
background: var(--field); border: 1px solid var(--panel-line); border-radius: 12px;
font: 12.5px/1.45 ui-monospace, Consolas, monospace; white-space: pre-wrap; word-break: break-word;
}
pre:empty { display: none; }
table { width: 100%; border-collapse: collapse; }
td { padding: 9px 4px; border-top: 1px solid var(--panel-line); vertical-align: middle; }
td:last-child { text-align: right; white-space: nowrap; }
.file { font-weight: 600; }
.file small { display: block; font-weight: 400; color: var(--ink-2); }
.empty { color: var(--ink-2); margin: 0; }
form.inline { margin: 0; }
</style>
</head>
<body>
<main>
<header>
<h1>MEK Charge Reconciliation</h1>
<form class="inline" method="post" action="/logout"><button class="pill small" type="submit">Sign out</button></form>
</header>
<section class="card" aria-labelledby="run-h">
<h2 id="run-h">Run</h2>
<p class="muted" style="margin:0 0 14px">Downloads Waystar claims and Aquilaehr encounters for every location at the same time,
then builds one reconciliation report per location (Patient Name + Date of Service).</p>
<div class="row">
<button class="pill primary" id="run" type="button">Run reconciliation</button>
<span class="status" id="status" aria-live="polite"></span>
</div>
<pre id="log" aria-label="Run log"></pre>
</section>
<section class="card" aria-labelledby="dl-h">
<div class="row" style="justify-content:space-between;margin-bottom:6px">
<h2 id="dl-h" style="margin:0">Reports</h2>
<a class="pill small" id="zip" href="/download-all">Download all (ZIP)</a>
</div>
<table><tbody id="files"></tbody></table>
<p class="empty" id="no-files" hidden>No reports yet. Run the reconciliation first.</p>
</section>
</main>
<script>
const runBtn = document.getElementById("run");
const statusEl = document.getElementById("status");
const logEl = document.getElementById("log");
const filesEl = document.getElementById("files");
const noFiles = document.getElementById("no-files");
const zip = document.getElementById("zip");
let timer;
const fmtTime = s => s ? new Date(s * 1000).toLocaleString() : "";
function render(st) {
runBtn.disabled = st.running;
statusEl.className = "status " + (st.running ? "running" : st.code === 0 ? "ok" : st.code == null ? "" : "fail");
statusEl.textContent = st.running ? `Running… started ${fmtTime(st.started)}`
: st.code === 0 ? `Finished ${fmtTime(st.finished)}`
: st.code == null ? "" : `Failed (exit ${st.code}) ${fmtTime(st.finished)}. See the log.`;
const atBottom = logEl.scrollTop + logEl.clientHeight >= logEl.scrollHeight - 8;
logEl.textContent = st.log;
if (atBottom) logEl.scrollTop = logEl.scrollHeight;
filesEl.innerHTML = "";
for (const f of st.files) {
const tr = document.createElement("tr");
const name = document.createElement("td");
name.className = "file";
name.textContent = f.name.replace(/\.xlsx$/, "");
const meta = document.createElement("small");
meta.textContent = `Updated ${fmtTime(f.mtime)} · ${(f.size / 1024).toFixed(0)} KB`;
name.append(meta);
const act = document.createElement("td");
const a = document.createElement("a");
a.className = "pill small";
a.href = "/download/" + encodeURIComponent(f.name);
a.textContent = "Download";
act.append(a);
tr.append(name, act);
filesEl.append(tr);
}
noFiles.hidden = st.files.length > 0;
zip.hidden = st.files.length === 0;
clearTimeout(timer);
timer = setTimeout(refresh, st.running ? 2000 : 15000);
}
async function refresh() {
const res = await fetch("/status");
if (res.status === 401) return location.href = "/";
render(await res.json());
}
runBtn.addEventListener("click", async () => {
runBtn.disabled = true;
const res = await fetch("/run", { method: "POST" });
if (res.status === 409) statusEl.textContent = "A run is already in progress.";
refresh();
});
refresh();
</script>
</body>
</html>