# -*- coding: utf-8 -*-
"""SERVER CALIB — Web UI huấn luyện/hiệu chỉnh màu CSDL B (cổng 8099).

Luồng: anh nhập hỗn hợp (màu CSDL B + tỉ lệ %) → AGY dự đoán màu → anh tải ẢNH THỰC TẾ
       → AGY đối chiếu (ΔE) và đề xuất hiệu chỉnh hệ số từng màu → ghi vào CSDL B.

Chạy: python server_calib.py   (mật khẩu trong mat_khau.txt; chạy được ở gốc và dưới /calib)
"""
import cgi, hashlib, hmac, html, io, json, mimetypes, os, re, secrets, sqlite3, sys, time, urllib.parse
from http.cookies import SimpleCookie
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

GOC = r'C:\Claw 2\Nghien cuu\Du-an-B-He-mau-Stain-Glaze\calib-app'
DB = r'C:\Claw 2\Nghien cuu\Du-an-B-He-mau-Stain-Glaze\CSDLB.db'
VIEC = os.path.join(GOC, 'viec')
CONG = 8099
MK_FILE = os.path.join(GOC, 'mat_khau.txt')
PASS_FILE = os.path.join(GOC, 'pass_hieu_chinh.txt')
DIEU_KIEN = ('Kéo màu bằng CÙNG thước kéo với màu mẫu · pha trong CÙNG loại base với mẫu · '
             'trên GIẤY TRẮNG cùng loại · tỉ lệ nhập là % màu pha trong base')
BM_FILE = os.path.join(GOC, 'bi_mat.txt')
os.makedirs(VIEC, exist_ok=True)


def _doc(p, md=''):
    try:
        return open(p, encoding='utf-8').read().strip()
    except Exception:
        return md


def mat_khau():
    mk = _doc(MK_FILE)
    if not mk:
        mk = 'calib-' + secrets.token_hex(3)
        open(MK_FILE, 'w', encoding='utf-8').write(mk)
    return mk


def pass_hieu_chinh():
    try:
        return open(PASS_FILE, encoding='utf-8').read().strip() or '19842026'
    except Exception:
        return '19842026'


def bi_mat():
    b = _doc(BM_FILE)
    if not b:
        b = secrets.token_hex(16)
        open(BM_FILE, 'w', encoding='utf-8').write(b)
    return b


def ma_phien():
    return hmac.new(bi_mat().encode(), mat_khau().encode(), hashlib.sha256).hexdigest()[:32]


def db():
    return sqlite3.connect(DB, timeout=30)


def ds_mau():
    con = db()
    r = [dict(zip(('id', 'ten', 'ma', 'phan_loai', 'hs'), x))
         for x in con.execute('SELECT id,ten,ma,phan_loai,COALESCE(he_so_cuong_do,1.0) FROM mau ORDER BY ten').fetchall()]
    con.close()
    return r


def nhip_worker():
    try:
        con = db()
        r = con.execute('SELECT dang_xu_ly,buoc,bat_dau FROM worker_nhip WHERE id=1').fetchone()
        cho = con.execute("SELECT COUNT(*) FROM hon_hop WHERE trang_thai='cho_du_doan' "
                          "OR (trang_thai='da_du_doan' AND anh_thuc_te IS NOT NULL AND anh_thuc_te<>'')").fetchone()[0]
        doi_anh = con.execute("SELECT COUNT(*) FROM hon_hop WHERE trang_thai='da_du_doan' AND (anh_thuc_te IS NULL OR anh_thuc_te='')").fetchone()[0]
        con.close()
        return (r[0] or '', r[1] or '', r[2] or '', cho, doi_anh)
    except Exception:
        return ('', '', '', 0, 0)


def swatch(hx, nhan=''):
    if not hx:
        return ''
    return ('<span style="display:inline-block;width:34px;height:18px;border:1px solid #999;vertical-align:middle;'
            'background:%s"></span> %s <small>%s</small>' % (html.escape(hx), html.escape(hx), html.escape(nhan or '')))


CSS = """
body{margin:0;font:15px/1.5 'Segoe UI',Arial,sans-serif;background:#f4f6f8;color:#1b2733}
header{background:#0b3d2c;color:#fff;padding:13px 20px}header b{font-size:17px}header span{opacity:.85;font-size:13px;margin-left:10px}
main{max-width:1040px;margin:16px auto;padding:0 14px}
.the{background:#fff;border:1px solid #dfe3e8;border-radius:10px;padding:15px 18px;margin-bottom:14px}
label{display:block;font-weight:600;margin:8px 0 3px}
input[type=text],input[type=password],select,textarea{width:100%;padding:7px 9px;border:1px solid #dfe3e8;border-radius:7px;font:inherit}
input[type=file]{padding:5px 0}
button{background:#0b6b3a;color:#fff;border:0;border-radius:8px;padding:10px 18px;font:inherit;font-weight:600;cursor:pointer;margin-top:12px}
button.phu{background:#5d6b7a;padding:5px 10px;margin:0 0 0 6px;font-size:13px}
table{border-collapse:collapse;width:100%;font-size:14px;margin:8px 0}td,th{border-bottom:1px solid #e7ebef;padding:6px 7px;text-align:left;vertical-align:top}
th{background:#eef1f4}
.hang{display:flex;gap:8px;align-items:end;margin-bottom:6px}.hang>div{flex:1}
small{color:#5d6b7a}a{color:#0b6b3a}
.tt{display:inline-block;padding:2px 9px;border-radius:20px;font-size:12px;font-weight:700}
.cho_du_doan{background:#fff3cd;color:#7a5b00}.da_du_doan{background:#cfe4ff;color:#0a3d75}
.da_doi_chieu,.cho_xac_nhan{background:#ffe4cc;color:#8a4a00}.da_hieu_chinh{background:#d6f4e0;color:#0b5c2e}
.de{background:#ffd9d9;color:#8a1414}
"""


def trang_dang_nhap(loi=''):
    return '''<!doctype html><html lang="vi"><meta charset="utf-8"><title>Calib — Đăng nhập</title>

<base href="./"><script>document.addEventListener("DOMContentLoaded",function(){var B=(location.pathname.indexOf("/calib")===0)?"/calib/":"/";var b=document.querySelector("base");if(b&&b.getAttribute("href")!==B)b.href=B;});</script><style>%s</style><header><b>CALIB CSDL B</b><span>Huấn luyện &amp; hiệu chỉnh màu</span></header>
<main><div class="the" style="max-width:420px"><h3 style="margin-top:0">Đăng nhập</h3>%s
<form method="post" action="dang-nhap"><label>Mật khẩu</label><input type="password" name="mat_khau" autofocus required>
<button type="submit">Vào</button></form></div></main></html>''' % (CSS, ('<p style="color:#8a1414"><b>%s</b></p>' % html.escape(loi)) if loi else '')


def email_mac_dinh():
    try:
        return open(os.path.join(GOC, 'email_bao.txt'), encoding='utf-8').read().strip()
    except Exception:
        return ''


def trang_chinh():
    ms = ds_mau()
    opts = ''.join('<option value="%s">%s (%s)</option>' % (html.escape(m['ma']), html.escape(m['ten']), html.escape(m['ma'])) for m in ms)
    return '''<!doctype html><html lang="vi"><meta charset="utf-8"><title>Calib CSDL B — hỗn hợp màu</title>

<base href="./"><script>document.addEventListener("DOMContentLoaded",function(){var B=(location.pathname.indexOf("/calib")===0)?"/calib/":"/";var b=document.querySelector("base");if(b&&b.getAttribute("href")!==B)b.href=B;});</script><style>%s</style>
<header><b>CALIB CSDL B</b><span>Hỗn hợp màu → AGY dự đoán → đối chiếu ảnh thực tế → hiệu chỉnh thông số màu</span></header>
<main>
<div class="the"><p><b>Cách dùng:</b> chọn các màu đã có trong CSDL B và ghi <b>tỉ lệ phần trăm</b> của từng màu trong hỗn hợp → bấm Gửi. Agent sẽ dự đoán màu của hỗn hợp (hex, L*a*b*, phương pháp, độ tin cậy). Sau đó anh chụp <b>ảnh màu hỗn hợp thực tế</b> và tải lên ở trang chi tiết để agent đối chiếu (ΔE) và <b>đề xuất hiệu chỉnh hệ số cường độ</b> của từng màu ⇒ CSDL B chính xác dần.</p>
<p><small>· <a href="lich-su">Lịch sử hỗn hợp</a> · <a href="hieu-chinh">Lịch sử hiệu chỉnh màu</a> · <a href="thoat">Thoát</a></small></p></div>
<form class="the" id="f" onsubmit="return gui(event)">
<div class="hang"><div><label>Người nhập</label><input type="text" name="nguoi_nhap" required placeholder="VD: Lab – Hùng"></div>
<div><label>Email nhận thông báo (tự báo khi agent xong)</label><input type="text" name="email" value="%s" placeholder="VD: ngoducminh84@gmail.com"></div>
<div><label>Ghi chú (tùy chọn)</label><input type="text" name="ghi_chu" placeholder="VD: kéo bằng thước 100 µm, base BA123, giấy trắng"></div></div>
<label>Thành phần — nhập <b>%% màu pha trong base</b> của từng màu (không cần cộng = 100%%)</label>
<div style="background:#eafaf0;border:1px solid #bfe3cd;border-radius:8px;padding:8px 10px;margin:4px 0 8px"><small><b>Điều kiện chuẩn (mặc định):</b> kéo màu bằng <b>cùng thước kéo</b> với màu mẫu · pha trong <b>cùng loại base</b> với mẫu · trên <b>giấy trắng cùng loại</b>.</small></div>
<div id="tp"></div>
<button class="phu" type="button" onclick="themHang()">+ Thêm màu</button>
<button type="submit" id="nut">Gửi cho AGY dự đoán</button>
<p id="tb"><small>Ví dụ: Black 27 điền <b>4</b> nghĩa là 4%% màu đó trong base. Tổng chỉ để tham chiếu, không cần = 100%%.</small></p>
</form>
<div class="the" id="ket" style="display:none"></div>
</main>
<script>
const DS=%s; const B=(location.pathname.indexOf('/calib')===0)?'/calib/':'/';
function themHang(ma,tl){const d=document.createElement('div');d.className='hang';
 d.innerHTML='<div><select name="ma">'+DS+'</select></div><div style="max-width:130px"><input type="text" name="ti_le" placeholder="%% trong base" value="'+(tl||'')+'"></div><div style="flex:0 0 auto"><button class="phu" type="button" onclick="this.closest(\\'.hang\\').remove()">Xoá</button></div>';
 document.getElementById('tp').appendChild(d); if(ma)d.querySelector('select').value=ma;}
themHang();themHang();
function gui(e){e.preventDefault();const f=document.getElementById('f'),nut=document.getElementById('nut');
 const fd=new FormData(f); const tp=[]; let tong=0;
 document.querySelectorAll('#tp .hang').forEach(h=>{const ma=h.querySelector('[name=ma]').value;const tl=parseFloat((h.querySelector('[name=ti_le]').value||'').replace(',','.'))||0;
   if(ma&&tl>0){tp.push({ma:ma,ti_le:tl});tong+=tl;}});
 if(!tp.length){document.getElementById('tb').innerHTML='<b style="color:#8a1414">Chưa có thành phần hợp lệ</b>';return false;}
 fd.delete('ma');fd.delete('ti_le'); fd.set('thanh_phan',JSON.stringify(tp)); fd.set('tong_ti_le',tong.toFixed(2));
 nut.disabled=true;nut.textContent='Đang gửi…';
 fetch(B+'tao-hon-hop',{method:'POST',body:fd}).then(r=>r.json()).then(d=>{
  if(!d.id){document.getElementById('tb').innerHTML='<b style="color:#8a1414">Lỗi: '+JSON.stringify(d)+'</b>';nut.disabled=false;return;}
  nut.textContent='Đã gửi — hỗn hợp #'+d.id;document.getElementById('ket').style.display='block';
  document.getElementById('ket').innerHTML='<p><b>Hỗn hợp #'+d.id+' đã tạo.</b> <a href="hon-hop?id='+d.id+'">Mở trang chi tiết (xem dự đoán &amp; tải ảnh thực tế)</a></p>';}).catch(err=>{document.getElementById('tb').textContent='Lỗi mạng: '+err;nut.disabled=false;});
 return false;}
</script></html>''' % (CSS, html.escape(email_mac_dinh()), json.dumps(opts))


def trang_chi_tiet(v, q):
    tp = json.loads(v['thanh_phan'] or '[]')
    chua_xong = (v['trang_thai'] == 'cho_du_doan') or (bool(v['anh_thuc_te']) and not v['thuc_te_hex'])
    nhip = nhip_worker()
    tu_cap_nhat = '<meta http-equiv="refresh" content="10">' if chua_xong else ''
    thanh_agent = ('<b>Agent đang xử lý:</b> %s · bước %s · từ %s' % (html.escape(nhip[0]), html.escape(nhip[1]), nhip[2])) \
        if nhip[0] else ('<b>Agent đang rảnh</b> — %d việc đang xếp hàng, %d hỗn hợp đang chờ chủ nhân tải ảnh' % (nhip[3], nhip[4]))
    hang = ''.join('<tr><td>%s</td><td>%s</td><td>%s%%</td></tr>' % (html.escape(x.get('ma', '')), html.escape(x.get('ten', x.get('ma', ''))), x.get('ti_le')) for x in tp)
    d = v['thu_muc']
    tt = '<span class="tt %s">%s</span>' % (v['trang_thai'], v['trang_thai'])
    # khối THÔNG BÁO CHO NGƯỜI DÙNG (email)
    bao = [('Dự đoán màu', v.get('bao_p')), ('Đối chiếu ảnh thực tế', v.get('bao_c')), ('Ghi vào CSDL B', v.get('bao_ghi'))]
    dong_bao = ''.join('<li>%s: %s</li>' % (t, ('<b style="color:#0b5c2e">✔ %s</b>' % html.escape(b)) if (b or '').startswith('đã gửi')
                                            else ('<b style="color:#8a1414">%s</b>' % html.escape(b or '')) if b else '<i>chưa có</i>')
                       for t, b in bao)
    canh_bao_ui = ''
    if q.get('da_bao'):
        canh_bao_ui = '<p style="background:#eafaf0;border-radius:8px;padding:8px 12px"><b>Đã gửi lại thông báo qua email.</b></p>'
    khoi_bao = ('<div style="margin-top:10px;border-top:1px solid #dfe3e8;padding-top:8px">'
                '<b>Thông báo cho người dùng qua email:</b> <code>%s</code><ul>%s</ul>'
                '<form method="post" action="bao-lai?id=%s"><button class="phu" type="submit" style="padding:7px 14px">Gửi lại thông báo qua email</button></form>%s</div>'
                % (html.escape((v.get('email') or '') or email_mac_dinh() or '(chưa đặt)'), dong_bao, v['id'], canh_bao_ui))
    dau = '''<!doctype html><html lang="vi"><meta charset="utf-8"><title>Hỗn hợp #%s</title>

%s
<base href="./"><script>document.addEventListener("DOMContentLoaded",function(){var B=(location.pathname.indexOf("/calib")===0)?"/calib/":"/";var b=document.querySelector("base");if(b&&b.getAttribute("href")!==B)b.href=B;});</script><style>%s</style><header><b>HỖN HỢP #%s</b><span>%s</span></header><main>
<div class="the"><p><b>Người nhập:</b> %s · <b>Lúc:</b> %s · Trạng thái: %s</p><p><small>%s</small></p>
<p><small><b>Điều kiện chuẩn:</b> %s</small></p>
<p><b>Thành phần</b> (tổng %s%%):</p><table><tr><th>Mã</th><th>Tên</th><th>Tỉ lệ (%% trong base)</th></tr>%s</table>
<p><small>Ghi chú: %s</small></p>
<p><small><a href="./">← Tạo hỗn hợp mới</a> · <a href="lich-su">Lịch sử</a>%s</small></p>%s</div>''' % (
        v['id'], tu_cap_nhat, CSS, v['id'], tt, html.escape(v['nguoi_nhap'] or ''), v['tao_luc'], tt, thanh_agent,
        html.escape(v['dieu_kien'] or DIEU_KIEN), v['tong_ti_le'], hang,
        html.escape(v['ghi_chu'] or '—'), (' · <a href="tep?id=%s&d=%s">tệp dự đoán</a>' % (v['id'], urllib.parse.quote(os.path.basename(v['du_doan_file'] or '')))) if v['du_doan_file'] else '',
        khoi_bao)
    # khối dự đoán
    kq = '<div class="the"><h3 style="margin-top:0">Dự đoán của AGY</h3>'
    if q.get('sai_pass'):
        kq = ('<div class="the" style="border-color:#8a1414;background:#ffecec"><b style="color:#8a1414">✖ Sai mật khẩu xác nhận.</b> '
              'CSDL B KHÔNG bị thay đổi. Nhập lại mật khẩu hiệu chỉnh ở mục ĐỀ XUẤT bên dưới.</div>') + kq
    if q.get('da_ap'):
        kq = ('<div class="the" style="border-color:#0b6b3a;background:#eafaf0"><b>✔ Đã ghi %s màu vào CSDL B.</b> '
              'Xem chi tiết ở mục <a href="hieu-chinh">Lịch sử hiệu chỉnh</a>.</div>' % q['da_ap'][0]) + kq
    if q.get('da_tai'):
        kq = ('<div class="the" style="border-color:#0b6b3a;background:#eafaf0"><b>✔ Đã nhận ảnh thực tế.</b> '
              'Agent sẽ tự chạy bước đối chiếu — trang tự cập nhật mỗi 10 giây; khi xong sẽ hiện ΔE và bảng hiệu chỉnh.</div>') + kq
    if v['du_doan_hex']:
        g = json.loads(v['du_doan_ghi_chu'] or '{}')
        kq += ('<p>%s</p><p><b>L*a*b*:</b> %s / %s / %s · <b>Phương pháp:</b> %s · <b>Độ tin cậy:</b> %s · <b>Sai số dự kiến:</b> ΔE ≈ %s</p>'
               '<p><small>Thời gian: %s (%.0f s)</small></p>' % (
                   swatch(v['du_doan_hex']), v['du_doan_L'], v['du_doan_a'], v['du_doan_b'],
                   html.escape(str(g.get('phuong_phap', ''))[:300]), html.escape(str(g.get('do_tin_cay', ''))),
                   g.get('sai_so_du_kien_deltaE', ''), v['du_doan_luc'], v['du_doan_giay'] or 0))
    elif v['trang_thai'] == 'cho_du_doan':
        kq += '<p><small>Agent đang dự đoán… (trang tự cập nhật mỗi 10 giây)</small></p>'
    else:
        kq += '<p style="color:#8a1414">Chưa có dự đoán: %s</p>' % html.escape((v['loi'] or '')[:300])
    kq += '</div>'
    # khối tải ảnh thực tế
    tai = ''
    chua_dc = v['trang_thai'] in ('cho_du_doan', 'da_du_doan', 'cho') or not v['du_doan_hex']
    if v['du_doan_hex'] and chua_dc and not v['thuc_te_hex']:
        tai = ('<div class="the"><h3 style="margin-top:0">Ảnh màu hỗn hợp THỰC TẾ</h3>'
               '<p><small>Ảnh vuông góc, đủ sáng, có ô giấy trắng/đen trong khung để agent so màu.</small></p>'
               '<form method="post" action="tai-anh?id=%s" enctype="multipart/form-data">'
               '<input type="file" name="anh" accept="image/*" required>'
               '<button type="submit">Tải lên &amp; cho AGY đối chiếu</button></form></div>' % v['id'])
    if v['anh_thuc_te'] and (v['thu_muc']):
        tai += ('<div class="the"><h3 style="margin-top:0">Ảnh anh đã tải</h3>'
                '<img src="tep?id=%s&d=%s" style="max-width:420px">' % (v['id'], urllib.parse.quote('anh/' + os.path.basename(v['anh_thuc_te']))))
    if v['thuc_te_hex']:
        dE = v['delta_E'] or 0
        con0 = db()
        cho_ap = con0.execute('SELECT id,mau_ten,gia_tri_cu,gia_tri_moi,lab_moi,ly_do FROM hieu_chinh_mau '
                              'WHERE hon_hop_id=? AND da_ap_dung=0 ORDER BY id', (v['id'],)).fetchall()
        con0.close()
        mau_de = 'de' if dE and dE >= 3 else ''
        tai += ('<h3>Đối chiếu</h3><p>%s</p><p><b>ΔE = <span class="tt %s">%s</span></b></p><p><small>%s</small></p>'
                '<p><small>Kết luận: %s</small></p>' % (swatch(v['thuc_te_hex']), mau_de, dE, v['doi_chieu_luc'] or '',
                                                        html.escape((v['ket_luan'] or '')[:800])))
        con = db()
        hc = con.execute('SELECT mau_ten,gia_tri_cu,gia_tri_moi,ly_do FROM hieu_chinh_mau WHERE hon_hop_id=? AND da_ap_dung=1', (v['id'],)).fetchall()
        con.close()
        if hc:
            tai += ('<p><b>Hiệu chỉnh đã ghi vào CSDL B:</b></p><table><tr><th>Màu</th><th>Hệ số cũ</th><th>Hệ số mới</th><th>Lý do</th></tr>'
                    + ''.join('<tr><td>%s</td><td>%s</td><td><b>%s</b></td><td><small>%s</small></td></tr>' % tuple(html.escape(str(x)) for x in r) for r in hc) + '</table>')
        elif v['trang_thai'] in ('cho_xac_nhan', 'da_doi_chieu'):
            tai += '<p><small>AGY kết luận chưa cần hiệu chỉnh (ΔE nhỏ) — không có đề xuất nào.</small></p>'
    elif v['trang_thai'] == 'da_du_doan':
        tai += '<p><small>Đã nhận ảnh — agent sẽ tự đối chiếu (trang tự cập nhật mỗi 10 giây).</small></p>'
    if v['thuc_te_hex'] and cho_ap:
        dong = ''.join('<tr><td>%s</td><td>%s</td><td><b>%s</b></td><td><small>%s</small></td></tr>'
                       % (html.escape(r[1] or ''), html.escape(str(r[2])), html.escape(str(r[3])), html.escape((r[5] or '')[:160]))
                       for r in cho_ap)
        tai += ('<h3 style="color:#8a1414">ĐỀ XUẤT HIỆU CHỈNH — chưa ghi vào CSDL B</h3>'
                '<table><tr><th>Màu</th><th>Hệ số cũ</th><th>Hệ số mới</th><th>Lý do</th></tr>%s</table>'
                '<form method="post" action="ap-dung?id=%s" style="margin-top:8px">'
                '<label>Nhập mật khẩu xác nhận để GHI vào CSDL B</label>'
                '<input type="password" name="mat_khau" required placeholder="mật khẩu hiệu chỉnh" style="max-width:240px">'
                '<button type="submit">Xác nhận &amp; ghi vào CSDL B</button></form>'
                '<p><small>Chưa nhập mật khẩu thì CSDL B giữ nguyên — AGY chỉ đề xuất.</small></p>' % (dong, v['id']))
    tai += '</div>'
    return (dau + kq + tai + '</main>').encode('utf-8')


class H(BaseHTTPRequestHandler):
    server_version = 'CalibCSDLB/1.0'

    def log_message(self, *a):
        pass

    def _tach(self):
        u = urllib.parse.urlparse(self.path)
        return ('/calib', u.path[6:] or '/') if u.path.startswith('/calib') else ('', u.path)

    def _ok(self):
        c = SimpleCookie(self.headers.get('Cookie') or '')
        m = c.get('calib')
        return bool(m and hmac.compare_digest(m.value, ma_phien()))

    def _gui(self, ma, nd, loai='text/html; charset=utf-8'):
        self.send_response(ma)
        self.send_header('Content-Type', loai)
        self.send_header('Content-Length', str(len(nd)))
        self.send_header('Cache-Control', 'no-store, must-revalidate')   # tránh trình duyệt giữ bản cũ gây lỗi ảo
        self.send_header('Pragma', 'no-cache')
        self.end_headers()
        self.wfile.write(nd)

    def _json(self, o):
        self._gui(200, json.dumps(o, ensure_ascii=False).encode('utf-8'), 'application/json; charset=utf-8')

    def do_GET(self):
        tien_to, duong = self._tach()
        q = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
        if not self._ok():
            return self._gui(200, trang_dang_nhap().encode('utf-8'))
        if duong in ('/', ''):
            self._gui(200, trang_chinh().encode('utf-8'))
        elif duong == '/thoat':
            self.send_response(302)
            self.send_header('Set-Cookie', 'calib=; Path=/; Max-Age=0')
            self.send_header('Location', './')
            self.end_headers()
        elif duong == '/hon-hop':
            con = db()
            con.row_factory = sqlite3.Row
            v = con.execute('SELECT * FROM hon_hop WHERE id=?', (q.get('id', ['0'])[0],)).fetchone()
            con.close()
            if not v:
                return self._gui(404, b'Khong thay')
            self._gui(200, trang_chi_tiet(dict(v), q))
        elif duong == '/trang-thai':
            con = db()
            r = con.execute('SELECT trang_thai,du_doan_hex,delta_E,loi FROM hon_hop WHERE id=?', (q.get('id', ['0'])[0],)).fetchone()
            con.close()
            self._json({'trang_thai': r[0] if r else 'khong-thay', 'du_doan_hex': (r[1] or '') if r else '',
                        'delta_E': r[2] if r else None, 'loi': ((r[3] or '')[:200] if r else '')})
        elif duong == '/lich-su':
            con = db()
            hang = ''.join('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td><span class="tt %s">%s</span></td><td>%s</td></tr>'
                           % (r[0], r[1], html.escape((r[2] or '')[:40]), html.escape((r[3] or '')[:26]), swatch(r[4]),
                              r[5], r[5], ('<a href="hon-hop?id=%s">mở</a>' % r[0]))
                           for r in con.execute('SELECT id,tao_luc,nguoi_nhap,thanh_phan,du_doan_hex,trang_thai,delta_E FROM hon_hop ORDER BY id DESC LIMIT 100'))
            con.close()
            self._gui(200, ('<base href="./"><script>document.addEventListener("DOMContentLoaded",function(){var B=(location.pathname.indexOf("/calib")===0)?"/calib/":"/";var b=document.querySelector("base");if(b&&b.getAttribute("href")!==B)b.href=B;});</script><style>%s</style><header><b>LỊCH SỬ HỖN HỢP</b></header><main><div class="the">'
                            '<table><tr><th>#</th><th>Lúc</th><th>Người</th><th>Thành phần</th><th>Dự đoán</th><th>Trạng thái</th><th></th></tr>%s</table>'
                            '<p><a href="./">← Tạo hỗn hợp mới</a></p></div></main>' % (CSS, hang)).encode('utf-8'))
        elif duong == '/hieu-chinh':
            con = db()
            m = ''.join('<tr><td>%s</td><td>%s</td><td><b>%s</b></td><td>%s</td><td>%s</td></tr>'
                        % (html.escape(r[0]), html.escape(r[1]), r[2], r[3] or 0, html.escape((r[4] or '')[:30]))
                        for r in con.execute('SELECT ten,ma,COALESCE(he_so_cuong_do,1.0),COALESCE(so_lan_hieu_chinh,0),ngay_hieu_chinh FROM mau ORDER BY ten'))
            hc = ''.join('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'
                         % (r[0], html.escape(r[1] or ''), r[2], r[3], r[4], r[5], html.escape((r[6] or '')[:70]))
                         for r in con.execute('SELECT ngay,mau_ten,gia_tri_cu,gia_tri_moi,delta_E_truoc,hon_hop_id,ly_do FROM hieu_chinh_mau ORDER BY id DESC LIMIT 200'))
            con.close()
            self._gui(200, ('<base href="./"><script>document.addEventListener("DOMContentLoaded",function(){var B=(location.pathname.indexOf("/calib")===0)?"/calib/":"/";var b=document.querySelector("base");if(b&&b.getAttribute("href")!==B)b.href=B;});</script><style>%s</style><header><b>HIỆU CHỈNH CSDL B</b></header><main>'
                            '<div class="the"><h3 style="margin-top:0">Hệ số cường độ hiện tại của từng màu</h3>'
                            '<table><tr><th>Màu</th><th>Mã</th><th>Hệ số cường độ</th><th>Số lần hiệu chỉnh</th><th>Lần cuối</th></tr>%s</table>'
                            '<p><small>1.0 = như số đo gốc · &gt;1 = màu mạnh hơn giả định (giảm tỉ lệ dùng) · &lt;1 = yếu hơn (tăng tỉ lệ).</small></p></div>'
                            '<div class="the"><h3 style="margin-top:0">Lịch sử hiệu chỉnh</h3>'
                            '<table><tr><th>Ngày</th><th>Màu</th><th>Cũ</th><th>Mới</th><th>ΔE trước</th><th>Hỗn hợp</th><th>Lý do</th></tr>%s</table>'
                            '<p><a href="./">← Tạo hỗn hợp mới</a></p></div></main>' % (CSS, m, hc)).encode('utf-8'))
        elif duong == '/tep':
            con = db()
            r = con.execute('SELECT thu_muc FROM hon_hop WHERE id=?', (q.get('id', ['0'])[0],)).fetchone()
            con.close()
            if not r:
                return self._gui(404, b'x')
            p = os.path.normpath(os.path.join(r[0], q.get('d', [''])[0]))
            if not p.startswith(os.path.normpath(r[0])) or not os.path.exists(p):
                return self._gui(404, b'x')
            self._gui(200, open(p, 'rb').read(), mimetypes.guess_type(p)[0] or 'application/octet-stream')
        else:
            self._gui(404, b'Khong co')

    def do_POST(self):
        tien_to, duong = self._tach()
        if duong == '/dang-nhap':
            dai = int(self.headers.get('Content-Length') or 0)
            fs = cgi.FieldStorage(fp=io.BytesIO(self.rfile.read(dai)), headers=self.headers,
                                  environ={'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': self.headers.get('Content-Type', '')})
            if fs.getvalue('mat_khau', '') == mat_khau():
                self.send_response(302)
                self.send_header('Set-Cookie', 'calib=%s; Path=/; Max-Age=43200; HttpOnly; SameSite=Lax' % ma_phien())
                self.send_header('Location', './')
                self.end_headers()
                return
            return self._gui(200, trang_dang_nhap('Sai mật khẩu — thử lại.').encode('utf-8'))
        if not self._ok():
            return self._gui(403, b'Chua dang nhap')
        dai = int(self.headers.get('Content-Length') or 0)
        if duong == '/tao-hon-hop':
            fs = cgi.FieldStorage(fp=io.BytesIO(self.rfile.read(dai)), headers=self.headers,
                                  environ={'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': self.headers.get('Content-Type', '')})
            try:
                tp = json.loads(fs.getvalue('thanh_phan', '[]'))
            except Exception:
                return self._json({'loi': 'thanh_phan không phải JSON'})
            if not tp:
                return self._json({'loi': 'chưa có thành phần'})
            con = db()
            ten = {r[0]: r[1] for r in con.execute('SELECT ma,ten FROM mau')}
            for x in tp:
                x['ten'] = ten.get(x['ma'], x['ma'])
            cur = con.execute('''INSERT INTO hon_hop (tao_luc,nguoi_nhap,ghi_chu,thanh_phan,tong_ti_le,trang_thai,dieu_kien,email)
                                 VALUES (?,?,?,?,?, 'cho_du_doan', ?,?)''',
                              (time.strftime('%Y-%m-%d %H:%M:%S'), fs.getvalue('nguoi_nhap', ''), fs.getvalue('ghi_chu', ''),
                               json.dumps(tp, ensure_ascii=False), float(fs.getvalue('tong_ti_le', 0) or 0), DIEU_KIEN,
                               (fs.getvalue('email', '') or '').strip() or email_mac_dinh()))
            vid = cur.lastrowid
            d = os.path.join(VIEC, 'H%04d_%s' % (vid, time.strftime('%d%m')))
            os.makedirs(os.path.join(d, 'anh'), exist_ok=True)
            con.execute('UPDATE hon_hop SET thu_muc=? WHERE id=?', (d, vid))
            con.commit()
            con.close()
            print('[%s] hỗn hợp #%d từ %s | %d thành phần' % (time.strftime('%H:%M:%S'), vid, fs.getvalue('nguoi_nhap', ''), len(tp)))
            return self._json({'id': vid})
        if duong == '/ap-dung':
            dai = int(self.headers.get('Content-Length') or 0)
            fs = cgi.FieldStorage(fp=io.BytesIO(self.rfile.read(dai)), headers=self.headers,
                                  environ={'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': self.headers.get('Content-Type', '')})
            hid = int(urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query).get('id', ['0'])[0])
            if fs.getvalue('mat_khau', '').strip() != pass_hieu_chinh():
                print('[%s] hỗn hợp #%d: SAI mật khẩu xác nhận hiệu chỉnh' % (time.strftime('%H:%M:%S'), hid))
                self.send_response(302)
                self.send_header('Location', './hon-hop?id=%d&sai_pass=1' % hid)
                self.end_headers()
                return
            import importlib.util
            spec = importlib.util.spec_from_file_location('wcal', os.path.join(GOC, 'worker_calib.py'))
            m = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(m)
            con = db()
            n, mo = m.ap_dung(con, hid)
            con.close()
            print('[%s] hỗn hợp #%d: ĐÃ GHI hiệu chỉnh %d màu vào CSDL B' % (time.strftime('%H:%M:%S'), hid, n))
            self.send_response(302)
            self.send_header('Location', './hon-hop?id=%d&da_ap=%d' % (hid, n))
            self.end_headers()
            return
        if duong == '/bao-lai':
            fs = cgi.FieldStorage(fp=io.BytesIO(self.rfile.read(dai)), headers=self.headers,
                                  environ={'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': self.headers.get('Content-Type', '')})
            hid = int(urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query).get('id', ['0'])[0])
            import importlib.util
            spec = importlib.util.spec_from_file_location('wcal', os.path.join(GOC, 'worker_calib.py'))
            m = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(m)
            try:
                ok, tt = m.gui_lai_bao(hid)
            except Exception as e:
                ok, tt = False, 'lỗi: %s' % e
            print('[%s] hỗn hợp #%d: GỬI LẠI THÔNG BÁO → %s' % (time.strftime('%H:%M:%S'), hid, tt))
            self.send_response(302)
            self.send_header('Location', './hon-hop?id=%d&da_bao=%d' % (hid, 1 if ok else 0))
            self.end_headers()
            return
        if duong == '/tai-anh':
            hid = int(urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query).get('id', ['0'])[0])
            fs = cgi.FieldStorage(fp=io.BytesIO(self.rfile.read(dai)), headers=self.headers,
                                  environ={'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': self.headers.get('Content-Type', '')})
            con = db()
            r = con.execute('SELECT thu_muc FROM hon_hop WHERE id=?', (hid,)).fetchone()
            if not r or 'anh' not in fs:
                con.close()
                return self._gui(400, b'Thieu anh')
            p = os.path.join(r[0], 'anh', os.path.basename(fs['anh'].filename or 'anh.jpg'))
            open(p, 'wb').write(fs['anh'].file.read())
            con.execute("UPDATE hon_hop SET anh_thuc_te=? WHERE id=?", (p, hid))
            con.commit()
            con.close()
            print('[%s] hỗn hợp #%d: đã nhận ảnh thực tế' % (time.strftime('%H:%M:%S'), hid))
            self.send_response(302)
            self.send_header('Location', './hon-hop?id=%d&da_tai=1' % hid)
            self.end_headers()
            return
        self._gui(404, b'Khong co')


if __name__ == '__main__':
    print('CALIB CSDL B: http://127.0.0.1:%d  (mật khẩu: %s)' % (CONG, mat_khau()))
    ThreadingHTTPServer(('127.0.0.1', CONG), H).serve_forever()
