Spaces:
Sleeping
Sleeping
| <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 & 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&S (Excess & 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 & 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 & Ranch</div><div class="cov-desc">Dwelling + equipment</div></div> | |
| </div> | |
| </div> | |
| <div class="fcard"> | |
| <div class="fcard-title">Limits & 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 & 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 <path_to_submission.json>"</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> | |