main_cr1.ino INO FILE
#include <WiFi.h>
#include <WebServer.h>
#include <SPI.h>
#include <MFRC522.h>

// ================= KONFIGURASI WIFI AP =================
const char* ssid = "Innoiti-CR1";
const char* password = "12345678";

IPAddress local_ip(192, 168, 4, 1);
IPAddress gateway(192, 168, 4, 1);
IPAddress subnet(255, 255, 255, 0);

// ================= KONFIGURASI RFID =================
#define SS_PIN   5
#define RST_PIN  22

MFRC522 mfrc522(SS_PIN, RST_PIN);
MFRC522::MIFARE_Key key;

// ================= WEB SERVER =================
WebServer server(80);

// ================= STATE SESSION =================
struct SessionData {
  String uid;
  String nama;
  String id;
  String kode;
  bool cardDetected;
  bool writeSuccess;
  String errorMessage;
};

SessionData session;

// ================= STATE DETEKSI KARTU STABIL =================
bool cardActive = false;
bool cardCurrentlyPresent = false;
unsigned long lastCardReadTime = 0;
MFRC522::Uid lastUid;

// ================= HELPER FUNCTIONS =================
String uidToString(byte* buffer, byte bufferSize) {
  String uid = "";
  for (byte i = 0; i < bufferSize; i++) {
    if (buffer[i] < 0x10) uid += "0";
    uid += String(buffer[i], HEX);
  }
  uid.toUpperCase();
  return uid;
}

bool isSameUid(MFRC522::Uid& uid1, MFRC522::Uid& uid2) {
  if (uid1.size != uid2.size) return false;
  for (byte i = 0; i < uid1.size; i++) {
    if (uid1.uidByte[i] != uid2.uidByte[i]) return false;
  }
  return true;
}

void copyUid(MFRC522::Uid& src, MFRC522::Uid& dst) {
  dst.size = src.size;
  for (byte i = 0; i < src.size; i++) {
    dst.uidByte[i] = src.uidByte[i];
  }
}

String readBlock(int block) {
  byte buffer[18];
  byte size = sizeof(buffer);
  
  MFRC522::StatusCode status = mfrc522.PCD_Authenticate(
    MFRC522::PICC_CMD_MF_AUTH_KEY_A,
    block,
    &key,
    &(mfrc522.uid)
  );
  
  if (status != MFRC522::STATUS_OK) {
    return "";
  }
  
  status = mfrc522.MIFARE_Read(block, buffer, &size);
  if (status != MFRC522::STATUS_OK) {
    return "";
  }
  
  String result = "";
  for (byte i = 0; i < 16; i++) {
    if (buffer[i] == 0) break;
    result += (char)buffer[i];
  }
  result.trim();
  return result;
}

bool readFromCard(String &nama, String &id, String &kode) {
  nama = readBlock(4);
  id = readBlock(5);
  kode = readBlock(6);
  
  // Jika semua blok gagal dibaca
  if (nama == "" && id == "" && kode == "") {
    return false;
  }
  return true;
}

bool writeBlock(int block, String data) {
  data.trim();
  if (data.length() > 16) data = data.substring(0, 16);
  
  byte buffer[16];
  memset(buffer, 0, 16);
  for (int i = 0; i < data.length(); i++) buffer[i] = (byte)data[i];
  
  MFRC522::StatusCode status = mfrc522.PCD_Authenticate(
    MFRC522::PICC_CMD_MF_AUTH_KEY_A,
    block,
    &key,
    &(mfrc522.uid)
  );
  
  if (status != MFRC522::STATUS_OK) {
    Serial.printf("Auth gagal blok %d: %s\n", block, mfrc522.GetStatusCodeName(status));
    return false;
  }
  
  status = mfrc522.MIFARE_Write(block, buffer, 16);
  if (status != MFRC522::STATUS_OK) {
    Serial.printf("Write gagal blok %d: %s\n", block, mfrc522.GetStatusCodeName(status));
    return false;
  }
  
  Serial.printf("Blok %d berhasil ditulis: %s\n", block, data.c_str());
  return true;
}

bool writeToCard() {
  bool success1 = writeBlock(4, session.nama);
  bool success2 = writeBlock(5, session.id);
  bool success3 = writeBlock(6, session.kode);
  
  if (success1 && success2 && success3) {
    session.writeSuccess = true;
    return true;
  }
  session.writeSuccess = false;
  return false;
}

// ================= HTML PAGE - HOME (PILIH MODE) =================
String getHomePage() {
  return R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Innoiti CR-1</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
      background: linear-gradient(135deg, #106CFF 0%, #2B1B7D 100%);
      min-height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 24px;
    }
    
    .wrapper {
      max-width: 420px;
      width: 100%;
      text-align: center;
    }
    
    .logo {
      margin-bottom: 48px;
    }
    
    .logo h1 {
      color: white;
      font-size: 32px;
      font-weight: 600;
      letter-spacing: -0.5px;
      margin-bottom: 8px;
    }
    
    .logo p {
      color: rgba(255,255,255,0.7);
      font-size: 14px;
    }
    
    .mode-container {
      display: flex;
      flex-direction: column;
      gap: 20px;
      margin-bottom: 32px;
    }
    
    .mode-card {
      background: rgba(255,255,255,0.05);
      backdrop-filter: blur(10px);
      border-radius: 24px;
      padding: 32px 24px;
      border: 1px solid rgba(255,255,255,0.1);
      cursor: pointer;
      transition: all 0.3s ease;
      text-decoration: none;
      display: block;
    }
    
    .mode-card:hover {
      background: rgba(255,255,255,0.1);
      transform: translateY(-4px);
    }
    
    .mode-icon {
      font-size: 48px;
      margin-bottom: 16px;
    }
    
    .mode-title {
      font-size: 22px;
      font-weight: 600;
      color: white;
      margin-bottom: 8px;
    }
    
    .mode-desc {
      font-size: 14px;
      color: rgba(255,255,255,0.6);
    }
    
    .footer {
      font-size: 12px;
      color: rgba(255,255,255,0.4);
      margin-top: 32px;
    }
  </style>
</head>
<body>
<div class="wrapper">
  <div class="logo">
    <h1>Innoiti CR-1</h1>
    <p>RFID Reader / Writer</p>
  </div>
  
  <div class="mode-container">
    <a href="/write" class="mode-card">
      <div class="mode-icon">✍️</div>
      <div class="mode-title">Mode Tulis</div>
      <div class="mode-desc">Tulis data ke kartu RFID</div>
    </a>
    
    <a href="/read" class="mode-card">
      <div class="mode-icon">📖</div>
      <div class="mode-title">Mode Baca</div>
      <div class="mode-desc">Baca data dari kartu RFID</div>
    </a>
  </div>
  
  <div class="footer">Pilih mode yang diinginkan</div>
</div>
</body>
</html>
  )rawliteral";
}

// ================= HTML PAGE - SCAN UNTUK MODE TULIS =================
String getWriteScanPage() {
  return R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Innoiti CR-1 - Scan Kartu</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
      background: linear-gradient(135deg, #106CFF 0%, #2B1B7D 100%);
      min-height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 24px;
    }
    
    .wrapper {
      max-width: 420px;
      width: 100%;
      text-align: center;
    }
    
    .back-link {
      display: inline-block;
      color: rgba(255,255,255,0.7);
      text-decoration: none;
      font-size: 14px;
      margin-bottom: 24px;
    }
    
    .back-link:hover {
      color: white;
    }
    
    .logo {
      margin-bottom: 32px;
    }
    
    .logo h1 {
      color: white;
      font-size: 28px;
      font-weight: 600;
      letter-spacing: -0.5px;
      margin-bottom: 4px;
    }
    
    .logo p {
      color: rgba(255,255,255,0.7);
      font-size: 14px;
    }
    
    .step-indicator {
      display: flex;
      justify-content: space-between;
      margin-bottom: 48px;
      padding: 0 16px;
    }
    
    .step-item {
      text-align: center;
      flex: 1;
    }
    
    .step {
      width: 32px;
      height: 32px;
      border-radius: 50%;
      background: rgba(255,255,255,0.2);
      color: white;
      line-height: 32px;
      text-align: center;
      font-weight: 600;
      font-size: 14px;
      margin: 0 auto 8px;
    }
    
    .step.active {
      background: white;
      color: #106CFF;
    }
    
    .step-label {
      font-size: 12px;
      color: rgba(255,255,255,0.6);
    }
    
    .card-area {
      background: rgba(255,255,255,0.05);
      backdrop-filter: blur(10px);
      border-radius: 32px;
      padding: 48px 32px;
      margin-bottom: 24px;
      border: 1px solid rgba(255,255,255,0.1);
    }
    
    .icon {
      font-size: 64px;
      margin-bottom: 24px;
    }
    
    .card-area h3 {
      color: white;
      font-size: 20px;
      font-weight: 500;
      margin-bottom: 16px;
    }
    
    .status {
      padding: 12px 20px;
      border-radius: 100px;
      font-size: 14px;
      font-weight: 500;
      display: inline-block;
    }
    
    .status.waiting {
      background: rgba(255,193,7,0.2);
      color: #FFD700;
    }
    
    .status.success {
      background: rgba(40,167,69,0.2);
      color: #4ade80;
    }
    
    .uid-display {
      background: rgba(255,255,255,0.1);
      border-radius: 16px;
      padding: 12px;
      font-family: 'SF Mono', Monaco, monospace;
      font-size: 18px;
      font-weight: 600;
      color: white;
      margin-top: 16px;
      letter-spacing: 1px;
    }
    
    .spinner {
      display: inline-block;
      width: 16px;
      height: 16px;
      border: 2px solid rgba(255,255,255,0.3);
      border-top-color: white;
      border-radius: 50%;
      animation: spin 0.8s linear infinite;
      margin-right: 8px;
      vertical-align: middle;
    }
    
    @keyframes spin {
      to { transform: rotate(360deg); }
    }
    
    .footer {
      font-size: 12px;
      color: rgba(255,255,255,0.5);
      margin-top: 24px;
    }
  </style>
</head>
<body>
<div class="wrapper">
  <a href="/" class="back-link">← Kembali ke Menu</a>
  
  <div class="logo">
    <h1>Mode Tulis</h1>
    <p>RFID Writer</p>
  </div>
  
  <div class="step-indicator">
    <div class="step-item"><div class="step active">1</div><div class="step-label">Scan Kartu</div></div>
    <div class="step-item"><div class="step">2</div><div class="step-label">Isi Data</div></div>
    <div class="step-item"><div class="step">3</div><div class="step-label">Selesai</div></div>
  </div>
  
  <div class="card-area">
    <div class="icon">💳</div>
    <h3>Tempelkan Kartu RFID</h3>
    <div id="statusBox" class="status waiting">⏳ Menunggu kartu...</div>
    <div id="uidBox" style="display:none;">
      <div class="uid-display" id="uidValue"></div>
    </div>
  </div>
  
  <div class="footer">Kartu akan otomatis melanjutkan</div>
</div>

<script>
  let cardDetected = false;
  let uid = "";
  let redirecting = false;
  
  async function checkStatus() {
    if (redirecting) return;
    try {
      const res = await fetch('/api/status');
      const data = await res.json();
      
      if (data.cardDetected && !cardDetected) {
        cardDetected = true;
        uid = data.uid;
        document.getElementById('statusBox').innerHTML = '<span class="spinner"></span> Kartu terdeteksi!';
        document.getElementById('statusBox').className = 'status success';
        document.getElementById('uidBox').style.display = 'block';
        document.getElementById('uidValue').innerHTML = 'UID: ' + uid;
        
        redirecting = true;
        setTimeout(() => {
          window.location.href = '/write/form?uid=' + uid;
        }, 800);
      } else if (!data.cardDetected && cardDetected) {
        cardDetected = false;
        redirecting = false;
        document.getElementById('statusBox').innerHTML = '⏳ Menunggu kartu...';
        document.getElementById('statusBox').className = 'status waiting';
        document.getElementById('uidBox').style.display = 'none';
      }
    } catch(e) {}
  }
  
  setInterval(checkStatus, 500);
  checkStatus();
</script>
</body>
</html>
  )rawliteral";
}

// ================= HTML PAGE - FORM TULIS =================
String getWriteFormPage(String uid) {
  String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Innoiti CR-1 - Isi Data</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
      background: linear-gradient(135deg, #106CFF 0%, #2B1B7D 100%);
      min-height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 24px;
    }
    
    .wrapper {
      max-width: 420px;
      width: 100%;
      text-align: center;
    }
    
    .back-link {
      display: inline-block;
      color: rgba(255,255,255,0.7);
      text-decoration: none;
      font-size: 14px;
      margin-bottom: 24px;
    }
    
    .back-link:hover {
      color: white;
    }
    
    .logo {
      margin-bottom: 32px;
    }
    
    .logo h1 {
      color: white;
      font-size: 28px;
      font-weight: 600;
      letter-spacing: -0.5px;
      margin-bottom: 4px;
    }
    
    .logo p {
      color: rgba(255,255,255,0.7);
      font-size: 14px;
    }
    
    .step-indicator {
      display: flex;
      justify-content: space-between;
      margin-bottom: 48px;
      padding: 0 16px;
    }
    
    .step-item {
      text-align: center;
      flex: 1;
    }
    
    .step {
      width: 32px;
      height: 32px;
      border-radius: 50%;
      background: rgba(255,255,255,0.2);
      color: white;
      line-height: 32px;
      text-align: center;
      font-weight: 600;
      font-size: 14px;
      margin: 0 auto 8px;
    }
    
    .step.completed {
      background: white;
      color: #106CFF;
    }
    
    .step.active {
      background: white;
      color: #106CFF;
    }
    
    .step-label {
      font-size: 12px;
      color: rgba(255,255,255,0.6);
    }
    
    .info-card {
      background: rgba(255,255,255,0.05);
      backdrop-filter: blur(10px);
      border-radius: 24px;
      padding: 20px;
      margin-bottom: 24px;
      border: 1px solid rgba(255,255,255,0.1);
    }
    
    .info-card strong {
      color: white;
      font-size: 14px;
    }
    
    .uid-display {
      font-family: 'SF Mono', Monaco, monospace;
      font-size: 16px;
      font-weight: 600;
      color: rgba(255,255,255,0.9);
      background: rgba(255,255,255,0.1);
      padding: 8px 12px;
      border-radius: 12px;
      margin-top: 8px;
      display: inline-block;
    }
    
    .info-card small {
      color: rgba(255,255,255,0.5);
      font-size: 11px;
      display: block;
      margin-top: 8px;
    }
    
    .form-group {
      margin-bottom: 16px;
    }
    
    input {
      width: 100%;
      padding: 14px 18px;
      background: rgba(255,255,255,0.1);
      border: 1px solid rgba(255,255,255,0.2);
      border-radius: 16px;
      font-size: 16px;
      color: white;
      transition: all 0.2s;
      outline: none;
    }
    
    input::placeholder {
      color: rgba(255,255,255,0.5);
    }
    
    input:focus {
      border-color: rgba(255,255,255,0.5);
      background: rgba(255,255,255,0.15);
    }
    
    button {
      width: 100%;
      padding: 14px;
      background: white;
      border: none;
      border-radius: 16px;
      font-size: 16px;
      font-weight: 600;
      color: #106CFF;
      cursor: pointer;
      transition: all 0.2s;
      margin-top: 8px;
    }
    
    button:hover {
      transform: scale(0.98);
      background: #f0f0f0;
    }
    
    button:disabled {
      opacity: 0.6;
      cursor: not-allowed;
      transform: none;
    }
    
    .warning {
      background: rgba(255,193,7,0.15);
      border-radius: 12px;
      padding: 12px;
      font-size: 12px;
      margin-top: 16px;
      color: #FFD700;
    }
    
    .warning.error {
      background: rgba(220,53,69,0.15);
      color: #ff6b6b;
    }
    
    .warning.success {
      background: rgba(40,167,69,0.15);
      color: #4ade80;
    }
    
    .footer {
      font-size: 12px;
      color: rgba(255,255,255,0.4);
      margin-top: 24px;
    }
  </style>
</head>
<body>
<div class="wrapper">
  <a href="/" class="back-link">← Kembali ke Menu</a>
  
  <div class="logo">
    <h1>Mode Tulis</h1>
    <p>RFID Writer</p>
  </div>
  
  <div class="step-indicator">
    <div class="step-item"><div class="step completed">✓</div><div class="step-label">Scan Kartu</div></div>
    <div class="step-item"><div class="step active">2</div><div class="step-label">Isi Data</div></div>
    <div class="step-item"><div class="step">3</div><div class="step-label">Selesai</div></div>
  </div>
  
  <div class="info-card">
    <strong>🔖 Kartu terdeteksi</strong>
    <div class="uid-display">)rawliteral" + uid + R"rawliteral(</div>
    <small>Jangan lepaskan kartu sampai selesai</small>
  </div>
  
  <form id="dataForm">
    <div class="form-group">
      <input type="text" id="nama" placeholder="Nama" required autocomplete="off">
    </div>
    <div class="form-group">
      <input type="text" id="id" placeholder="ID" required autocomplete="off">
    </div>
    <div class="form-group">
      <input type="text" id="kode" placeholder="Kode" required autocomplete="off">
    </div>
    <button type="submit">Tulis ke Kartu</button>
  </form>
  
  <div id="warningBox" class="warning">Pastikan kartu masih menempel</div>
  <div class="footer">Data akan ditulis ke blok 4, 5, 6 kartu RFID</div>
</div>

<script>
  const form = document.getElementById('dataForm');
  const warningBox = document.getElementById('warningBox');
  let isWriting = false;
  let checkInterval = null;
  let timeoutId = null;
  
  const urlParams = new URLSearchParams(window.location.search);
  const uid = urlParams.get('uid');
  
  async function waitForCardAndWrite(nama, id, kode, uid) {
    let cardDetected = false;
    
    const statusCheck = async () => {
      try {
        const res = await fetch('/api/status');
        const data = await res.json();
        
        if (data.cardDetected && data.uid === uid) {
          cardDetected = true;
          if (checkInterval) clearInterval(checkInterval);
          if (timeoutId) clearTimeout(timeoutId);
          await doWrite(nama, id, kode, uid);
        } else if (!data.cardDetected) {
          warningBox.innerHTML = '⏳ Menunggu kartu terdeteksi...';
          warningBox.className = 'warning error';
        }
      } catch(e) {}
    };
    
    checkInterval = setInterval(statusCheck, 300);
    
    timeoutId = setTimeout(() => {
      if (checkInterval) clearInterval(checkInterval);
      if (!cardDetected) {
        const submitBtn = form.querySelector('button');
        submitBtn.disabled = false;
        submitBtn.textContent = 'Tulis ke Kartu';
        warningBox.innerHTML = '❌ Timeout! Kartu tidak terdeteksi.';
        warningBox.className = 'warning error';
        isWriting = false;
      }
    }, 5000);
  }
  
  async function doWrite(nama, id, kode, uid) {
    const submitBtn = form.querySelector('button');
    submitBtn.textContent = 'Menulis...';
    warningBox.innerHTML = '✍️ Sedang menulis...';
    warningBox.className = 'warning';
    
    try {
      const res = await fetch('/api/write', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ nama, id, kode, uid })
      });
      
      const result = await res.json();
      
      if (result.success) {
        window.location.href = '/write/result?success=1';
      } else {
        window.location.href = '/write/result?success=0&error=' + encodeURIComponent(result.error);
      }
    } catch(e) {
      window.location.href = '/write/result?success=0&error=Network+error';
    }
  }
  
  form.addEventListener('submit', async (e) => {
    e.preventDefault();
    if (isWriting) return;
    isWriting = true;
    
    const nama = document.getElementById('nama').value;
    const id = document.getElementById('id').value;
    const kode = document.getElementById('kode').value;
    
    if (!nama || !id || !kode) {
      warningBox.innerHTML = '❌ Semua field harus diisi!';
      warningBox.className = 'warning error';
      isWriting = false;
      return;
    }
    
    const submitBtn = form.querySelector('button');
    submitBtn.disabled = true;
    submitBtn.textContent = 'Menunggu kartu...';
    warningBox.innerHTML = '🔄 Menunggu kartu (maks 5 detik)...';
    warningBox.className = 'warning error';
    
    await waitForCardAndWrite(nama, id, kode, uid);
  });
  
  async function checkCardStillPresent() {
    try {
      const res = await fetch('/api/status');
      const data = await res.json();
      if (!data.cardDetected) {
        warningBox.innerHTML = '⚠️ Kartu tidak terdeteksi!';
        warningBox.className = 'warning error';
      } else if (data.uid === uid) {
        warningBox.innerHTML = '✅ Kartu siap, klik tombol tulis';
        warningBox.className = 'warning success';
      }
    } catch(e) {}
  }
  
  setInterval(checkCardStillPresent, 1000);
  checkCardStillPresent();
</script>
</body>
</html>
  )rawliteral";
  return html;
}

// ================= HTML PAGE - HASIL TULIS =================
String getWriteResultPage(bool success, String errorMsg = "") {
  String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Innoiti CR-1 - Selesai</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
      background: linear-gradient(135deg, #106CFF 0%, #2B1B7D 100%);
      min-height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 24px;
    }
    
    .wrapper {
      max-width: 420px;
      width: 100%;
      text-align: center;
    }
    
    .back-link {
      display: inline-block;
      color: rgba(255,255,255,0.7);
      text-decoration: none;
      font-size: 14px;
      margin-bottom: 24px;
    }
    
    .back-link:hover {
      color: white;
    }
    
    .logo {
      margin-bottom: 32px;
    }
    
    .logo h1 {
      color: white;
      font-size: 28px;
      font-weight: 600;
      letter-spacing: -0.5px;
      margin-bottom: 4px;
    }
    
    .logo p {
      color: rgba(255,255,255,0.7);
      font-size: 14px;
    }
    
    .step-indicator {
      display: flex;
      justify-content: space-between;
      margin-bottom: 48px;
      padding: 0 16px;
    }
    
    .step-item {
      text-align: center;
      flex: 1;
    }
    
    .step {
      width: 32px;
      height: 32px;
      border-radius: 50%;
      background: rgba(255,255,255,0.2);
      color: white;
      line-height: 32px;
      text-align: center;
      font-weight: 600;
      font-size: 14px;
      margin: 0 auto 8px;
    }
    
    .step.completed {
      background: white;
      color: #106CFF;
    }
    
    .step-label {
      font-size: 12px;
      color: rgba(255,255,255,0.6);
    }
    
    .result-area {
      background: rgba(255,255,255,0.05);
      backdrop-filter: blur(10px);
      border-radius: 32px;
      padding: 48px 32px;
      margin-bottom: 24px;
      border: 1px solid rgba(255,255,255,0.1);
    }
    
    .result-icon {
      font-size: 72px;
      margin-bottom: 24px;
    }
    
    .result-title {
      font-size: 28px;
      font-weight: 700;
      margin-bottom: 12px;
    }
    
    .result-title.success {
      color: #4ade80;
    }
    
    .result-title.error {
      color: #ff6b6b;
    }
    
    .result-message {
      color: rgba(255,255,255,0.7);
      font-size: 14px;
      line-height: 1.6;
    }
    
    .button-group {
      display: flex;
      gap: 12px;
      margin-top: 24px;
    }
    
    button {
      flex: 1;
      padding: 14px;
      background: rgba(255,255,255,0.1);
      border: 1px solid rgba(255,255,255,0.2);
      border-radius: 16px;
      font-size: 14px;
      font-weight: 500;
      color: white;
      cursor: pointer;
      transition: all 0.2s;
    }
    
    button:hover {
      background: rgba(255,255,255,0.2);
    }
    
    .btn-primary {
      background: white;
      color: #106CFF;
    }
    
    .btn-primary:hover {
      transform: scale(0.98);
      background: #f0f0f0;
    }
    
    .footer {
      font-size: 12px;
      color: rgba(255,255,255,0.4);
      margin-top: 24px;
    }
  </style>
</head>
<body>
<div class="wrapper">
  <a href="/" class="back-link">← Kembali ke Menu</a>
  
  <div class="logo">
    <h1>Mode Tulis</h1>
    <p>RFID Writer</p>
  </div>
  
  <div class="step-indicator">
    <div class="step-item"><div class="step completed">✓</div><div class="step-label">Scan Kartu</div></div>
    <div class="step-item"><div class="step completed">✓</div><div class="step-label">Isi Data</div></div>
    <div class="step-item"><div class="step completed">3</div><div class="step-label">Selesai</div></div>
  </div>
  
  <div class="result-area">
    <div class="result-icon">)rawliteral";
  
  if (success) {
    html += R"rawliteral(✅</div>
    <div class="result-title success">BERHASIL!</div>
    <div class="result-message">Data berhasil ditulis ke kartu RFID.<br>Kartu siap digunakan.</div>)rawliteral";
  } else {
    html += R"rawliteral(❌</div>
    <div class="result-title error">GAGAL!</div>
    <div class="result-message">)rawliteral" + errorMsg + R"rawliteral(<br><br>Pastikan kartu MIFARE Classic 1K dan masih menempel.</div>)rawliteral";
  }
  
  html += R"rawliteral(
  </div>
  
  <div class="button-group">
    <button class="btn-primary" onclick="location.href='/write'">Tulis Lagi</button>
    <button onclick="location.href='/'">Menu Utama</button>
  </div>
  <div class="footer">Kartu bisa dilepas sekarang</div>
</div>
</body>
</html>
  )rawliteral";
  return html;
}

// ================= HTML PAGE - SCAN UNTUK MODE BACA =================
String getReadScanPage() {
  return R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Innoiti CR-1 - Baca Kartu</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
      background: linear-gradient(135deg, #106CFF 0%, #2B1B7D 100%);
      min-height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 24px;
    }
    
    .wrapper {
      max-width: 420px;
      width: 100%;
      text-align: center;
    }
    
    .back-link {
      display: inline-block;
      color: rgba(255,255,255,0.7);
      text-decoration: none;
      font-size: 14px;
      margin-bottom: 24px;
    }
    
    .back-link:hover {
      color: white;
    }
    
    .logo {
      margin-bottom: 32px;
    }
    
    .logo h1 {
      color: white;
      font-size: 28px;
      font-weight: 600;
      letter-spacing: -0.5px;
      margin-bottom: 4px;
    }
    
    .logo p {
      color: rgba(255,255,255,0.7);
      font-size: 14px;
    }
    
    .step-indicator {
      display: flex;
      justify-content: space-between;
      margin-bottom: 48px;
      padding: 0 16px;
    }
    
    .step-item {
      text-align: center;
      flex: 1;
    }
    
    .step {
      width: 32px;
      height: 32px;
      border-radius: 50%;
      background: rgba(255,255,255,0.2);
      color: white;
      line-height: 32px;
      text-align: center;
      font-weight: 600;
      font-size: 14px;
      margin: 0 auto 8px;
    }
    
    .step.active {
      background: white;
      color: #106CFF;
    }
    
    .step-label {
      font-size: 12px;
      color: rgba(255,255,255,0.6);
    }
    
    .card-area {
      background: rgba(255,255,255,0.05);
      backdrop-filter: blur(10px);
      border-radius: 32px;
      padding: 48px 32px;
      margin-bottom: 24px;
      border: 1px solid rgba(255,255,255,0.1);
    }
    
    .icon {
      font-size: 64px;
      margin-bottom: 24px;
    }
    
    .card-area h3 {
      color: white;
      font-size: 20px;
      font-weight: 500;
      margin-bottom: 16px;
    }
    
    .status {
      padding: 12px 20px;
      border-radius: 100px;
      font-size: 14px;
      font-weight: 500;
      display: inline-block;
    }
    
    .status.waiting {
      background: rgba(255,193,7,0.2);
      color: #FFD700;
    }
    
    .status.success {
      background: rgba(40,167,69,0.2);
      color: #4ade80;
    }
    
    .status.error {
      background: rgba(220,53,69,0.2);
      color: #ff6b6b;
    }
    
    .uid-display {
      background: rgba(255,255,255,0.1);
      border-radius: 16px;
      padding: 12px;
      font-family: 'SF Mono', Monaco, monospace;
      font-size: 18px;
      font-weight: 600;
      color: white;
      margin-top: 16px;
      letter-spacing: 1px;
    }
    
    .spinner {
      display: inline-block;
      width: 16px;
      height: 16px;
      border: 2px solid rgba(255,255,255,0.3);
      border-top-color: white;
      border-radius: 50%;
      animation: spin 0.8s linear infinite;
      margin-right: 8px;
      vertical-align: middle;
    }
    
    @keyframes spin {
      to { transform: rotate(360deg); }
    }
    
    .footer {
      font-size: 12px;
      color: rgba(255,255,255,0.5);
      margin-top: 24px;
    }
  </style>
</head>
<body>
<div class="wrapper">
  <a href="/" class="back-link">← Kembali ke Menu</a>
  
  <div class="logo">
    <h1>Mode Baca</h1>
    <p>RFID Reader</p>
  </div>
  
  <div class="step-indicator">
    <div class="step-item"><div class="step active">1</div><div class="step-label">Scan Kartu</div></div>
    <div class="step-item"><div class="step">2</div><div class="step-label">Informasi</div></div>
  </div>
  
  <div class="card-area">
    <div class="icon">📖</div>
    <h3>Tempelkan Kartu RFID</h3>
    <div id="statusBox" class="status waiting">⏳ Menunggu kartu...</div>
    <div id="uidBox" style="display:none;">
      <div class="uid-display" id="uidValue"></div>
    </div>
  </div>
  
  <div class="footer">Kartu akan otomatis dibaca</div>
</div>

<script>
  let cardDetected = false;
  let uid = "";
  let reading = false;
  let readTimeout = null;
  let readInterval = null;
  
  async function checkAndReadCard() {
    if (reading) return;
    
    try {
      const res = await fetch('/api/status');
      const data = await res.json();
      
      if (data.cardDetected && !cardDetected) {
        cardDetected = true;
        uid = data.uid;
        document.getElementById('statusBox').innerHTML = '<span class="spinner"></span> Membaca data kartu...';
        document.getElementById('statusBox').className = 'status success';
        document.getElementById('uidBox').style.display = 'block';
        document.getElementById('uidValue').innerHTML = 'UID: ' + uid;
        
        reading = true;
        
        // Mulai proses baca
        readInterval = setInterval(async () => {
          try {
            const readRes = await fetch('/api/read', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ uid: uid })
            });
            const result = await readRes.json();
            
            if (result.success) {
              clearInterval(readInterval);
              if (readTimeout) clearTimeout(readTimeout);
              window.location.href = '/read/info?uid=' + uid + '&nama=' + encodeURIComponent(result.nama) + '&id=' + encodeURIComponent(result.id) + '&kode=' + encodeURIComponent(result.kode);
            }
          } catch(e) {}
        }, 500);
        
        readTimeout = setTimeout(() => {
          if (readInterval) clearInterval(readInterval);
          document.getElementById('statusBox').innerHTML = '❌ Gagal membaca kartu!';
          document.getElementById('statusBox').className = 'status error';
          reading = false;
          cardDetected = false;
        }, 5000);
        
      } else if (!data.cardDetected && cardDetected) {
        cardDetected = false;
        if (readInterval) clearInterval(readInterval);
        if (readTimeout) clearTimeout(readTimeout);
        reading = false;
        document.getElementById('statusBox').innerHTML = '⏳ Menunggu kartu...';
        document.getElementById('statusBox').className = 'status waiting';
        document.getElementById('uidBox').style.display = 'none';
      }
    } catch(e) {}
  }
  
  setInterval(checkAndReadCard, 500);
</script>
</body>
</html>
  )rawliteral";
}

// ================= HTML PAGE - INFORMASI KARTU (HASIL BACA) =================
String getReadInfoPage(String uid, String nama, String id, String kode) {
  String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Innoiti CR-1 - Informasi Kartu</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
      background: linear-gradient(135deg, #106CFF 0%, #2B1B7D 100%);
      min-height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 24px;
    }
    
    .wrapper {
      max-width: 420px;
      width: 100%;
      text-align: center;
    }
    
    .back-link {
      display: inline-block;
      color: rgba(255,255,255,0.7);
      text-decoration: none;
      font-size: 14px;
      margin-bottom: 24px;
    }
    
    .back-link:hover {
      color: white;
    }
    
    .logo {
      margin-bottom: 32px;
    }
    
    .logo h1 {
      color: white;
      font-size: 28px;
      font-weight: 600;
      letter-spacing: -0.5px;
      margin-bottom: 4px;
    }
    
    .logo p {
      color: rgba(255,255,255,0.7);
      font-size: 14px;
    }
    
    .step-indicator {
      display: flex;
      justify-content: space-between;
      margin-bottom: 48px;
      padding: 0 16px;
    }
    
    .step-item {
      text-align: center;
      flex: 1;
    }
    
    .step {
      width: 32px;
      height: 32px;
      border-radius: 50%;
      background: rgba(255,255,255,0.2);
      color: white;
      line-height: 32px;
      text-align: center;
      font-weight: 600;
      font-size: 14px;
      margin: 0 auto 8px;
    }
    
    .step.completed {
      background: white;
      color: #106CFF;
    }
    
    .step-label {
      font-size: 12px;
      color: rgba(255,255,255,0.6);
    }
    
    .info-area {
      background: rgba(255,255,255,0.05);
      backdrop-filter: blur(10px);
      border-radius: 32px;
      padding: 32px;
      margin-bottom: 24px;
      border: 1px solid rgba(255,255,255,0.1);
    }
    
    .info-icon {
      font-size: 64px;
      margin-bottom: 24px;
    }
    
    .info-row {
      background: rgba(255,255,255,0.08);
      border-radius: 16px;
      padding: 16px;
      margin-bottom: 12px;
      text-align: left;
    }
    
    .info-label {
      font-size: 12px;
      color: rgba(255,255,255,0.5);
      margin-bottom: 4px;
    }
    
    .info-value {
      font-size: 18px;
      font-weight: 600;
      color: white;
      word-break: break-all;
    }
    
    .info-value.empty {
      color: rgba(255,255,255,0.4);
      font-style: italic;
    }
    
    .uid-value {
      font-family: 'SF Mono', Monaco, monospace;
      font-size: 14px;
      letter-spacing: 0.5px;
    }
    
    .button-group {
      display: flex;
      gap: 12px;
      margin-top: 24px;
    }
    
    button {
      flex: 1;
      padding: 14px;
      background: rgba(255,255,255,0.1);
      border: 1px solid rgba(255,255,255,0.2);
      border-radius: 16px;
      font-size: 14px;
      font-weight: 500;
      color: white;
      cursor: pointer;
      transition: all 0.2s;
    }
    
    button:hover {
      background: rgba(255,255,255,0.2);
    }
    
    .btn-primary {
      background: white;
      color: #106CFF;
    }
    
    .btn-primary:hover {
      transform: scale(0.98);
      background: #f0f0f0;
    }
    
    .footer {
      font-size: 12px;
      color: rgba(255,255,255,0.4);
      margin-top: 24px;
    }
  </style>
</head>
<body>
<div class="wrapper">
  <a href="/" class="back-link">← Kembali ke Menu</a>
  
  <div class="logo">
    <h1>Mode Baca</h1>
    <p>RFID Reader</p>
  </div>
  
  <div class="step-indicator">
    <div class="step-item"><div class="step completed">✓</div><div class="step-label">Scan Kartu</div></div>
    <div class="step-item"><div class="step completed">2</div><div class="step-label">Informasi</div></div>
  </div>
  
  <div class="info-area">
    <div class="info-icon">📇</div>
    
    <div class="info-row">
      <div class="info-label">UID Kartu</div>
      <div class="info-value uid-value">)rawliteral" + uid + R"rawliteral(</div>
    </div>
    
    <div class="info-row">
      <div class="info-label">Nama</div>
      <div class="info-value )rawliteral" + (nama == "" ? "empty" : "") + R"rawliteral(">)rawliteral" + (nama == "" ? "(kosong)" : nama) + R"rawliteral(</div>
    </div>
    
    <div class="info-row">
      <div class="info-label">ID</div>
      <div class="info-value )rawliteral" + (id == "" ? "empty" : "") + R"rawliteral(">)rawliteral" + (id == "" ? "(kosong)" : id) + R"rawliteral(</div>
    </div>
    
    <div class="info-row">
      <div class="info-label">Kode</div>
      <div class="info-value )rawliteral" + (kode == "" ? "empty" : "") + R"rawliteral(">)rawliteral" + (kode == "" ? "(kosong)" : kode) + R"rawliteral(</div>
    </div>
  </div>
  
  <div class="button-group">
    <button class="btn-primary" onclick="location.href='/read'">Baca Kartu Lain</button>
    <button onclick="location.href='/'">Menu Utama</button>
  </div>
  <div class="footer">Kartu bisa dilepas sekarang</div>
</div>
</body>
</html>
  )rawliteral";
  return html;
}

// ================= API HANDLER =================
void handleRoot() {
  server.send(200, "text/html", getHomePage());
}

void handleWrite() {
  server.send(200, "text/html", getWriteScanPage());
}

void handleWriteForm() {
  if (server.hasArg("uid")) {
    String uid = server.arg("uid");
    server.send(200, "text/html", getWriteFormPage(uid));
  } else {
    server.sendHeader("Location", "/write");
    server.send(302, "text/plain", "");
  }
}

void handleWriteResult() {
  bool success = server.hasArg("success") && server.arg("success") == "1";
  String error = server.hasArg("error") ? server.arg("error") : "";
  server.send(200, "text/html", getWriteResultPage(success, error));
}

void handleRead() {
  server.send(200, "text/html", getReadScanPage());
}

void handleReadInfo() {
  if (server.hasArg("uid")) {
    String uid = server.arg("uid");
    String nama = server.hasArg("nama") ? server.arg("nama") : "";
    String id = server.hasArg("id") ? server.arg("id") : "";
    String kode = server.hasArg("kode") ? server.arg("kode") : "";
    server.send(200, "text/html", getReadInfoPage(uid, nama, id, kode));
  } else {
    server.sendHeader("Location", "/read");
    server.send(302, "text/plain", "");
  }
}

void handleApiStatus() {
  String json = "{\"cardDetected\":" + String(session.cardDetected ? "true" : "false");
  if (session.cardDetected) {
    json += ",\"uid\":\"" + session.uid + "\"";
  }
  json += "}";
  server.send(200, "application/json", json);
}

void handleApiRead() {
  if (!server.hasArg("plain")) {
    server.send(400, "application/json", "{\"success\":false,\"error\":\"No data\"}");
    return;
  }
  
  String body = server.arg("plain");
  String uid = "";
  
  int idxUid = body.indexOf("\"uid\":\"");
  if (idxUid != -1) {
    int start = idxUid + 7;
    int end = body.indexOf("\"", start);
    uid = body.substring(start, end);
  }
  
  // Cek apakah kartu masih ada
  if (!session.cardDetected || !cardCurrentlyPresent) {
    server.send(400, "application/json", "{\"success\":false,\"error\":\"Kartu tidak terdeteksi\"}");
    return;
  }
  
  // Verifikasi UID cocok
  if (session.uid != uid) {
    server.send(400, "application/json", "{\"success\":false,\"error\":\"UID kartu berbeda\"}");
    return;
  }
  
  // Baca data dari kartu
  String nama, id, kode;
  bool success = readFromCard(nama, id, kode);
  
  if (success) {
    String json = "{\"success\":true,\"nama\":\"" + nama + "\",\"id\":\"" + id + "\",\"kode\":\"" + kode + "\"}";
    server.send(200, "application/json", json);
  } else {
    server.send(500, "application/json", "{\"success\":false,\"error\":\"Gagal membaca kartu\"}");
  }
}

void handleApiWrite() {
  if (!server.hasArg("plain")) {
    server.send(400, "application/json", "{\"success\":false,\"error\":\"No data\"}");
    return;
  }
  
  String body = server.arg("plain");
  String nama = "", id = "", kode = "", uid = "";
  
  int idxNama = body.indexOf("\"nama\":\"");
  if (idxNama != -1) {
    int start = idxNama + 8;
    int end = body.indexOf("\"", start);
    nama = body.substring(start, end);
  }
  
  int idxId = body.indexOf("\"id\":\"");
  if (idxId != -1) {
    int start = idxId + 6;
    int end = body.indexOf("\"", start);
    id = body.substring(start, end);
  }
  
  int idxKode = body.indexOf("\"kode\":\"");
  if (idxKode != -1) {
    int start = idxKode + 8;
    int end = body.indexOf("\"", start);
    kode = body.substring(start, end);
  }
  
  int idxUid = body.indexOf("\"uid\":\"");
  if (idxUid != -1) {
    int start = idxUid + 7;
    int end = body.indexOf("\"", start);
    uid = body.substring(start, end);
  }
  
  // Cek apakah kartu masih ada
  if (!session.cardDetected || !cardCurrentlyPresent) {
    server.send(400, "application/json", "{\"success\":false,\"error\":\"Kartu tidak terdeteksi, pastikan masih menempel\"}");
    return;
  }
  
  // Verifikasi UID cocok
  if (session.uid != uid) {
    server.send(400, "application/json", "{\"success\":false,\"error\":\"UID kartu berbeda, gunakan kartu yang sama\"}");
    return;
  }
  
  // Tulis data
  session.nama = nama;
  session.id = id;
  session.kode = kode;
  
  bool success = writeToCard();
  
  // Setelah write, halt kartu dan reset status aktif
  mfrc522.PICC_HaltA();
  mfrc522.PCD_StopCrypto1();
  cardActive = false;
  cardCurrentlyPresent = false;
  session.cardDetected = false;
  
  if (success) {
    server.send(200, "application/json", "{\"success\":true}");
  } else {
    server.send(500, "application/json", "{\"success\":false,\"error\":\"Gagal menulis ke kartu. Pastikan kartu MIFARE Classic 1K\"}");
  }
}

// ================= SETUP =================
void setup() {
  Serial.begin(115200);
  Serial.println("\n");
  Serial.println("╔════════════════════════════════════════╗");
  Serial.println("║     INNOITI CR-1 RFID READER/WRITER   ║");
  Serial.println("║     Versi Final - Mode Baca & Tulis   ║");
  Serial.println("╚════════════════════════════════════════╝\n");
  
  // Setup WiFi AP
  WiFi.mode(WIFI_AP);
  WiFi.softAPConfig(local_ip, gateway, subnet);
  WiFi.softAP(ssid, password);
  
  Serial.printf("📡 WiFi AP: %s\n", ssid);
  Serial.printf("🔑 Password: %s\n", password);
  Serial.printf("🌐 IP Address: http://%s\n\n", WiFi.softAPIP().toString().c_str());
  
  // Setup RFID
  SPI.begin(18, 19, 23, 5);
  mfrc522.PCD_Init();
  for (byte i = 0; i < 6; i++) key.keyByte[i] = 0xFF;
  
  Serial.println("🔍 MFRC522 RFID Reader siap");
  Serial.println("💡 Tempelkan kartu MIFARE Classic 1K\n");
  
  // Setup Web Server
  server.on("/", handleRoot);
  server.on("/write", handleWrite);
  server.on("/write/form", handleWriteForm);
  server.on("/write/result", handleWriteResult);
  server.on("/read", handleRead);
  server.on("/read/info", handleReadInfo);
  server.on("/api/status", handleApiStatus);
  server.on("/api/read", HTTP_POST, handleApiRead);
  server.on("/api/write", HTTP_POST, handleApiWrite);
  server.begin();
  
  // Inisialisasi state
  session.cardDetected = false;
  cardActive = false;
  cardCurrentlyPresent = false;
  
  Serial.println("✅ Web server siap!");
  Serial.println("========================================\n");
}

// ================= LOOP (STABIL - TIDAK BERKEDIP) =================
void loop() {
  server.handleClient();
  
  // Scan setiap 200ms (tidak terlalu cepat)
  static unsigned long lastScan = 0;
  if (millis() - lastScan < 200) return;
  lastScan = millis();
  
  // Cek keberadaan kartu dengan PICC_RequestA (non-intrusive)
  byte bufferATQA[2];
  byte bufferSize = sizeof(bufferATQA);
  MFRC522::StatusCode status = mfrc522.PICC_RequestA(bufferATQA, &bufferSize);
  
  if (status == MFRC522::STATUS_OK) {
    // Ada kartu di field
    if (!cardCurrentlyPresent) {
      // Kartu baru masuk! Baca UID sekali saja
      cardCurrentlyPresent = true;
      if (mfrc522.PICC_ReadCardSerial()) {
        cardActive = true;
        copyUid(mfrc522.uid, lastUid);
        session.cardDetected = true;
        session.uid = uidToString(mfrc522.uid.uidByte, mfrc522.uid.size);
        Serial.printf("✅ Kartu terdeteksi: %s\n", session.uid.c_str());
        // JANGAN panggil HaltA() - biarkan kartu tetap aktif
      }
    }
    lastCardReadTime = millis();
  } else {
    // Tidak ada kartu di field
    if (cardCurrentlyPresent) {
      cardCurrentlyPresent = false;
      cardActive = false;
      session.cardDetected = false;
      session.uid = "";
      Serial.println("❌ Kartu dilepas");
      mfrc522.PCD_StopCrypto1();
    }
  }
  
  // Timeout safety: jika lebih dari 1,5 detik tidak terdeteksi, anggap lepas
  if (cardCurrentlyPresent && (millis() - lastCardReadTime > 1500)) {
    cardCurrentlyPresent = false;
    cardActive = false;
    session.cardDetected = false;
    session.uid = "";
    mfrc522.PCD_StopCrypto1();
    Serial.println("⚠️ Kartu timeout (dilepas tidak terdeteksi)");
  }
}