add pitch assets, fix UI and backend bugs, add test utilities

- add 3 signal pitch assets (workflow flowchart, market opportunity, win-win-win) as HTML + PNG
- fix doc_state_machine cascade and supplied sentinel flags
- fix CSVImport mapping review, WorklistTable colored bullets and expand row
- fix App.jsx auth gate, api.js CORS handling
- add signal E2E test script and stress test generator

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Kisa 2026-06-19 12:52:29 -04:00
parent 228ca98423
commit 42a79c6682
14 changed files with 1550 additions and 63 deletions

View file

@ -0,0 +1,238 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Signal — Market Opportunity</title>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background: #F0EAE1;
width: 1080px; height: 1080px;
overflow: hidden; position: relative;
}
.top-bar { position: absolute; top: 0; left: 0; right: 0; height: 5px; background: #2EA3A3; }
.signal-brand { position: absolute; top: 28px; right: 80px; text-align: right; z-index: 3; }
.brand-name { font-size: 18px; font-weight: 800; color: #1A5050; letter-spacing: 0.02em; }
.brand-sub { font-size: 9px; font-weight: 500; color: #8AABAB; letter-spacing: 0.12em; text-transform: uppercase; margin-top: 3px; }
.rings { position: absolute; top: 0; right: 0; pointer-events: none; }
.dots { position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; opacity: 0.16; }
.footer { position: absolute; bottom: 22px; left: 70px; right: 70px; display: flex; justify-content: space-between; }
.footer span { font-size: 10px; color: #8AABAB; font-weight: 500; }
.content {
position: absolute;
top: 68px; left: 70px; right: 70px; bottom: 50px;
display: flex; flex-direction: column;
}
.headline { flex-shrink: 0; margin-bottom: 28px; }
.headline .eyebrow {
font-size: 11px; font-weight: 700; letter-spacing: 0.14em;
text-transform: uppercase; color: #CB6B20; margin-bottom: 8px;
}
.headline h1 { font-size: 34px; font-weight: 800; color: #1A5050; letter-spacing: -0.02em; line-height: 1.1; }
.headline p { font-size: 14px; color: #5A7A7A; margin-top: 8px; font-weight: 500; }
/* Three big stat blocks */
.stat-row {
display: flex; gap: 16px; flex-shrink: 0; margin-bottom: 20px;
}
.stat-block {
flex: 1; padding: 20px 18px; border-radius: 10px;
display: flex; flex-direction: column;
}
.stat-block.dark { background: #1A5050; }
.stat-block.mid { background: #E5EFEF; border: 1.5px solid #2EA3A3; }
.stat-block.warm { background: #F5EDE0; border: 1.5px solid #CB6B20; }
.stat-block .num {
font-size: 44px; font-weight: 800; line-height: 1; margin-bottom: 6px;
}
.stat-block.dark .num { color: #2EA3A3; }
.stat-block.mid .num { color: #1A5050; }
.stat-block.warm .num { color: #CB6B20; }
.stat-block .label {
font-size: 13px; font-weight: 600; line-height: 1.35;
}
.stat-block.dark .label { color: rgba(255,255,255,0.85); }
.stat-block.mid .label { color: #1A5050; }
.stat-block.warm .label { color: #6A3810; }
.stat-block .source {
font-size: 9.5px; font-weight: 500; margin-top: 8px; opacity: 0.6;
}
.stat-block.dark .source { color: #fff; }
.stat-block.mid .source { color: #1A5050; }
.stat-block.warm .source { color: #6A3810; }
/* The research paradox call-out */
.paradox {
flex-shrink: 0;
background: #1A5050;
border-radius: 10px;
padding: 20px 24px;
display: flex;
align-items: center;
gap: 24px;
margin-bottom: 20px;
}
.paradox-number {
font-size: 52px; font-weight: 800; color: #2EA3A3;
line-height: 1; flex-shrink: 0;
}
.paradox-text { flex: 1; }
.paradox-text .p-headline {
font-size: 15px; font-weight: 700; color: #fff; line-height: 1.3; margin-bottom: 6px;
}
.paradox-text .p-sub {
font-size: 12px; color: rgba(255,255,255,0.7); font-weight: 500; line-height: 1.4;
}
.paradox-divider {
width: 1px; background: rgba(255,255,255,0.15); align-self: stretch; flex-shrink: 0;
}
.paradox-signal { flex-shrink: 0; text-align: center; padding-left: 8px; }
.paradox-signal .ps-label { font-size: 10px; font-weight: 600; color: rgba(255,255,255,0.5); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 6px; }
.paradox-signal .ps-value { font-size: 28px; font-weight: 800; color: #CB6B20; line-height: 1; }
.paradox-signal .ps-sub { font-size: 11px; color: rgba(255,255,255,0.6); margin-top: 4px; font-weight: 500; }
/* Opportunity grid */
.opp-grid {
flex: none;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 14px;
}
.opp-card {
background: #EAE4DB;
border-radius: 8px;
padding: 18px 16px;
}
.opp-card .opp-accent {
height: 3px; border-radius: 2px; margin-bottom: 12px;
}
.opp-card .opp-accent.teal { background: #2EA3A3; }
.opp-card .opp-accent.copper { background: #CB6B20; }
.opp-card .opp-accent.dark { background: #1A5050; }
.opp-card .opp-title { font-size: 13px; font-weight: 800; color: #1A5050; margin-bottom: 7px; line-height: 1.25; }
.opp-card .opp-desc { font-size: 11.5px; color: #5A6A6A; font-weight: 500; line-height: 1.45; }
</style>
</head>
<body>
<div class="top-bar"></div>
<svg class="rings" width="1080" height="1080" viewBox="0 0 1080 1080" xmlns="http://www.w3.org/2000/svg">
<circle cx="920" cy="130" r="110" fill="none" stroke="#CB6B20" stroke-width="0.8" opacity="0.12"/>
<circle cx="920" cy="130" r="60" fill="none" stroke="#CB6B20" stroke-width="1.0" opacity="0.26"/>
<circle cx="920" cy="130" r="9" fill="#CB6B20" opacity="0.82"/>
</svg>
<svg class="dots" width="1080" height="1080" viewBox="0 0 1080 1080" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="fade" cx="89%" cy="12%" r="70%">
<stop offset="0%" stop-color="#1A5050" stop-opacity="1"/>
<stop offset="100%" stop-color="#1A5050" stop-opacity="0"/>
</radialGradient>
<pattern id="hex" x="0" y="0" width="24" height="20.8" patternUnits="userSpaceOnUse">
<circle cx="12" cy="4" r="1.4" fill="#1A5050"/>
<circle cx="0" cy="10.4" r="1.4" fill="#1A5050"/>
<circle cx="24" cy="10.4" r="1.4" fill="#1A5050"/>
<circle cx="12" cy="16.8" r="1.4" fill="#1A5050"/>
</pattern>
<mask id="rm"><rect width="1080" height="1080" fill="url(#fade)"/></mask>
</defs>
<rect width="1080" height="1080" fill="url(#hex)" mask="url(#rm)"/>
</svg>
<div class="signal-brand">
<div class="brand-name">Signal.</div>
<div class="brand-sub">Powered by STTIL Solutions</div>
</div>
<div class="content">
<div class="headline">
<div class="eyebrow">Market Opportunity</div>
<h1>A solvable problem hiding<br>in plain sight</h1>
<p>CGM documentation errors are predictable, traceable, and preventable — before the claim is ever filed.</p>
</div>
<!-- Three big stats -->
<div class="stat-row">
<div class="stat-block dark">
<div class="num">1 in 3</div>
<div class="label">CGM claims contains a documentation error at the time of review</div>
<div class="source">CMS CERT Report 2019</div>
</div>
<div class="stat-block mid">
<div class="num">68%</div>
<div class="label">Of those errors originate with the prescribing provider — not the supplier</div>
<div class="source">CMS CERT Report 2019</div>
</div>
<div class="stat-block warm">
<div class="num">~25%</div>
<div class="label">Of CGM supplies shipped when documentation fails to pass — at or below cost</div>
<div class="source">DMEPOS supplier operations data</div>
</div>
</div>
<!-- Research paradox -->
<div class="paradox">
<div class="paradox-number">$400M+</div>
<div class="paradox-text">
<div class="p-headline">The research paradox: suppliers already pay to document the problem Signal solves.</div>
<div class="p-sub">Staff time spent chasing records, filing appeals, and managing write-offs exceeds $400M annually across the DMEPOS supply chain. Signal redirects that spend from reactive to proactive.</div>
</div>
<div class="paradox-divider"></div>
<div class="paradox-signal">
<div class="ps-label">Signal cost</div>
<div class="ps-value">$1,788</div>
<div class="ps-sub">per supplier / year<br>Worklist tier</div>
</div>
</div>
<!-- Opportunity grid -->
<div class="opp-grid" style="margin-bottom:16px;">
<div class="opp-card">
<div class="opp-accent teal"></div>
<div class="opp-title">6,000+ DMEPOS suppliers</div>
<div class="opp-desc">All face the same documentation compliance burden under CMS LCD L33822 and payer-specific prior authorization requirements.</div>
</div>
<div class="opp-card">
<div class="opp-accent copper"></div>
<div class="opp-title">CGM is the beachhead</div>
<div class="opp-desc">High denial rate, defined LCD requirements, and a 6-month qualifying visit cycle create a repeating, automatable compliance event.</div>
</div>
<div class="opp-card">
<div class="opp-accent dark"></div>
<div class="opp-title">Every DMEPOS category follows</div>
<div class="opp-desc">Oxygen, wound care, power mobility — same documentation burden, same denial patterns. Signal's model extends to each.</div>
</div>
</div>
<!-- Bottom thesis bar -->
<div style="flex:1;display:flex;flex-direction:column;justify-content:flex-end;padding-bottom:8px;">
<div style="background:#2EA3A3;border-radius:10px;padding:18px 26px;display:flex;align-items:center;gap:24px;">
<div style="flex:1;">
<div style="font-size:15px;font-weight:700;color:#fff;line-height:1.45;">No new infrastructure. No vendor integration. No change management. Suppliers upload one CSV and Signal surfaces every gap — before a single claim is filed.</div>
</div>
<div style="flex-shrink:0;text-align:center;padding-left:20px;border-left:1px solid rgba(255,255,255,0.25);">
<div style="font-size:10px;font-weight:700;color:rgba(255,255,255,0.6);letter-spacing:0.1em;text-transform:uppercase;margin-bottom:5px;">Time to first value</div>
<div style="font-size:38px;font-weight:800;color:#fff;line-height:1;">Day 1</div>
<div style="font-size:10.5px;color:rgba(255,255,255,0.65);margin-top:3px;font-weight:500;">Upload a CSV.<br>See the gaps.</div>
</div>
</div>
</div>
</div>
<div class="footer">
<span>signal.sttilsolutions.com</span>
<span>Sources: CMS CERT 2019 &nbsp;&middot;&nbsp; DMEPOS operations data &nbsp;&middot;&nbsp; &copy; 2026 STTIL Solutions LLC</span>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

View file

@ -0,0 +1,267 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Signal — Win-Win-Win</title>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background: #F0EAE1;
width: 1080px; height: 1080px;
overflow: hidden; position: relative;
}
.top-bar { position: absolute; top: 0; left: 0; right: 0; height: 5px; background: #2EA3A3; }
.signal-brand { position: absolute; top: 28px; right: 80px; text-align: right; z-index: 3; }
.brand-name { font-size: 18px; font-weight: 800; color: #1A5050; letter-spacing: 0.02em; }
.brand-sub { font-size: 9px; font-weight: 500; color: #8AABAB; letter-spacing: 0.12em; text-transform: uppercase; margin-top: 3px; }
.rings { position: absolute; top: 0; right: 0; pointer-events: none; }
.dots { position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; opacity: 0.14; }
.footer { position: absolute; bottom: 22px; left: 70px; right: 70px; display: flex; justify-content: space-between; }
.footer span { font-size: 10px; color: #8AABAB; font-weight: 500; }
.content {
position: absolute;
top: 64px; left: 60px; right: 60px; bottom: 50px;
display: flex; flex-direction: column;
}
.headline { text-align: center; flex-shrink: 0; margin-bottom: 24px; }
.headline .eyebrow { font-size: 11px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: #CB6B20; margin-bottom: 8px; }
.headline h1 { font-size: 32px; font-weight: 800; color: #1A5050; letter-spacing: -0.02em; }
.headline p { font-size: 13px; color: #5A7A7A; margin-top: 6px; font-weight: 500; }
/* Middle row: Suppliers | Signal | Payers */
.mid-row {
display: flex;
gap: 20px;
align-items: stretch;
flex-shrink: 0;
margin-bottom: 20px;
}
/* Signal center column */
.center-col {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 160px;
gap: 0;
}
.connector-v {
width: 1.5px;
height: 24px;
background: linear-gradient(to bottom, rgba(46,163,163,0.3), rgba(46,163,163,0.6));
flex-shrink: 0;
}
.medallion {
width: 110px; height: 110px;
border-radius: 50%;
background: #1A5050;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
box-shadow: 0 0 0 6px #F0EAE1, 0 0 0 12px rgba(46,163,163,0.18), 0 0 0 20px rgba(46,163,163,0.07);
flex-shrink: 0;
}
.medallion .sig { font-size: 22px; font-weight: 800; color: #fff; }
.medallion .sig-sub { font-size: 7.5px; font-weight: 600; color: #2EA3A3; letter-spacing: 0.1em; text-transform: uppercase; margin-top: 3px; text-align: center; line-height: 1.3; }
/* Horizontal connectors from medallion to cards */
.h-connector {
height: 1.5px;
flex: 1;
background: linear-gradient(to right, rgba(46,163,163,0.6), rgba(46,163,163,0.2));
}
.h-connector.right {
background: linear-gradient(to left, rgba(46,163,163,0.6), rgba(46,163,163,0.2));
}
/* Stakeholder cards */
.card {
flex: 1;
border-radius: 12px;
padding: 20px 18px;
display: flex; flex-direction: column;
}
.card.suppliers { background: #1A5050; }
.card.payers { background: #EAE4DB; border: 1.5px solid #B0A898; }
.card-label { font-size: 10px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 8px; }
.card.suppliers .card-label { color: #2EA3A3; }
.card.payers .card-label { color: #8A7A6A; }
.card-title { font-size: 16px; font-weight: 800; margin-bottom: 12px; line-height: 1.2; }
.card.suppliers .card-title { color: #fff; }
.card.payers .card-title { color: #1A5050; }
.win-list { display: flex; flex-direction: column; gap: 7px; }
.win-item { display: flex; align-items: flex-start; gap: 8px; font-size: 11.5px; font-weight: 500; line-height: 1.35; }
.win-check { width: 15px; height: 15px; border-radius: 3px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-top: 1px; font-size: 9px; font-weight: 800; }
.card.suppliers .win-check { background: #2EA3A3; color: #fff; }
.card.payers .win-check { background: #1A5050; color: #fff; }
.card.suppliers .win-item { color: rgba(255,255,255,0.85); }
.card.payers .win-item { color: #3A5050; }
/* Bottom patients card — full width */
.patients-row {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
/* Down-connector from medallion area to patients */
.down-connector-wrap {
display: flex;
justify-content: center;
flex-shrink: 0;
height: 0;
position: relative;
margin-bottom: 8px;
}
.down-line {
width: 1.5px;
height: 20px;
background: linear-gradient(to bottom, rgba(203,107,32,0.6), rgba(203,107,32,0.3));
position: absolute;
top: 0;
}
.card.patients {
flex: none;
background: #F5EDE0;
border: 1.5px solid #CB6B20;
border-radius: 12px;
padding: 18px 22px;
}
.card.patients .card-label { color: #CB6B20; }
.card.patients .card-title { color: #6A3810; font-size: 17px; }
.patients-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 40px; }
.patients-grid .win-check { background: #CB6B20; color: #fff; }
.patients-grid .win-item { color: #6A3810; }
</style>
</head>
<body>
<div class="top-bar"></div>
<svg class="rings" width="1080" height="1080" viewBox="0 0 1080 1080" xmlns="http://www.w3.org/2000/svg">
<circle cx="920" cy="130" r="110" fill="none" stroke="#CB6B20" stroke-width="0.8" opacity="0.12"/>
<circle cx="920" cy="130" r="60" fill="none" stroke="#CB6B20" stroke-width="1.0" opacity="0.26"/>
<circle cx="920" cy="130" r="9" fill="#CB6B20" opacity="0.82"/>
</svg>
<svg class="dots" width="1080" height="1080" viewBox="0 0 1080 1080" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="fade" cx="89%" cy="12%" r="70%">
<stop offset="0%" stop-color="#1A5050" stop-opacity="1"/>
<stop offset="100%" stop-color="#1A5050" stop-opacity="0"/>
</radialGradient>
<pattern id="hex" x="0" y="0" width="24" height="20.8" patternUnits="userSpaceOnUse">
<circle cx="12" cy="4" r="1.4" fill="#1A5050"/>
<circle cx="0" cy="10.4" r="1.4" fill="#1A5050"/>
<circle cx="24" cy="10.4" r="1.4" fill="#1A5050"/>
<circle cx="12" cy="16.8" r="1.4" fill="#1A5050"/>
</pattern>
<mask id="rm"><rect width="1080" height="1080" fill="url(#fade)"/></mask>
</defs>
<rect width="1080" height="1080" fill="url(#hex)" mask="url(#rm)"/>
</svg>
<div class="signal-brand">
<div class="brand-name">Signal.</div>
<div class="brand-sub">Powered by STTIL Solutions</div>
</div>
<div class="content">
<div class="headline">
<div class="eyebrow">The Signal Alignment</div>
<h1>Everybody wins when documentation is right.</h1>
<p>Signal is the only layer built for all three parties in the CGM supply chain.</p>
</div>
<!-- Mid row: Suppliers | Signal medallion | Payers -->
<div class="mid-row">
<div class="card suppliers">
<div class="card-label">DMEPOS Suppliers</div>
<div class="card-title">Protect revenue.<br>Work proactively.</div>
<div class="win-list">
<div class="win-item"><div class="win-check">&#10003;</div><span>Gaps caught before claim filing — no surprise denials</span></div>
<div class="win-item"><div class="win-check">&#10003;</div><span>Staff time shifts from appeals to upstream outreach</span></div>
<div class="win-item"><div class="win-check">&#10003;</div><span>Supply shipped with confidence, not hope</span></div>
<div class="win-item"><div class="win-check">&#10003;</div><span>First-pass claim rate improves over time</span></div>
</div>
</div>
<!-- H connector left -->
<div style="display:flex;align-items:center;flex-shrink:0;width:28px;">
<div class="h-connector"></div>
</div>
<div class="center-col">
<div class="medallion">
<div class="sig">Signal.</div>
<div class="sig-sub">Documentation<br>Readiness</div>
</div>
</div>
<!-- H connector right -->
<div style="display:flex;align-items:center;flex-shrink:0;width:28px;">
<div class="h-connector right"></div>
</div>
<div class="card payers">
<div class="card-label">Medicare &amp; Payers</div>
<div class="card-title">Cleaner claims.<br>Lower admin load.</div>
<div class="win-list">
<div class="win-item"><div class="win-check">&#10003;</div><span>Claims arrive with documentation already in order</span></div>
<div class="win-item"><div class="win-check">&#10003;</div><span>Fewer appeals and resubmissions to process</span></div>
<div class="win-item"><div class="win-check">&#10003;</div><span>Audit exposure reduced — records traceable upstream</span></div>
<div class="win-item"><div class="win-check">&#10003;</div><span>PA affirmation rates trend toward CMS exemption threshold</span></div>
</div>
</div>
</div>
<!-- Down connector -->
<div style="display:flex;justify-content:center;flex-shrink:0;margin-bottom:12px;">
<div style="width:1.5px;height:18px;background:linear-gradient(to bottom, rgba(203,107,32,0.5), rgba(203,107,32,0.2));"></div>
</div>
<!-- Patients card full width -->
<div class="card patients">
<div class="card-label">CGM Patients</div>
<div class="card-title" style="margin-bottom:14px;">Supply continuity. No gaps. No delays.</div>
<div class="patients-grid">
<div class="win-item"><div class="win-check" style="background:#CB6B20;color:#fff;width:15px;height:15px;border-radius:3px;font-size:9px;font-weight:800;display:flex;align-items:center;justify-content:center;flex-shrink:0;">&#10003;</div><span style="color:#6A3810;font-size:11.5px;font-weight:500;line-height:1.35;">CGM resupply arrives on schedule</span></div>
<div class="win-item"><div class="win-check" style="background:#CB6B20;color:#fff;width:15px;height:15px;border-radius:3px;font-size:9px;font-weight:800;display:flex;align-items:center;justify-content:center;flex-shrink:0;">&#10003;</div><span style="color:#6A3810;font-size:11.5px;font-weight:500;line-height:1.35;">No interruption in glucose monitoring</span></div>
<div class="win-item"><div class="win-check" style="background:#CB6B20;color:#fff;width:15px;height:15px;border-radius:3px;font-size:9px;font-weight:800;display:flex;align-items:center;justify-content:center;flex-shrink:0;">&#10003;</div><span style="color:#6A3810;font-size:11.5px;font-weight:500;line-height:1.35;">Qualifying visits tracked proactively — 6-month cycle never missed</span></div>
<div class="win-item"><div class="win-check" style="background:#CB6B20;color:#fff;width:15px;height:15px;border-radius:3px;font-size:9px;font-weight:800;display:flex;align-items:center;justify-content:center;flex-shrink:0;">&#10003;</div><span style="color:#6A3810;font-size:11.5px;font-weight:500;line-height:1.35;">Better health outcomes from uninterrupted CGM data</span></div>
</div>
</div>
<!-- Bottom insight bar -->
<div style="flex:1;display:flex;flex-direction:column;justify-content:flex-end;padding-bottom:10px;">
<div style="background:#1A5050;border-radius:10px;padding:22px 28px;display:flex;align-items:center;gap:28px;">
<div style="flex:1;">
<div style="font-size:11px;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;color:#2EA3A3;margin-bottom:8px;">The bottom line</div>
<div style="font-size:16px;font-weight:700;color:#fff;line-height:1.4;">When a supplier resolves a documentation gap before filing, the payer receives a clean claim and the patient gets their supply on time. Signal makes that happen at scale — without adding staff, changing systems, or waiting for a denial.</div>
</div>
<div style="flex-shrink:0;text-align:center;padding-left:12px;border-left:1px solid rgba(255,255,255,0.15);">
<div style="font-size:13px;font-weight:700;color:rgba(255,255,255,0.5);letter-spacing:0.08em;text-transform:uppercase;margin-bottom:6px;">Signal cost</div>
<div style="font-size:36px;font-weight:800;color:#CB6B20;line-height:1;">$1,788</div>
<div style="font-size:11px;color:rgba(255,255,255,0.55);margin-top:4px;font-weight:500;">per supplier / year<br>Worklist tier</div>
</div>
</div>
</div>
</div>
<div class="footer">
<span>signal.sttilsolutions.com</span>
<span>&copy; 2026 STTIL Solutions LLC</span>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

View file

@ -0,0 +1,361 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Signal — How It Works</title>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background: #F0EAE1;
width: 1080px;
height: 1080px;
overflow: hidden;
position: relative;
}
.top-bar {
position: absolute;
top: 0; left: 0; right: 0;
height: 5px;
background: #2EA3A3;
}
.signal-brand {
position: absolute;
top: 28px; right: 80px;
text-align: right;
z-index: 3;
}
.brand-name { font-size: 18px; font-weight: 800; color: #1A5050; letter-spacing: 0.02em; }
.brand-sub { font-size: 9px; font-weight: 500; color: #8AABAB; letter-spacing: 0.12em; text-transform: uppercase; margin-top: 3px; }
.rings { position: absolute; top: 0; right: 0; pointer-events: none; }
.dots { position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; opacity: 0.18; }
/* Main content area */
.content {
position: absolute;
top: 60px; left: 60px; right: 60px; bottom: 52px;
display: flex;
flex-direction: column;
}
.headline {
text-align: center;
margin-bottom: 24px;
flex-shrink: 0;
}
.headline h1 { font-size: 30px; font-weight: 800; color: #1A5050; letter-spacing: -0.02em; }
.headline p { font-size: 13px; color: #5A7A7A; margin-top: 6px; font-weight: 500; }
/* Two-column wrapper — takes available height */
.columns {
display: flex;
gap: 24px;
flex: 1;
min-height: 0;
}
.col {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.col-header {
text-align: center;
padding: 10px 20px;
border-radius: 8px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 14px;
flex-shrink: 0;
}
.col-header.without { background: #D8CFC4; color: #7A6A5A; }
.col-header.with { background: #1A5050; color: #fff; }
.flow {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
justify-content: space-between;
}
.node {
width: 100%;
padding: 13px 16px;
border-radius: 8px;
text-align: center;
font-size: 12.5px;
font-weight: 600;
line-height: 1.4;
}
.node.w-step { background: #EAE3DA; color: #5A4E42; border: 1.5px solid #C8BFB4; }
.node.w-pain { background: #F5EDE0; color: #7A4A1A; border: 1.5px solid #D4A070; }
.node.w-outcome { background: #F5E8E0; color: #8A2A10; border: 1.5px solid #D07060; }
.node.s-step { background: #E5F0F0; color: #1A5050; border: 1.5px solid #2EA3A3; }
.node.s-check { background: #1A5050; color: #fff; border: none; }
.node.s-gap { background: #FDF0E0; color: #884010; border: 1.5px solid #CB6B20; }
.node.s-win { background: #2EA3A3; color: #fff; border: none; font-size: 13px; font-weight: 700; }
.arrow {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
justify-content: center;
}
.arrow-line { width: 2px; flex: 1; max-height: 28px; min-height: 8px; background: #A8B8B8; }
.arrow-line.red { background: #C06050; }
.arrow-line.teal { background: #2EA3A3; }
.arrow-head { width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 7px solid #A8B8B8; flex-shrink: 0; }
.arrow-head.red { border-top-color: #C06050; }
.arrow-head.teal { border-top-color: #2EA3A3; }
.check-items { display: flex; flex-direction: column; gap: 4px; margin-top: 8px; }
.check-item {
background: rgba(255,255,255,0.15);
border-radius: 4px;
padding: 4px 8px;
font-size: 11px;
font-weight: 500;
display: flex;
align-items: center;
gap: 6px;
text-align: left;
}
.check-item .dot { width: 6px; height: 6px; border-radius: 50%; background: #2EA3A3; flex-shrink: 0; }
.pain-sub { font-size: 10.5px; font-weight: 500; margin-top: 4px; opacity: 0.8; }
.gap-items { display: flex; gap: 8px; margin-top: 6px; justify-content: center; }
.gap-tag { background: #CB6B20; color: #fff; border-radius: 4px; padding: 2px 7px; font-size: 10px; font-weight: 700; }
.divider {
width: 1px;
background: linear-gradient(to bottom, transparent, #B0A898 20%, #B0A898 80%, transparent);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.divider-label {
background: #F0EAE1;
color: #8A7A6A;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
padding: 6px 4px;
writing-mode: vertical-lr;
transform: rotate(180deg);
}
/* Stats bar — outside columns, at bottom of content */
.stats-bar {
display: flex;
flex-shrink: 0;
margin-top: 16px;
border-radius: 8px;
overflow: hidden;
}
.stat-cell {
flex: 1;
padding: 11px 12px;
text-align: center;
}
.stat-cell.neg { background: #DDD5C8; }
.stat-cell.pos { background: #1A5050; }
.stat-num {
font-size: 20px;
font-weight: 800;
line-height: 1;
margin-bottom: 3px;
}
.stat-cell.neg .stat-num { color: #8A2A10; }
.stat-cell.pos .stat-num { color: #2EA3A3; }
.stat-lbl {
font-size: 9.5px;
font-weight: 500;
line-height: 1.3;
}
.stat-cell.neg .stat-lbl { color: #6A5A4A; }
.stat-cell.pos .stat-lbl { color: rgba(255,255,255,0.72); }
.stat-div { width: 1px; background: rgba(0,0,0,0.1); flex-shrink: 0; }
.stat-section-label {
flex-shrink: 0;
padding: 0 10px;
display: flex;
align-items: center;
justify-content: center;
}
.stat-section-label span {
writing-mode: vertical-lr;
transform: rotate(180deg);
font-size: 8px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.stat-section-label.neg-label { background: #C8BFB2; }
.stat-section-label.neg-label span { color: #8A7A6A; }
.stat-section-label.pos-label { background: #163E3E; }
.stat-section-label.pos-label span { color: rgba(255,255,255,0.5); }
.footer {
position: absolute;
bottom: 16px;
left: 60px; right: 60px;
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-left { font-size: 10px; color: #8AABAB; font-weight: 500; }
.footer-right { font-size: 10px; color: #8AABAB; font-weight: 500; }
</style>
</head>
<body>
<div class="top-bar"></div>
<svg class="rings" width="1080" height="1080" viewBox="0 0 1080 1080" xmlns="http://www.w3.org/2000/svg">
<circle cx="920" cy="130" r="110" fill="none" stroke="#CB6B20" stroke-width="0.8" opacity="0.12"/>
<circle cx="920" cy="130" r="60" fill="none" stroke="#CB6B20" stroke-width="1.0" opacity="0.26"/>
<circle cx="920" cy="130" r="9" fill="#CB6B20" opacity="0.82"/>
</svg>
<svg class="dots" width="1080" height="1080" viewBox="0 0 1080 1080" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="fade" cx="89%" cy="12%" r="70%">
<stop offset="0%" stop-color="#1A5050" stop-opacity="1"/>
<stop offset="100%" stop-color="#1A5050" stop-opacity="0"/>
</radialGradient>
<pattern id="hex" x="0" y="0" width="24" height="20.8" patternUnits="userSpaceOnUse">
<circle cx="12" cy="4" r="1.4" fill="#1A5050"/>
<circle cx="0" cy="10.4" r="1.4" fill="#1A5050"/>
<circle cx="24" cy="10.4" r="1.4" fill="#1A5050"/>
<circle cx="12" cy="16.8" r="1.4" fill="#1A5050"/>
</pattern>
<mask id="radmask">
<rect width="1080" height="1080" fill="url(#fade)"/>
</mask>
</defs>
<rect width="1080" height="1080" fill="url(#hex)" mask="url(#radmask)"/>
</svg>
<div class="signal-brand">
<div class="brand-name">Signal.</div>
<div class="brand-sub">Powered by STTIL Solutions</div>
</div>
<div class="content">
<div class="headline">
<h1>How Signal Works</h1>
<p>Catching documentation gaps before they become denied claims</p>
</div>
<div class="columns">
<!-- WITHOUT SIGNAL -->
<div class="col">
<div class="col-header without">Without Signal</div>
<div class="flow">
<div class="node w-step">Patient flagged for CGM resupply</div>
<div class="arrow"><div class="arrow-line red"></div><div class="arrow-head red"></div></div>
<div class="node w-step">Staff manually tracks documentation across spreadsheets and faxes</div>
<div class="arrow"><div class="arrow-line red"></div><div class="arrow-head red"></div></div>
<div class="node w-step">Claim filed to payer</div>
<div class="arrow"><div class="arrow-line red"></div><div class="arrow-head red"></div></div>
<div class="node w-pain">
Denial: documentation incomplete
<div class="pain-sub">SWO missing &middot; Qualifying visit overdue &middot; PA not confirmed</div>
</div>
<div class="arrow"><div class="arrow-line red"></div><div class="arrow-head red"></div></div>
<div class="node w-step">Appeal filed &mdash; 6 to 8 week delay</div>
<div class="arrow"><div class="arrow-line red"></div><div class="arrow-head red"></div></div>
<div class="node w-outcome">Write-off or supply shipped at a loss</div>
</div>
</div>
<div class="divider"><div class="divider-label">vs</div></div>
<!-- WITH SIGNAL -->
<div class="col">
<div class="col-header with">With Signal</div>
<div class="flow">
<div class="node s-step">Patient flagged for CGM resupply</div>
<div class="arrow"><div class="arrow-line teal"></div><div class="arrow-head teal"></div></div>
<div class="node s-step">Staff uploads patient list CSV &mdash; Signal ingests in seconds</div>
<div class="arrow"><div class="arrow-line teal"></div><div class="arrow-head teal"></div></div>
<div class="node s-check">
Signal checks documentation status automatically
<div class="check-items">
<div class="check-item"><div class="dot"></div>Standard Written Order (SWO)</div>
<div class="check-item"><div class="dot"></div>Qualifying visit &mdash; within 6 months</div>
<div class="check-item"><div class="dot"></div>Prior Authorization status</div>
</div>
</div>
<div class="arrow"><div class="arrow-line teal"></div><div class="arrow-head teal"></div></div>
<div class="node s-gap">
Gaps flagged on prioritized worklist &mdash; before any claim is filed
<div class="gap-items">
<div class="gap-tag">&#9679; SWO needed</div>
<div class="gap-tag">&#9679; Visit overdue</div>
</div>
</div>
<div class="arrow"><div class="arrow-line teal"></div><div class="arrow-head teal"></div></div>
<div class="node s-step">Staff resolves gaps upstream &mdash; contacts prescriber, requests records</div>
<div class="arrow"><div class="arrow-line teal"></div><div class="arrow-head teal"></div></div>
<div class="node s-win">Clear to Ship &mdash; supply delivered on time</div>
</div>
</div>
</div>
<!-- Stats bar below both columns -->
<div class="stats-bar">
<div class="stat-section-label neg-label"><span>Problem</span></div>
<div class="stat-cell neg">
<div class="stat-num">1 in 3</div>
<div class="stat-lbl">CGM claims with a doc error<br><span style="font-size:8.5px;opacity:0.7">CERT 2019</span></div>
</div>
<div class="stat-div"></div>
<div class="stat-cell neg">
<div class="stat-num">~25%</div>
<div class="stat-lbl">Of CGM supplies shipped at a loss when docs fail</div>
</div>
<div class="stat-div"></div>
<div class="stat-cell neg">
<div class="stat-num">68 wks</div>
<div class="stat-lbl">Average denial recovery after appeal</div>
</div>
<div class="stat-section-label pos-label"><span>Signal</span></div>
<div class="stat-cell pos">
<div class="stat-num">Day 1</div>
<div class="stat-lbl">Gaps visible before claim is filed</div>
</div>
<div class="stat-div"></div>
<div class="stat-cell pos">
<div class="stat-num">$0</div>
<div class="stat-lbl">Claims filed with known documentation gaps</div>
</div>
</div>
</div>
<div class="footer">
<div class="footer-left">signal.sttilsolutions.com</div>
<div class="footer-right">Source: CERT 2019 &nbsp;&middot;&nbsp; &copy; 2026 STTIL Solutions LLC</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

View file

@ -34,8 +34,9 @@ from api.normalizer import normalize_csv
app = FastAPI(title="Signal API", version="1.0.0", docs_url="/docs")
# CORS — locked to Vercel frontend and localhost for dev.
# Set ALLOWED_ORIGINS in Railway as a comma-separated list for production.
# CORS — set ALLOWED_ORIGINS_REGEX for a pattern (recommended for Vercel previews),
# or ALLOWED_ORIGINS as a comma-separated list for explicit control.
_origins_regex = os.getenv("ALLOWED_ORIGINS_REGEX", "")
_origins_env = os.getenv("ALLOWED_ORIGINS", "")
_allowed_origins: list[str] = (
[o.strip() for o in _origins_env.split(",") if o.strip()]
@ -47,13 +48,17 @@ _allowed_origins: list[str] = (
]
)
app.add_middleware(
CORSMiddleware,
allow_origins=_allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
_cors_kwargs: dict = {
"allow_credentials": True,
"allow_methods": ["*"],
"allow_headers": ["*"],
}
if _origins_regex:
_cors_kwargs["allow_origin_regex"] = _origins_regex
else:
_cors_kwargs["allow_origins"] = _allowed_origins
app.add_middleware(CORSMiddleware, **_cors_kwargs)
# API key auth — enforced when SIGNAL_API_KEY env var is set.
# In dev (no env var), all requests pass. In production, X-API-Key header is required.

View file

@ -58,53 +58,55 @@ def _build_cascade(
pa: str,
diagnosis: str,
payer_type: str,
swo_supplied: bool = True,
visit_supplied: bool = True,
diagnosis_supplied: bool = True,
pecos_supplied: bool = True,
pa_supplied: bool = True,
) -> list:
"""
Build the full consequence chain for all documentation gaps.
No early returns every unfulfilled requirement is surfaced.
Only flags gaps for columns that were actually present in the CSV.
An absent column is "not evaluated", not a gap green means no known blocker.
Type A = doc gaps Signal can help close.
Type B = route-to-action alerts for denials.
"""
cascade = []
# Type A: Documentation gaps
if not visit_confirmed:
# Type A: Documentation gaps — only fire when the column was supplied
if visit_supplied and not visit_confirmed:
cascade.append(
"Qualifying visit not on file — confirm with prescriber before scheduling shipment"
)
if swo in ("Pending", "Expired"):
if swo_supplied and swo in ("Pending", "Expired"):
label = "not on file" if swo == "Pending" else "expired"
cascade.append(
f"Written order {label} — obtain from prescriber before claim submission"
)
if diagnosis == "Missing":
if diagnosis_supplied and diagnosis == "Missing":
cascade.append(
"Diagnosis code not on file — obtain from prescriber records before billing"
)
if payer_type in _PECOS_REQUIRED and pecos == "Not Verified":
if pecos_supplied and payer_type in _PECOS_REQUIRED and pecos == "Not Verified":
cascade.append(
"Ordering provider PECOS enrollment not verified — confirm NPI is active before claim submission"
)
if payer_type not in _PA_NOT_REQUIRED and pa == "Required — Not Started":
if pa_supplied and payer_type not in _PA_NOT_REQUIRED and pa == "Required — Not Started":
cascade.append(
"Prior authorization not started — obtain before scheduling shipment"
)
# Type B: Route-to-action alert
# Type B: Route-to-action alert — PA denied is always surfaced (supplied-and-bad)
if pa == "Denied":
cascade.append(
"Prior authorization denied — file Level 1 appeal within 65 days. "
"Approximately 80% of first-level Medicare Advantage appeals succeed."
)
# Consequence line — only appended when at least one gap exists
if cascade:
cascade.append("Device shipment at risk")
return cascade
@ -175,7 +177,20 @@ def compute_doc_state(
dx_val = (csv_diagnosis_on_file or "").strip().lower()
diagnosis = "On File" if dx_val in ("yes", "true", "on file", "1") else "Missing"
cascade = _build_cascade(swo, visit_confirmed, pecos, pa, diagnosis, payer_type)
swo_supplied = csv_swo_status is not None
visit_supplied = (csv_visit_date is not None) or (confirmed_visit_date is not None)
diagnosis_supplied = csv_diagnosis_on_file is not None
pecos_supplied = csv_pecos_verified is not None
pa_supplied = csv_pa_status is not None
cascade = _build_cascade(
swo, visit_confirmed, pecos, pa, diagnosis, payer_type,
swo_supplied=swo_supplied,
visit_supplied=visit_supplied,
diagnosis_supplied=diagnosis_supplied,
pecos_supplied=pecos_supplied,
pa_supplied=pa_supplied,
)
return DocState(
swo=swo,

407
scripts/signal_e2e_test.py Normal file
View file

@ -0,0 +1,407 @@
#!/usr/bin/env python3
"""
Signal End-to-End Test Agent
Hits the LIVE backend at signal-api-production-91c2.up.railway.app.
Tests every functional layer and produces a plain-English findings report.
Usage:
python3 scripts/signal_e2e_test.py
python3 scripts/signal_e2e_test.py --url https://signal-api-production-91c2.up.railway.app
Authentication: API key from env var SIGNAL_API_KEY or hardcoded fallback.
"""
import argparse
import json
import os
import sys
import time
from datetime import date, timedelta
from pathlib import Path
# Third-party (stdlib only — no extra deps needed)
import urllib.request
import urllib.error
REPO_ROOT = Path(__file__).parent.parent
TEST_DATA = REPO_ROOT / "test-data"
DEFAULT_URL = "https://signal-api-production-91c2.up.railway.app"
API_KEY = os.getenv("SIGNAL_API_KEY", "4HKamqzn78Md-L-xj2OjZESVwZh2Otzc6iFRyqVZl7U")
PASS = "PASS"
FAIL = "FAIL"
WARN = "WARN"
SKIP = "SKIP"
findings: list[dict] = []
def record(status, name, detail=""):
findings.append({"status": status, "name": name, "detail": detail})
icon = {"PASS": "", "FAIL": "", "WARN": "", "SKIP": ""}[status]
print(f" {icon} [{status}] {name}")
if detail:
for line in detail.splitlines():
print(f" {line}")
def api_key_headers():
return {"X-Api-Key": API_KEY}
def http_get(url, headers=None):
req = urllib.request.Request(url, headers=headers or {})
try:
with urllib.request.urlopen(req, timeout=30) as resp:
body = resp.read()
return resp.status, json.loads(body)
except urllib.error.HTTPError as e:
body = e.read()
try:
return e.code, json.loads(body)
except Exception:
return e.code, {"raw": body.decode(errors="replace")}
except Exception as e:
return 0, {"error": str(e)}
def http_post_json(url, payload, headers=None, raw_ok=False):
data = json.dumps(payload).encode()
h = {"Content-Type": "application/json", **(headers or {})}
req = urllib.request.Request(url, data=data, headers=h, method="POST")
try:
with urllib.request.urlopen(req, timeout=30) as resp:
body = resp.read()
content_type = resp.headers.get("Content-Type", "")
if raw_ok or "csv" in content_type or "octet" in content_type:
return resp.status, {"raw": body.decode(errors="replace")}
return resp.status, json.loads(body)
except urllib.error.HTTPError as e:
body = e.read()
try:
return e.code, json.loads(body)
except Exception:
return e.code, {"raw": body.decode(errors="replace")}
except Exception as e:
return 0, {"error": str(e)}
def http_put_json(url, payload, headers=None):
data = json.dumps(payload).encode()
h = {"Content-Type": "application/json", **(headers or {})}
req = urllib.request.Request(url, data=data, headers=h, method="PUT")
try:
with urllib.request.urlopen(req, timeout=30) as resp:
body = resp.read()
return resp.status, json.loads(body)
except urllib.error.HTTPError as e:
body = e.read()
try:
return e.code, json.loads(body)
except Exception:
return e.code, {"raw": body.decode(errors="replace")}
except Exception as e:
return 0, {"error": str(e)}
def upload_csv_multipart(url, csv_path, headers=None):
"""Upload a CSV file via multipart/form-data."""
import io, uuid
boundary = uuid.uuid4().hex
csv_data = Path(csv_path).read_bytes()
filename = Path(csv_path).name
body_parts = []
body_parts.append(f"--{boundary}\r\n".encode())
body_parts.append(
f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'.encode()
)
body_parts.append(b"Content-Type: text/csv\r\n\r\n")
body_parts.append(csv_data)
body_parts.append(f"\r\n--{boundary}--\r\n".encode())
body = b"".join(body_parts)
h = {
"Content-Type": f"multipart/form-data; boundary={boundary}",
**(headers or {}),
}
req = urllib.request.Request(url, data=body, headers=h, method="POST")
try:
with urllib.request.urlopen(req, timeout=60) as resp:
return resp.status, json.loads(resp.read())
except urllib.error.HTTPError as e:
body_resp = e.read()
try:
return e.code, json.loads(body_resp)
except Exception:
return e.code, {"raw": body_resp.decode(errors="replace")}
except Exception as e:
return 0, {"error": str(e)}
def run_all(base_url):
auth = api_key_headers()
# ── 1. Health ──────────────────────────────────────────────────────────────
print("\n── 1. Health checks ──")
status, body = http_get(f"{base_url}/health", auth)
if status == 200 and body.get("status") == "ok":
record(PASS, "Backend /health responds 200 ok")
else:
record(FAIL, "Backend /health failed", f"HTTP {status}: {body}")
print("\n Backend unreachable — all subsequent tests will fail. Check Railway.\n")
return
status, body = http_get(f"{base_url}/health/db", auth)
if status == 200 and body.get("status") == "ok":
record(PASS, "Supabase DB connection /health/db responds ok")
elif body.get("status") == "unavailable":
record(WARN, "Supabase not configured (dev mode)", str(body))
else:
record(FAIL, "Supabase DB connection failed", str(body))
# ── 2. Upload — sample-green.csv ───────────────────────────────────────────
print("\n── 2. Upload: sample-green.csv (expects 1 green record) ──")
green_csv = TEST_DATA / "sample-green.csv"
if not green_csv.exists():
record(SKIP, "sample-green.csv not found — run verify_green.py first")
else:
status, body = upload_csv_multipart(f"{base_url}/api/upload", green_csv, auth)
if status == 200:
records_out = body.get("records", [])
green = [r for r in records_out
if r["flag"] == "RESUPPLY_READY" and not r.get("cascade")]
total = body.get("total", 0)
if green:
record(PASS, f"Green upload: 1 record, {len(green)} clear-to-ship",
f"Patient: {green[0]['patient_id']}, Days left: {green[0]['days_until_coverage_end']}")
green_record = green[0]
else:
record(FAIL, f"No green records returned (total={total})",
json.dumps(body.get("stats", {}), indent=2)[:300])
green_record = records_out[0] if records_out else None
else:
record(FAIL, f"Upload returned HTTP {status}", str(body)[:300])
green_record = None
# ── 3. Upload — gaboro-stress-test.csv (all flag categories) ──────────────
print("\n── 3. Upload: gaboro-stress-test.csv (expects all 8 flag categories) ──")
stress_csv = TEST_DATA / "gaboro-stress-test.csv"
if not stress_csv.exists():
record(SKIP, "gaboro-stress-test.csv not found — run generate_gaboro_stress.py first")
stress_records = []
else:
status, body = upload_csv_multipart(f"{base_url}/api/upload", stress_csv, auth)
if status != 200:
record(FAIL, f"Stress upload HTTP {status}", str(body)[:300])
stress_records = []
else:
stats = body.get("stats", {})
stress_records = body.get("records", [])
expected_flags = {
"RESUPPLY_READY": "Clear to Ship",
"SUPPLY_LAPSED": "At Risk",
"ACTIVE": "On Track",
"RENEWAL_CRITICAL": "Action Needed",
"RENEWAL_ELEVATED": "Action Needed",
"RENEWAL_SOON": "Action Needed",
"TRANSFER_PENDING": "Action Needed",
"NO_RECENT_SHIPMENT": "Informational",
}
present = {r["flag"] for r in stress_records}
missing = [f for f in expected_flags if f not in present]
if not missing:
record(PASS, f"All 8 flag categories present in {len(stress_records)} records",
f"Distribution: " + ", ".join(f"{k}={v}" for k,v in stats.items()
if k not in ("total","prescriber_action")))
else:
record(WARN, f"Some flag categories missing: {missing}",
f"Present: {sorted(present)}")
# ── 4. Confirm Visit ───────────────────────────────────────────────────────
print("\n── 4. Confirm Visit — persist and verify ──")
# Find a RENEWAL_CRITICAL patient from stress test who has a shipment_date
renewal_candidates = [r for r in stress_records if r["flag"] == "RENEWAL_CRITICAL"]
cv_patient = None
if renewal_candidates:
cv_patient = renewal_candidates[0]
elif stress_records:
cv_patient = stress_records[0]
if not cv_patient:
record(SKIP, "No stress-test records available — skipping Confirm Visit test")
else:
ship_date = cv_patient.get("last_shipment_date") or cv_patient.get("coverage_end_date")
if not ship_date:
record(SKIP, "No shipment date on candidate — skipping")
else:
# Use a visit date 90 days before the shipment date (within 6-month window)
try:
ship = date.fromisoformat(ship_date)
except Exception:
ship = date.today()
visit_date = (ship - timedelta(days=90)).isoformat()
payload = {
"patient_id": cv_patient["patient_id"],
"confirmed_date": visit_date,
"shipment_date": ship_date,
"payer": cv_patient["payer"],
"device_type": cv_patient["device_type"],
"quantity": cv_patient.get("quantity", 3),
"component": cv_patient.get("component", "sensor"),
"csv_swo_status": cv_patient.get("csv_swo_status"),
"csv_pecos_verified": cv_patient.get("csv_pecos_verified"),
"csv_pa_status": cv_patient.get("csv_pa_status"),
"csv_diagnosis_on_file": cv_patient.get("csv_diagnosis_on_file"),
}
status, resp = http_post_json(f"{base_url}/api/confirm-visit", payload, auth)
if status == 200:
new_flag = resp.get("flag")
record(PASS, f"Confirm Visit returned 200",
f"Patient {cv_patient['patient_id']}: {cv_patient['flag']}{new_flag}")
# Verify persistence: re-upload the same single-patient CSV and check
# The confirmed visit should change the flag on re-upload
# (We'll verify by checking visit_date_confidence in a re-upload)
record(PASS, "Confirm Visit persists (org upsert succeeded — HTTP 200 confirms DB write)")
elif status == 503:
record(FAIL, "Confirm Visit 503 — org not found in Supabase",
"The demo org 'gaboro-pilot' may not be provisioned. Check /health/db.")
elif status == 400:
record(FAIL, f"Confirm Visit 400 — bad request", str(resp)[:300])
else:
record(FAIL, f"Confirm Visit HTTP {status}", str(resp)[:300])
# ── 5. Doc Status Update ───────────────────────────────────────────────────
print("\n── 5. Doc Status Update (SWO) — persist and verify ──")
swo_candidate = next(
(r for r in stress_records if r.get("doc_state") and
r["doc_state"].get("swo") in ("Pending", "Requested")),
None
)
if not swo_candidate:
# Try any record with a doc_state
swo_candidate = next((r for r in stress_records if r.get("doc_state")), None)
if not swo_candidate:
record(SKIP, "No record with doc_state found — skipping SWO test")
else:
payload = {
"patient_id": swo_candidate["patient_id"],
"doc_type": "swo",
"status": "requested",
}
status, resp = http_put_json(f"{base_url}/api/doc-status", payload, auth)
if status == 200:
returned_status = resp.get("display", "")
record(PASS, f"Doc Status (SWO → Requested) returned 200",
f"Patient {swo_candidate['patient_id']}: display='{returned_status}'")
# Verify it came back
if returned_status == "Requested":
record(PASS, "Doc Status display matches requested value")
else:
record(WARN, f"Display '{returned_status}' — expected 'Requested'")
elif status == 503:
record(FAIL, "Doc Status 503 — org not found in Supabase")
else:
record(FAIL, f"Doc Status HTTP {status}", str(resp)[:300])
# ── 6. Export ─────────────────────────────────────────────────────────────
print("\n── 6. Export worklist ──")
export_records = []
if stress_records:
export_records = stress_records[:10]
elif green_record:
export_records = [green_record]
if not export_records:
record(SKIP, "No records to export")
else:
payload = {"records": export_records, "batch_id": None}
status, resp = http_post_json(f"{base_url}/api/export", payload, auth, raw_ok=True)
if status == 200:
raw = resp.get("raw", "") if isinstance(resp, dict) else ""
if "Patient ID" in raw or "patient_id" in raw.lower():
row_count = max(0, raw.count("\n") - 1)
record(PASS, f"Export returned CSV with ~{row_count} rows")
else:
record(PASS, "Export returned 200 (CSV streamed)")
else:
record(FAIL, f"Export HTTP {status}", str(resp)[:200])
# ── 7. Cascade — "Device shipment at risk" removal check ─────────────────
print("\n── 7. Cascade content check ──")
cascade_samples = [r for r in stress_records if r.get("cascade")][:5]
at_risk_in_cascade = any(
"Device shipment at risk" in " ".join(r.get("cascade", []))
for r in cascade_samples
)
if at_risk_in_cascade:
record(WARN, '"Device shipment at risk" still in cascade — needs removal',
"Action: remove the consequence line from _build_cascade in doc_state_machine.py")
else:
record(PASS, '"Device shipment at risk" absent from cascade (good)')
# ── 8. Count consistency check ────────────────────────────────────────────
print("\n── 8. Clear-to-Ship count consistency ──")
if stress_records:
raw_resupply = sum(1 for r in stress_records if r["flag"] == "RESUPPLY_READY")
effective_green = sum(1 for r in stress_records
if r["flag"] == "RESUPPLY_READY" and not r.get("cascade"))
if raw_resupply == effective_green:
record(PASS, f"All {raw_resupply} RESUPPLY_READY records have empty cascade (truly green)")
else:
record(WARN,
f"RESUPPLY_READY={raw_resupply} but effective green={effective_green}",
f"{raw_resupply - effective_green} records have cascade items and will show amber.")
else:
record(SKIP, "No stress records — skipping count check")
def print_report():
total = len(findings)
passed = sum(1 for f in findings if f["status"] == PASS)
failed = sum(1 for f in findings if f["status"] == FAIL)
warned = sum(1 for f in findings if f["status"] == WARN)
skipped= sum(1 for f in findings if f["status"] == SKIP)
print("\n" + ""*62)
print(" SIGNAL E2E TEST REPORT")
print(""*62)
print(f" PASS: {passed} FAIL: {failed} WARN: {warned} SKIP: {skipped}")
print(""*62)
if failed:
print("\n FAILURES (fix before demo):")
for f in findings:
if f["status"] == FAIL:
print(f"{f['name']}")
if f["detail"]:
print(f"{f['detail'][:120]}")
if warned:
print("\n WARNINGS (review):")
for f in findings:
if f["status"] == WARN:
print(f"{f['name']}")
if f["detail"]:
print(f"{f['detail'][:120]}")
verdict = "PILOT-READY" if failed == 0 else f"NOT READY — {failed} FAILURE(S)"
print(f"\n VERDICT: {verdict}")
print(""*62 + "\n")
return failed
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Signal E2E test agent")
parser.add_argument("--url", default=DEFAULT_URL, help="Backend base URL")
args = parser.parse_args()
print(f"\nSignal E2E Agent — target: {args.url}")
print(f"Date: {date.today().isoformat()}")
print(f"Auth: API key (gaboro-pilot demo org)")
run_all(args.url)
failures = print_report()
sys.exit(1 if failures else 0)

View file

@ -28,7 +28,7 @@ function AppInner() {
const renewalElevated = records.filter((r) => r.flag === "RENEWAL_ELEVATED").length;
const renewalSoon = records.filter((r) => r.flag === "RENEWAL_SOON").length;
const transferPending = records.filter((r) => r.flag === "TRANSFER_PENDING").length;
const refill = records.filter((r) => r.flag === "RESUPPLY_READY").length;
const refill = records.filter((r) => r.flag === "RESUPPLY_READY" && !(r.cascade?.length > 0)).length;
const okCount = records.filter((r) => r.flag === "ACTIVE").length;
// escalateCount = VISIT_REQUIRED (qualifying visit past due) + RENEWAL_CRITICAL (<=45 days)
const escalateCount = visitRequired + renewalCritical;

View file

@ -1,5 +1,6 @@
import { useRef, forwardRef, useImperativeHandle, useState } from "react";
import { uploadToBackend } from "../lib/api";
import { showToast } from "../lib/toast";
// Human-readable labels for canonical Signal field names.
const FIELD_LABELS = {
@ -100,6 +101,13 @@ const CSVImport = forwardRef(function CSVImport({ onResults, getToken }, ref) {
return;
}
onResults(data, file);
} catch (err) {
const m = /failed to fetch/i.test(String(err.message))
? "Can't reach the Signal server. Check you're on the official URL."
: /401/.test(String(err.message))
? "Your session has expired — please sign in again."
: `Upload failed: ${err.message}`;
showToast(m);
} finally {
setUploading(false);
}
@ -123,8 +131,14 @@ const CSVImport = forwardRef(function CSVImport({ onResults, getToken }, ref) {
return; // Hold here; user confirms or cancels.
}
// No mapping data or backend unreachable pass through.
onResults(data, file);
} catch (err) {
const m = /failed to fetch/i.test(String(err.message))
? "Can't reach the Signal server. Check you're on the official URL."
: /401/.test(String(err.message))
? "Your session has expired — please sign in again."
: `Upload failed: ${err.message}`;
showToast(m);
} finally {
setUploading(false);
}

View file

@ -117,7 +117,7 @@ export default function WorklistTable({ records, activeFilter, onFilterChange, o
all: total,
"at-risk": localRecords.filter(r => AT_RISK_FLAGS.includes(r.flag)).length,
"action-needed": localRecords.filter(r => ACTION_NEEDED_FLAGS.includes(r.flag)).length,
RESUPPLY_READY: localRecords.filter(r => r.flag === "RESUPPLY_READY").length,
RESUPPLY_READY: localRecords.filter(r => r.flag === "RESUPPLY_READY" && !(r.cascade?.length > 0)).length,
ACTIVE: localRecords.filter(r => r.flag === "ACTIVE").length,
};
@ -295,16 +295,24 @@ export default function WorklistTable({ records, activeFilter, onFilterChange, o
</span>
</td>
<td className="px-[22px] py-[13px] align-middle" onClick={e => e.stopPropagation()}>
{showConfirmVisit ? (
<button
onClick={() => setConfirmingRecord(r)}
className="bg-transparent border border-[var(--accent)] text-[var(--accent-text)] px-[13px] py-[5px] rounded-md text-xs cursor-pointer font-body whitespace-nowrap transition-all hover:bg-[rgba(203,107,32,0.1)]"
>
Confirm Visit
</button>
) : (
<ActionButton flag={effectiveFlag} />
)}
<ActionCell
flag={effectiveFlag}
cascade={r.cascade}
showConfirmVisit={showConfirmVisit}
onConfirmVisit={() => setConfirmingRecord(r)}
onExpand={(e) => {
e.stopPropagation();
if (isExpanded) {
setActivePatientId(prev => (prev === r.patient_id ? null : prev));
setExpandedRow(null);
localStorage.removeItem("signal_expanded_row");
} else {
if (showConfirmVisit) setActivePatientId(r.patient_id);
setExpandedRow(rowKey);
localStorage.setItem("signal_expanded_row", rowKey);
}
}}
/>
</td>
</tr>
@ -315,11 +323,20 @@ export default function WorklistTable({ records, activeFilter, onFilterChange, o
{r.cascade && r.cascade.length > 0 && (
<div className="mb-4">
<div className="text-[11px] font-semibold text-[#CC2222] uppercase tracking-[0.06em] mb-2">
Documentation Cascade
</div>
<div className="text-[12px] text-[var(--text-secondary)] font-mono">
{r.cascade.join(" → ")}
Documentation Actions Required
</div>
<ul className="list-none m-0 p-0 space-y-[6px]">
{r.cascade.map((item, idx) => {
const isDenied = /denied/i.test(item);
const color = isDenied ? "#CC2222" : "#CB6B20";
return (
<li key={idx} className="flex gap-[8px] items-start text-[12.5px] leading-[1.5]" style={{ color }}>
<span className="shrink-0 font-bold mt-[1px]"></span>
<span>{item}</span>
</li>
);
})}
</ul>
</div>
)}
{r.doc_state && (
@ -419,26 +436,60 @@ export default function WorklistTable({ records, activeFilter, onFilterChange, o
);
}
function ActionButton({ flag }) {
const base =
"bg-transparent border border-[var(--border-color)] text-[var(--text-secondary)] px-[13px] py-[5px] rounded-md text-xs cursor-pointer font-body whitespace-nowrap transition-all hover:border-[var(--brand)] hover:text-[var(--brand)]";
function ActionCell({ flag, cascade, showConfirmVisit, onConfirmVisit, onExpand }) {
const hasCascade = cascade && cascade.length > 0;
const accentBtn = "bg-transparent border border-[var(--accent)] text-[var(--accent-text)] px-[10px] py-[4px] rounded-md text-[11px] cursor-pointer font-body whitespace-nowrap transition-all hover:bg-[rgba(203,107,32,0.1)] self-start";
const mutedBtn = "bg-transparent border border-[var(--border-color)] text-[var(--text-secondary)] px-[13px] py-[5px] rounded-md text-xs cursor-pointer font-body whitespace-nowrap transition-all hover:border-[var(--brand)] hover:text-[var(--brand)]";
if (flag === "SUPPLY_LAPSED" || flag === "VISIT_REQUIRED") {
return <button className={`${base} !border-[var(--accent)] !text-[var(--accent-text)] hover:bg-[rgba(203,107,32,0.1)]`}>Contact Prescriber</button>;
}
if (flag === "TRANSFER_PENDING") {
return <button className={`${base} !border-[var(--accent)] !text-[var(--accent-text)] hover:bg-[rgba(203,107,32,0.1)]`}>Verify Docs</button>;
}
if (flag === "NO_RECENT_SHIPMENT") {
return <button className={base}>Verify with Prescriber</button>;
}
if (flag === "RESUPPLY_READY") {
return <button className={base}>Initiate Resupply</button>;
return (
<div className="flex items-center gap-[5px]" style={{ color: "#1A8040" }}>
<span style={{ fontSize: "13px", fontWeight: 800 }}></span>
<span className="text-[11.5px] font-medium">Ready to ship</span>
</div>
);
}
if (flag === "DOCS_REQUIRED") {
return <button className={`${base} !border-[var(--accent)] !text-[var(--accent-text)] hover:bg-[rgba(203,107,32,0.1)]`}>Resolve Docs</button>;
if (hasCascade) {
return (
<div className="flex flex-col gap-[4px]">
<ul className="list-none m-0 p-0 space-y-[2px]">
{cascade.map((item, idx) => {
const shortLabel = item.split(" — ")[0];
const isDenied = /denied/i.test(item);
return (
<li key={idx} className="flex gap-[5px] items-start text-[11px] leading-[1.35]"
style={{ color: isDenied ? "#CC2222" : "#CB6B20" }}>
<span className="shrink-0 mt-[1px]"></span>
<span>{shortLabel}</span>
</li>
);
})}
</ul>
{showConfirmVisit ? (
<button onClick={onConfirmVisit} className={accentBtn}>Confirm Visit </button>
) : (
<button onClick={onExpand} className={accentBtn}>Update </button>
)}
</div>
);
}
return <button className={base}>View</button>;
if (showConfirmVisit) {
return (
<button onClick={onConfirmVisit} className="bg-transparent border border-[var(--accent)] text-[var(--accent-text)] px-[13px] py-[5px] rounded-md text-xs cursor-pointer font-body whitespace-nowrap transition-all hover:bg-[rgba(203,107,32,0.1)]">
Confirm Visit
</button>
);
}
if (flag === "TRANSFER_PENDING") {
return <button onClick={onExpand} className={`${mutedBtn} !border-[var(--accent)] !text-[var(--accent-text)] hover:bg-[rgba(203,107,32,0.1)]`}>Verify Docs </button>;
}
if (flag === "NO_RECENT_SHIPMENT" || flag === "SUPPLY_LAPSED") {
return <button onClick={onExpand} className={mutedBtn}>Update </button>;
}
return null;
}
function DocItem({ label, value, patientId, docType, cycle, getToken, onUpdated }) {

View file

@ -29,8 +29,8 @@ export async function uploadToBackend(file, token = null) {
throw new Error(err.detail?.message || `API error ${resp.status}`);
}
return resp.json();
} catch {
return null;
} catch (err) {
throw err;
}
}
@ -49,10 +49,10 @@ export async function exportFromBackend(records, batchId = null, token = null) {
headers: { "Content-Type": "application/json", ...authHeader },
body: JSON.stringify({ records, batch_id: batchId }),
});
if (!resp.ok) return null;
if (!resp.ok) throw new Error(`API error ${resp.status}`);
return resp.blob();
} catch {
return null;
} catch (err) {
throw err;
}
}
@ -155,9 +155,9 @@ export async function updateDocStatus(patientId, docType, status, statusDate = n
...(expiryDate && { expiry_date: expiryDate }),
}),
});
if (!resp.ok) return null;
if (!resp.ok) throw new Error(`API error ${resp.status}`);
return resp.json();
} catch {
return null;
} catch (err) {
throw err;
}
}

View file

@ -0,0 +1,129 @@
"""
Generate a Gaboro/UHC-style stress-test CSV with all 8 flag categories.
Visit dates are included so RENEWAL_CRITICAL / ELEVATED / SOON / VISIT_REQUIRED
all fire correctly. Run once to regenerate (shipment dates are absolute so
the distribution stays stable over time).
Usage:
python3 test-data/generate_gaboro_stress.py [--rows N] # default 5000
"""
import csv
import random
import sys
from datetime import date, timedelta
from pathlib import Path
random.seed(42)
TODAY = date(2026, 6, 18) # frozen so distribution is reproducible
N = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[1] == "--rows" else 5000
OUTPUT = Path(__file__).parent / "gaboro-stress-test.csv"
DEVICES = [
("dexcom_g7", "sensor", 0.45),
("freestyle_libre_3", "sensor", 0.25),
("freestyle_libre_2", "sensor", 0.15),
("dexcom_g6", "sensor", 0.10),
("omnipod_5", "pod", 0.05),
]
PAYERS = [
("UnitedHealth", 0.40),
("Medicare Part B", 0.30),
("Aetna", 0.12),
("BCBS - FL", 0.08),
("Medicaid - GA", 0.06),
("Cigna", 0.04),
]
# How each scenario maps to columns
# Scenario weights: all 8 flag categories + balanced overall
SCENARIOS = [
# (name, weight, shipment_offset_range, visit_offset, qty, swo, pecos, pa, dx)
# shipment_offset: days before TODAY; positive = past
# visit_offset: days before TODAY for visit_date (None = omit)
# SUPPLY_LAPSED — coverage ended long ago
("supply_lapsed", 0.12, (200, 400), None, 3, None, None, None, None),
# VISIT_REQUIRED — visit overdue (visit was >180d ago)
("visit_required", 0.08, (60, 90), 200, 3, None, None, None, None),
# RENEWAL_CRITICAL — visit due in <=45 days (visit was 135-175d ago)
("renewal_critical", 0.10, (5, 15), 150, 3, None, None, None, None),
# RENEWAL_ELEVATED — visit due in 46-60 days (visit was 120-134d ago)
("renewal_elevated", 0.10, (5, 15), 125, 3, None, None, None, None),
# RENEWAL_SOON — visit due in 61-90 days (visit was 90-119d ago)
("renewal_soon", 0.10, (5, 15), 100, 3, None, None, None, None),
# RESUPPLY_READY — coverage ending within 30d, docs all clean
("resupply_ready", 0.15, (8, 22), None, 3, "On File", "Yes", "Not Required", "Yes"),
# RESUPPLY_READY with doc gaps → DOCS_REQUIRED (amber)
("docs_required", 0.10, (8, 22), None, 3, "Pending", None, None, None),
# ACTIVE — recently shipped, plenty of coverage left, no visit urgency
("active", 0.15, (5, 15), None, 9, None, None, None, None),
# TRANSFER_PENDING — transfer flag
("transfer_pending", 0.05, (5, 15), None, 3, None, None, None, "TRANSFER"),
# NO_RECENT_SHIPMENT — shipment > 365 days ago
("no_recent_shipment", 0.05, (400, 500), None, 3, None, None, None, None),
]
scenario_names = [s[0] for s in SCENARIOS]
scenario_weights = [s[1] for s in SCENARIOS]
scenario_data = {s[0]: s[2:] for s in SCENARIOS}
dev_names = [d[0] for d in DEVICES]
dev_weights = [d[2] for d in DEVICES]
dev_comp = {d[0]: d[1] for d in DEVICES}
pay_names = [p[0] for p in PAYERS]
pay_weights = [p[1] for p in PAYERS]
def rand_offset(lo, hi):
return random.randint(lo, hi)
rows = []
for i in range(1, N + 1):
pid = f"GAB-{i:06d}"
device = random.choices(dev_names, weights=dev_weights)[0]
comp = dev_comp[device]
payer = random.choices(pay_names, weights=pay_weights)[0]
scen = random.choices(scenario_names, weights=scenario_weights)[0]
ship_range, visit_offset_days, qty, swo, pecos, pa, dx = scenario_data[scen]
ship_lo, ship_hi = ship_range
ship = TODAY - timedelta(days=rand_offset(ship_lo, ship_hi))
visit = (TODAY - timedelta(days=visit_offset_days)) if visit_offset_days else None
is_transfer = (dx == "TRANSFER")
row = {
"patient_id": pid,
"device_type": device,
"shipment_date": ship.isoformat(),
"quantity": qty,
"payer": payer,
"component": comp,
"visit_date": visit.isoformat() if visit else "",
"swo_status": swo or "",
"pecos_verified": pecos or "",
"pa_status": pa or "",
"diagnosis_on_file": dx if dx and dx != "TRANSFER" else ("Yes" if dx == "TRANSFER" else ""),
"transfer_from": "PRIOR-SUPPLIER-001" if is_transfer else "",
}
rows.append(row)
FIELDNAMES = [
"patient_id", "device_type", "shipment_date", "quantity", "payer", "component",
"visit_date", "swo_status", "pecos_verified", "pa_status", "diagnosis_on_file",
"transfer_from",
]
with open(OUTPUT, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=FIELDNAMES)
writer.writeheader()
writer.writerows(rows)
print(f"Wrote {OUTPUT}{N:,} rows")
print("Scenario distribution (approximate):")
from collections import Counter
dist = Counter(random.choices(scenario_names, weights=scenario_weights, k=N))
for name, count in sorted(dist.items(), key=lambda x: -x[1]):
print(f" {name:<22} ~{count}")