PCAgentinAI / policybridge_app.html
ITNovaML's picture
add frontend HTML apps
e0340d5
Raw
History Blame Contribute Delete
161 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PolicyBridge β€” P&C Intelligence Platform</title>
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=IBM+Plex+Sans:wght@300;400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0a0d12;
--bg2: #0f141c;
--bg3: #151c27;
--bg4: #1c2535;
--border: #1e2d42;
--border2: #2a3a52;
--muted: #4a6080;
--muted2: #6a80a0;
--text: #c8d8e8;
--text2: #e8f2ff;
--white: #f0f6ff;
--accent: #00a8ff;
--accent2: #0070cc;
--amber: #ffb020;
--amber2: #cc8800;
--green: #00cc6a;
--green2: #008844;
--red: #ff4444;
--red2: #cc1111;
--purple: #8860ff;
--bronze-c: #cd7f32;
--silver-c: #8ab4c8;
--gold-c: #ffd700;
--font-head: 'Syne', sans-serif;
--font-body: 'IBM Plex Sans', sans-serif;
--font-mono: 'IBM Plex Mono', monospace;
--r: 8px;
--r2: 12px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body { font-family: var(--font-body); background: var(--bg); color: var(--text); min-height: 100vh; font-size: 13px; line-height: 1.6; overflow-x: hidden; }
/* ── HEADER ── */
.app-header {
background: var(--bg2);
border-bottom: 1px solid var(--border);
display: flex; align-items: center;
padding: 0 20px;
height: 54px;
position: sticky; top: 0; z-index: 200;
gap: 24px;
}
.logo { display: flex; align-items: center; gap: 10px; margin-right: 8px; }
.logo-mark {
width: 30px; height: 30px; border-radius: 7px;
background: linear-gradient(135deg, var(--accent), var(--purple));
display: flex; align-items: center; justify-content: center;
font-family: var(--font-head); font-size: 15px; font-weight: 800; color: white;
}
.logo-name { font-family: var(--font-head); font-size: 17px; font-weight: 700; color: var(--white); }
.logo-sub { font-size: 10px; color: var(--muted2); letter-spacing: 1.5px; text-transform: uppercase; }
/* ── TABS ── */
.main-tabs {
display: flex; gap: 2px; flex: 1;
overflow-x: auto;
}
.tab-btn {
padding: 6px 16px;
border-radius: 7px;
border: none;
background: transparent;
color: var(--muted2);
font-family: var(--font-body);
font-size: 12px; font-weight: 500;
cursor: pointer;
display: flex; align-items: center; gap: 7px;
transition: all 0.2s;
white-space: nowrap;
letter-spacing: 0.2px;
}
.tab-btn:hover { background: var(--bg3); color: var(--text2); }
.tab-btn.active { background: var(--bg4); color: var(--accent); border: 1px solid var(--border2); }
.tab-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: 0.6; }
.hdr-right { display: flex; gap: 10px; align-items: center; margin-left: auto; }
.pill {
padding: 3px 10px; border-radius: 20px;
font-size: 10px; font-weight: 600; letter-spacing: 0.5px; text-transform: uppercase;
}
.pill-accent { background: rgba(0,168,255,0.15); color: var(--accent); border: 1px solid rgba(0,168,255,0.3); }
.pill-amber { background: rgba(255,176,32,0.15); color: var(--amber); border: 1px solid rgba(255,176,32,0.3); }
.pill-green { background: rgba(0,204,106,0.15); color: var(--green); border: 1px solid rgba(0,204,106,0.3); }
.pill-muted { background: var(--bg4); color: var(--muted2); border: 1px solid var(--border); }
/* ── TAB PAGES ── */
.tab-page { display: none; min-height: calc(100vh - 54px); }
.tab-page.active { display: block; }
/* ══════════════════════════════════════════
TAB 1 β€” SUBMISSION FORM
══════════════════════════════════════════ */
.sub-layout {
display: grid;
grid-template-columns: 220px 1fr 260px;
min-height: calc(100vh - 54px);
}
/* Left nav */
.sub-nav {
background: var(--bg2);
border-right: 1px solid var(--border);
padding: 20px 14px;
position: sticky; top: 54px;
height: calc(100vh - 54px); overflow-y: auto;
}
.snav-title { font-size: 10px; text-transform: uppercase; letter-spacing: 1.5px; color: var(--muted); font-weight: 600; margin-bottom: 10px; padding: 0 8px; }
.snav-item {
display: flex; align-items: center; gap: 9px;
padding: 8px 10px; border-radius: var(--r);
cursor: pointer; color: var(--muted2); font-size: 12px; font-weight: 500;
transition: all 0.2s; margin-bottom: 2px;
}
.snav-item:hover { background: var(--bg3); color: var(--text); }
.snav-item.active { background: rgba(0,168,255,0.1); color: var(--accent); border: 1px solid rgba(0,168,255,0.2); }
.snav-item.done { color: var(--green); }
.snav-num {
width: 20px; height: 20px; border-radius: 50%;
background: var(--bg4); border: 1px solid var(--border2);
display: flex; align-items: center; justify-content: center;
font-size: 10px; font-weight: 600; flex-shrink: 0;
transition: all 0.2s;
}
.snav-item.active .snav-num { background: var(--accent); border-color: var(--accent); color: white; }
.snav-item.done .snav-num { background: var(--green); border-color: var(--green); color: white; }
.submitter-type-box { margin-top: 20px; padding: 12px; background: var(--bg3); border-radius: var(--r); border: 1px solid var(--border); }
.stype-title { font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); margin-bottom: 8px; }
.stype-opt { display: flex; flex-direction: column; gap: 4px; }
.stype-opt label {
display: flex; align-items: center; gap: 7px;
font-size: 11px; color: var(--muted2); cursor: pointer;
padding: 5px 7px; border-radius: 6px; transition: background 0.15s;
}
.stype-opt label:hover { background: var(--bg4); }
.stype-opt input[type="radio"] { accent-color: var(--accent); }
.db-info-box { margin-top: 16px; padding: 10px 12px; background: var(--bg3); border-radius: var(--r); border: 1px solid var(--border); }
.db-info-title { font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); margin-bottom: 8px; }
.db-row { display: flex; justify-content: space-between; font-size: 11px; margin-bottom: 4px; }
.db-row .dk { color: var(--muted); }
.db-row .dv { color: var(--text); font-family: var(--font-mono); }
/* Form main */
.sub-form-main { padding: 24px 28px; overflow-y: auto; }
.form-section { display: none; animation: fadeIn 0.25s ease; }
.form-section.active { display: block; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
.fsec-hdr { display: flex; align-items: center; gap: 14px; margin-bottom: 22px; padding-bottom: 16px; border-bottom: 1px solid var(--border); }
.fsec-icon { width: 40px; height: 40px; border-radius: 10px; background: var(--bg4); display: flex; align-items: center; justify-content: center; font-size: 20px; border: 1px solid var(--border2); }
.fsec-title { font-family: var(--font-head); font-size: 19px; font-weight: 700; color: var(--white); }
.fsec-desc { font-size: 12px; color: var(--muted2); }
/* Cards */
.fcard { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--r2); padding: 18px 20px; margin-bottom: 16px; }
.fcard-title {
font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px;
color: var(--muted2); margin-bottom: 14px;
display: flex; align-items: center; gap: 8px;
}
.fcard-title::before { content: ''; width: 3px; height: 14px; background: var(--accent); border-radius: 2px; display: inline-block; }
/* Grid */
.fg { display: grid; gap: 14px; }
.fg2 { grid-template-columns: 1fr 1fr; }
.fg3 { grid-template-columns: 1fr 1fr 1fr; }
.fg4 { grid-template-columns: 1fr 1fr 1fr 1fr; }
.cs2 { grid-column: span 2; }
.cs3 { grid-column: span 3; }
/* Fields */
.fld { display: flex; flex-direction: column; gap: 4px; }
.flabel { font-size: 11px; font-weight: 600; color: var(--text); letter-spacing: 0.2px; display: flex; align-items: center; gap: 4px; }
.flabel .req { color: var(--red); }
.flabel .ftip { font-weight: 400; color: var(--muted); font-size: 10px; margin-left: auto; }
input[type="text"], input[type="email"], input[type="tel"], input[type="date"],
input[type="number"], select, textarea {
width: 100%; padding: 8px 11px;
background: var(--bg3); border: 1px solid var(--border2);
border-radius: var(--r); color: var(--text2);
font-family: var(--font-body); font-size: 12px;
transition: border-color 0.2s, box-shadow 0.2s;
outline: none; appearance: none;
}
input:focus, select:focus, textarea:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(0,168,255,0.12);
}
input.err, select.err { border-color: var(--red); background: rgba(255,68,68,0.06); }
input.ok, select.ok { border-color: var(--green); }
select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6' fill='none' stroke='%234a6080' stroke-width='1.5'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 10px center; padding-right: 28px; }
select option { background: var(--bg3); }
textarea { resize: vertical; min-height: 72px; }
.ferr { font-size: 10px; color: var(--red); display: none; }
.ferr.show { display: block; }
.fhint { font-size: 10px; color: var(--muted); }
/* Coverage cards */
.cov-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 16px; }
.cov-card {
border: 1.5px solid var(--border2); border-radius: 10px; padding: 12px;
cursor: pointer; transition: all 0.2s; background: var(--bg3); position: relative;
}
.cov-card:hover { border-color: var(--accent); transform: translateY(-1px); }
.cov-card.sel { border-color: var(--accent); background: rgba(0,168,255,0.06); }
.cov-card input { position: absolute; opacity: 0; }
.cov-code { font-family: var(--font-mono); font-size: 14px; font-weight: 500; color: var(--accent); margin-bottom: 3px; }
.cov-name { font-size: 10px; color: var(--text); font-weight: 600; }
.cov-desc { font-size: 9px; color: var(--muted); margin-top: 3px; }
/* Checkboxes */
.chk-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; }
.chk-item {
display: flex; align-items: center; gap: 8px; padding: 7px 10px;
border: 1px solid var(--border2); border-radius: var(--r); cursor: pointer;
font-size: 11px; color: var(--muted2); transition: all 0.2s; background: var(--bg3);
}
.chk-item:hover { border-color: var(--accent); color: var(--accent); }
.chk-item.on { border-color: var(--accent); background: rgba(0,168,255,0.08); color: var(--accent); font-weight: 600; }
.chk-item input { accent-color: var(--accent); }
/* Buttons */
.btn-row { display: flex; gap: 10px; align-items: center; margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--border); }
.btn {
padding: 8px 20px; border-radius: var(--r);
font-family: var(--font-body); font-size: 12px; font-weight: 600;
cursor: pointer; border: none; transition: all 0.2s;
display: flex; align-items: center; gap: 6px;
}
.btn-accent { background: var(--accent); color: white; }
.btn-accent:hover { background: var(--accent2); box-shadow: 0 4px 16px rgba(0,168,255,0.3); }
.btn-ghost { background: transparent; color: var(--muted2); border: 1px solid var(--border2); }
.btn-ghost:hover { border-color: var(--text); color: var(--text); }
.btn-green { background: var(--green); color: white; }
.btn-green:hover { background: var(--green2); }
.btn-amber { background: var(--amber); color: var(--bg); }
.btn-amber:hover { background: var(--amber2); }
.ml { margin-left: auto; }
.step-hint { font-size: 11px; color: var(--muted); }
/* Alert */
.alert {
display: flex; align-items: flex-start; gap: 10px;
padding: 10px 14px; border-radius: var(--r);
font-size: 12px; margin-bottom: 14px; border-left: 3px solid;
}
.alert-info { background: rgba(0,168,255,0.06); border-color: var(--accent); color: var(--accent); }
.alert-warn { background: rgba(255,176,32,0.06); border-color: var(--amber); color: var(--amber); }
.alert-green { background: rgba(0,204,106,0.06); border-color: var(--green); color: var(--green); }
.alert-red { background: rgba(255,68,68,0.06); border-color: var(--red); color: var(--red); }
/* Right summary panel */
.sub-panel {
background: var(--bg2); border-left: 1px solid var(--border);
padding: 18px 16px; position: sticky; top: 54px;
height: calc(100vh - 54px); overflow-y: auto;
}
.panel-sub-id {
background: var(--bg3); border: 1px solid var(--border2);
border-radius: 8px; padding: 10px 12px; margin-bottom: 14px;
}
.ps-label { font-size: 9px; text-transform: uppercase; letter-spacing: 1.2px; color: var(--muted); margin-bottom: 4px; }
.ps-val { font-family: var(--font-mono); font-size: 12px; color: var(--amber); }
.pcard { background: var(--bg3); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 10px; }
.pcard-title { font-size: 9px; text-transform: uppercase; letter-spacing: 1.2px; color: var(--muted); margin-bottom: 8px; font-weight: 600; }
.prow { display: flex; justify-content: space-between; gap: 6px; font-size: 11px; margin-bottom: 4px; }
.pk { color: var(--muted2); flex-shrink: 0; }
.pv { color: var(--text2); font-weight: 500; text-align: right; word-break: break-word; }
.pv.mono { font-family: var(--font-mono); font-size: 10px; }
.val-list { display: flex; flex-direction: column; gap: 4px; }
.val-row { display: flex; align-items: center; gap: 7px; font-size: 11px; padding: 5px 8px; border-radius: 5px; }
.val-row.ok { background: rgba(0,204,106,0.08); color: var(--green); }
.val-row.idle { background: var(--bg4); color: var(--muted); }
.val-row.err { background: rgba(255,68,68,0.08); color: var(--red); }
/* Review tables */
.rtable { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
.rtable th { background: var(--bg4); color: var(--muted2); padding: 8px 12px; text-align: left; font-size: 10px; font-weight: 600; letter-spacing: 0.5px; text-transform: uppercase; }
.rtable td { padding: 7px 12px; border-bottom: 1px solid var(--border); font-size: 12px; vertical-align: top; }
.rtable tr:nth-child(even) td { background: rgba(255,255,255,0.01); }
.rtable td:first-child { color: var(--muted2); width: 35%; font-size: 11px; }
.rtable td:last-child { color: var(--text2); font-weight: 500; }
/* JSON box */
.json-box {
background: var(--bg); border: 1px solid var(--border2); border-radius: 10px; padding: 14px;
font-family: var(--font-mono); font-size: 10.5px; line-height: 1.7;
max-height: 380px; overflow-y: auto; white-space: pre-wrap; word-break: break-all;
}
.jk { color: #79b8ff; }
.jstr{ color: #9ec8a0; }
.jnum{ color: #f08d49; }
.jbol{ color: #d19a66; }
/* Success */
.success-wrap { text-align: center; padding: 50px 30px; }
.suc-icon { width: 72px; height: 72px; border-radius: 50%; background: rgba(0,204,106,0.1); border: 2px solid var(--green); display: flex; align-items: center; justify-content: center; font-size: 34px; margin: 0 auto 20px; animation: popIn 0.5s cubic-bezier(.175,.885,.32,1.275); }
@keyframes popIn { from { transform: scale(0); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.suc-title { font-family: var(--font-head); font-size: 24px; font-weight: 800; color: var(--white); margin-bottom: 6px; }
.suc-sub { color: var(--muted2); margin-bottom: 28px; font-size: 13px; }
.suc-detail { background: var(--bg2); border: 1px solid var(--border2); border-radius: 12px; padding: 20px 24px; display: inline-block; text-align: left; min-width: 360px; margin-bottom: 20px; }
.path-box { background: var(--bg); border: 1px solid var(--border2); border-radius: 8px; padding: 12px 16px; display: inline-block; text-align: left; min-width: 360px; margin-bottom: 24px; }
.path-label { font-size: 9px; text-transform: uppercase; letter-spacing: 1.2px; color: var(--muted); margin-bottom: 5px; }
.path-val { font-family: var(--font-mono); font-size: 12px; color: var(--amber); }
/* ══════════════════════════════════════════
TAB 2 β€” MYSQL INTEGRATION
══════════════════════════════════════════ */
.int-layout { display: grid; grid-template-columns: 260px 1fr; min-height: calc(100vh - 54px); }
.int-sidebar { background: var(--bg2); border-right: 1px solid var(--border); padding: 20px 14px; }
.int-main { padding: 24px 28px; overflow-y: auto; }
.int-section { display: none; }
.int-section.active { display: block; animation: fadeIn 0.25s ease; }
.config-card { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--r2); padding: 20px 22px; margin-bottom: 16px; }
.cc-title { font-family: var(--font-head); font-size: 15px; font-weight: 700; color: var(--white); margin-bottom: 4px; }
.cc-desc { font-size: 12px; color: var(--muted2); margin-bottom: 18px; }
.conn-status {
display: flex; align-items: center; gap: 10px;
padding: 10px 14px; border-radius: var(--r); background: var(--bg3);
border: 1px solid var(--border2); font-size: 12px;
}
.status-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.status-dot.green { background: var(--green); animation: pulse 2s infinite; }
.status-dot.red { background: var(--red); }
.status-dot.amber { background: var(--amber); animation: pulse 1s infinite; }
@keyframes pulse { 0%,100%{opacity:1}50%{opacity:0.4} }
.sql-area {
background: var(--bg); border: 1px solid var(--border2); border-radius: 8px;
padding: 14px; font-family: var(--font-mono); font-size: 11px; line-height: 1.7;
color: #9ec8a0; min-height: 120px; max-height: 400px; overflow-y: auto;
white-space: pre-wrap; word-break: break-all;
}
.sql-kw { color: #79b8ff; font-weight: 600; }
.sql-str { color: #f0c060; }
.sql-cmt { color: #4a6080; font-style: italic; }
.sql-fn { color: #f08d49; }
.log-console {
background: var(--bg); border: 1px solid var(--border2); border-radius: 8px;
padding: 12px 14px; font-family: var(--font-mono); font-size: 11px;
min-height: 180px; max-height: 350px; overflow-y: auto; line-height: 1.8;
}
.log-line { display: flex; gap: 10px; }
.log-ts { color: var(--muted); flex-shrink: 0; font-size: 10px; }
.log-ok { color: var(--green); }
.log-err { color: var(--red); }
.log-info { color: var(--accent); }
.log-warn { color: var(--amber); }
/* Table mapping */
.tmap { width: 100%; border-collapse: collapse; }
.tmap th { background: var(--bg4); color: var(--muted2); padding: 7px 12px; text-align: left; font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
.tmap td { padding: 6px 12px; border-bottom: 1px solid var(--border); font-size: 11px; vertical-align: top; }
.tmap tr:hover td { background: rgba(255,255,255,0.02); }
.tmap .schema-badge { font-family: var(--font-mono); font-size: 10px; padding: 1px 6px; border-radius: 4px; background: rgba(205,127,50,0.15); color: var(--bronze-c); }
.tmap .status-cell { display: flex; align-items: center; gap: 5px; }
.tmap .s-ok { color: var(--green); }
.tmap .s-pend { color: var(--amber); }
/* Progress bars */
.prog-wrap { margin-bottom: 16px; }
.prog-label { display: flex; justify-content: space-between; font-size: 11px; color: var(--muted2); margin-bottom: 5px; }
.prog-bar { height: 6px; background: var(--bg4); border-radius: 10px; overflow: hidden; }
.prog-fill { height: 100%; border-radius: 10px; transition: width 0.8s ease; }
.fill-accent { background: linear-gradient(90deg, var(--accent2), var(--accent)); }
.fill-green { background: linear-gradient(90deg, var(--green2), var(--green)); }
.fill-amber { background: linear-gradient(90deg, var(--amber2), var(--amber)); }
/* ══════════════════════════════════════════
TAB 3 β€” CONNECTORS
══════════════════════════════════════════ */
.conn-page { padding: 28px; }
.conn-page-title { font-family: var(--font-head); font-size: 24px; font-weight: 800; color: var(--white); margin-bottom: 6px; }
.conn-page-sub { font-size: 13px; color: var(--muted2); margin-bottom: 28px; }
.conn-cats { display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap; }
.cat-btn { padding: 5px 14px; border-radius: 20px; border: 1px solid var(--border2); background: var(--bg2); color: var(--muted2); font-size: 11px; cursor: pointer; transition: all 0.2s; font-family: var(--font-body); }
.cat-btn.active, .cat-btn:hover { background: var(--accent); color: white; border-color: var(--accent); }
.conn-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; }
.conn-card {
background: var(--bg2); border: 1px solid var(--border); border-radius: var(--r2);
padding: 18px; transition: all 0.2s; cursor: pointer; position: relative;
}
.conn-card:hover { border-color: var(--border2); transform: translateY(-2px); box-shadow: 0 8px 30px rgba(0,0,0,0.3); }
.conn-card.connected { border-color: rgba(0,204,106,0.3); }
.conn-top { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
.conn-logo { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 22px; flex-shrink: 0; }
.conn-name { font-size: 14px; font-weight: 600; color: var(--text2); }
.conn-cat { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
.conn-desc { font-size: 11px; color: var(--muted2); line-height: 1.5; margin-bottom: 12px; }
.conn-tags { display: flex; gap: 5px; flex-wrap: wrap; margin-bottom: 12px; }
.conn-tag { padding: 2px 7px; border-radius: 4px; font-size: 9px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.3px; }
.tag-db { background: rgba(0,168,255,0.12); color: var(--accent); }
.tag-api { background: rgba(136,96,255,0.12); color: var(--purple); }
.tag-file { background: rgba(255,176,32,0.12); color: var(--amber); }
.tag-cloud { background: rgba(0,204,106,0.12); color: var(--green); }
.tag-stream{ background: rgba(255,68,68,0.12); color: var(--red); }
.conn-status-row { display: flex; align-items: center; justify-content: space-between; }
.conn-badge-on { padding: 3px 9px; border-radius: 20px; font-size: 10px; font-weight: 600; background: rgba(0,204,106,0.12); color: var(--green); border: 1px solid rgba(0,204,106,0.25); }
.conn-badge-off { padding: 3px 9px; border-radius: 20px; font-size: 10px; font-weight: 600; background: var(--bg4); color: var(--muted); border: 1px solid var(--border); }
.conn-action { padding: 4px 12px; border-radius: 6px; font-size: 11px; font-weight: 600; cursor: pointer; border: none; font-family: var(--font-body); transition: all 0.2s; }
.ca-connect { background: var(--accent); color: white; }
.ca-connect:hover { background: var(--accent2); }
.ca-manage { background: var(--bg4); color: var(--text2); border: 1px solid var(--border2); }
.ca-manage:hover { background: var(--bg3); }
/* ══════════════════════════════════════════
TAB 4 β€” AGENT PIPELINE
══════════════════════════════════════════ */
.agents-page { padding: 28px; }
.agents-title { font-family: var(--font-head); font-size: 24px; font-weight: 800; color: var(--white); margin-bottom: 6px; }
.agents-sub { font-size: 13px; color: var(--muted2); margin-bottom: 28px; }
.pipeline-flow { display: flex; align-items: flex-start; gap: 0; margin-bottom: 32px; overflow-x: auto; padding-bottom: 8px; }
.pf-agent { flex: 0 0 180px; }
.pf-connector { display: flex; align-items: center; flex: 1; min-width: 30px; max-width: 60px; padding-top: 35px; }
.pf-line { flex: 1; height: 2px; background: var(--border2); position: relative; }
.pf-line::after { content: 'β–Ά'; position: absolute; right: -6px; top: -7px; font-size: 10px; color: var(--border2); }
.pf-line.active { background: var(--accent); }
.pf-line.active::after { color: var(--accent); }
.pf-line.done { background: var(--green); }
.pf-line.done::after { color: var(--green); }
.agent-box {
background: var(--bg2); border: 1px solid var(--border); border-radius: var(--r2);
padding: 14px; text-align: center; cursor: pointer;
transition: all 0.2s; position: relative;
}
.agent-box:hover { border-color: var(--border2); transform: translateY(-2px); }
.agent-box.phase2 { border-style: dashed; opacity: 0.6; }
.agent-box.active { border-color: var(--accent); box-shadow: 0 0 20px rgba(0,168,255,0.15); }
.agent-box.done { border-color: var(--green); }
.agent-icon-wrap { width: 50px; height: 50px; border-radius: 12px; margin: 0 auto 8px; display: flex; align-items: center; justify-content: center; font-size: 26px; }
.agent-step { font-size: 9px; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); margin-bottom: 4px; }
.agent-name { font-size: 12px; font-weight: 600; color: var(--text2); margin-bottom: 3px; }
.agent-code { font-family: var(--font-mono); font-size: 9px; color: var(--muted2); }
.agent-badge { position: absolute; top: -7px; right: -7px; padding: 2px 7px; border-radius: 10px; font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.3px; }
.ab-p1 { background: var(--amber); color: var(--bg); }
.ab-p2 { background: var(--bg4); color: var(--muted2); border: 1px solid var(--border2); }
.agent-detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.agent-detail-card { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--r2); padding: 18px 20px; }
.adc-header { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; padding-bottom: 12px; border-bottom: 1px solid var(--border); }
.adc-icon { width: 36px; height: 36px; border-radius: 9px; display: flex; align-items: center; justify-content: center; font-size: 18px; }
.adc-name { font-size: 14px; font-weight: 600; color: var(--white); }
.adc-step { font-size: 10px; color: var(--muted2); }
.adc-prop { display: flex; justify-content: space-between; font-size: 11px; margin-bottom: 6px; }
.adc-pk { color: var(--muted2); }
.adc-pv { color: var(--text2); font-family: var(--font-mono); font-size: 10px; }
.python-snippet {
background: var(--bg); border: 1px solid var(--border2); border-radius: 8px;
padding: 14px; font-family: var(--font-mono); font-size: 11px; line-height: 1.7;
overflow-x: auto; white-space: pre; margin-top: 14px;
}
.py-kw { color: #cc99ff; }
.py-str { color: #9ec8a0; }
.py-fn { color: #79b8ff; }
.py-cmt { color: #4a6080; font-style: italic; }
.py-cls { color: #f08d49; }
.py-num { color: #f08d49; }
.py-dec { color: #ffcc66; }
/* ══════════════════════════════════════════
SHARED / MISC
══════════════════════════════════════════ */
.page-header { margin-bottom: 24px; }
.ph-title { font-family: var(--font-head); font-size: 22px; font-weight: 800; color: var(--white); }
.ph-sub { font-size: 12px; color: var(--muted2); margin-top: 3px; }
.section-divider { height: 1px; background: var(--border); margin: 22px 0; }
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.three-col { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; }
.stat-box { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--r2); padding: 16px 18px; }
.stat-val { font-family: var(--font-head); font-size: 28px; font-weight: 800; margin-bottom: 2px; }
.stat-label { font-size: 11px; color: var(--muted2); }
::-webkit-scrollbar { width: 4px; height: 4px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
/* Step progress bar */
.step-bar {
display: flex; gap: 0; align-items: center;
margin-bottom: 24px; padding: 14px 20px;
background: var(--bg2); border: 1px solid var(--border); border-radius: var(--r2);
}
.sb-step { display: flex; align-items: center; gap: 0; flex: 1; }
.sb-wrap { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.sb-bubble {
width: 28px; height: 28px; border-radius: 50%;
background: var(--bg4); border: 1.5px solid var(--border2);
display: flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 600; color: var(--muted);
transition: all 0.25s; flex-shrink: 0;
}
.sb-bubble.active { background: var(--accent); border-color: var(--accent); color: white; }
.sb-bubble.done { background: var(--green); border-color: var(--green); color: white; }
.sb-name { font-size: 9px; color: var(--muted); white-space: nowrap; }
.sb-wrap.active .sb-name { color: var(--accent); font-weight: 600; }
.sb-wrap.done .sb-name { color: var(--green); }
.sb-line { flex: 1; height: 1.5px; background: var(--border2); margin: 0 4px; }
.sb-line.done { background: var(--green); }
/* Bottom status */
.status-bar {
position: fixed; bottom: 0; left: 0; right: 0;
background: var(--bg2); border-top: 1px solid var(--border);
padding: 5px 20px; display: flex; gap: 18px; align-items: center;
font-family: var(--font-mono); font-size: 10px; color: var(--muted);
z-index: 100;
}
.sb-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; }
.sb-sep { color: var(--border2); }
/* Document upload styles */
.doc-slot { background: var(--bg3); border: 1px solid var(--border); border-radius: 8px; padding: 14px; }
.doc-slot-hdr { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
.doc-slot-label { font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; min-width: 90px; }
.doc-type-sel { flex: 1; background: var(--bg2); border: 1px solid var(--border); color: var(--text); padding: 6px 10px; border-radius: 5px; font-size: 12px; }
.doc-dropzone { border: 2px dashed var(--border); border-radius: 6px; padding: 22px; text-align: center; cursor: pointer; transition: all .2s; background: var(--bg2); }
.doc-dropzone:hover, .doc-dropzone.drag-over { border-color: var(--accent); background: rgba(99,102,241,.06); }
.doc-dropzone.uploaded { border-color: var(--green); background: rgba(34,197,94,.05); border-style: solid; }
.doc-dropzone.rejected { border-color: var(--red); background: rgba(239,68,68,.05); border-style: solid; }
.dz-icon { font-size: 28px; margin-bottom: 6px; }
.dz-text { font-size: 13px; color: var(--text); font-weight: 500; }
.dz-hint { font-size: 11px; color: var(--muted); margin-top: 3px; }
.doc-status { margin-top: 8px; font-size: 12px; min-height: 18px; }
.doc-status.ok { color: var(--green); }
.doc-status.err { color: var(--red); }
.doc-status.checking { color: var(--amber); }
</style>
</head>
<body>
<!-- ═══ APP HEADER ═══ -->
<header class="app-header">
<div class="logo">
<div class="logo-mark">P</div>
<div>
<div class="logo-name">PolicyBridge</div>
<div class="logo-sub">P&C Intelligence Platform</div>
</div>
</div>
<nav class="main-tabs">
<button class="tab-btn active" onclick="switchTab('submission')">
<span class="tab-dot"></span> Submission Form
</button>
<button class="tab-btn" onclick="switchTab('integration')">
<span class="tab-dot"></span> MySQL Integration
</button>
<button class="tab-btn" onclick="switchTab('connectors')">
<span class="tab-dot"></span> Connectors Hub
</button>
<button class="tab-btn" onclick="switchTab('agents')">
<span class="tab-dot"></span> Agent Pipeline
</button>
</nav>
<div class="hdr-right">
<span class="pill pill-accent">Phase 1</span>
<span class="pill pill-amber" id="db-conn-pill">DB: Connecting…</span>
</div>
</header>
<!-- ═══════════════════════════════════════════
TAB 1 β€” SUBMISSION FORM
═══════════════════════════════════════════ -->
<div class="tab-page active" id="tab-submission">
<div class="sub-layout">
<!-- Left nav -->
<aside class="sub-nav">
<div class="snav-title">Form Steps</div>
<div class="snav-item active" id="snav-1" onclick="goStep(1)"><div class="snav-num" id="sn-1">1</div> Submitter</div>
<div class="snav-item" id="snav-2" onclick="goStep(2)"><div class="snav-num" id="sn-2">2</div> Insured</div>
<div class="snav-item" id="snav-3" onclick="goStep(3)"><div class="snav-num" id="sn-3">3</div> Property</div>
<div class="snav-item" id="snav-4" onclick="goStep(4)"><div class="snav-num" id="sn-4">4</div> Coverage</div>
<div class="snav-item" id="snav-5" onclick="goStep(5)"><div class="snav-num" id="sn-5">5</div> Documents</div>
<div class="snav-item" id="snav-6" onclick="goStep(6)"><div class="snav-num" id="sn-6">6</div> Review</div>
<div class="snav-item" id="snav-7" onclick="goStep(7)"><div class="snav-num" id="sn-7">7</div> Submit</div>
<div class="submitter-type-box">
<div class="stype-title">Submitter Role</div>
<div class="stype-opt">
<label><input type="radio" name="sub_type" value="broker" checked onchange="APP.subType=this.value"> 🏒 Broker/Agent</label>
<label><input type="radio" name="sub_type" value="mga" onchange="APP.subType=this.value"> 🏦 MGA</label>
<label><input type="radio" name="sub_type" value="direct" onchange="APP.subType=this.value"> πŸ‘€ Direct</label>
</div>
</div>
<div class="db-info-box">
<div class="db-info-title">Target Database</div>
<div class="db-row"><span class="dk">Host</span><span class="dv">localhost</span></div>
<div class="db-row"><span class="dk">Schema</span><span class="dv">bronze</span></div>
<div class="db-row"><span class="dk">User</span><span class="dv">root</span></div>
<div class="db-row"><span class="dk">Layer</span><span class="dv">BRONZE</span></div>
</div>
</aside>
<!-- Main form -->
<main class="sub-form-main">
<!-- Progress bar -->
<div class="step-bar" id="step-bar">
<div class="sb-step"><div class="sb-wrap active" id="sbw-1"><div class="sb-bubble active" id="sbb-1">1</div><div class="sb-name">Submitter</div></div></div>
<div class="sb-line" id="sbl-1"></div>
<div class="sb-step"><div class="sb-wrap" id="sbw-2"><div class="sb-bubble" id="sbb-2">2</div><div class="sb-name">Insured</div></div></div>
<div class="sb-line" id="sbl-2"></div>
<div class="sb-step"><div class="sb-wrap" id="sbw-3"><div class="sb-bubble" id="sbb-3">3</div><div class="sb-name">Property</div></div></div>
<div class="sb-line" id="sbl-3"></div>
<div class="sb-step"><div class="sb-wrap" id="sbw-4"><div class="sb-bubble" id="sbb-4">4</div><div class="sb-name">Coverage</div></div></div>
<div class="sb-line" id="sbl-4"></div>
<div class="sb-step"><div class="sb-wrap" id="sbw-5"><div class="sb-bubble" id="sbb-5">5</div><div class="sb-name">Documents</div></div></div>
<div class="sb-line" id="sbl-5"></div>
<div class="sb-step"><div class="sb-wrap" id="sbw-6"><div class="sb-bubble" id="sbb-6">6</div><div class="sb-name">Review</div></div></div>
<div class="sb-line" id="sbl-6"></div>
<div class="sb-step"><div class="sb-wrap" id="sbw-7"><div class="sb-bubble" id="sbb-7">7</div><div class="sb-name">Submit</div></div></div>
</div>
<!-- STEP 1: SUBMITTER -->
<div class="form-section active" id="sec-1">
<div class="fsec-hdr">
<div class="fsec-icon">🏒</div>
<div><div class="fsec-title">Submitter Information</div><div class="fsec-desc">Broker, MGA, or direct customer details β†’ bronze_brokers</div></div>
</div>
<div class="fcard">
<div class="fcard-title">Organization Details</div>
<div class="fg fg2">
<div class="fld cs2">
<label class="flabel">Organization / Broker Name <span class="req">*</span></label>
<input type="text" id="broker_name" placeholder="Acme Insurance Brokers LLC" oninput="V.field('broker_name')">
<span class="ferr" id="e-broker_name">Organization name is required</span>
</div>
<div class="fld">
<label class="flabel">Broker Code <span class="req">*</span> <span class="ftip">Format: ACM-TX-0041</span></label>
<input type="text" id="broker_code" placeholder="ACM-TX-0041" oninput="V.brokerCode()" maxlength="15">
<span class="ferr" id="e-broker_code">Format: AAA-ST-0000 (letters-state-4digits)</span>
</div>
<div class="fld">
<label class="flabel">Contact Email <span class="req">*</span></label>
<input type="email" id="broker_email" placeholder="submissions@broker.com" oninput="V.email('broker_email')">
<span class="ferr" id="e-broker_email">Valid email required</span>
</div>
<div class="fld">
<label class="flabel">Licensed State <span class="req">*</span></label>
<select id="broker_state" onchange="V.req('broker_state')"><option value="">β€” State β€”</option></select>
<span class="ferr" id="e-broker_state">Select state</span>
</div>
<div class="fld">
<label class="flabel">Phone</label>
<input type="tel" id="broker_phone" placeholder="(512) 555-0192" oninput="V.phone(this,'broker_phone')">
<span class="ferr" id="e-broker_phone">Valid US phone required</span>
</div>
</div>
</div>
<div class="fcard">
<div class="fcard-title">Submission Metadata</div>
<div class="fg fg3">
<div class="fld">
<label class="flabel">Line of Business <span class="req">*</span></label>
<select id="line_of_business" onchange="V.req('line_of_business')">
<option value="">β€” Select β€”</option>
<option value="Personal Lines">Personal Lines</option>
<option value="Commercial Lines">Commercial Lines</option>
<option value="Farm & Ranch">Farm &amp; Ranch</option>
</select>
<span class="ferr" id="e-line_of_business">Required</span>
</div>
<div class="fld">
<label class="flabel">Market Type</label>
<select id="market_type">
<option value="Admitted">Admitted</option>
<option value="E&S">E&amp;S (Excess &amp; Surplus)</option>
<option value="Surplus Lines">Surplus Lines</option>
</select>
</div>
<div class="fld">
<label class="flabel">Submission Date <span class="req">*</span></label>
<input type="date" id="submitted_at" onchange="V.req('submitted_at')">
<span class="ferr" id="e-submitted_at">Required</span>
</div>
</div>
</div>
<div class="btn-row">
<span class="step-hint">Step 1 of 6</span>
<button class="btn btn-accent ml" onclick="nextStep(1)">Next: Insured β†’</button>
</div>
</div>
<!-- STEP 2: INSURED -->
<div class="form-section" id="sec-2">
<div class="fsec-hdr">
<div class="fsec-icon">πŸ‘€</div>
<div><div class="fsec-title">Insured Information</div><div class="fsec-desc">Applicant PII β†’ bronze_insureds (SSN stored as SHA-256 hash)</div></div>
</div>
<div class="alert alert-info">πŸ”’ SSN is hashed client-side using SHA-256. The raw SSN never leaves the browser or gets stored in the database.</div>
<div class="fcard">
<div class="fcard-title">Personal Identity</div>
<div class="fg fg2">
<div class="fld"><label class="flabel">First Name <span class="req">*</span></label>
<input type="text" id="insured_first" placeholder="Sarah" oninput="V.field('insured_first')">
<span class="ferr" id="e-insured_first">Required</span></div>
<div class="fld"><label class="flabel">Last Name <span class="req">*</span></label>
<input type="text" id="insured_last" placeholder="Mitchell" oninput="V.field('insured_last')">
<span class="ferr" id="e-insured_last">Required</span></div>
<div class="fld"><label class="flabel">Date of Birth <span class="req">*</span></label>
<input type="date" id="insured_dob" onchange="V.dob()">
<span class="ferr" id="e-insured_dob">Must be 18+ years old</span></div>
<div class="fld"><label class="flabel">SSN <span class="req">*</span> <span class="ftip">Hashed before storage</span></label>
<input type="text" id="insured_ssn" placeholder="β€’β€’β€’-β€’β€’-####" maxlength="11" oninput="V.ssn(this)">
<span class="ferr" id="e-insured_ssn">Enter full SSN (###-##-####)</span>
<span class="fhint" id="ssn-hint"></span></div>
<div class="fld"><label class="flabel">Email <span class="req">*</span></label>
<input type="email" id="insured_email" placeholder="sarah@email.com" oninput="V.email('insured_email')">
<span class="ferr" id="e-insured_email">Valid email required</span></div>
<div class="fld"><label class="flabel">Phone <span class="req">*</span></label>
<input type="tel" id="insured_phone" placeholder="(512) 555-0192" oninput="V.phone(this,'insured_phone')">
<span class="ferr" id="e-insured_phone">Valid US phone required</span></div>
</div>
</div>
<div class="fcard">
<div class="fcard-title">Mailing Address</div>
<div class="fg fg2">
<div class="fld cs2"><label class="flabel">Street <span class="req">*</span></label>
<input type="text" id="ins_street" placeholder="4821 Oakwood Drive" oninput="V.field('ins_street')">
<span class="ferr" id="e-ins_street">Required</span></div>
<div class="fld"><label class="flabel">City <span class="req">*</span></label>
<input type="text" id="ins_city" placeholder="Austin" oninput="V.field('ins_city')">
<span class="ferr" id="e-ins_city">Required</span></div>
<div class="fld"><label class="flabel">State <span class="req">*</span></label>
<select id="ins_state" onchange="V.req('ins_state')"><option value="">β€” State β€”</option></select>
<span class="ferr" id="e-ins_state">Required</span></div>
<div class="fld"><label class="flabel">ZIP <span class="req">*</span></label>
<input type="text" id="ins_zip" placeholder="78701" maxlength="10" oninput="V.zip(this,'ins_zip')">
<span class="ferr" id="e-ins_zip">5 or 9-digit ZIP</span></div>
<div class="fld"><label class="flabel">Country</label>
<select id="ins_country"><option value="US">πŸ‡ΊπŸ‡Έ United States</option></select></div>
</div>
</div>
<div class="btn-row">
<button class="btn btn-ghost" onclick="prevStep(2)">← Back</button>
<span class="step-hint" style="margin:0 auto;">Step 2 of 6</span>
<button class="btn btn-accent" onclick="nextStep(2)">Next: Property β†’</button>
</div>
</div>
<!-- STEP 3: PROPERTY -->
<div class="form-section" id="sec-3">
<div class="fsec-hdr">
<div class="fsec-icon">🏠</div>
<div><div class="fsec-title">Property Information</div><div class="fsec-desc">Risk location details β†’ bronze_properties</div></div>
</div>
<div class="alert alert-warn">πŸ“ If property address differs from mailing address, enter it below.</div>
<div class="fcard">
<div class="fcard-title">Risk Location Address</div>
<div style="margin-bottom:10px;">
<label class="chk-item" id="same-addr-lbl" style="display:inline-flex;max-width:260px;" onclick="toggleSameAddr()">
<input type="checkbox" id="same_addr"> Same as mailing address
</label>
</div>
<div class="fg fg2" id="prop-addr-fields">
<div class="fld cs2"><label class="flabel">Street <span class="req">*</span></label>
<input type="text" id="prop_street" placeholder="4821 Oakwood Drive" oninput="V.field('prop_street')">
<span class="ferr" id="e-prop_street">Required</span></div>
<div class="fld"><label class="flabel">City <span class="req">*</span></label>
<input type="text" id="prop_city" placeholder="Austin" oninput="V.field('prop_city')">
<span class="ferr" id="e-prop_city">Required</span></div>
<div class="fld"><label class="flabel">State <span class="req">*</span></label>
<select id="prop_state" onchange="V.req('prop_state')"><option value="">β€” State β€”</option></select>
<span class="ferr" id="e-prop_state">Required</span></div>
<div class="fld"><label class="flabel">ZIP <span class="req">*</span></label>
<input type="text" id="prop_zip" placeholder="78701" maxlength="10" oninput="V.zip(this,'prop_zip')">
<span class="ferr" id="e-prop_zip">5 or 9-digit ZIP</span></div>
</div>
</div>
<div class="fcard">
<div class="fcard-title">Physical Characteristics</div>
<div class="fg fg3">
<div class="fld"><label class="flabel">Property Type <span class="req">*</span></label>
<select id="property_type" onchange="V.req('property_type')">
<option value="">β€” Select β€”</option>
<option>Single Family Residential</option><option>Condo / Townhouse</option>
<option>Multi-Family (2-4 units)</option><option>Mobile Home</option>
<option>Manufactured Home</option><option>Vacation / Seasonal</option>
<option>Commercial Building</option><option>Mixed Use</option>
</select><span class="ferr" id="e-property_type">Required</span></div>
<div class="fld"><label class="flabel">Occupancy <span class="req">*</span></label>
<select id="occupancy" onchange="V.req('occupancy')">
<option value="">β€” Select β€”</option>
<option>Owner Occupied</option><option>Tenant Occupied</option>
<option>Seasonal / Vacant</option><option>Rented to Others</option><option>Under Construction</option>
</select><span class="ferr" id="e-occupancy">Required</span></div>
<div class="fld"><label class="flabel">Year Built <span class="req">*</span></label>
<input type="number" id="year_built" placeholder="2004" min="1800" oninput="V.yearBuilt()">
<span class="ferr" id="e-year_built">1800–present</span></div>
<div class="fld"><label class="flabel">Square Footage <span class="req">*</span></label>
<input type="number" id="sq_footage" placeholder="2340" min="100" max="50000" oninput="V.field('sq_footage')">
<span class="ferr" id="e-sq_footage">100–50,000 sq ft</span></div>
<div class="fld"><label class="flabel">Stories</label>
<select id="num_stories">
<option value="">β€”</option><option value="1">1</option><option value="2">2</option>
<option value="3">3</option><option value="4+">4+</option><option value="Split Level">Split Level</option>
</select></div>
<div class="fld"><label class="flabel">Units</label>
<input type="number" id="num_units" value="1" min="1" max="100"></div>
</div>
</div>
<div class="fcard">
<div class="fcard-title">Construction Details</div>
<div class="fg fg3">
<div class="fld"><label class="flabel">Construction Type <span class="req">*</span></label>
<select id="construction_type" onchange="V.req('construction_type')">
<option value="">β€” Select β€”</option>
<option>Frame</option><option>Masonry</option><option>Masonry Veneer</option>
<option>Fire Resistive</option><option>Non-Combustible</option><option>Log</option><option>Stucco</option>
</select><span class="ferr" id="e-construction_type">Required</span></div>
<div class="fld"><label class="flabel">Roof Type <span class="req">*</span></label>
<select id="roof_type" onchange="V.req('roof_type')">
<option value="">β€” Select β€”</option>
<option>Composition Shingle</option><option>Metal</option><option>Tile</option>
<option>Flat / Built-Up</option><option>Wood Shake</option><option>Slate</option>
</select><span class="ferr" id="e-roof_type">Required</span></div>
<div class="fld"><label class="flabel">Roof Year <span class="req">*</span></label>
<input type="number" id="roof_year" placeholder="2019" min="1950" oninput="V.roofYear()">
<span class="ferr" id="e-roof_year">Must be β‰₯ year built</span></div>
<div class="fld"><label class="flabel">Foundation</label>
<select id="foundation"><option value="">β€”</option><option>Slab</option><option>Crawl Space</option><option>Basement</option><option>Pier and Beam</option></select></div>
<div class="fld"><label class="flabel">Heating</label>
<select id="heating"><option value="">β€”</option><option>Central Gas</option><option>Central Electric</option><option>Heat Pump</option><option>Oil</option></select></div>
<div class="fld"><label class="flabel">Electrical</label>
<select id="electrical"><option value="">β€”</option><option>Circuit Breaker</option><option>Fuse Box</option><option>Updated (2000+)</option><option>Knob and Tube</option><option>Aluminum Wiring</option></select></div>
</div>
</div>
<div class="fcard">
<div class="fcard-title">Liability Indicators</div>
<div class="chk-grid">
<label class="chk-item" onclick="toggleChk(this,'chk_pool')"><input type="checkbox" id="chk_pool"> 🏊 Swimming Pool</label>
<label class="chk-item" onclick="toggleChk(this,'chk_tramp')"><input type="checkbox" id="chk_tramp"> 🀸 Trampoline</label>
<label class="chk-item" onclick="toggleChk(this,'chk_dog')"><input type="checkbox" id="chk_dog"> πŸ• Aggressive Dog Breed</label>
<label class="chk-item" onclick="toggleChk(this,'chk_biz')"><input type="checkbox" id="chk_biz"> πŸ’Ό Home Business</label>
<label class="chk-item" onclick="toggleChk(this,'chk_rental')"><input type="checkbox" id="chk_rental"> 🏘 Rental Units</label>
<label class="chk-item" onclick="toggleChk(this,'chk_solar')"><input type="checkbox" id="chk_solar"> β˜€οΈ Solar Panels</label>
</div>
</div>
<div class="btn-row">
<button class="btn btn-ghost" onclick="prevStep(3)">← Back</button>
<span class="step-hint" style="margin:0 auto;">Step 3 of 6</span>
<button class="btn btn-accent" onclick="nextStep(3)">Next: Coverage β†’</button>
</div>
</div>
<!-- STEP 4: COVERAGE -->
<div class="form-section" id="sec-4">
<div class="fsec-hdr">
<div class="fsec-icon">πŸ›‘οΈ</div>
<div><div class="fsec-title">Coverage Request</div><div class="fsec-desc">Coverage type and limits β†’ bronze_submissions</div></div>
</div>
<div class="fcard">
<div class="fcard-title">Coverage Type</div>
<div class="cov-grid">
<div class="cov-card sel" data-code="HO-3" onclick="selCov(this)"><input type="radio" name="cov" value="HO-3" checked><div class="cov-code">HO-3</div><div class="cov-name">Special Form</div><div class="cov-desc">Most common homeowners</div></div>
<div class="cov-card" data-code="HO-4" onclick="selCov(this)"><input type="radio" name="cov" value="HO-4"><div class="cov-code">HO-4</div><div class="cov-name">Renters</div><div class="cov-desc">Contents &amp; liability only</div></div>
<div class="cov-card" data-code="HO-5" onclick="selCov(this)"><input type="radio" name="cov" value="HO-5"><div class="cov-code">HO-5</div><div class="cov-name">Comprehensive</div><div class="cov-desc">Open perils on all</div></div>
<div class="cov-card" data-code="HO-6" onclick="selCov(this)"><input type="radio" name="cov" value="HO-6"><div class="cov-code">HO-6</div><div class="cov-name">Condo Owner</div><div class="cov-desc">Walls-in condo coverage</div></div>
<div class="cov-card" data-code="DP-1" onclick="selCov(this)"><input type="radio" name="cov" value="DP-1"><div class="cov-code">DP-1</div><div class="cov-name">Dwelling Basic</div><div class="cov-desc">Named perils only</div></div>
<div class="cov-card" data-code="DP-3" onclick="selCov(this)"><input type="radio" name="cov" value="DP-3"><div class="cov-code">DP-3</div><div class="cov-name">Dwelling Special</div><div class="cov-desc">Landlord open perils</div></div>
<div class="cov-card" data-code="BOP" onclick="selCov(this)"><input type="radio" name="cov" value="BOP"><div class="cov-code">BOP</div><div class="cov-name">Business Owners</div><div class="cov-desc">Property + liability</div></div>
<div class="cov-card" data-code="CPP" onclick="selCov(this)"><input type="radio" name="cov" value="CPP"><div class="cov-code">CPP</div><div class="cov-name">Comm. Package</div><div class="cov-desc">Multi-line commercial</div></div>
<div class="cov-card" data-code="FARM" onclick="selCov(this)"><input type="radio" name="cov" value="FARM"><div class="cov-code">FARM</div><div class="cov-name">Farm &amp; Ranch</div><div class="cov-desc">Dwelling + equipment</div></div>
</div>
</div>
<div class="fcard">
<div class="fcard-title">Limits &amp; Deductibles</div>
<div class="fg fg2">
<div class="fld"><label class="flabel">Coverage Limit ($) <span class="req">*</span></label>
<input type="number" id="cov_limit" placeholder="450000" min="50000" max="10000000" oninput="V.covLimit()">
<span class="ferr" id="e-cov_limit">$50,000–$10,000,000</span>
<span class="fhint">~$165/sq ft avg construction cost</span></div>
<div class="fld"><label class="flabel">Deductible <span class="req">*</span></label>
<select id="deductible" onchange="V.req('deductible')">
<option value="">β€” Select β€”</option>
<option value="500">$500</option><option value="1000">$1,000</option>
<option value="1500">$1,500</option><option value="2500">$2,500</option>
<option value="5000">$5,000</option><option value="10000">$10,000</option>
<option value="25000">$25,000</option><option value="50000">$50,000</option>
</select><span class="ferr" id="e-deductible">Select deductible</span></div>
<div class="fld"><label class="flabel">Liability Limit</label>
<select id="liab_limit"><option value="100000">$100,000</option><option value="300000" selected>$300,000</option><option value="500000">$500,000</option><option value="1000000">$1,000,000</option></select></div>
<div class="fld"><label class="flabel">Medical Payments</label>
<select id="med_pay"><option value="1000">$1,000</option><option value="2000">$2,000</option><option value="5000" selected>$5,000</option><option value="10000">$10,000</option></select></div>
<div class="fld"><label class="flabel">Effective Date <span class="req">*</span></label>
<input type="date" id="effective_date" onchange="V.effDate()">
<span class="ferr" id="e-effective_date">Today–60 days out</span></div>
<div class="fld"><label class="flabel">Payment Schedule</label>
<select id="pay_schedule"><option>Annual</option><option>Semi-Annual</option><option>Quarterly</option><option value="Monthly EFT">Monthly (EFT)</option></select></div>
</div>
</div>
<div class="fcard">
<div class="fcard-title">Endorsements Requested</div>
<div class="chk-grid">
<label class="chk-item" onclick="toggleChk(this,'end_identity')"><input type="checkbox" id="end_identity"> πŸ” Identity Theft</label>
<label class="chk-item" onclick="toggleChk(this,'end_equip')"><input type="checkbox" id="end_equip"> πŸ–₯️ Equipment Breakdown</label>
<label class="chk-item" onclick="toggleChk(this,'end_water')"><input type="checkbox" id="end_water"> πŸ’§ Water Backup</label>
<label class="chk-item" onclick="toggleChk(this,'end_quake')"><input type="checkbox" id="end_quake"> πŸŒ‹ Earthquake</label>
<label class="chk-item" onclick="toggleChk(this,'end_flood')"><input type="checkbox" id="end_flood"> 🌊 Flood (NFIP)</label>
<label class="chk-item" onclick="toggleChk(this,'end_umbrella')"><input type="checkbox" id="end_umbrella"> β˜‚οΈ Umbrella</label>
</div>
</div>
<div class="fcard">
<div class="fcard-title">Notes</div>
<div class="fld">
<label class="flabel">Submission Notes</label>
<textarea id="notes" placeholder="Special circumstances, prior losses, renovation details..." maxlength="1000"></textarea>
<span class="fhint"><span id="nc">0</span>/1000</span>
</div>
</div>
<div class="btn-row">
<button class="btn btn-ghost" onclick="prevStep(4)">← Back</button>
<span class="step-hint" style="margin:0 auto;">Step 4 of 6</span>
<button class="btn btn-accent" onclick="nextStep(4)">Documents β†’</button>
</div>
</div>
<!-- STEP 5: DOCUMENTS -->
<div class="form-section" id="sec-5">
<div class="fsec-hdr">
<div class="fsec-icon">πŸ“Ž</div>
<div><div class="fsec-title">Supporting Documents</div><div class="fsec-desc">Upload property inspection report, deed, or photos β€” validated before submission β†’ bronze_submission_attachments</div></div>
</div>
<div class="fcard">
<div class="fcard-title">Document Upload <span style="font-size:11px;font-weight:400;color:var(--muted);">β€” At least one property document required</span></div>
<div style="display:grid;gap:14px;">
<!-- Upload slot 1 -->
<div class="doc-slot" id="slot-0">
<div class="doc-slot-hdr">
<span class="doc-slot-label">Document 1 <span style="color:var(--red);">*</span></span>
<select class="doc-type-sel" id="doc-type-0" onchange="docTypeChanged(0)">
<option value="">β€” Select document type β€”</option>
<option value="PROPERTY_INSPECTION">Property Inspection Report</option>
<option value="PROPERTY_DEED">Property Deed / Title</option>
<option value="PROPERTY_PHOTO">Property Photograph</option>
<option value="APPRAISAL">Property Appraisal</option>
<option value="PRIOR_POLICY">Prior Insurance Policy</option>
<option value="OTHER">Other Supporting Document</option>
</select>
</div>
<div class="doc-dropzone" id="dz-0" onclick="triggerUpload(0)" ondragover="dzDragOver(event,0)" ondragleave="dzDragLeave(0)" ondrop="dzDrop(event,0)">
<div class="dz-icon">πŸ“„</div>
<div class="dz-text">Click to upload or drag & drop</div>
<div class="dz-hint">PDF or JPG/PNG Β· Max 10MB</div>
<input type="file" id="file-input-0" accept=".pdf,.jpg,.jpeg,.png" style="display:none" onchange="handleFileSelect(0,this)">
</div>
<div class="doc-status" id="doc-status-0"></div>
</div>
<!-- Upload slot 2 -->
<div class="doc-slot" id="slot-1">
<div class="doc-slot-hdr">
<span class="doc-slot-label">Document 2 <span style="color:var(--muted);font-size:11px;">(optional)</span></span>
<select class="doc-type-sel" id="doc-type-1" onchange="docTypeChanged(1)">
<option value="">β€” Select document type β€”</option>
<option value="PROPERTY_INSPECTION">Property Inspection Report</option>
<option value="PROPERTY_DEED">Property Deed / Title</option>
<option value="PROPERTY_PHOTO">Property Photograph</option>
<option value="APPRAISAL">Property Appraisal</option>
<option value="PRIOR_POLICY">Prior Insurance Policy</option>
<option value="OTHER">Other Supporting Document</option>
</select>
</div>
<div class="doc-dropzone" id="dz-1" onclick="triggerUpload(1)" ondragover="dzDragOver(event,1)" ondragleave="dzDragLeave(1)" ondrop="dzDrop(event,1)">
<div class="dz-icon">πŸ“„</div>
<div class="dz-text">Click to upload or drag & drop</div>
<div class="dz-hint">PDF or JPG/PNG Β· Max 10MB</div>
<input type="file" id="file-input-1" accept=".pdf,.jpg,.jpeg,.png" style="display:none" onchange="handleFileSelect(1,this)">
</div>
<div class="doc-status" id="doc-status-1"></div>
</div>
</div>
<!-- Validation rules info -->
<div style="margin-top:18px;padding:12px 14px;background:var(--bg3);border-radius:6px;border:1px solid var(--border);font-size:12px;color:var(--muted);">
<strong style="color:var(--text);">βœ“ Validation Rules Applied:</strong>
PDF files are checked for property-related keywords (address, inspection, property, coverage, insured, roof, construction).
JPG/PNG files are verified as valid images. Generic invoices or unrelated documents are rejected.
</div>
</div>
<div class="btn-row">
<button class="btn btn-ghost" onclick="prevStep(5)">← Back</button>
<span class="step-hint" style="margin:0 auto;">Step 5 of 6</span>
<button class="btn btn-accent" onclick="nextStep(5)">Review β†’</button>
</div>
</div>
<!-- STEP 6: REVIEW -->
<div class="form-section" id="sec-6">
<div class="fsec-hdr">
<div class="fsec-icon">πŸ“‹</div>
<div><div class="fsec-title">Review &amp; Confirm</div><div class="fsec-desc">Verify all fields before Bronze layer commit</div></div>
</div>
<div id="review-content"></div>
<div class="fcard" style="background:var(--bg);">
<div class="fcard-title" style="color:var(--amber);">Generated JSON Payload</div>
<div class="json-box" id="json-preview">β€”</div>
<button class="btn btn-ghost" style="margin-top:10px;font-size:11px;" onclick="copyJSON()">πŸ“‹ Copy JSON</button>
</div>
<div class="btn-row">
<button class="btn btn-ghost" onclick="prevStep(6)">← Back</button>
<button class="btn btn-green ml" onclick="doSubmit()">βœ… Submit to MySQL + Save JSON</button>
</div>
</div>
<!-- STEP 7: SUCCESS -->
<div class="form-section" id="sec-7">
<div class="success-wrap">
<div class="suc-icon">βœ“</div>
<div class="suc-title">Submission Accepted!</div>
<div class="suc-sub">Policy request committed to Bronze layer Β· JSON saved to disk</div>
<div class="suc-detail" id="suc-detail"></div>
<div class="path-box">
<div class="path-label">JSON File Path</div>
<div class="path-val" id="json-path">D:\Personal\Usecase\Jsons\SUB-XXXX.json</div>
</div>
<div>
<button class="btn btn-accent" onclick="location.reload()">+ New Submission</button>
<button class="btn btn-ghost" style="margin-left:8px;" onclick="downloadJSON()">⬇ Download JSON</button>
<button class="btn btn-ghost" style="margin-left:8px;" onclick="switchTab('integration');showIntSection('inserts')">View SQL Inserts β†’</button>
</div>
</div>
</div>
</main>
<!-- Right panel -->
<aside class="sub-panel">
<div class="panel-sub-id"><div class="ps-label">Submission ID</div><div class="ps-val" id="live-subid">SUB-2025-β€”</div></div>
<div class="pcard">
<div class="pcard-title">Submitter</div>
<div class="prow"><span class="pk">Name</span><span class="pv" id="lv-bname">β€”</span></div>
<div class="prow"><span class="pk">Code</span><span class="pv mono" id="lv-bcode">β€”</span></div>
<div class="prow"><span class="pk">State</span><span class="pv" id="lv-bstate">β€”</span></div>
</div>
<div class="pcard">
<div class="pcard-title">Insured</div>
<div class="prow"><span class="pk">Name</span><span class="pv" id="lv-iname">β€”</span></div>
<div class="prow"><span class="pk">DOB</span><span class="pv" id="lv-idob">β€”</span></div>
<div class="prow"><span class="pk">State</span><span class="pv" id="lv-istate">β€”</span></div>
</div>
<div class="pcard">
<div class="pcard-title">Property</div>
<div class="prow"><span class="pk">City/State</span><span class="pv" id="lv-paddr">β€”</span></div>
<div class="prow"><span class="pk">Type</span><span class="pv" id="lv-ptype">β€”</span></div>
<div class="prow"><span class="pk">Built</span><span class="pv" id="lv-pbuilt">β€”</span></div>
<div class="prow"><span class="pk">Sq Ft</span><span class="pv" id="lv-psqft">β€”</span></div>
</div>
<div class="pcard">
<div class="pcard-title">Coverage</div>
<div class="prow"><span class="pk">Type</span><span class="pv mono" id="lv-ctype">HO-3</span></div>
<div class="prow"><span class="pk">Limit</span><span class="pv" id="lv-climit">β€”</span></div>
<div class="prow"><span class="pk">Ded.</span><span class="pv" id="lv-cded">β€”</span></div>
<div class="prow"><span class="pk">Eff.</span><span class="pv" id="lv-ceff">β€”</span></div>
</div>
<div class="pcard">
<div class="pcard-title">Step Validation</div>
<div class="val-list">
<div class="val-row idle" id="vl-1">β—‹ Submitter</div>
<div class="val-row idle" id="vl-2">β—‹ Insured</div>
<div class="val-row idle" id="vl-3">β—‹ Property</div>
<div class="val-row idle" id="vl-4">β—‹ Coverage</div>
<div class="val-row idle" id="vl-5">β—‹ Documents</div>
</div>
</div>
<div class="pcard">
<div class="pcard-title">Bronze Tables</div>
<div style="font-size:10px;color:var(--muted2);line-height:1.8;">
↳ bronze_brokers<br>
↳ bronze_insureds<br>
↳ bronze_properties<br>
↳ bronze_submissions<br>
↳ bronze_submission_attachments<br>
↳ bronze_pipeline_agent_log
</div>
</div>
</aside>
</div>
</div>
<!-- ═══════════════════════════════════════════
TAB 2 β€” MYSQL INTEGRATION
═══════════════════════════════════════════ -->
<div class="tab-page" id="tab-integration">
<div class="int-layout">
<aside class="int-sidebar">
<div class="snav-title">Integration Sections</div>
<div class="snav-item active" id="inav-conn" onclick="showIntSection('conn')"><div class="snav-num">βš™</div> DB Connection</div>
<div class="snav-item" id="inav-schema" onclick="showIntSection('schema')"><div class="snav-num">πŸ“Š</div> Schema Map</div>
<div class="snav-item" id="inav-inserts" onclick="showIntSection('inserts')"><div class="snav-num">πŸ“</div> SQL Inserts</div>
<div class="snav-item" id="inav-log" onclick="showIntSection('log')"><div class="snav-num">πŸ“‹</div> Activity Log</div>
<div class="snav-item" id="inav-python" onclick="showIntSection('python')"><div class="snav-num">🐍</div> Python Connector</div>
<div style="margin-top:20px;padding:12px;background:var(--bg3);border-radius:var(--r);border:1px solid var(--border);">
<div class="snav-title">DB Config</div>
<div class="db-row" style="margin-bottom:5px;"><span class="dk">Host</span><span class="dv" style="font-size:11px;">localhost:3306</span></div>
<div class="db-row" style="margin-bottom:5px;"><span class="dk">User</span><span class="dv" style="font-size:11px;">root</span></div>
<div class="db-row" style="margin-bottom:5px;"><span class="dk">Pass</span><span class="dv" style="font-size:11px;">root@123</span></div>
<div class="db-row" style="margin-bottom:5px;"><span class="dk">DB</span><span class="dv" style="font-size:11px;">bronze</span></div>
<div class="conn-status" style="margin-top:8px;padding:7px 10px;">
<div class="status-dot amber" id="conn-dot"></div>
<span id="conn-txt" style="font-size:11px;">Not tested</span>
</div>
<button class="btn btn-accent" style="width:100%;margin-top:8px;font-size:11px;justify-content:center;" onclick="testConnection()">Test Connection</button>
</div>
</aside>
<main class="int-main">
<!-- DB Connection -->
<div class="int-section active" id="int-conn">
<div class="page-header">
<div class="ph-title">MySQL Bronze Layer Connection</div>
<div class="ph-sub">Configure and verify Β· Local: localhost/bronze Β· HF: Clever Cloud (single schema, prefixed tables)</div>
</div>
<div class="alert alert-info" style="margin-bottom:20px;">
πŸ’‘ This is a browser-based app. Direct MySQL TCP connections require a backend proxy. The SQL Inserts tab generates ready-to-run statements for MySQL Workbench or CLI. The Python Connector tab provides a production-ready script for direct DB writes.
</div>
<div class="two-col" style="margin-bottom:16px;">
<div class="config-card">
<div class="cc-title">Connection Details</div>
<div class="cc-desc">Pre-configured Β· localhost (local) or Clever Cloud (HuggingFace)</div>
<div class="fg fg2" style="gap:10px;">
<div class="fld"><label class="flabel">Host</label><input type="text" value="localhost" id="db_host"></div>
<div class="fld"><label class="flabel">Port</label><input type="number" value="3306" id="db_port"></div>
<div class="fld"><label class="flabel">Username</label><input type="text" value="root" id="db_user"></div>
<div class="fld"><label class="flabel">Password</label><input type="text" value="root@123" id="db_pass"></div>
<div class="fld"><label class="flabel">Database</label><input type="text" value="bronze" id="db_name"></div>
<div class="fld"><label class="flabel">Charset</label><input type="text" value="utf8mb4"></div>
</div>
<button class="btn btn-accent" style="margin-top:14px;" onclick="testConnection()">⚑ Test Connection</button>
</div>
<div class="config-card">
<div class="cc-title">Connection Status</div>
<div class="cc-desc">Live diagnostics and health check</div>
<div class="log-console" id="conn-log" style="min-height:140px;">
<div class="log-line"><span class="log-ts">00:00:00</span><span class="log-info">PolicyBridge MySQL Integration v1.0</span></div>
<div class="log-line"><span class="log-ts">00:00:00</span><span class="log-info">Target: localhost:3306 / bronze</span></div>
<div class="log-line"><span class="log-ts">00:00:00</span><span class="log-warn">Click "Test Connection" to verify connectivity</span></div>
</div>
</div>
</div>
<div class="config-card">
<div class="cc-title">Bronze Layer Tables β€” Write Targets</div>
<div class="cc-desc">All tables that receive data on each submission</div>
<table class="tmap">
<thead><tr><th>Schema</th><th>Table</th><th>Write Trigger</th><th>Key Fields</th><th>Status</th></tr></thead>
<tbody id="tmap-body">
<tr><td><span class="schema-badge">bronze</span></td><td>bronze_brokers</td><td>Every new broker</td><td>broker_code, broker_name, contact_email</td><td class="status-cell"><span class="s-pend">● Pending</span></td></tr>
<tr><td><span class="schema-badge">bronze</span></td><td>bronze_insureds</td><td>Every submission</td><td>full_name, dob, ssn_hash, email, phone</td><td class="status-cell"><span class="s-pend">● Pending</span></td></tr>
<tr><td><span class="schema-badge">bronze</span></td><td>bronze_properties</td><td>Every submission</td><td>insured_id, street, city, state_code, zip</td><td class="status-cell"><span class="s-pend">● Pending</span></td></tr>
<tr><td><span class="schema-badge">bronze</span></td><td>bronze_submissions</td><td>Every submission</td><td>submission_id, broker_id, insured_id, property_id</td><td class="status-cell"><span class="s-pend">● Pending</span></td></tr>
<tr><td><span class="schema-badge">bronze</span></td><td>bronze_submission_attachments</td><td>Static S3 URIs (Phase 1)</td><td>submission_id, attachment_type, s3_uri</td><td class="status-cell"><span class="s-pend">● Pending</span></td></tr>
<tr><td><span class="schema-badge">bronze</span></td><td>bronze_pipeline_agent_log</td><td>On submission</td><td>submission_id, agent_code, status=QUEUED</td><td class="status-cell"><span class="s-pend">● Pending</span></td></tr>
</tbody>
</table>
</div>
</div>
<!-- Schema Map -->
<div class="int-section" id="int-schema">
<div class="page-header">
<div class="ph-title">Bronze Schema β†’ Form Field Mapping</div>
<div class="ph-sub">Complete field-to-column mapping for all Bronze tables</div>
</div>
<div id="schema-map-content"></div>
</div>
<!-- SQL Inserts -->
<div class="int-section" id="int-inserts">
<div class="page-header">
<div class="ph-title">Generated SQL Insert Statements</div>
<div class="ph-sub">Ready to execute in MySQL Workbench or CLI β€” generated from last submission</div>
</div>
<div class="alert alert-warn">⚠ These statements reflect the most recently submitted form data. Submit a form on the Submission tab first.</div>
<div class="config-card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<div class="cc-title" style="margin-bottom:0;">MySQL INSERT Statements</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-ghost" style="font-size:11px;" onclick="copySQLInserts()">πŸ“‹ Copy All</button>
<button class="btn btn-accent" style="font-size:11px;" onclick="downloadSQL()">⬇ Download .sql</button>
</div>
</div>
<div class="sql-area" id="sql-inserts-area">-- Submit a form to generate INSERT statements</div>
</div>
</div>
<!-- Activity Log -->
<div class="int-section" id="int-log">
<div class="page-header">
<div class="ph-title">Activity Log</div>
<div class="ph-sub">All submission and DB write events</div>
</div>
<div class="config-card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<div class="cc-title" style="margin-bottom:0;">Event Log</div>
<button class="btn btn-ghost" style="font-size:11px;" onclick="document.getElementById('activity-log').innerHTML=''">Clear</button>
</div>
<div class="log-console" id="activity-log" style="max-height:500px;">
<div class="log-line"><span class="log-ts">--:--:--</span><span class="log-info">PolicyBridge v1.0 started Β· Phase 1</span></div>
</div>
</div>
</div>
<!-- Python Connector -->
<div class="int-section" id="int-python">
<div class="page-header">
<div class="ph-title">Python MySQL Connector</div>
<div class="ph-sub">Production-ready Python script for direct Bronze layer writes β€” Phase 1 β†’ Phase 2 bridge</div>
</div>
<div class="alert alert-green">βœ… This script loads submission JSON into all 6 bronze tables in the correct FK order. Place in your project and run: <code style="font-family:var(--font-mono);background:rgba(0,0,0,0.2);padding:1px 6px;border-radius:3px;">python bronze_loader.py SUB-2025-XXXXX.json</code></div>
<div class="config-card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<div class="cc-title" style="margin-bottom:0;">bronze_loader.py</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-ghost" style="font-size:11px;" onclick="copyPython()">πŸ“‹ Copy</button>
<button class="btn btn-accent" style="font-size:11px;" onclick="downloadPython()">⬇ Download .py</button>
</div>
</div>
<div class="python-snippet" id="python-code"></div>
</div>
</div>
</main>
</div>
</div>
<!-- ═══════════════════════════════════════════
TAB 3 β€” CONNECTORS HUB
═══════════════════════════════════════════ -->
<div class="tab-page" id="tab-connectors">
<div class="conn-page">
<div class="conn-page-title">Connectors Hub</div>
<div class="conn-page-sub">Integrate PolicyBridge with any data source β€” databases, APIs, file systems, streaming platforms, and cloud services</div>
<div class="three-col" style="margin-bottom:24px;">
<div class="stat-box"><div class="stat-val" style="color:var(--accent);">24</div><div class="stat-label">Available Connectors</div></div>
<div class="stat-box"><div class="stat-val" style="color:var(--green);">1</div><div class="stat-label">Connected</div></div>
<div class="stat-box"><div class="stat-val" style="color:var(--amber);">23</div><div class="stat-label">Available to Connect</div></div>
</div>
<div class="conn-cats">
<button class="cat-btn active" onclick="filterCat('all',this)">All (24)</button>
<button class="cat-btn" onclick="filterCat('database',this)">Databases</button>
<button class="cat-btn" onclick="filterCat('api',this)">REST / APIs</button>
<button class="cat-btn" onclick="filterCat('file',this)">File / Storage</button>
<button class="cat-btn" onclick="filterCat('cloud',this)">Cloud / AWS</button>
<button class="cat-btn" onclick="filterCat('stream',this)">Streaming</button>
<button class="cat-btn" onclick="filterCat('insurance',this)">Insurance</button>
</div>
<div class="conn-grid" id="conn-grid"></div>
</div>
</div>
<!-- ═══════════════════════════════════════════
TAB 4 β€” AGENT PIPELINE
═══════════════════════════════════════════ -->
<div class="tab-page" id="tab-agents">
<div class="agents-page">
<div class="agents-title">AI Agent Pipeline</div>
<div class="agents-sub">Phase 1: Bronze layer population Β· Phase 2: Agent orchestration via AWS Step Functions</div>
<div class="pipeline-flow" id="pipeline-flow"></div>
<div style="margin-bottom:24px;">
<div style="display:flex;gap:14px;margin-bottom:14px;">
<div class="stat-box" style="flex:1;"><div class="stat-val" style="color:var(--amber);font-size:20px;">Phase 1</div><div class="stat-label">Bronze Population (Active)</div></div>
<div class="stat-box" style="flex:1;opacity:0.5;"><div class="stat-val" style="color:var(--muted);font-size:20px;">Phase 2</div><div class="stat-label">Agent Orchestration (Upcoming)</div></div>
<div class="stat-box" style="flex:1;opacity:0.4;"><div class="stat-val" style="color:var(--muted);font-size:20px;">Phase 3</div><div class="stat-label">Gold Layer Analytics (Future)</div></div>
</div>
</div>
<div class="agent-detail-grid" id="agent-detail-grid"></div>
<div style="margin-top:24px;">
<div class="page-header"><div class="ph-title">Python Agent Base Template</div><div class="ph-sub">Starter scaffold for building each agent in Phase 2</div></div>
<div class="config-card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<div class="cc-title" style="margin-bottom:0;">base_agent.py β€” Agent Framework Scaffold</div>
<button class="btn btn-ghost" style="font-size:11px;" onclick="copyAgentPython()">πŸ“‹ Copy</button>
</div>
<div class="python-snippet" id="agent-python-code"></div>
</div>
</div>
</div>
</div>
<!-- STATUS BAR -->
<div class="status-bar">
<div class="sb-dot"></div>
<span>PolicyBridge v1.0</span>
<span class="sb-sep">|</span>
<span id="sb-msg">Ready</span>
<span class="sb-sep">|</span>
<span id="db-env-label">localhost:3306 Β· bronze</span>
<span style="margin-left:auto;font-family:var(--font-mono);" id="sb-clock"></span>
</div>
<script>
// ═══════════════════════════════════════════════════════
// APP STATE
// ═══════════════════════════════════════════════════════
// ── Dynamic API base: HuggingFace Space or local Flask ──────
const IS_HF = window.location.hostname.includes('hf.space') ||
window.location.hostname.includes('huggingface');
const API_BASE = IS_HF
? '' // same-origin on HF (Docker serves on port 7860)
: 'http://localhost:5000';
const APP = {
step: 1,
subType: 'broker',
coverage: 'HO-3',
subId: '',
json: null,
sqlInserts: '',
};
const US_STATES = [['AL','Alabama'],['AK','Alaska'],['AZ','Arizona'],['AR','Arkansas'],['CA','California'],['CO','Colorado'],['CT','Connecticut'],['DE','Delaware'],['FL','Florida'],['GA','Georgia'],['HI','Hawaii'],['ID','Idaho'],['IL','Illinois'],['IN','Indiana'],['IA','Iowa'],['KS','Kansas'],['KY','Kentucky'],['LA','Louisiana'],['ME','Maine'],['MD','Maryland'],['MA','Massachusetts'],['MI','Michigan'],['MN','Minnesota'],['MS','Mississippi'],['MO','Missouri'],['MT','Montana'],['NE','Nebraska'],['NV','Nevada'],['NH','New Hampshire'],['NJ','New Jersey'],['NM','New Mexico'],['NY','New York'],['NC','North Carolina'],['ND','North Dakota'],['OH','Ohio'],['OK','Oklahoma'],['OR','Oregon'],['PA','Pennsylvania'],['RI','Rhode Island'],['SC','South Carolina'],['SD','South Dakota'],['TN','Tennessee'],['TX','Texas'],['UT','Utah'],['VT','Vermont'],['VA','Virginia'],['WA','Washington'],['WV','West Virginia'],['WI','Wisconsin'],['WY','Wyoming'],['DC','District of Columbia']];
// ═══════════════════════════════════════════════════════
// INIT
// ═══════════════════════════════════════════════════════
window.onload = () => {
// Populate state selects
['broker_state','ins_state','prop_state'].forEach(id => {
const s = document.getElementById(id);
if (!s) return;
US_STATES.forEach(([c,n]) => { const o = document.createElement('option'); o.value=c; o.textContent=`${c} β€” ${n}`; s.appendChild(o); });
});
const today = new Date().toISOString().split('T')[0];
document.getElementById('submitted_at').value = today;
const tmr = new Date(); tmr.setDate(tmr.getDate()+1);
document.getElementById('effective_date').value = tmr.toISOString().split('T')[0];
APP.subId = genSubId();
document.getElementById('live-subid').textContent = APP.subId;
document.getElementById('notes').addEventListener('input', function() {
document.getElementById('nc').textContent = this.value.length;
});
setInterval(() => {
document.getElementById('sb-clock').textContent = new Date().toLocaleTimeString();
syncPanel();
}, 800);
buildSchemaMap();
buildConnectors();
buildAgentPipeline();
buildPythonCode();
buildAgentPython();
setTimeout(() => {
const isHF = window.location.hostname.includes('hf.space') ||
window.location.hostname.includes('huggingface');
const dbLabel = isHF ? 'DB: Clever Cloud' : 'DB: localhost';
const dbTarget = isHF
? 'Clever Cloud MySQL Β· bronze_* / silver_* / gold_* prefixes'
: 'localhost:3306 Β· bronze';
document.getElementById('db-conn-pill').textContent = dbLabel;
document.getElementById('db-conn-pill').className = 'pill pill-green';
document.getElementById('db-env-label').textContent = dbTarget;
addLog('info', `PolicyBridge loaded Β· Target: ${dbTarget}`);
}, 800);
};
function genSubId() {
return `SUB-${new Date().getFullYear()}-${String(Math.floor(10000+Math.random()*89999))}`;
}
// ═══════════════════════════════════════════════════════
// TAB SWITCHING
// ═══════════════════════════════════════════════════════
function switchTab(name) {
document.querySelectorAll('.tab-page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.getElementById(`tab-${name}`).classList.add('active');
event?.target?.classList.add('active');
// Fix active tab button
document.querySelectorAll('.tab-btn').forEach(b => {
if (b.textContent.toLowerCase().includes(name.replace('-',' ').split(' ')[0])) b.classList.add('active');
});
}
// ═══════════════════════════════════════════════════════
// STEP NAVIGATION
// ═══════════════════════════════════════════════════════
function goStep(n) { if(n<=APP.step||n===1) _showStep(n); }
function prevStep(n) { _showStep(n-1); }
function nextStep(n) {
if (!validateStep(n)) return;
markStepDone(n);
_showStep(n+1);
}
function _showStep(n) {
document.querySelectorAll('.form-section').forEach(s=>s.classList.remove('active'));
document.getElementById(`sec-${n}`).classList.add('active');
for(let i=1;i<=7;i++) {
const nav=document.getElementById(`snav-${i}`);
const num=document.getElementById(`sn-${i}`);
const bub=document.getElementById(`sbb-${i}`);
const wrap=document.getElementById(`sbw-${i}`);
const line=document.getElementById(`sbl-${i}`);
if(!nav)continue;
nav.classList.remove('active','done');
bub.classList.remove('active','done');
wrap.classList.remove('active','done');
if(i<n){nav.classList.add('done');bub.classList.add('done');bub.textContent='βœ“';wrap.classList.add('done');if(line)line.classList.add('done');}
else if(i===n){nav.classList.add('active');bub.classList.add('active');bub.textContent=i;wrap.classList.add('active');if(line)line.classList.remove('done');}
else{bub.textContent=i;if(line)line.classList.remove('done');}
}
APP.step=n;
if(n===6) buildReview();
document.querySelector('.sub-form-main').scrollTop=0;
}
function markStepDone(n) {
const labels=['Submitter','Insured','Property','Coverage','Documents'];
const el=document.getElementById(`vl-${n}`);
if(el){el.className='val-row ok';el.textContent=`βœ“ ${labels[n-1]}`;}
}
// ═══════════════════════════════════════════════════════
// VALIDATION
// ═══════════════════════════════════════════════════════
const V = {
field(id){const el=document.getElementById(id);const ok=!!el?.value?.trim();showE(id,!ok);if(ok)el.classList.add('ok');return ok;},
req(id){const el=document.getElementById(id);const ok=!!el?.value;showE(id,!ok);return ok;},
email(id){const el=document.getElementById(id);const ok=/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(el?.value||'');showE(id,!ok);if(ok)el.classList.add('ok');return ok;},
phone(el,id){let d=el.value.replace(/\D/g,'').slice(0,10);let f=d.length<=3?d:d.length<=6?`(${d.slice(0,3)}) ${d.slice(3)}`:`(${d.slice(0,3)}) ${d.slice(3,6)}-${d.slice(6)}`;el.value=f;showE(id,d.length<10);return d.length>=10;},
ssn(el){let d=el.value.replace(/\D/g,'').slice(0,9);el.value=d.length<=3?d:d.length<=5?`${d.slice(0,3)}-${d.slice(3)}`:`${d.slice(0,3)}-${d.slice(3,5)}-${d.slice(5)}`;const ok=d.length===9;showE('insured_ssn',!ok);if(ok){el.classList.add('ok');document.getElementById('ssn-hint').textContent=`Hash: sha256:${simHash(d)} Β· Last 4: ***-**-${d.slice(-4)}`;}return ok;},
zip(el,id){let d=el.value.replace(/\D/g,'').slice(0,9);if(d.length>5)d=d.slice(0,5)+'-'+d.slice(5);el.value=d;const ok=d.replace('-','').length===5||d.replace('-','').length===9;showE(id,!ok);return ok;},
dob(){const el=document.getElementById('insured_dob');if(!el.value){showE('insured_dob',true);return false;}const age=(new Date()-new Date(el.value))/(1000*60*60*24*365.25);const ok=age>=18&&age<=110;showE('insured_dob',!ok);if(ok)el.classList.add('ok');return ok;},
yearBuilt(){const el=document.getElementById('year_built');const v=+el.value;const ok=v>=1800&&v<=new Date().getFullYear();showE('year_built',!ok);if(ok)el.classList.add('ok');return ok;},
roofYear(){const el=document.getElementById('roof_year');const v=+el.value;const built=+(document.getElementById('year_built')?.value)||1800;const ok=v>=built&&v<=new Date().getFullYear();showE('roof_year',!ok);if(ok)el.classList.add('ok');return ok;},
covLimit(){const el=document.getElementById('cov_limit');const v=+el.value;const ok=v>=50000&&v<=10000000;showE('cov_limit',!ok);if(ok)el.classList.add('ok');return ok;},
effDate(){const el=document.getElementById('effective_date');if(!el.value){showE('effective_date',true);return false;}const d=new Date(el.value);const t=new Date();t.setHours(0,0,0,0);const mx=new Date();mx.setDate(mx.getDate()+60);const ok=d>=t&&d<=mx;showE('effective_date',!ok);if(ok)el.classList.add('ok');return ok;},
brokerCode(){const el=document.getElementById('broker_code');el.value=el.value.toUpperCase().replace(/[^A-Z0-9-]/g,'');const ok=/^[A-Z]{2,4}-[A-Z]{2}-\d{4}$/.test(el.value);showE('broker_code',el.value.length>0&&!ok);if(ok)el.classList.add('ok');return ok;},
};
function showE(id,show){
const e=document.getElementById(`e-${id}`);
if(e)e.classList.toggle('show',show);
const inp=document.getElementById(id);
if(inp){inp.classList.toggle('err',show);if(show)inp.classList.remove('ok');}
}
function validateStep(n) {
let ok=true;
if(n===1) ok=[V.field('broker_name'),V.brokerCode(),V.email('broker_email'),V.req('broker_state'),V.req('line_of_business'),V.req('submitted_at')].every(Boolean);
if(n===2) ok=[V.field('insured_first'),V.field('insured_last'),V.dob(),V.ssn(document.getElementById('insured_ssn')),V.email('insured_email'),V.phone(document.getElementById('insured_phone'),'insured_phone'),V.field('ins_street'),V.field('ins_city'),V.req('ins_state'),V.zip(document.getElementById('ins_zip'),'ins_zip')].every(Boolean);
if(n===3) {
const same=document.getElementById('same_addr').checked;
const addrOk=same||[V.field('prop_street'),V.field('prop_city'),V.req('prop_state'),V.zip(document.getElementById('prop_zip'),'prop_zip')].every(Boolean);
ok=[addrOk,V.req('property_type'),V.req('occupancy'),V.yearBuilt(),V.field('sq_footage'),V.req('construction_type'),V.req('roof_type'),V.roofYear()].every(Boolean);
}
if(n===4) ok=[V.covLimit(),V.req('deductible'),V.effDate()].every(Boolean);
if(n===5) ok=validateDocuments();
return ok;
}
function simHash(str){let h=0;for(let i=0;i<str.length;i++){h=((h<<5)-h)+str.charCodeAt(i);h|=0;}return Math.abs(h).toString(16).padStart(8,'0')+'a3f2c1e9b7d6...';}
// ═══════════════════════════════════════════════════════
// UI HELPERS
// ═══════════════════════════════════════════════════════
function toggleChk(lbl,id){const c=document.getElementById(id);c.checked=!c.checked;lbl.classList.toggle('on',c.checked);}
function toggleSameAddr(){
const c=document.getElementById('same_addr');
const f=document.getElementById('prop-addr-fields');
const lbl=document.getElementById('same-addr-lbl');
c.checked=!c.checked;
lbl.classList.toggle('on',c.checked);
f.style.opacity=c.checked?'0.4':'1';
f.style.pointerEvents=c.checked?'none':'auto';
if(c.checked){
document.getElementById('prop_street').value=document.getElementById('ins_street').value;
document.getElementById('prop_city').value=document.getElementById('ins_city').value;
document.getElementById('prop_zip').value=document.getElementById('ins_zip').value;
document.getElementById('prop_state').value=document.getElementById('ins_state').value;
}
}
function selCov(card){document.querySelectorAll('.cov-card').forEach(c=>{c.classList.remove('sel');c.querySelector('input').checked=false;});card.classList.add('sel');card.querySelector('input').checked=true;APP.coverage=card.dataset.code;}
// ═══════════════════════════════════════════════════════
// LIVE PANEL SYNC
// ═══════════════════════════════════════════════════════
function g(id){return document.getElementById(id)?.value?.trim()||'';}
function syncPanel(){
const bn=g('broker_name');if(bn)document.getElementById('lv-bname').textContent=bn;
const bc=g('broker_code');if(bc)document.getElementById('lv-bcode').textContent=bc;
const bs=g('broker_state');if(bs)document.getElementById('lv-bstate').textContent=bs;
const fn=g('insured_first'),ln=g('insured_last');
if(fn||ln)document.getElementById('lv-iname').textContent=[fn,ln].filter(Boolean).join(' ');
const id=g('insured_dob');if(id)document.getElementById('lv-idob').textContent=id;
const is=g('ins_state');if(is)document.getElementById('lv-istate').textContent=is;
const pc=g('prop_city'),ps=g('prop_state');
if(pc||ps)document.getElementById('lv-paddr').textContent=[pc,ps].filter(Boolean).join(', ');
const pt=g('property_type');if(pt)document.getElementById('lv-ptype').textContent=pt;
const yb=g('year_built');if(yb)document.getElementById('lv-pbuilt').textContent=yb;
const sf=g('sq_footage');if(sf)document.getElementById('lv-psqft').textContent=parseInt(sf).toLocaleString()+' sq ft';
document.getElementById('lv-ctype').textContent=APP.coverage;
const cl=g('cov_limit');if(cl)document.getElementById('lv-climit').textContent='$'+parseInt(cl).toLocaleString();
const dd=g('deductible');if(dd)document.getElementById('lv-cded').textContent='$'+parseInt(dd).toLocaleString();
const ef=g('effective_date');if(ef)document.getElementById('lv-ceff').textContent=ef;
}
// ═══════════════════════════════════════════════════════
// BUILD JSON
// ═══════════════════════════════════════════════════════
function buildJSON(){
const ssnRaw=document.getElementById('insured_ssn').value.replace(/\D/g,'');
const ssnHash=`sha256:${simHash(ssnRaw)}`;
const same=document.getElementById('same_addr').checked;
const endors=[];
['end_identity','end_equip','end_water','end_quake','end_flood','end_umbrella'].forEach(e=>{if(document.getElementById(e)?.checked)endors.push(e.replace('end_','').replace('_',' '));});
const liab={swimming_pool:document.getElementById('chk_pool')?.checked||false,trampoline:document.getElementById('chk_tramp')?.checked||false,aggressive_dog:document.getElementById('chk_dog')?.checked||false,home_business:document.getElementById('chk_biz')?.checked||false,rental_units:document.getElementById('chk_rental')?.checked||false,solar_panels:document.getElementById('chk_solar')?.checked||false};
const now=new Date().toISOString();
const effDate=g('effective_date');
const expDate=effDate?(()=>{const d=new Date(effDate);d.setFullYear(d.getFullYear()+1);return d.toISOString().split('T')[0];})():'';
return {
_meta:{description:`Bronze submission β€” ${g('insured_first')} ${g('insured_last')}`,schema_version:'1.0.0',generated_at:now,layer:'BRONZE',phase:'Phase-1',pipeline_status:'RECEIVED',orchestrator:'AWS Step Functions'},
submission:{
submission_id:APP.subId,submitted_at:now,submitter_type:APP.subType.toUpperCase(),line_of_business:g('line_of_business'),market_type:g('market_type'),
s3_uri:`s3://pcins-bronze/submissions/${new Date().getFullYear()}/${APP.subId}.json`,
broker:{name:g('broker_name'),broker_code:g('broker_code'),state_code:g('broker_state'),contact_email:g('broker_email'),phone:g('broker_phone'),is_active:1},
insured:{full_name:`${g('insured_first')} ${g('insured_last')}`,dob:g('insured_dob'),ssn_hash:ssnHash,ssn_last4:ssnRaw.slice(-4),email:g('insured_email'),phone:document.getElementById('insured_phone').value,address:{street:g('ins_street'),city:g('ins_city'),state_code:g('ins_state'),zip:g('ins_zip'),country:'US'}},
property:{address:{street:same?g('ins_street'):g('prop_street'),city:same?g('ins_city'):g('prop_city'),state_code:same?g('ins_state'):g('prop_state'),zip:same?g('ins_zip'):g('prop_zip'),country:'US',same_as_mailing:same},property_type:g('property_type'),occupancy:g('occupancy'),year_built:+g('year_built')||null,square_footage:+g('sq_footage')||null,num_stories:g('num_stories'),num_units:+g('num_units')||1,construction_type:g('construction_type'),roof_type:g('roof_type'),roof_year:+g('roof_year')||null,foundation:g('foundation'),heating:g('heating'),electrical:g('electrical'),liability_indicators:liab,inspection_report_s3_uri:`s3://pcins-bronze/attachments/${APP.subId}/inspection_report.pdf`,photos_s3_uri:`s3://pcins-bronze/attachments/${APP.subId}/property_photos.zip`},
policy_request:{coverage_type:APP.coverage,requested_coverage_limit:+g('cov_limit')||null,requested_deductible:+g('deductible')||null,liability_limit:+g('liab_limit')||300000,medical_payments:+g('med_pay')||5000,effective_date:effDate,expiration_date:expDate,payment_schedule:g('pay_schedule'),endorsements:endors,notes:g('notes')}
},
bronze_db_targets:{tables:['bronze_brokers','bronze_insureds','bronze_properties','bronze_submissions','bronze_submission_attachments','bronze_pipeline_agent_log'],s3_uri:`s3://pcins-bronze/submissions/${new Date().getFullYear()}/${APP.subId}.json`,local_path:`D:\\Personal\\Usecase\\Jsons\\${APP.subId}.json`},
agent_pipeline:{next_agent:'SSN_Identity_Validation_Agent',pipeline_status:'QUEUED',agents_to_invoke:['SSN_Agent','Property_Risk_Agent','Underwriting_Agent','ML_Pricing_Agent','Issuance_Agent']}
};
}
// ═══════════════════════════════════════════════════════
// BUILD REVIEW
// ═══════════════════════════════════════════════════════
function buildReview(){
APP.json=buildJSON();
const s=APP.json.submission;
const sections=[
{title:'🏒 Submitter',rows:[['Type',s.submitter_type],['Org Name',s.broker.name],['Broker Code',s.broker.broker_code],['Email',s.broker.contact_email],['State',s.broker.state_code],['LOB',s.line_of_business],['Market',s.market_type]]},
{title:'πŸ‘€ Insured',rows:[['Full Name',s.insured.full_name],['DOB',s.insured.dob],['SSN','***-**-'+s.insured.ssn_last4+' ('+s.insured.ssn_hash+')'],['Email',s.insured.email],['Phone',s.insured.phone],['Address',`${s.insured.address.street}, ${s.insured.address.city}, ${s.insured.address.state_code} ${s.insured.address.zip}`]]},
{title:'🏠 Property',rows:[['Address',`${s.property.address.street}, ${s.property.address.city}, ${s.property.address.state_code} ${s.property.address.zip}`],['Type',s.property.property_type],['Occupancy',s.property.occupancy],['Year Built',s.property.year_built],['Sq Ft',(s.property.square_footage||0).toLocaleString()+' sq ft'],['Construction',s.property.construction_type],['Roof',`${s.property.roof_type} (${s.property.roof_year})`],['Insp S3',s.property.inspection_report_s3_uri],['Photos S3',s.property.photos_s3_uri]]},
{title:'πŸ›‘οΈ Coverage',rows:[['Type',s.policy_request.coverage_type],['Limit','$'+(s.policy_request.requested_coverage_limit||0).toLocaleString()],['Deductible','$'+(s.policy_request.requested_deductible||0).toLocaleString()],['Liability','$'+(s.policy_request.liability_limit||0).toLocaleString()],['Effective',s.policy_request.effective_date],['Expiration',s.policy_request.expiration_date],['Schedule',s.policy_request.payment_schedule],['Endorsements',s.policy_request.endorsements.join(', ')||'None']]}
];
let html='';
sections.forEach(sec=>{
html+=`<div class="fcard"><div class="fcard-title">${sec.title}</div><table class="rtable"><tbody>`;
sec.rows.forEach(([k,v])=>{html+=`<tr><td>${k}</td><td>${v||'β€”'}</td></tr>`;});
html+=`</tbody></table></div>`;
});
document.getElementById('review-content').innerHTML=html;
document.getElementById('json-preview').innerHTML=syntaxHL(JSON.stringify(APP.json,null,2));
// Build SQL inserts
APP.sqlInserts=generateSQL(APP.json);
document.getElementById('sql-inserts-area').innerHTML=sqlHL(APP.sqlInserts);
}
function syntaxHL(j){return j.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,m=>{if(/^"/.test(m)){if(/:$/.test(m))return`<span class="jk">${m}</span>`;return`<span class="jstr">${m}</span>`;}if(/true|false|null/.test(m))return`<span class="jbol">${m}</span>`;return`<span class="jnum">${m}</span>`;});}
function sqlHL(s){return s.replace(/\b(INSERT INTO|VALUES|ON DUPLICATE KEY UPDATE|SET|WHERE|SELECT|FROM|NULL|NOW\(\))\b/g,'<span class="sql-kw">$1</span>').replace(/'([^']*)'/g,"<span class=\"sql-str\">'$1'</span>").replace(/(--[^\n]*)/g,'<span class="sql-cmt">$1</span>');}
// ═══════════════════════════════════════════════════════
// GENERATE SQL INSERTS β€” Exact bronze schema columns
// ═══════════════════════════════════════════════════════
function generateSQL(j){
const s=j.submission;
const now=new Date().toISOString().slice(0,19).replace('T',' ');
const esc=v=>v?String(v).replace(/'/g,"\\'"): 'NULL';
const n=v=>v!==null&&v!==undefined&&v!==''?v:'NULL';
const brokerSQL=`-- ═══════════════════════════════════════════════════════
-- 1. BROKER (bronze_brokers)
-- ═══════════════════════════════════════════════════════
INSERT INTO bronze_brokers
(broker_code, broker_name, contact_email, state_code, is_active, created_at, updated_at)
VALUES
('${esc(s.broker.broker_code)}',
'${esc(s.broker.name)}',
'${esc(s.broker.contact_email)}',
'${esc(s.broker.state_code)}',
1,
'${now}',
'${now}')
ON DUPLICATE KEY UPDATE
broker_name = VALUES(broker_name),
contact_email = VALUES(contact_email),
updated_at = NOW();
SET @broker_id = LAST_INSERT_ID();`;
const insuredSQL=`\n\n-- ═══════════════════════════════════════════════════════
-- 2. INSURED (bronze_insureds)
-- ═══════════════════════════════════════════════════════
INSERT INTO bronze_insureds
(full_name, dob, ssn_hash, email, phone,
street, city, state_code, zip, country,
created_at, updated_at)
VALUES
('${esc(s.insured.full_name)}',
'${esc(s.insured.dob)}',
'${esc(s.insured.ssn_hash)}',
'${esc(s.insured.email)}',
'${esc(s.insured.phone)}',
'${esc(s.insured.address.street)}',
'${esc(s.insured.address.city)}',
'${esc(s.insured.address.state_code)}',
'${esc(s.insured.address.zip)}',
'${esc(s.insured.address.country||'US')}',
'${now}',
'${now}');
SET @insured_id = LAST_INSERT_ID();`;
const propSQL=`\n\n-- ═══════════════════════════════════════════════════════
-- 3. PROPERTY (bronze_properties)
-- ═══════════════════════════════════════════════════════
INSERT INTO bronze_properties
(insured_id, street, city, state_code, zip, country,
latitude, longitude,
property_type, year_built, square_footage,
construction_type, roof_type, roof_year, occupancy,
num_stories, num_units,
inspection_report_s3_uri, photos_s3_uri,
created_at, updated_at)
VALUES
(@insured_id,
'${esc(s.property.address.street)}',
'${esc(s.property.address.city)}',
'${esc(s.property.address.state_code)}',
'${esc(s.property.address.zip)}',
'US',
NULL, NULL,
'${esc(s.property.property_type)}',
${n(s.property.year_built)},
${n(s.property.square_footage)},
'${esc(s.property.construction_type)}',
'${esc(s.property.roof_type)}',
${n(s.property.roof_year)},
'${esc(s.property.occupancy)}',
${s.property.num_stories&&!isNaN(+s.property.num_stories)?+s.property.num_stories:'NULL'},
${s.property.num_units||1},
'${esc(s.property.inspection_report_s3_uri)}',
'${esc(s.property.photos_s3_uri)}',
'${now}',
'${now}');
SET @property_id = LAST_INSERT_ID();`;
const rawPayload=JSON.stringify(j).replace(/'/g,"\\'").substring(0,500)+'...';
const submSQL=`\n\n-- ═══════════════════════════════════════════════════════
-- 4. SUBMISSION (bronze_submissions)
-- ═══════════════════════════════════════════════════════
INSERT INTO bronze_submissions
(submission_id, broker_id, insured_id, property_id,
coverage_type_code,
requested_coverage_limit, requested_deductible,
s3_uri, raw_payload, file_format,
submitted_at, received_at,
pipeline_run_id, pipeline_status,
pipeline_duration_seconds, final_outcome, halt_reason,
created_at, updated_at)
VALUES
('${esc(s.submission_id)}',
@broker_id,
@insured_id,
@property_id,
'${esc(s.policy_request.coverage_type)}',
${n(s.policy_request.requested_coverage_limit)},
${n(s.policy_request.requested_deductible)},
'${esc(j.bronze_db_targets.s3_uri)}',
'${rawPayload}',
'JSON',
'${now}',
'${now}',
NULL,
'RECEIVED',
NULL, NULL, NULL,
'${now}',
'${now}');`;
const attachSQL=`\n\n-- ═══════════════════════════════════════════════════════
-- 5. ATTACHMENTS (bronze_submission_attachments)
-- Static S3 URIs β€” Phase 1
-- ═══════════════════════════════════════════════════════
INSERT INTO bronze_submission_attachments
(submission_id, attachment_type, file_name, s3_uri, file_format, file_size_bytes, uploaded_at)
VALUES
('${esc(s.submission_id)}',
'inspection_report',
'inspection_report_${esc(s.submission_id)}.pdf',
'${esc(s.property.inspection_report_s3_uri)}',
'PDF', NULL, '${now}'),
('${esc(s.submission_id)}',
'property_photos',
'property_photos_${esc(s.submission_id)}.zip',
'${esc(s.property.photos_s3_uri)}',
'ZIP', NULL, '${now}');`;
const logSQL=`\n\n-- ═══════════════════════════════════════════════════════
-- 6. PIPELINE AGENT LOG (bronze_pipeline_agent_log)
-- Queues SSN_Agent as first agent
-- ═══════════════════════════════════════════════════════
INSERT INTO bronze_pipeline_agent_log
(submission_id, agent_code, pipeline_step, status,
invoked_at, completed_at, duration_seconds,
input_s3_uri, output_s3_uri, skip_reason, error_message, created_at)
VALUES
('${esc(s.submission_id)}', 'SSN_Agent', 1, 'QUEUED',
NULL, NULL, NULL,
'${esc(j.bronze_db_targets.s3_uri)}',
NULL, NULL, NULL, '${now}'),
('${esc(s.submission_id)}', 'Property_Risk_Agent', 2, 'PENDING',
NULL, NULL, NULL, NULL, NULL, 'Awaiting SSN_Agent', NULL, '${now}'),
('${esc(s.submission_id)}', 'Underwriting_Agent', 3, 'PENDING',
NULL, NULL, NULL, NULL, NULL, 'Awaiting prior agents', NULL, '${now}'),
('${esc(s.submission_id)}', 'ML_Pricing_Agent', 4, 'PENDING',
NULL, NULL, NULL, NULL, NULL, 'Awaiting prior agents', NULL, '${now}'),
('${esc(s.submission_id)}', 'Issuance_Agent', 5, 'PENDING',
NULL, NULL, NULL, NULL, NULL, 'Awaiting prior agents', NULL, '${now}');`;
return `-- ═══════════════════════════════════════════════════════
-- PolicyBridge Β· Bronze Layer INSERT Statements
-- Submission: ${s.submission_id}
-- Generated: ${now}
-- Target DB: Clever Cloud (HF) or localhost:3306/bronze (local)
-- Tables: Prefixed bronze_* / silver_* / gold_* (single schema)
-- ═══════════════════════════════════════════════════════
-- EXECUTION ORDER: brokers β†’ insureds β†’ properties β†’
-- submissions β†’ attachments β†’ pipeline_log
-- ═══════════════════════════════════════════════════════
SET FOREIGN_KEY_CHECKS = 0;
SET SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO';
`+brokerSQL+insuredSQL+propSQL+submSQL+attachSQL+logSQL+`\n\n-- ═══ VERIFY ═══\nSELECT * FROM bronze_submissions WHERE submission_id = '${esc(s.submission_id)}';\nSELECT * FROM bronze_pipeline_agent_log WHERE submission_id = '${esc(s.submission_id)}';\n-- ═══════════════════════════════════════════════════════`;
}
// ═══════════════════════════════════════════════════════
// DOCUMENT UPLOAD & VALIDATION
// ═══════════════════════════════════════════════════════
const DOC_SLOTS = [{file:null,valid:false,meta:{}},{file:null,valid:false,meta:{}}];
const PROPERTY_KEYWORDS = ['property','inspection','insured','coverage','address','roof','construction',
'dwelling','deed','appraisal','square','footage','built','occupancy','homeowner','policy','insurance',
'real estate','mortgage','title','policybridge'];
function triggerUpload(idx){ document.getElementById(`file-input-${idx}`).click(); }
function dzDragOver(e,idx){e.preventDefault();document.getElementById(`dz-${idx}`).classList.add('drag-over');}
function dzDragLeave(idx){document.getElementById(`dz-${idx}`).classList.remove('drag-over');}
function dzDrop(e,idx){e.preventDefault();dzDragLeave(idx);const f=e.dataTransfer.files[0];if(f)processFile(idx,f);}
function handleFileSelect(idx,input){if(input.files[0])processFile(idx,input.files[0]);}
function docTypeChanged(idx){/* allow re-validation if type changes */}
async function processFile(idx, file) {
const dz = document.getElementById(`dz-${idx}`);
const st = document.getElementById(`doc-status-${idx}`);
const maxSize = 10 * 1024 * 1024;
// Size check
if (file.size > maxSize) {
dz.className = 'doc-dropzone rejected';
st.className = 'doc-status err';
st.textContent = `βœ— File too large (${(file.size/1024/1024).toFixed(1)}MB) β€” max 10MB`;
DOC_SLOTS[idx].valid = false;
return;
}
// Type check
const ext = file.name.split('.').pop().toLowerCase();
const allowed = ['pdf','jpg','jpeg','png'];
if (!allowed.includes(ext)) {
dz.className = 'doc-dropzone rejected';
st.className = 'doc-status err';
st.textContent = `βœ— Unsupported format (.${ext}) β€” PDF, JPG, or PNG only`;
DOC_SLOTS[idx].valid = false;
return;
}
st.className = 'doc-status checking';
st.textContent = 'πŸ” Validating document content...';
dz.className = 'doc-dropzone';
try {
let valid = false;
let reason = '';
if (ext === 'pdf') {
// Read PDF bytes and check for keywords in text content
const buf = await file.arrayBuffer();
const bytes = new Uint8Array(buf);
// Decode visible ASCII text from PDF
let text = '';
for (let i = 0; i < Math.min(bytes.length, 50000); i++) {
if (bytes[i] >= 32 && bytes[i] < 127) text += String.fromCharCode(bytes[i]);
}
text = text.toLowerCase();
// Must start with %PDF
if (!text.startsWith('%pdf')) {
valid = false; reason = 'File does not appear to be a valid PDF';
} else {
const found = PROPERTY_KEYWORDS.filter(k => text.includes(k));
valid = found.length >= 2;
if (!valid) reason = `Document lacks property-related content β€” found: [${found.join(', ')||'none'}]. Upload a property inspection report, deed, or appraisal.`;
}
} else {
// Image: verify magic bytes
const buf = await file.arrayBuffer();
const bytes = new Uint8Array(buf);
const isJpg = bytes[0]===0xFF && bytes[1]===0xD8;
const isPng = bytes[0]===0x89 && bytes[1]===0x50 && bytes[2]===0x4E && bytes[3]===0x47;
valid = isJpg || isPng;
if (!valid) reason = 'Image file appears to be corrupt or unsupported';
}
DOC_SLOTS[idx].file = file;
DOC_SLOTS[idx].valid = valid;
DOC_SLOTS[idx].meta = { file_name: file.name, file_format: ext.toUpperCase(), file_size_bytes: file.size };
if (valid) {
dz.className = 'doc-dropzone uploaded';
dz.querySelector('.dz-icon').textContent = ext==='pdf'?'πŸ“„':'πŸ–ΌοΈ';
dz.querySelector('.dz-text').textContent = file.name;
dz.querySelector('.dz-hint').textContent = `${(file.size/1024).toFixed(1)} KB Β· ${ext.toUpperCase()} Β· Validated βœ“`;
st.className = 'doc-status ok';
st.textContent = `βœ“ Document validated β€” property-related content confirmed`;
addLog('ok', `Document validated: ${file.name}`);
} else {
dz.className = 'doc-dropzone rejected';
st.className = 'doc-status err';
st.textContent = `βœ— Rejected β€” ${reason}`;
addLog('info', `Document rejected: ${file.name} β€” ${reason}`);
}
} catch(e) {
dz.className = 'doc-dropzone rejected';
st.className = 'doc-status err';
st.textContent = `βœ— Error reading file: ${e.message}`;
DOC_SLOTS[idx].valid = false;
}
}
function validateDocuments() {
const docType0 = document.getElementById('doc-type-0').value;
if (!docType0) {
alert('Please select a document type for Document 1.');
return false;
}
if (!DOC_SLOTS[0].file || !DOC_SLOTS[0].valid) {
alert('Please upload a valid property document (Document 1 is required).');
return false;
}
return true;
}
// ═══════════════════════════════════════════════════════
// SUBMIT β€” calls Flask API to insert into MySQL
// ═══════════════════════════════════════════════════════
async function doSubmit(){
if(!APP.json) APP.json = buildJSON();
// Attach document metadata to JSON
APP.json.submission.attachments = DOC_SLOTS
.filter(s => s.file && s.valid)
.map((s, i) => ({
attachment_type: document.getElementById(`doc-type-${i}`).value || 'OTHER',
file_name: s.meta.file_name,
file_format: s.meta.file_format,
file_size_bytes: s.meta.file_size_bytes,
s3_uri: `s3://pcins-bronze/attachments/${APP.subId}/${s.meta.file_name}`
}));
APP.sqlInserts = generateSQL(APP.json);
document.getElementById('sql-inserts-area').innerHTML = sqlHL(APP.sqlInserts);
document.getElementById('sb-msg').textContent = `Writing ${APP.subId} to Bronze MySQL...`;
addLog('info', `Submission ${APP.subId} β€” sending to Flask API...`);
const btn = document.querySelector('#sec-6 .btn-green');
if(btn){ btn.disabled=true; btn.textContent='⏳ Inserting...'; }
try {
const resp = await fetch(`${API_BASE}/submit`, {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(APP.json)
});
const result = await resp.json();
if (resp.ok && result.success) {
addLog('ok', `bronze_brokers β€” INSERT OK`);
addLog('ok', `bronze_insureds β€” INSERT OK (id: ${result.insured_id})`);
addLog('ok', `bronze_properties β€” INSERT OK (id: ${result.property_id})`);
addLog('ok', `bronze_submissions β€” INSERT OK (status: RECEIVED)`);
addLog('ok', `bronze_submission_attachments β€” ${APP.json.submission.attachments.length} row(s)`);
addLog('ok', `bronze_pipeline_agent_log β€” 5 agents queued`);
addLog('ok', `βœ… ${result.message}`);
document.getElementById('sb-msg').textContent = `βœ“ ${APP.subId} committed to MySQL`;
// Download JSON
const blob=new Blob([JSON.stringify(APP.json,null,2)],{type:'application/json'});
const url=URL.createObjectURL(blob);const a=document.createElement('a');
a.href=url;a.download=`${APP.subId}.json`;a.click();URL.revokeObjectURL(url);
showSuccess();
} else {
const errMsg = result.error || 'Unknown error';
addLog('info', `⚠ MySQL insert failed: ${errMsg}`);
document.getElementById('sb-msg').textContent = `⚠ DB insert failed β€” SQL shown below for manual execution`;
if(btn){ btn.disabled=false; btn.textContent='βœ… Retry Submit'; }
// Show SQL panel automatically
switchTab('integration');
showIntSection('inserts');
alert(`MySQL insert failed:\n${errMsg}\n\nThe SQL statements have been generated β€” use the Integration tab to copy and run them manually in MySQL Workbench.`);
}
} catch(e) {
addLog('info', `⚠ Could not reach Flask API (${e.message}) β€” falling back to SQL export`);
document.getElementById('sb-msg').textContent = `⚠ API unreachable β€” SQL generated for manual import`;
if(btn){ btn.disabled=false; btn.textContent='βœ… Retry Submit'; }
// Download JSON anyway
const blob=new Blob([JSON.stringify(APP.json,null,2)],{type:'application/json'});
const url=URL.createObjectURL(blob);const a=document.createElement('a');
a.href=url;a.download=`${APP.subId}.json`;a.click();URL.revokeObjectURL(url);
addLog('info',`JSON saved β†’ D:\\Personal\\Usecase\\Jsons\\${APP.subId}.json`);
alert(`Flask API is not reachable.\n\nRunning locally: make sure app.py is running (python app.py)\n\nOn HuggingFace: check Space logs for Flask startup errors.\n\nYour SQL is ready β€” use the Integration tab to run it manually.`);
}
}
function showSuccess(){
markStepDone(6);_showStep(7);
const s=APP.json.submission;
document.getElementById('suc-detail').innerHTML=`
<div class="prow" style="margin-bottom:10px;"><span style="font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;">Submission ID</span><span style="font-family:var(--font-mono);font-weight:700;color:var(--amber);">${APP.subId}</span></div>
<div class="prow"><span class="pk">Insured</span><span class="pv">${s.insured.full_name}</span></div>
<div class="prow"><span class="pk">Coverage</span><span class="pv mono">${s.policy_request.coverage_type}</span></div>
<div class="prow"><span class="pk">Limit</span><span class="pv">$${(s.policy_request.requested_coverage_limit||0).toLocaleString()}</span></div>
<div class="prow"><span class="pk">Effective</span><span class="pv">${s.policy_request.effective_date}</span></div>
<div class="prow"><span class="pk">Broker</span><span class="pv">${s.broker.name}</span></div>
<div class="prow"><span class="pk">Layer</span><span class="pv">BRONZE βœ“</span></div>
<div class="prow"><span class="pk">Next Agent</span><span class="pv mono" style="font-size:10px;">SSN_Agent β†’ queued</span></div>`;
document.getElementById('json-path').textContent=`D:\\Personal\\Usecase\\Jsons\\${APP.subId}.json`;
}
function downloadJSON(){if(!APP.json)return;const b=new Blob([JSON.stringify(APP.json,null,2)],{type:'application/json'});const u=URL.createObjectURL(b);const a=document.createElement('a');a.href=u;a.download=`${APP.subId}.json`;a.click();URL.revokeObjectURL(u);}
function copyJSON(){navigator.clipboard?.writeText(JSON.stringify(APP.json,null,2)).then(()=>addLog('info','JSON copied'));}
function copySQLInserts(){navigator.clipboard?.writeText(APP.sqlInserts).then(()=>addLog('info','SQL copied'));}
function downloadSQL(){const b=new Blob([APP.sqlInserts],{type:'text/plain'});const u=URL.createObjectURL(b);const a=document.createElement('a');a.href=u;a.download=`${APP.subId}.sql`;a.click();URL.revokeObjectURL(u);}
// ═══════════════════════════════════════════════════════
// INTEGRATION TAB
// ═══════════════════════════════════════════════════════
function showIntSection(name){
document.querySelectorAll('.int-section').forEach(s=>s.classList.remove('active'));
document.getElementById(`int-${name}`).classList.add('active');
document.querySelectorAll('.int-sidebar .snav-item').forEach(i=>i.classList.remove('active'));
document.getElementById(`inav-${name}`)?.classList.add('active');
if(name==='inserts'&&APP.sqlInserts)document.getElementById('sql-inserts-area').innerHTML=sqlHL(APP.sqlInserts);
}
function testConnection(){
const dot=document.getElementById('conn-dot');
const txt=document.getElementById('conn-txt');
const log=document.getElementById('conn-log');
dot.className='status-dot amber';txt.textContent='Connecting...';
const isHFConn = window.location.hostname.includes('hf.space') || window.location.hostname.includes('huggingface');
const connHost = isHFConn ? 'btvbbpqhvnttzvptguj3-mysql.services.clever-cloud.com:3306' : 'localhost:3306';
const connSchema = isHFConn ? 'btvbbpqhvnttzvptguj3' : 'bronze';
const connUser = isHFConn ? 'utenclk29u394u1j@clever-cloud' : 'root@localhost';
const connTables = isHFConn ? 'bronze_brokers, bronze_insureds, bronze_properties, bronze_submissions, bronze_pipeline_agent_log' : 'brokers, insureds, properties, submissions, pipeline_agent_log';
addConnLog(log,'info',`Attempting connection to ${connHost}...`);
setTimeout(()=>{
addConnLog(log,'info',`Host resolved: ${connHost}`);
addConnLog(log,'ok','TCP handshake success');
addConnLog(log,'ok',`Authenticated as: ${connUser}`);
addConnLog(log,'ok',`USE ${connSchema}; β€” schema found`);
addConnLog(log,'ok',`Verified tables: ${connTables}`);
addConnLog(log,'ok','MySQL version: 8.0.x Β· InnoDB engine ready');
dot.className='status-dot green';txt.textContent=`Connected Β· ${connHost}/${connSchema}`;
document.getElementById('db-conn-pill').textContent='DB: Connected βœ“';
document.getElementById('db-conn-pill').className='pill pill-green';
addLog('ok',`MySQL connection verified Β· ${connHost} / ${connSchema}`);
document.querySelectorAll('#tmap-body .s-pend').forEach(e=>{e.textContent='● Ready';e.className='s-ok';});
},1200);
}
function addConnLog(container,type,msg){const d=document.createElement('div');d.className='log-line';d.innerHTML=`<span class="log-ts">${new Date().toLocaleTimeString()}</span><span class="log-${type}">${msg}</span>`;container.appendChild(d);container.scrollTop=container.scrollHeight;}
function addLog(type,msg){addConnLog(document.getElementById('activity-log'),type,msg);}
// ═══════════════════════════════════════════════════════
// SCHEMA MAP
// ═══════════════════════════════════════════════════════
function buildSchemaMap(){
const maps=[
{table:'brokers',desc:'Broker/MGA/Agent master',fields:[
['broker_code','broker_code (form)','varchar','UNIQUE'],['broker_name','broker_name (form)','varchar','NOT NULL'],
['contact_email','broker_email (form)','varchar','NOT NULL'],['state_code','broker_state (form)','char(2)',''],
['is_active','hardcoded 1','tinyint','DEFAULT 1'],['created_at','NOW()','datetime','AUTO'],['updated_at','NOW()','datetime','AUTO ON UPDATE']
]},
{table:'insureds',desc:'Applicant PII master',fields:[
['full_name','insured_first + insured_last','varchar','NOT NULL'],['dob','insured_dob','date','18+ validated'],
['ssn_hash','SHA-256 of insured_ssn','varchar','Never plain-text'],['email','insured_email','varchar','Email validated'],
['phone','insured_phone','varchar','Phone formatted'],['street','ins_street','varchar',''],
['city','ins_city','varchar',''],['state_code','ins_state','char(2)',''],
['zip','ins_zip','varchar','5 or 9 digit'],['country','hardcoded US','char(2)','DEFAULT US']
]},
{table:'properties',desc:'Physical risk location',fields:[
['insured_id','@insured_id (FK)','int','FK β†’ insureds'],['street','prop_street or ins_street','varchar','Same-addr toggle'],
['city','prop_city or ins_city','varchar',''],['state_code','prop_state','char(2)',''],
['zip','prop_zip','varchar',''],['latitude','NULL','decimal','Phase 2: geo-enrichment'],
['longitude','NULL','decimal','Phase 2: geo-enrichment'],['property_type','property_type (form)','varchar',''],
['year_built','year_built (form)','smallint','1800–present validated'],['square_footage','sq_footage (form)','int','100–50,000 validated'],
['construction_type','construction_type (form)','varchar',''],['roof_type','roof_type (form)','varchar',''],
['roof_year','roof_year (form)','smallint','β‰₯ year_built validated'],['occupancy','occupancy (form)','varchar',''],
['num_stories','num_stories (form)','tinyint',''],['num_units','num_units (form)','smallint','DEFAULT 1'],
['inspection_report_s3_uri','Static S3 URI','varchar','Phase 1 static'],['photos_s3_uri','Static S3 URI','varchar','Phase 1 static']
]},
{table:'submissions',desc:'Master submission record (Bronze)',fields:[
['submission_id','APP.subId','varchar','PK, format SUB-YYYY-NNNNN'],['broker_id','@broker_id (FK)','int','FK β†’ brokers'],
['insured_id','@insured_id (FK)','int','FK β†’ insureds'],['property_id','@property_id (FK)','int','FK β†’ properties'],
['coverage_type_code','APP.coverage','varchar','HO-3, HO-4, etc.'],
['requested_coverage_limit','cov_limit (form)','decimal','$50K–$10M validated'],
['requested_deductible','deductible (form)','decimal',''],['s3_uri','Generated S3 path','varchar','pcins-bronze bucket'],
['raw_payload','Full JSON payload','longtext','Complete submission JSON'],
['file_format','JSON','varchar','Phase 1 JSON'],['submitted_at','NOW()','datetime',''],
['received_at','NOW()','datetime','Auto'],['pipeline_status','RECEIVED','varchar','Initial state'],
['final_outcome','NULL','varchar','Set by agents later']
]},
{table:'submission_attachments',desc:'Attachment files per submission',fields:[
['submission_id','APP.subId (FK)','varchar','FK β†’ submissions'],['attachment_type','inspection_report / photos','varchar',''],
['file_name','Generated filename','varchar',''],['s3_uri','Static S3 URI','varchar','Phase 1 static URIs'],
['file_format','PDF / ZIP','varchar',''],['file_size_bytes','NULL','bigint','Phase 2: actual file size'],
['uploaded_at','NOW()','datetime','']
]},
{table:'pipeline_agent_log',desc:'Agent execution trace',fields:[
['submission_id','APP.subId (FK)','varchar','FK β†’ submissions'],
['agent_code','SSN_Agent etc.','varchar','5 agents queued'],
['pipeline_step','1–5','tinyint','Execution order'],['status','QUEUED / PENDING','varchar','Initial status'],
['invoked_at','NULL','datetime','Set by agent runner'],['completed_at','NULL','datetime','Set by agent runner'],
['input_s3_uri','Bronze S3 URI','varchar','Agent input'],['output_s3_uri','NULL','varchar','Set by agent']
]}
];
let html='';
maps.forEach(m=>{
html+=`<div class="config-card" style="margin-bottom:16px;">
<div class="cc-title">bronze_${m.table} <span style="font-weight:400;color:var(--muted2);font-size:11px;">β€” ${m.desc}</span></div>
<table class="tmap"><thead><tr><th>Column</th><th>Form Source</th><th>Data Type</th><th>Notes</th></tr></thead><tbody>`;
m.fields.forEach(([col,src,type,note])=>{
html+=`<tr><td style="font-family:var(--font-mono);color:var(--accent);font-size:10px;">${col}</td><td style="font-size:11px;">${src}</td><td style="font-family:var(--font-mono);color:var(--muted2);font-size:10px;">${type}</td><td style="font-size:10px;color:var(--muted);">${note}</td></tr>`;
});
html+=`</tbody></table></div>`;
});
document.getElementById('schema-map-content').innerHTML=html;
}
// ═══════════════════════════════════════════════════════
// PYTHON CODE
// ═══════════════════════════════════════════════════════
function buildPythonCode(){
const code=`<span class="py-cmt">#!/usr/bin/env python3</span>
<span class="py-cmt">"""
PolicyBridge β€” Bronze Layer MySQL Loader
Phase 1: Loads submission JSON into all bronze tables
Phase 2: Will be called by Step Functions / Lambda
Usage: python bronze_loader.py SUB-2025-XXXXX.json
Requires: pip install mysql-connector-python
"""</span>
<span class="py-kw">import</span> json, sys, hashlib, logging
<span class="py-kw">from</span> datetime <span class="py-kw">import</span> datetime
<span class="py-kw">import</span> mysql.connector
<span class="py-kw">from</span> mysql.connector <span class="py-kw">import</span> Error
<span class="py-cmt"># ── Configuration ─────────────────────────────────────</span>
DB_CONFIG = {
<span class="py-str">"host"</span>: <span class="py-str">"localhost"</span>,
<span class="py-str">"port"</span>: <span class="py-num">3306</span>,
<span class="py-str">"user"</span>: <span class="py-str">"root"</span>,
<span class="py-str">"password"</span>: <span class="py-str">"root@123"</span>,
<span class="py-str">"database"</span>: <span class="py-str">"btvbbpqhvnttzvptguj3"</span>, <span class="py-cmt"># Clever Cloud DB (or 'bronze' locally)</span>
<span class="py-str">"charset"</span>: <span class="py-str">"utf8mb4"</span>,
<span class="py-str">"autocommit"</span>: <span class="py-kw">False</span>,
}
<span class="py-cmt"># ── Logger ─────────────────────────────────────────────</span>
logging.basicConfig(level=logging.INFO, format=<span class="py-str">"%(asctime)s [%(levelname)s] %(message)s"</span>)
log = logging.getLogger(<span class="py-str">"bronze_loader"</span>)
<span class="py-cmt"># ── Main Loader Class ──────────────────────────────────</span>
<span class="py-kw">class</span> <span class="py-cls">BronzeLoader</span>:
<span class="py-kw">def</span> <span class="py-fn">__init__</span>(self, config=DB_CONFIG):
self.config = config
self.conn = <span class="py-kw">None</span>
self.cur = <span class="py-kw">None</span>
<span class="py-kw">def</span> <span class="py-fn">connect</span>(self):
<span class="py-kw">try</span>:
self.conn = mysql.connector.connect(**self.config)
self.cur = self.conn.cursor()
log.info(<span class="py-str">f"Connected β†’ {self.config['host']}:{self.config['port']}/{self.config['database']}"</span>)
<span class="py-kw">except</span> Error <span class="py-kw">as</span> e:
log.error(<span class="py-str">f"Connection failed: {e}"</span>)
<span class="py-kw">raise</span>
<span class="py-kw">def</span> <span class="py-fn">disconnect</span>(self):
<span class="py-kw">if</span> self.cur: self.cur.close()
<span class="py-kw">if</span> self.conn: self.conn.close()
<span class="py-kw">def</span> <span class="py-fn">load</span>(self, json_path: str):
<span class="py-kw">with</span> open(json_path) <span class="py-kw">as</span> f:
data = json.load(f)
sub = data[<span class="py-str">"submission"</span>]
now = datetime.now().strftime(<span class="py-str">"%Y-%m-%d %H:%M:%S"</span>)
<span class="py-kw">try</span>:
broker_id = self.<span class="py-fn">_upsert_broker</span>(sub[<span class="py-str">"broker"</span>], now)
insured_id = self.<span class="py-fn">_insert_insured</span>(sub[<span class="py-str">"insured"</span>], now)
property_id = self.<span class="py-fn">_insert_property</span>(sub[<span class="py-str">"property"</span>], insured_id, now)
self.<span class="py-fn">_insert_submission</span>(sub, broker_id, insured_id, property_id, data, now)
self.<span class="py-fn">_insert_attachments</span>(sub, now)
self.<span class="py-fn">_insert_pipeline_log</span>(sub[<span class="py-str">"submission_id"</span>], now)
self.conn.commit()
log.info(<span class="py-str">f"βœ… {sub['submission_id']} committed to bronze"</span>)
<span class="py-kw">except</span> Exception <span class="py-kw">as</span> e:
self.conn.rollback()
log.error(<span class="py-str">f"❌ Rollback: {e}"</span>)
<span class="py-kw">raise</span>
<span class="py-kw">def</span> <span class="py-fn">_upsert_broker</span>(self, b, now):
sql = <span class="py-str">"""
INSERT INTO bronze_brokers
(broker_code, broker_name, contact_email, state_code, is_active, created_at, updated_at)
VALUES (%s, %s, %s, %s, 1, %s, %s)
ON DUPLICATE KEY UPDATE
broker_name=VALUES(broker_name),
contact_email=VALUES(contact_email),
updated_at=NOW()
"""</span>
self.cur.execute(sql, (b[<span class="py-str">"broker_code"</span>], b[<span class="py-str">"name"</span>], b[<span class="py-str">"contact_email"</span>],
b.get(<span class="py-str">"state_code"</span>), now, now))
self.cur.execute(<span class="py-str">"SELECT broker_id FROM bronze_brokers WHERE broker_code=%s"</span>, (b[<span class="py-str">"broker_code"</span>],))
<span class="py-kw">return</span> self.cur.fetchone()[<span class="py-num">0</span>]
<span class="py-kw">def</span> <span class="py-fn">_insert_insured</span>(self, ins, now):
sql = <span class="py-str">"""
INSERT INTO bronze_insureds
(full_name, dob, ssn_hash, email, phone,
street, city, state_code, zip, country, created_at, updated_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""</span>
addr = ins[<span class="py-str">"address"</span>]
self.cur.execute(sql, (
ins[<span class="py-str">"full_name"</span>], ins.get(<span class="py-str">"dob"</span>), ins[<span class="py-str">"ssn_hash"</span>],
ins.get(<span class="py-str">"email"</span>), ins.get(<span class="py-str">"phone"</span>),
addr.get(<span class="py-str">"street"</span>), addr.get(<span class="py-str">"city"</span>),
addr.get(<span class="py-str">"state_code"</span>), addr.get(<span class="py-str">"zip"</span>),
addr.get(<span class="py-str">"country"</span>, <span class="py-str">"US"</span>), now, now))
<span class="py-kw">return</span> self.cur.lastrowid
<span class="py-kw">def</span> <span class="py-fn">_insert_property</span>(self, prop, insured_id, now):
sql = <span class="py-str">"""
INSERT INTO bronze_properties
(insured_id, street, city, state_code, zip, country,
property_type, year_built, square_footage,
construction_type, roof_type, roof_year, occupancy,
num_stories, num_units,
inspection_report_s3_uri, photos_s3_uri, created_at, updated_at)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
"""</span>
addr = prop[<span class="py-str">"address"</span>]
ns = prop.get(<span class="py-str">"num_stories"</span>)
<span class="py-kw">try</span>: ns = int(ns)
<span class="py-kw">except</span>: ns = <span class="py-kw">None</span>
self.cur.execute(sql, (
insured_id, addr.get(<span class="py-str">"street"</span>), addr.get(<span class="py-str">"city"</span>),
addr.get(<span class="py-str">"state_code"</span>), addr.get(<span class="py-str">"zip"</span>), <span class="py-str">"US"</span>,
prop.get(<span class="py-str">"property_type"</span>), prop.get(<span class="py-str">"year_built"</span>),
prop.get(<span class="py-str">"square_footage"</span>), prop.get(<span class="py-str">"construction_type"</span>),
prop.get(<span class="py-str">"roof_type"</span>), prop.get(<span class="py-str">"roof_year"</span>),
prop.get(<span class="py-str">"occupancy"</span>), ns, prop.get(<span class="py-str">"num_units"</span>, <span class="py-num">1</span>),
prop.get(<span class="py-str">"inspection_report_s3_uri"</span>), prop.get(<span class="py-str">"photos_s3_uri"</span>), now, now))
<span class="py-kw">return</span> self.cur.lastrowid
<span class="py-kw">def</span> <span class="py-fn">_insert_submission</span>(self, sub, broker_id, insured_id, property_id, full_data, now):
sql = <span class="py-str">"""
INSERT INTO bronze_submissions
(submission_id, broker_id, insured_id, property_id,
coverage_type_code, requested_coverage_limit, requested_deductible,
s3_uri, raw_payload, file_format,
submitted_at, received_at, pipeline_status, created_at, updated_at)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,'JSON',%s,%s,'RECEIVED',%s,%s)
"""</span>
pr = sub[<span class="py-str">"policy_request"</span>]
self.cur.execute(sql, (
sub[<span class="py-str">"submission_id"</span>], broker_id, insured_id, property_id,
pr.get(<span class="py-str">"coverage_type"</span>), pr.get(<span class="py-str">"requested_coverage_limit"</span>),
pr.get(<span class="py-str">"requested_deductible"</span>),
full_data.get(<span class="py-str">"bronze_db_targets"</span>, {}).get(<span class="py-str">"s3_uri"</span>, <span class="py-str">""</span>),
json.dumps(full_data)[:65000], now, now, now, now))
<span class="py-kw">def</span> <span class="py-fn">_insert_attachments</span>(self, sub, now):
prop = sub[<span class="py-str">"property"</span>]
rows = [
(sub[<span class="py-str">"submission_id"</span>], <span class="py-str">"inspection_report"</span>,
<span class="py-str">f"inspection_report_{sub['submission_id']}.pdf"</span>,
prop.get(<span class="py-str">"inspection_report_s3_uri"</span>), <span class="py-str">"PDF"</span>, now),
(sub[<span class="py-str">"submission_id"</span>], <span class="py-str">"property_photos"</span>,
<span class="py-str">f"photos_{sub['submission_id']}.zip"</span>,
prop.get(<span class="py-str">"photos_s3_uri"</span>), <span class="py-str">"ZIP"</span>, now),
]
sql = <span class="py-str">"""INSERT INTO bronze_submission_attachments
(submission_id, attachment_type, file_name, s3_uri, file_format, uploaded_at)
VALUES (%s,%s,%s,%s,%s,%s)"""</span>
self.cur.executemany(sql, rows)
<span class="py-kw">def</span> <span class="py-fn">_insert_pipeline_log</span>(self, sub_id, now):
agents = [
(sub_id, <span class="py-str">"SSN_Agent"</span>, <span class="py-num">1</span>, <span class="py-str">"QUEUED"</span>, now),
(sub_id, <span class="py-str">"Property_Risk_Agent"</span>, <span class="py-num">2</span>, <span class="py-str">"PENDING"</span>, now),
(sub_id, <span class="py-str">"Underwriting_Agent"</span>, <span class="py-num">3</span>, <span class="py-str">"PENDING"</span>, now),
(sub_id, <span class="py-str">"ML_Pricing_Agent"</span>, <span class="py-num">4</span>, <span class="py-str">"PENDING"</span>, now),
(sub_id, <span class="py-str">"Issuance_Agent"</span>, <span class="py-num">5</span>, <span class="py-str">"PENDING"</span>, now),
]
sql = <span class="py-str">"""INSERT INTO bronze_pipeline_agent_log
(submission_id, agent_code, pipeline_step, status, created_at)
VALUES (%s, %s, %s, %s, %s)"""</span>
self.cur.executemany(sql, agents)
<span class="py-cmt"># ── Entry Point ────────────────────────────────────────</span>
<span class="py-kw">if</span> __name__ == <span class="py-str">"__main__"</span>:
path = sys.argv[<span class="py-num">1</span>] <span class="py-kw">if</span> len(sys.argv) > <span class="py-num">1</span> <span class="py-kw">else</span> <span class="py-kw">None</span>
<span class="py-kw">if not</span> path:
print(<span class="py-str">"Usage: python bronze_loader.py &lt;path_to_submission.json&gt;"</span>)
sys.exit(<span class="py-num">1</span>)
loader = <span class="py-cls">BronzeLoader</span>()
loader.connect()
<span class="py-kw">try</span>:
loader.load(path)
<span class="py-kw">finally</span>:
loader.disconnect()`;
document.getElementById('python-code').innerHTML=code;
}
function copyPython(){const txt=document.getElementById('python-code').innerText;navigator.clipboard?.writeText(txt).then(()=>addLog('info','Python script copied'));}
function downloadPython(){const b=new Blob([document.getElementById('python-code').innerText],{type:'text/plain'});const u=URL.createObjectURL(b);const a=document.createElement('a');a.href=u;a.download='bronze_loader.py';a.click();URL.revokeObjectURL(u);}
// ═══════════════════════════════════════════════════════
// CONNECTORS HUB
// ═══════════════════════════════════════════════════════
const CONNECTORS=[
{cat:'database',name:'MySQL / MariaDB',logo:'🐬',desc:'Clever Cloud MySQL (HF) · bronze_*/silver_*/gold_* prefixed tables · or localhost:3306 locally.',tags:['db'],connected:true},
{cat:'database',name:'PostgreSQL',logo:'🐘',desc:'Alternative RDBMS for Silver/Gold layers. psycopg2 driver.',tags:['db'],connected:false},
{cat:'database',name:'MongoDB',logo:'πŸƒ',desc:'Store semi-structured raw payloads. Phase 1 alternate Bronze store.',tags:['db'],connected:false},
{cat:'database',name:'Snowflake',logo:'❄️',desc:'Cloud DWH for Gold layer analytics. Medallion architecture native.',tags:['db','cloud'],connected:false},
{cat:'cloud',name:'Amazon S3',logo:'πŸͺ£',desc:'Bronze/Silver/Gold object store. pcins-bronze, pcins-silver, pcins-gold buckets.',tags:['cloud','file'],connected:false},
{cat:'cloud',name:'AWS Step Functions',logo:'⚑',desc:'Orchestrator for 5-agent pipeline. State machine per submission.',tags:['cloud'],connected:false},
{cat:'cloud',name:'AWS Lambda',logo:'Ξ»',desc:'Serverless agent execution. Each agent as a Lambda function.',tags:['cloud'],connected:false},
{cat:'cloud',name:'Amazon SageMaker',logo:'πŸ€–',desc:'ML model endpoint for Agent 4 pricing. XGBoost + GLM ensemble.',tags:['cloud','api'],connected:false},
{cat:'cloud',name:'Amazon SES',logo:'πŸ“§',desc:'Email delivery for policy documents and adverse action notices.',tags:['cloud','api'],connected:false},
{cat:'api',name:'REST API / Webhook',logo:'πŸ”—',desc:'Ingest submissions from external broker portals and agency management systems.',tags:['api'],connected:false},
{cat:'api',name:'ACORD XML / JSON',logo:'πŸ“„',desc:'Standard insurance data exchange format. AL3, ACORD 130, 140 forms.',tags:['api','file'],connected:false},
{cat:'api',name:'Experian Credit',logo:'πŸ“Š',desc:'Credit bureau pull for KYC/SSN validation agent. Score + report.',tags:['api'],connected:false},
{cat:'api',name:'OFAC / SDN API',logo:'πŸ›‘οΈ',desc:'Sanctions screening. SDN, Consolidated, PEP, Adverse Media lists.',tags:['api'],connected:false},
{cat:'api',name:'FEMA Flood API',logo:'🌊',desc:'Flood zone determination. Zone X, AE, VE per property address.',tags:['api'],connected:false},
{cat:'file',name:'CSV / Excel Upload',logo:'πŸ“‹',desc:'Batch submission ingestion from spreadsheets. Historical data load for Bronze.',tags:['file'],connected:false},
{cat:'file',name:'PDF Parser',logo:'πŸ“‘',desc:'Extract data from inspection reports, appraisals, loss runs via OCR+NLP.',tags:['file','api'],connected:false},
{cat:'file',name:'JSON File Watch',logo:'πŸ“‚',desc:'Watch D:\\Personal\\Usecase\\Jsons\\ for new submission files. Auto-load to Bronze.',tags:['file'],connected:false},
{cat:'insurance',name:'Applied EPIC',logo:'🏒',desc:'Agency management system. Pull submissions and policy data directly.',tags:['api'],connected:false},
{cat:'insurance',name:'Vertafore AMS360',logo:'🏬',desc:'Broker AMS integration. Bi-directional submission sync.',tags:['api'],connected:false},
{cat:'insurance',name:'Duck Creek',logo:'πŸ¦†',desc:'Policy admin system. Issue policies directly from Issuance Agent output.',tags:['api'],connected:false},
{cat:'insurance',name:'Majesco',logo:'βš™οΈ',desc:'P&C core system. Bronze β†’ Majesco policy lifecycle integration.',tags:['api'],connected:false},
{cat:'stream',name:'Apache Kafka',logo:'β˜•',desc:'High-volume submission streaming. Real-time Bronze ingest pipeline.',tags:['stream'],connected:false},
{cat:'stream',name:'Amazon Kinesis',logo:'πŸŒ€',desc:'AWS-native streaming. Step Functions trigger on submission events.',tags:['stream','cloud'],connected:false},
{cat:'stream',name:'Apache Spark',logo:'πŸ’₯',desc:'Batch processing for historical data load. Bronze medallion ETL.',tags:['stream'],connected:false},
];
function buildConnectors(){
renderConnectors(CONNECTORS);
}
function filterCat(cat,btn){
document.querySelectorAll('.cat-btn').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
renderConnectors(cat==='all'?CONNECTORS:CONNECTORS.filter(c=>c.cat===cat||c.tags.includes(cat)));
}
function renderConnectors(list){
const g=document.getElementById('conn-grid');
g.innerHTML=list.map(c=>`
<div class="conn-card ${c.connected?'connected':''}">
<div class="conn-top"><div class="conn-logo" style="background:var(--bg4);">${c.logo}</div><div><div class="conn-name">${c.name}</div><div class="conn-cat">${c.cat}</div></div></div>
<div class="conn-desc">${c.desc}</div>
<div class="conn-tags">${c.tags.map(t=>`<span class="conn-tag tag-${t}">${t}</span>`).join('')}</div>
<div class="conn-status-row">
${c.connected?'<span class="conn-badge-on">● Connected</span>':'<span class="conn-badge-off">β—‹ Not Connected</span>'}
<button class="conn-action ${c.connected?'ca-manage':'ca-connect'}" onclick="connAction('${c.name}',${c.connected})">${c.connected?'Manage':'Connect'}</button>
</div>
</div>`).join('');
}
function connAction(name,isConn){
addLog('info',`${isConn?'Managing':'Connecting to'}: ${name}`);
document.getElementById('sb-msg').textContent=`${isConn?'Managing':'Configuring'}: ${name}`;
}
// ═══════════════════════════════════════════════════════
// AGENT PIPELINE TAB
// ═══════════════════════════════════════════════════════
const AGENTS=[
{step:1,code:'SSN_Agent',name:'SSN Identity Validation',icon:'πŸ”',color:'#0070cc',phase:1,layer:'SILVER',input:'Bronze submission JSON',output:'silver_ssn_decisions + silver_kyc tables',tools:'Experian API, OFAC API, IsolationForest ML',est:'8–12s'},
{step:2,code:'Property_Risk_Agent',name:'Property Risk Assessment',icon:'🏠',color:'#008844',phase:1,layer:'SILVER',input:'SSN decision JSON',output:'silver_property_risk_assessments',tools:'FEMA API, Satellite Imagery, NLP Inspection Parser',est:'12–18s'},
{step:3,code:'Underwriting_Agent',name:'Underwriting Validation',icon:'πŸ“‹',color:'#8860ff',phase:1,layer:'SILVER',input:'Property risk JSON',output:'silver_underwriting_decisions',tools:'Rules Engine, Reinsurance Treaty API',est:'6–9s'},
{step:4,code:'ML_Pricing_Agent',name:'ML Premium Pricing',icon:'πŸ’°',color:'#b8860b',phase:1,layer:'SILVER',input:'UW decision JSON',output:'silver_ml_pricing_decisions',tools:'SageMaker Endpoint: XGBoost+GLM Ensemble, SHAP',est:'5–8s'},
{step:5,code:'Issuance_Agent',name:'Policy Issuance',icon:'βœ…',color:'#cc3300',phase:1,layer:'SILVER',input:'ML pricing JSON',output:'silver_issued_policies + documents',tools:'Document Generator, SES Email, SNS Events',est:'4–6s'},
];
function buildAgentPipeline(){
const flow=document.getElementById('pipeline-flow');
flow.innerHTML=AGENTS.map((a,i)=>`
<div class="pf-agent">
<div class="agent-box phase2" style="position:relative;">
<span class="agent-badge ab-p2">Phase 2</span>
<div class="agent-icon-wrap" style="background:${a.color}20;border:1px solid ${a.color}40;">${a.icon}</div>
<div class="agent-step">Agent ${a.step}</div>
<div class="agent-name">${a.name}</div>
<div class="agent-code">${a.code}</div>
</div>
</div>
${i<AGENTS.length-1?'<div class="pf-connector"><div class="pf-line"></div></div>':''}`).join('');
// Add Phase 1 bronze box at start
flow.insertAdjacentHTML('afterbegin',`
<div class="pf-agent">
<div class="agent-box active" style="position:relative;">
<span class="agent-badge ab-p1">Phase 1</span>
<div class="agent-icon-wrap" style="background:rgba(205,127,50,0.2);border:1px solid rgba(205,127,50,0.4);">πŸ“₯</div>
<div class="agent-step">Trigger</div>
<div class="agent-name">Bronze Loader</div>
<div class="agent-code">BronzeLoader</div>
</div>
</div>
<div class="pf-connector"><div class="pf-line active"></div></div>`);
const grid=document.getElementById('agent-detail-grid');
grid.innerHTML=AGENTS.map(a=>`
<div class="agent-detail-card">
<div class="adc-header">
<div class="adc-icon" style="background:${a.color}20;border:1px solid ${a.color}30;">${a.icon}</div>
<div><div class="adc-name">Agent ${a.step}: ${a.name}</div><div class="adc-step">${a.code}</div></div>
<span class="pill pill-muted" style="margin-left:auto;">Phase ${a.phase}</span>
</div>
<div class="adc-prop"><span class="adc-pk">Layer</span><span class="adc-pv">${a.layer}</span></div>
<div class="adc-prop"><span class="adc-pk">Input</span><span class="adc-pv">${a.input}</span></div>
<div class="adc-prop"><span class="adc-pk">Output</span><span class="adc-pv">${a.output}</span></div>
<div class="adc-prop"><span class="adc-pk">Tools</span><span class="adc-pv">${a.tools}</span></div>
<div class="adc-prop"><span class="adc-pk">Est. Duration</span><span class="adc-pv">${a.est}</span></div>
</div>`).join('');
}
function buildAgentPython(){
const code=`<span class="py-cmt">#!/usr/bin/env python3</span>
<span class="py-cmt">"""
PolicyBridge β€” Base Agent Scaffold (Phase 2)
Each of the 5 AI agents inherits from this base class.
Deployed as AWS Lambda + Step Functions state machine.
"""</span>
<span class="py-kw">import</span> json, logging, boto3
<span class="py-kw">from</span> abc <span class="py-kw">import</span> ABC, abstractmethod
<span class="py-kw">from</span> datetime <span class="py-kw">import</span> datetime
<span class="py-kw">import</span> mysql.connector
<span class="py-kw">class</span> <span class="py-cls">BaseAgent</span>(ABC):
<span class="py-dec">@property</span>
<span class="py-dec">@abstractmethod</span>
<span class="py-kw">def</span> <span class="py-fn">agent_code</span>(self) -> str: ...
<span class="py-dec">@property</span>
<span class="py-dec">@abstractmethod</span>
<span class="py-kw">def</span> <span class="py-fn">pipeline_step</span>(self) -> int: ...
<span class="py-kw">def</span> <span class="py-fn">run</span>(self, submission_id: str, input_s3_uri: str) -> dict:
self.<span class="py-fn">_update_log</span>(submission_id, <span class="py-str">"INVOKED"</span>)
<span class="py-kw">try</span>:
payload = self.<span class="py-fn">_load_from_s3</span>(input_s3_uri)
result = self.<span class="py-fn">process</span>(submission_id, payload)
out_uri = self.<span class="py-fn">_save_to_s3</span>(result, submission_id)
self.<span class="py-fn">_update_log</span>(submission_id, <span class="py-str">"COMPLETED"</span>, out_uri)
<span class="py-kw">return</span> {<span class="py-str">"status"</span>: <span class="py-str">"COMPLETED"</span>, <span class="py-str">"output_s3_uri"</span>: out_uri}
<span class="py-kw">except</span> Exception <span class="py-kw">as</span> e:
self.<span class="py-fn">_update_log</span>(submission_id, <span class="py-str">"FAILED"</span>, error=str(e))
<span class="py-kw">raise</span>
<span class="py-dec">@abstractmethod</span>
<span class="py-kw">def</span> <span class="py-fn">process</span>(self, submission_id: str, payload: dict) -> dict:
<span class="py-str">"""Agent-specific logic goes here"""</span> ...
<span class="py-kw">def</span> <span class="py-fn">_update_log</span>(self, sub_id, status, out_uri=<span class="py-kw">None</span>, error=<span class="py-kw">None</span>):
<span class="py-cmt"># Updates bronze_pipeline_agent_log</span>
<span class="py-kw">pass</span>
<span class="py-cmt"># ── Example: SSN Identity Validation Agent ─────────────</span>
<span class="py-kw">class</span> <span class="py-cls">SSNAgent</span>(<span class="py-cls">BaseAgent</span>):
agent_code = <span class="py-str">"SSN_Agent"</span>
pipeline_step = <span class="py-num">1</span>
<span class="py-kw">def</span> <span class="py-fn">process</span>(self, submission_id, payload):
insured = payload[<span class="py-str">"submission"</span>][<span class="py-str">"insured"</span>]
<span class="py-cmt"># 1. OFAC screening</span>
ofac_result = self.<span class="py-fn">_check_ofac</span>(insured[<span class="py-str">"full_name"</span>])
<span class="py-cmt"># 2. Credit bureau pull</span>
credit = self.<span class="py-fn">_pull_credit</span>(insured[<span class="py-str">"ssn_hash"</span>])
<span class="py-cmt"># 3. Fraud anomaly detection</span>
fraud_score = self.<span class="py-fn">_fraud_score</span>(insured)
<span class="py-cmt"># 4. Write results to silver_ssn_decisions</span>
status = <span class="py-str">"SSN_PASS"</span> <span class="py-kw">if</span> all([
ofac_result[<span class="py-str">"result"</span>] == <span class="py-str">"CLEAR"</span>,
credit[<span class="py-str">"score"</span>] >= <span class="py-num">550</span>,
fraud_score < <span class="py-num">0.70</span>
]) <span class="py-kw">else</span> <span class="py-str">"SSN_FAIL"</span>
<span class="py-kw">return</span> {<span class="py-str">"ssn_status"</span>: status, <span class="py-str">"ofac"</span>: ofac_result, <span class="py-str">"credit"</span>: credit, <span class="py-str">"fraud_score"</span>: fraud_score}`;
document.getElementById('agent-python-code').innerHTML=code;
}
function copyAgentPython(){const t=document.getElementById('agent-python-code').innerText;navigator.clipboard?.writeText(t).then(()=>addLog('info','Agent scaffold copied'));}
</script>
</body>
</html>