import time
import random
from datetime import datetime
import streamlit as st
from simulator import generate_accounts, generate_transaction, score_transaction
# ─── Page config ──────────────────────────────────────────────────────────────
st.set_page_config(
page_title="Fraud Analytics — Enterprise Monitor",
page_icon="🏛️",
layout="wide",
initial_sidebar_state="expanded",
)
# ─── Styling ──────────────────────────────────────────────────────────────────
st.markdown("""
""", unsafe_allow_html=True)
# ─── Session state init ───────────────────────────────────────────────────────
def _init():
st.session_state.setdefault("running", False)
st.session_state.setdefault("accounts", [])
st.session_state.setdefault("feed", []) # สะสมจากบนลงล่าง
st.session_state.setdefault("alerts", []) # สะสมจากบนลงล่าง
st.session_state.setdefault("step", 1)
st.session_state.setdefault("tx_counter", 1)
st.session_state.setdefault("total_tx", 0)
st.session_state.setdefault("total_block", 0)
st.session_state.setdefault("total_alert", 0)
st.session_state.setdefault("total_pass", 0)
st.session_state.setdefault("amount_risk", 0.0)
_init()
# ─── Sidebar (Configuration & Hidden Controls) ────────────────────────────────
with st.sidebar:
st.markdown("### ⚙️ Engine Settings")
st.caption("Adjust variables for the transaction generator.")
st.divider()
speed = st.slider("Generator Delay (seconds)", 0.3, 3.0, 1.0, 0.1)
fraud_pct = st.slider("Mule Account Ratio (%)", 5, 40, 12, 1)
n_accs = st.slider("Accounts Network Size", 3, 8, 5, 1)
st.divider()
st.markdown("**System Actions**")
reset_btn = st.button("↺ Reset Simulation Data", use_container_width=True)
if reset_btn:
for k in ["running","accounts","feed","alerts","step","tx_counter",
"total_tx","total_block","total_alert","total_pass","amount_risk"]:
st.session_state.pop(k, None)
_init()
st.rerun()
# GitHub Link ตรึงไว้ล่างสุดของ Sidebar
st.markdown("""
""", unsafe_allow_html=True)
# ─── Main Header & Dynamic Bank Controls ──────────────────────────────────────
title_col, ctrl_col = st.columns([2, 1])
with title_col:
st.markdown("## 🏛️ Fraud Operations — Live Matrix Dashboard")
status_txt = "🟢 MONITORING ACTIVE" if st.session_state.running else "⏸️ MONITORING PAUSED"
st.caption(f"{status_txt} · Model Ensemble Framework A × B · Threshold Rules Enabled")
with ctrl_col:
st.markdown("", unsafe_allow_html=True)
c1, c2 = st.columns(2)
start_btn = c1.button("▶ Start / Resume", use_container_width=True, type="primary", disabled=st.session_state.running)
pause_btn = c2.button("⏸ Pause", use_container_width=True, disabled=not st.session_state.running)
if start_btn:
if not st.session_state.accounts:
seed = random.randint(0, 99999)
st.session_state.accounts = generate_accounts(n=n_accs, seed=seed)
st.session_state.running = True
st.rerun()
if pause_btn:
st.session_state.running = False
st.rerun()
st.divider()
# ─── Top Split Grid: Financial Metrics & Accounts Grid ────────────────────────
metric_col, watchlist_col = st.columns([6, 4], gap="medium")
with metric_col:
m1, m2, m3, m4 = st.columns(4)
m1.metric("Processed Transactions", f"{st.session_state.total_tx} tx")
m2.metric("Critical Blocked", st.session_state.total_block)
m3.metric("OTP Challenges", st.session_state.total_alert)
m4.metric("Funds at Risk Exposure", f"฿{st.session_state.amount_risk:,.0f}")
with watchlist_col:
st.markdown("🎯 Account Audit Matrix
", unsafe_allow_html=True)
if st.session_state.accounts:
acc_table = """
| Account ID | Baseline Risk | Operational Status |
"""
for acc in st.session_state.accounts:
icon, status_color = {
"normal": ("🟢 Normal", "background:#dcfce7;color:#16a34a"),
"alert": ("🟡 Warning", "background:#fef3c7;color:#d97706"),
"blocked": ("🔴 Blocked", "background:#fee2e2;color:#dc2626")
}.get(acc["status"], ("⚪ Unknown", ""))
risk_badge = f"🔥 {acc['risk_level'].upper()}" if acc['risk_level'] == 'high' else acc['risk_level'].upper()
acc_table += f"""
{acc['account_id']} |
{risk_badge} |
{icon} |
"""
acc_table += "
"
st.markdown(acc_table, unsafe_allow_html=True)
else:
st.caption("No accounts mapped. Click 'Start / Resume' to link core systems.")
st.markdown("
", unsafe_allow_html=True)
# ─── Bottom Split Grid: Fixed-Height Containers ───────────────────────────────
feed_col, alert_col = st.columns([55, 45], gap="medium")
with feed_col:
st.markdown("#### ⏱️ Real-Time Transaction Ledger")
st.caption("Global financial traffic logging (Ordered: $001 \\rightarrow 002 \\rightarrow 003$)")
feed_box = st.container(height=480)
with alert_col:
st.markdown("#### 🚨 Security Incident Alerts Queue")
st.caption("Decisions requiring compliance review or automated intercept")
alert_box = st.container(height=480)
# ─── Function to build table HTML (Append-friendly) ──────────────────────────
def build_feed_table(feed_list) -> str:
html_table = """
| Time | Tx ID | Routing Sequence | Amount | Score | Action |
"""
for f_tx in feed_list[-40:]:
style_cls = "text-block" if "Block" in f_tx["decision"] else "text-alert" if "Alert" in f_tx["decision"] else "text-pass"
html_table += f"""
| {f_tx.get('tx_time', '00:00:00')} |
{f_tx['tx_id']} |
{f_tx['account_id']} ➔ {f_tx['dest_id']} |
฿{f_tx['amount']:,.0f} |
{f_tx['final_score']:.2f} |
{f_tx['decision'].upper()} |
"""
html_table += "
"
return html_table
# ─── Simulation Core Loop & Rendering Logic ──────────────────────────────────
if st.session_state.running and st.session_state.accounts:
# 📌 สร้าง Placeholder ไว้ "นอกลูป" แค่ครั้งเดียว เพื่อล็อคหัวตารางให้ไม่วาดซ้ำซาก
with feed_box:
feed_placeholder = st.empty()
with alert_box:
alert_placeholder = st.empty()
while st.session_state.running:
# Pick account & process transaction
account = random.choice(st.session_state.accounts)
raw_tx = generate_transaction(account, step=st.session_state.step, tx_counter=st.session_state.tx_counter)
tx = score_transaction(raw_tx)
# ฝังเวลาปัจจุบันของระบบธนาคารระดับวินาที
tx['tx_time'] = datetime.now().strftime("%H:%M:%S")
# Handle status state mutations
d = tx["decision"]
if "Block" in d: account["status"] = "blocked"
elif "Alert" in d: account["status"] = "alert"
# สะสมข้อมูลธุรกรรมต่อท้ายลิสต์เดิมลงไปข้างล่าง
st.session_state.feed = st.session_state.feed + [tx]
st.session_state.total_tx += 1
st.session_state.tx_counter += 1
if "Block" in d:
st.session_state.total_block += 1
st.session_state.alerts = st.session_state.alerts + [tx]
st.session_state.amount_risk += tx["amount"]
elif "Alert" in d:
st.session_state.total_alert += 1
st.session_state.alerts = st.session_state.alerts + [tx]
st.session_state.amount_risk += tx["amount"] * 0.5
else:
st.session_state.total_pass += 1
if st.session_state.tx_counter % 6 == 0:
st.session_state.step += 1
# 📌 เขียนทับกล่องเดิมเพื่อเพิ่มแถวใหม่ (+1 แถวต่อท้ายในตารางเดี่ยว) หัวตารางจะไม่ซ้ำแล้ว
feed_placeholder.markdown(build_feed_table(st.session_state.feed), unsafe_allow_html=True)
# อัปเดตฝั่ง Alerts Queue ในกล่องเดิมเช่นกันแบบเรียบเนียน
with alert_placeholder.container():
if st.session_state.alerts:
for a_tx in st.session_state.alerts[-20:]:
title_text = f"🚨 [{a_tx['tx_time']}] {a_tx['tx_id']} | ฿{a_tx['amount']:,.0f} ➔ {a_tx['decision'].upper()}"
with st.expander(title_text):
st.markdown(f"""
**Incident Deep-Dive Metrics:**
* **Timestamp:** `{a_tx['tx_time']}`
* **Source Account:** `{a_tx['account_id']}`
* **Destination Target:** `{a_tx['dest_id']}`
* **Model A Probability (Behavior Engine):** `{a_tx['prob_a']:.3f}`
* **Model B Probability (Counterparty Network):** `{a_tx['prob_b']:.3f}`
* **Weighted Ensemble Score:** `{a_tx['final_score']:.3f}`
* **Pipeline Detection Layer:** `{a_tx['layer']}`
* **Hard Rules Active:** `Rule 1 (Remote Access): {a_tx['rule1']}` | `Rule 2 (Hesitation Pattern): {a_tx['rule2']}`
""")
else:
st.success("🟢 OPERATIONAL STATUS: SECURED — No anomalies flagged by models currently.")
time.sleep(speed)
else:
# Static render loop เมื่อกด PAUSE (ตารางคงสภาพล่าสุดนิ่งๆ ไว้ให้ตรวจสอบ)
with feed_box:
if st.session_state.feed:
st.markdown(build_feed_table(st.session_state.feed), unsafe_allow_html=True)
else:
st.info("System Ready. Click **▶ Start / Resume** to map ledger feeds.")
with alert_box:
if st.session_state.alerts:
for a_tx in st.session_state.alerts[-20:]:
title_text = f"🚨 [{a_tx['tx_time']}] {a_tx['tx_id']} | ฿{a_tx['amount']:,.0f} ➔ {a_tx['decision'].upper()}"
with st.expander(title_text):
st.markdown(f"""
**Incident Deep-Dive Metrics:**
* **Timestamp:** `{a_tx['tx_time']}`
* **Source Account:** `{a_tx['account_id']}`
* **Destination Target:** `{a_tx['dest_id']}`
* **Model A Probability:** `{a_tx['prob_a']:.3f}`
* **Model B Probability:** `{a_tx['prob_b']:.3f}`
* **Weighted Ensemble Score:** `{a_tx['final_score']:.3f}`
""")
else:
st.success("🟢 OPERATIONAL STATUS: SECURED — No anomalies flagged by models currently.")