SISTEM UJIAN BERBASIS ONLINE

 SISTEM UJIAN BERBASIS ONLINE




pada kesempatan kali ini sejarah31.com akan berbagi mengenai sistem ujian berbasis online. sebenarnya ujian seperti ini sudah banyak dibuat atau diperjual belikan online oleh banyak pihak tapi kali ini kita akan membuatnya step by step hingga dapat di gunakan oleh pribadi maupun kelompok.

apa saja kelebihan sistem ini?

  1. menggunakan sistem waktu (timer)
  2. Setelah token dimasukan maka layar akan mengunci.
  3. Saat siswa keluar dari tab, sistem alarm berbunyi
  4. administrator dapat melihat log peserta didik, baik masuk maupun kecurangan.
  5. Soal dibuat menggunkan google form, sehingga admin hanya tinggal menginputkan linknya saja
Apa saja perangkat yang diperlukan, untuk membuat aplikasi ini?
  1. akun google.com/gmail.com
  2. spreadsheet
  3. apps script
sekarang mari kita mulai dari awal, jangan terlewatkan langkah-langkahnya ya.
  1. buka akun google mu masuk ke spreeadsheet
  2. buat nama-nama sheet seperti ini   Siswa, Ujian, Log_Siswa, dan Pengaturan
  3. pastikan penulisan besar kecil huruf sudah sesuai ya


  4. Sheet Siswa Kolom A1 Berisi (Username) kolom B1 Berisi (Password)
  5. Sheet Ujian Kolom A1 Berisi (Token) kolom B1 (Nama Ujian) kolom C1 (Link Form) kolom D1 (Status)  kolom E1 (Durasi (Menit)
  6. Sheet Log_Siswa Kolom A1 (Timestamp) Kolom B1 (Username) kolom C1 (Password) kolom D1 (Token Ujian) Kolom E1 (Status Aktivitas)
Selanjutnya kita masuk ke Extension/ekstensi-Pilik Apps Script, Sekarang kita ada di Apps Script.
  1. Sekarang kita akan membuat file html di apps sript, klik tanda + tambah pilih HTML


  2. Tambah kemudian beri nama Index, AdminView dan SiswaView, 4 nama termasuk Code.gs yang akan kita gunakan untuk menimpan file htmlnya. 
  3. sebelum menyalin kodenya pastikan semua isinya bersih dan hanya kode yang di copykan saja. 


Kita mula dengan Code.gs
Code.gs
function doGet() {
  return HtmlService.createTemplateFromFile('Index')
      .evaluate()
      .setTitle('CBT SMAN 23 GARUT')
      .addMetaTag('viewport', 'width=device-width, initial-scale=1')
      .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

// =======================================================
// FUNGSI PROSES LOGIN
// =======================================================
function cekLogin(username, password) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  if (username === "admin" && password === "sejarah31.com") {
    return { status: "success", role: "admin" };
  }
  var sheetSiswa = ss.getSheetByName("Siswa");
  if (!sheetSiswa) return { status: "fail", message: "Database Siswa tidak ditemukan!" };
  
  var data = sheetSiswa.getDataRange().getValues();
  for (var i = 1; i < data.length; i++) {
    if (data[i][0].toString().trim() === username.trim() && data[i][1].toString().trim() === password.trim()) {
      return { status: "success", role: "siswa", name: username };
    }
  }
  return { status: "fail", message: "Username atau Password salah!" };
}

// =======================================================
// MANAJEMEN CONFIG / PENGATURAN TOKEN BYPASS GURU (ANTI-CRASH)
// =======================================================

function getTokenReset() {
  try {
    var ss = SpreadsheetApp.getActiveSpreadsheet();
    var sheet = ss.getSheetByName("Pengaturan");
    
    if (!sheet) {
      sheet = ss.insertSheet("Pengaturan");
      sheet.getRange("A1").setValue("LOCKRESET99");
      return "LOCKRESET99";
    }
    
    var nilaiA1 = sheet.getRange("A1").getValue();
    if (!nilaiA1) {
      sheet.getRange("A1").setValue("LOCKRESET99");
      return "LOCKRESET99";
    }
    
    return nilaiA1.toString().trim();
  } catch(e) {
    Logger.log("Gagal mengambil token reset: " + e.toString());
    return "LOCKRESET99";
  }
}

function simpanTokenResetBaru(tokenBaru) {
  try {
    if (!tokenBaru) return { status: "fail", message: "Token baru tidak boleh kosong!" };
    
    var ss = SpreadsheetApp.getActiveSpreadsheet();
    var sheet = ss.getSheetByName("Pengaturan");
    if (!sheet) {
      sheet = ss.insertSheet("Pengaturan");
    }
    
    var tokenClean = tokenBaru.toString().trim().toUpperCase();
    sheet.getRange("A1").setValue(tokenClean);
    return { status: "success", message: "Token bypass pengawas berhasil diubah menjadi: " + tokenClean };
  } catch(e) {
    return { status: "fail", message: "Gagal menyimpan ke spreadsheet: " + e.toString() };
  }
}

// =======================================================
// MANAJEMEN UJIAN (ADMIN & SISWA)
// =======================================================

function getDaftarUjian() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("Ujian");
  if (!sheet) return [];
  
  var data = sheet.getDataRange().getValues();
  var list = [];
  for (var i = 1; i < data.length; i++) {
    list.push({ 
      token: data[i][0], 
      nama: data[i][1], 
      link: data[i][2],
      status: data[i][3] ? data[i][3].toString().trim() : "NON-AKTIF",
      durasi: data[i][4] ? Number(data[i][4]) : 90
    });
  }
  return list;
}

function toggleStatusUjian(token, statusBaru) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("Ujian");
  if (!sheet) return { status: "fail", message: "Sheet tidak ditemukan!" };
  
  var data = sheet.getDataRange().getValues();
  for (var i = 1; i < data.length; i++) {
    if (data[i][0].toString().trim() === token.trim()) {
      var formatStatusSheets = statusBaru.toString().toUpperCase();
      if (formatStatusSheets === "NONAKTIF") formatStatusSheets = "NON-AKTIF"; 
      sheet.getRange(i + 1, 4).setValue(formatStatusSheets); 
      return { status: "success", message: "Status ujian berhasil diubah!" };
    }
  }
  return { status: "fail", message: "Token tidak ditemukan." };
}

function tambahUjian(nama, link, token, durasi) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("Ujian");
  if (!sheet) {
    sheet = ss.insertSheet("Ujian");
    sheet.appendRow(["Token", "Nama Ujian", "Link Form", "Status", "Durasi (Menit)"]);
  }
  var menitUjian = durasi ? Number(durasi) : 90; 
  sheet.appendRow([token, nama, link, "NON-AKTIF", menitUjian]); 
  return { status: "success", message: "Ujian berhasil didaftarkan!" };
}

function cekTokenUjian(username, password, token) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("Ujian");
  if (!sheet) return { status: "fail", message: "Belum ada jadwal ujian." };
  
  var data = sheet.getDataRange().getValues();
  var tokenSiswa = token.toString().trim().toUpperCase();

  for (var i = 1; i < data.length; i++) {
    var tokenDatabase = data[i][0].toString().trim().toUpperCase();
    
    if (tokenDatabase === tokenSiswa) {
      var statusUjian = data[i][3] ? data[i][3].toString().trim().toUpperCase() : "NON-AKTIF";
      
      if (statusUjian !== "AKTIF") {
        return { status: "fail", message: "Maaf, ujian ini belum dimulai atau sudah dinonaktifkan!" };
      }
      
      var linkForm = data[i][2] ? data[i][2].toString().trim() : "";
      if (linkForm === "") return { status: "fail", message: "Link Google Form kosong!" };
      
      var menitUjian = data[i][4] ? Number(data[i][4]) : 90;
      
      catatLogSiswa(username, password, tokenSiswa, "MASUK APLIKASI UJIAN");
      
      return { 
        status: "success", 
        link: linkForm, 
        nama: data[i][1].toString(),
        durasi: menitUjian 
      };
    }
  }
  return { status: "fail", message: "Token tidak valid atau salah!" };
}

function catatLogSiswa(username, password, tokenUjian, statusAktivitas) {
  try {
    var ss = SpreadsheetApp.getActiveSpreadsheet();
    var sheetLog = ss.getSheetByName("Log_Siswa");
    if (!sheetLog) {
      sheetLog = ss.insertSheet("Log_Siswa");
      sheetLog.appendRow(["Timestamp", "Username", "Password", "Token Ujian", "Status Aktivitas"]);
    }
    var waktuSekarang = Utilities.formatDate(new Date(), "GMT+7", "yyyy-MM-dd HH:mm:ss");
    sheetLog.appendRow([waktuSekarang, username?username.toString().trim():"Unknown", password?password.toString().trim():"-", tokenUjian?tokenUjian.toString().trim().toUpperCase():"-", statusAktivitas]);
    return true;
  } catch(e) {
    return false;
  }
}

function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
Selanjutnya Index.html
Index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script src="https://cdn.tailwindcss.com"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
    <style>
      .hidden-view { display: none !important; }
    </style>
  </head>
  <body class="bg-gray-100 font-sans antialiased">

    <div id="view-login" class="min-h-screen flex items-center justify-center p-4">
      <div class="bg-white p-8 rounded-2xl shadow-xl w-full max-w-md border border-gray-100">
        <div class="text-center mb-6">
          <div class="bg-blue-600 text-white w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl shadow-md">
            <i class="fas fa-graduation-cap"></i>
          </div>
          <h2 class="text-2xl font-bold text-gray-800 tracking-wide">UJIAN SEKOLAH</h2>
          <p class="text-gray-500 text-sm mt-1">Masuk untuk memulai ujian</p>
        </div>

        <div class="space-y-4">
          <div>
            <label class="block text-xs font-semibold text-gray-600 uppercase mb-1">Username</label>
            <div class="relative">
              <span class="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-400"><i class="fas fa-user"></i></span>
              <input type="text" id="username" class="w-full pl-10 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm" placeholder="Masukkan Username">
            </div>
          </div>
          <div>
            <label class="block text-xs font-semibold text-gray-600 uppercase mb-1">Password</label>
            <div class="relative">
              <span class="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-400"><i class="fas fa-lock"></i></span>
              <input type="password" id="password" class="w-full pl-10 pr-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm" placeholder="Masukkan Password">
            </div>
          </div>
          <button onclick="prosesLogin()" id="btn-login" class="w-full py-3 bg-blue-700 hover:bg-blue-800 text-white font-bold rounded-xl shadow-lg transition duration-200 uppercase tracking-wider text-sm mt-2">
            Masuk Sekarang
          </button>
          <p id="login-err" class="text-red-500 text-xs text-center font-medium mt-2 hidden"></p>
        </div>
      </div>
    </div>

    <div id="view-admin" class="hidden-view"><?!= include('AdminView'); ?></div>
    <div id="view-siswa" class="hidden-view"><?!= include('SiswaView'); ?></div>

    <div id="lock-screen" class="fixed inset-0 bg-red-700/95 z-[9999] flex flex-col items-center justify-center p-6 text-center shadow-inner hidden">
      <div class="bg-white text-red-700 p-5 rounded-full text-5xl mb-4 animate-bounce"><i class="fas fa-exclamation-triangle"></i></div>
      <h1 class="text-3xl font-black tracking-wider text-white mb-2">SISTEM TERKUNCI!</h1>
      <p id="pesan-pelanggaran" class="text-yellow-200 font-medium text-sm max-w-md mb-6">Anda terdeteksi keluar dari layar ujian/membuka tab baru. Pelanggaran dicatat oleh sistem!</p>
      
      <div class="bg-slate-900/40 p-5 rounded-2xl border border-white/20 w-full max-w-xs space-y-3 backdrop-blur-sm">
        <p class="text-xs text-white/80">Hubungi pengawas untuk memasukkan Token Reset Pengunci Layar.</p>
        <input type="password" id="input-token-reset" class="w-full p-2.5 bg-white text-slate-950 font-bold rounded-xl text-center tracking-widest uppercase text-base focus:outline-none" placeholder="Token Reset Guru">
        <button onclick="matikanAlarmSistem()" class="w-full py-2.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl font-bold text-xs uppercase shadow transition">Buka Kunci Layar</button>
      </div>
    </div>

    <div id="universal-alert-modal" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-[9999] flex items-center justify-center p-4 hidden">
      <div class="bg-slate-800 border border-slate-700 p-6 rounded-2xl shadow-2xl w-full max-w-sm text-center transform scale-95 transition-all duration-300">
        <div id="universal-modal-icon-box" class="mx-auto flex items-center justify-center h-14 w-14 rounded-full mb-4">
          <i id="universal-modal-icon" class="fas text-2xl"></i>
        </div>
        <h3 id="universal-modal-title" class="text-base font-bold text-white mb-2">Informasi Sistem</h3>
        <p id="universal-modal-message" class="text-gray-400 text-xs leading-relaxed mb-6">Pesan pemberitahuan...</p>
        <div class="flex justify-center">
          <button id="universal-modal-btn" class="w-full py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-xl font-bold text-xs uppercase tracking-wider transition shadow-md">
            Mengerti
          </button>
        </div>
      </div>
    </div>

    <div id="custom-confirm-modal" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-[9999] flex items-center justify-center p-4 hidden">
      <div class="bg-slate-800 border border-slate-700 p-6 rounded-2xl shadow-2xl w-full max-w-sm text-center transform scale-95 transition-all duration-300">
        <div class="mx-auto flex items-center justify-center h-14 w-14 rounded-full bg-yellow-500/10 mb-4 animate-pulse">
          <i class="fas fa-exclamation-circle text-2xl text-yellow-500"></i>
        </div>
        <h3 class="text-base font-bold text-white mb-2">Konfirmasi Keluar Aplikasi</h3>
        <p class="text-gray-400 text-xs leading-relaxed mb-6">
          Apakah Anda yakin telah mengirimkan jawaban di Google Form dan ingin keluar dari aplikasi ujian ini?
        </p>
        <div class="flex space-x-3">
          <button onclick="eksekusiKeluarAplikasi()" class="flex-1 py-2.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl font-bold text-xs uppercase tracking-wider transition shadow-md">
            Ya, Keluar
          </button>
          <button onclick="tutupCustomModal()" class="flex-1 py-2.5 bg-slate-700 hover:bg-slate-600 text-gray-200 rounded-xl font-bold text-xs uppercase tracking-wider transition">
            Batal
          </button>
        </div>
      </div>
    </div>

    <script>
      // === GLOBAL VARIABLES ===
      var alarmSuara = new Audio('https://www.soundjay.com/clock/sounds/alarm-clock-elapsed-01.mp3');
      var pelanggaranKe = 0;
      var ujianAktif = false;
      var statusSistemTerkunci = false; 
      var hitungMundur; 
      var sisaWaktuDetik = 0;
      
      // Web Audio API Synth (Pembangkit Suara Hardware Internal - Kebal Blokir Browser)
      var audioCtx = null;
      var intervalSuaraBip = null;

      function inisialisasiWebAudioAman() {
        try {
          if (!audioCtx) {
            audioCtx = new (window.AudioContext || window.webkitAudioContext)();
          }
          if (audioCtx.state === 'suspended') {
            audioCtx.resume();
          }
          alarmSuara.play().then(function(){ alarmSuara.pause(); }).catch(function(e){});
          console.log(" Web Audio API Cadangan Siap Tempur.");
        } catch (error) {
          console.log("Web Audio API Error: " + error);
        }
      }

      // Jebak interaksi untuk mengaktifkan izin audio browser semenjak awal login
      document.addEventListener('click', inisialisasiWebAudioAman, { once: false });
      document.addEventListener('keydown', inisialisasiWebAudioAman, { once: false });

      // === FUNGSI CUSTOM ALERT UNIVERSAL ===
      function tampilkanAlertKustom(judul, pesan, jenis, aksiFungsi) {
        const modal = document.getElementById('universal-alert-modal');
        const iconBox = document.getElementById('universal-modal-icon-box');
        const icon = document.getElementById('universal-modal-icon');
        const titleEl = document.getElementById('universal-modal-title');
        const msgEl = document.getElementById('universal-modal-message');
        const btn = document.getElementById('universal-modal-btn');

        if(!modal) return;
        titleEl.innerText = judul;
        msgEl.innerText = pesan;

        if (jenis === 'danger') {
          iconBox.className = "mx-auto flex items-center justify-center h-14 w-14 rounded-full bg-red-500/10 mb-4 animate-bounce";
          icon.className = "fas fa-times-circle text-2xl text-red-500";
          btn.className = "w-full py-2.5 bg-red-600 hover:bg-red-700 text-white rounded-xl font-bold text-xs uppercase tracking-wider transition shadow-md";
        } else {
          iconBox.className = "mx-auto flex items-center justify-center h-14 w-14 rounded-full bg-yellow-500/10 mb-4 animate-pulse";
          icon.className = "fas fa-exclamation-triangle text-2xl text-yellow-500";
          btn.className = "w-full py-2.5 bg-amber-500 hover:bg-amber-600 text-white rounded-xl font-bold text-xs uppercase tracking-wider transition shadow-md";
        }

        modal.classList.remove('hidden');
        btn.onclick = function() {
          modal.classList.add('hidden');
          if (typeof aksiFungsi === "function") aksiFungsi();
        };
      }

      // === 1. FUNGSI PROSES LOGIN ===
      function prosesLogin() {
        var user = document.getElementById('username').value;
        var pass = document.getElementById('password').value;
        var btn = document.getElementById('btn-login');
        var err = document.getElementById('login-err');

        if(!user || !pass) {
          err.innerText = "Semua bidang wajib diisi!";
          err.classList.remove('hidden');
          return;
        }

        inisialisasiWebAudioAman();

        btn.disabled = true;
        btn.innerText = "Memverifikasi...";
        if(err) err.classList.add('hidden');

        google.script.run.withSuccessHandler(function(res) {
          btn.disabled = false;
          btn.innerText = "Masuk Sekarang";
          
          if(res.status === "success") {
            document.getElementById('view-login').classList.add('hidden-view');
            if(res.role === "admin") {
              document.getElementById('view-admin').classList.remove('hidden-view');
              if (typeof loadDataAdmin === "function") loadDataAdmin(); 
            } else {
              document.getElementById('view-siswa').classList.remove('hidden-view');
              var nameDisplay = document.getElementById('nama-siswa-display');
              if(nameDisplay) nameDisplay.innerText = res.name;
            }
          } else {
            if(err) {
              err.innerText = res.message;
              err.classList.remove('hidden');
            }
          }
        }).cekLogin(user, pass);
      }

      // === 2. FUNGSI MULAI UJIAN SISWA ===
      function mulaiUjianSiswa() {
        var tokenInput = document.getElementById('siswa-token-input');
        var token = tokenInput ? tokenInput.value : "";
        
        if (!token) {
          tampilkanAlertKustom("Peringatan", "Isi token terlebih dahulu sebelum memulai ujian!", "warning");
          return;
        }

        inisialisasiWebAudioAman();

        var userSiswa = document.getElementById('username').value;
        var passSiswa = document.getElementById('password').value;

        google.script.run
          .withSuccessHandler(function(res) {
            if (res && res.status === "success") {
              ujianAktif = true; 
              
              var boxToken = document.getElementById('box-token-siswa');
              if(boxToken) boxToken.classList.add('hidden');
              
              var infoUjian = document.getElementById('info-ujian-berjalan');
              if(infoUjian) infoUjian.classList.remove('hidden');
              
              var judulUjian = document.getElementById('judul-ujian-berjalan');
              if(judulUjian) judulUjian.innerText = res.nama;
              
              var iframe = document.getElementById('iframe-gform');
              if(iframe) iframe.src = res.link;
              
              var containerIframe = document.getElementById('container-iframe-ujian');
              if(containerIframe) containerIframe.classList.remove('hidden');
              
              startTimerSistem(res.durasi);
            } else {
              tampilkanAlertKustom("Verifikasi Gagal", res.message || "Kode token salah.", "danger");
            }
          })
          .withFailureHandler(function(error) {
            tampilkanAlertKustom("Koneksi Terputus", "Kesalahan sistem: " + error.message, "danger");
          })
          .cekTokenUjian(userSiswa, passSiswa, token);
      }

      // === TIMER SYSTEM ===
      function startTimerSistem(menit) {
        clearInterval(hitungMundur); 
        sisaWaktuDetik = menit * 60;

        var lokasiTopBar = document.getElementById('info-ujian-berjalan');
        var timerLama = document.getElementById('cbt-timer-display');
        if(timerLama) timerLama.remove();

        if (lokasiTopBar) {
          var divTimer = document.createElement('div');
          divTimer.id = "cbt-timer-display";
          divTimer.className = "bg-amber-500 text-white px-4 py-1.5 rounded-lg font-mono font-bold text-sm flex items-center gap-2 shadow-sm ml-auto";
          divTimer.innerHTML = `<i class="fas fa-clock"></i> Sisa Waktu: --:--:--`;
          lokasiTopBar.appendChild(divTimer);
        }

        hitungMundur = setInterval(function() {
          var jam = Math.floor(sisaWaktuDetik / 3600);
          var mnt = Math.floor((sisaWaktuDetik % 3600) / 60);
          var dtk = sisaWaktuDetik % 60;

          jam = jam < 10 ? "0" + jam : jam;
          mnt = mnt < 10 ? "0" + mnt : mnt;
          dtk = dtk < 10 ? "0" + dtk : dtk;

          var elTimer = document.getElementById('cbt-timer-display');
          if (elTimer) {
            elTimer.innerHTML = `<i class="fas fa-clock"></i> Sisa Waktu: ${jam}:${mnt}:${dtk}`;
            if(sisaWaktuDetik <= 300) {
              elTimer.className = "bg-rose-600 text-white px-4 py-1.5 rounded-lg font-mono font-bold text-sm flex items-center gap-2 shadow-sm animate-bounce ml-auto";
            }
          }

          if (sisaWaktuDetik <= 0) {
            clearInterval(hitungMundur);
            waktuUjianHabisOtomatis();
          }
          sisaWaktuDetik--;
        }, 1000);
      }

      function waktuUjianHabisOtomatis() {
        ujianAktif = false;
        clearInterval(hitungMundur);
        matikanBunyiAlarmSecaraFisik();
        statusSistemTerkunci = false;
        location.reload();
      }

      function konfirmasiSelesai() {
        var modal = document.getElementById('custom-confirm-modal');
        if(modal) modal.classList.remove('hidden');
      }

      function tutupCustomModal() {
        var modal = document.getElementById('custom-confirm-modal');
        if(modal) modal.classList.add('hidden');
      }
      function logoutAdmin() {
        // 1. Reset input form login utama agar bersih
        var usernameInput = document.getElementById('username');
        var passwordInput = document.getElementById('password');
        if(usernameInput) usernameInput.value = "";
        if(passwordInput) passwordInput.value = "";

        // 2. Sembunyikan halaman admin & Tampilkan kembali halaman login utama
        document.getElementById('view-admin').classList.add('hidden-view');
        document.getElementById('view-login').classList.remove('hidden-view');
        
        // 3. (Opsional) Jika Anda memiliki fungsi untuk membersihkan tabel/data admin, panggil di sini
        console.log("Admin berhasil keluar dari sistem.");
      }

      function eksekusiKeluarAplikasi() {
        tutupCustomModal();
        
        // 1. Matikan semua status ujian dan alarm secara total
        ujianAktif = false; 
        clearInterval(hitungMundur); 
        matikanBunyiAlarmSecaraFisik();
        statusSistemTerkunci = false;

        // 2. Kosongkan kembali url iframe Google Form agar tidak bocor/terbuka lagi
        var iframe = document.getElementById('iframe-gform');
        if(iframe) iframe.src = "";

        // 3. Reset form input login dan token siswa agar kosong kembali
        var usernameInput = document.getElementById('username');
        var passwordInput = document.getElementById('password');
        var tokenInput = document.getElementById('siswa-token-input');
        if(usernameInput) usernameInput.value = "";
        if(passwordInput) passwordInput.value = "";
        if(tokenInput) tokenInput.value = "";

        // 4. Sembunyikan kontainer ujian & Tampilkan kembali halaman Login Utama
        document.getElementById('view-siswa').classList.add('hidden-view');
        document.getElementById('view-login').classList.remove('hidden-view');
        
        // 5. Sembunyikan info ujian berjalan dan tampilkan kembali box token untuk ujian berikutnya
        var infoUjian = document.getElementById('info-ujian-berjalan');
        if(infoUjian) infoUjian.classList.add('hidden');
        
        var boxToken = document.getElementById('box-token-siswa');
        if(boxToken) boxToken.classList.remove('hidden');
        
        var containerIframe = document.getElementById('container-iframe-ujian');
        if(containerIframe) containerIframe.classList.add('hidden');
      }

      // === MONITORING DETEKSI PINDAH TAB (ALARM UTAMA) ===
      document.addEventListener("visibilitychange", function() {
        if (document.hidden && ujianAktif) {
          statusSistemTerkunci = true;
          pelanggaranKe++;
          
          var txtPelanggaran = document.getElementById('pesan-pelanggaran');
          if (txtPelanggaran) {
            txtPelanggaran.innerText = `Anda terdeteksi meninggalkan halaman ujian secara sengaja! (Total Pelanggaran: ${pelanggaranKe})`;
          }
          
          var scrLock = document.getElementById('lock-screen');
          if (scrLock) scrLock.classList.remove('hidden');

          var userSiswa = document.getElementById('username').value;
          var passSiswa = document.getElementById('password').value;
          var tokenInput = document.getElementById('siswa-token-input');
          var token = tokenInput ? tokenInput.value : "-";

          google.script.run.catatLogSiswa(userSiswa, passSiswa, token, `MELAKUKAN PELANGGARAN KELUAR APLIKASI KE-${pelanggaranKe}`);
          
          // Bunyikan Alarm Kencang
          putarAlarmMembahana();
        }
      });

      window.addEventListener('focus', function() {
        if (ujianAktif && statusSistemTerkunci) {
          putarAlarmMembahana();
        }
      });

      function putarAlarmMembahana() {
        if (!ujianAktif || !statusSistemTerkunci) return;
        
        // JALUR 1: Putar File MP3
        try {
          alarmSuara.muted = false; 
          alarmSuara.loop = true; 
          alarmSuara.volume = 1.0; 
          alarmSuara.play().catch(function(e){});
        } catch(e) {}

        // JALUR 2: Sinyal Osilator Nada Tinggi (Pasti Bersuara Lewat Engine Browser Internal)
        try {
          if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
          if (audioCtx.state === 'suspended') audioCtx.resume();

          if(!intervalSuaraBip) {
            intervalSuaraBip = setInterval(function() {
              if (!statusSistemTerkunci) {
                clearInterval(intervalSuaraBip);
                intervalSuaraBip = null;
                return;
              }
              
              var osc = audioCtx.createOscillator();
              var gain = audioCtx.createGain();
              
              osc.type = 'sawtooth'; // Gelombang bising ambulans melengking
              osc.frequency.setValueAtTime(1000, audioCtx.currentTime); // Frekuensi 1000Hz kencang
              
              gain.gain.setValueAtTime(0.9, audioCtx.currentTime);
              gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.25);
              
              osc.connect(gain);
              gain.connect(audioCtx.destination);
              
              osc.start();
              osc.stop(audioCtx.currentTime + 0.25);
            }, 350);
          }
        } catch (err) {}
      }

      // === UNLOCK SYSTEM OLEH PENGAWAS ===
      function matikanAlarmSistem() {
        var elInput = document.getElementById('input-token-reset');
        var tokenResetInput = elInput ? elInput.value.toString().trim().toUpperCase() : "";
        
        if (!tokenResetInput) {
          tampilkanAlertKustom("Kolom Kosong", "Silakan masukkan Token Reset Guru terlebih dahulu!", "warning");
          return;
        }

        google.script.run.withSuccessHandler(function(tokenResetValidDatabase) {
          if (tokenResetInput === tokenResetValidDatabase.toUpperCase()) {
            matikanBunyiAlarmSecaraFisik();   
            statusSistemTerkunci = false;
            
            if(elInput) elInput.value = "";
            
            var scrLock = document.getElementById('lock-screen');
            if (scrLock) scrLock.classList.add('hidden');

            var userSiswa = document.getElementById('username').value;
            var passSiswa = document.getElementById('password').value;
            var tokenInput = document.getElementById('siswa-token-input');
            var token = tokenInput ? tokenInput.value : "-";
            
            google.script.run.catatLogSiswa(userSiswa, passSiswa, token, "ALARM DIBYPASS OLEH GURU / PENGAWAS");
          } else {
            tampilkanAlertKustom("Akses Ditolak", "Token Atasan Salah! Alarm pengunci tidak bisa dimatikan.", "danger");
          }
        }).getTokenReset();
      }

      function matikanBunyiAlarmSecaraFisik() {
        try {
          alarmSuara.pause();
          alarmSuara.currentTime = 0;
        } catch(e){}
        if (intervalSuaraBip) {
          clearInterval(intervalSuaraBip);
          intervalSuaraBip = null;
        }
      }

      document.addEventListener('contextmenu', e => e.preventDefault());
    </script>
  </body>
</html>
Selanjutnya AdminView.html
AdminView.html
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>

<div class="min-h-screen flex flex-col md:flex-row bg-gray-50">
  <div class="w-full md:w-64 bg-slate-900 text-white p-6 flex flex-col justify-between">
    <div>
      <div class="flex items-center space-x-3 mb-8">
        <i class="fas fa-laptop-code text-2xl text-blue-400"></i>
        <span class="text-xl font-bold tracking-wider">CBT SYS ADMIN</span>
      </div>
      <nav class="space-y-2">
        <a href="#" class="flex items-center space-x-3 bg-blue-700 p-3 rounded-xl text-sm font-semibold transition">
          <i class="fas fa-th-large"></i> <span>Dashboard</span>
        </a>
      </nav>
    </div>
    <button onclick="logoutAdmin()" class="w-full mt-8 py-2 border border-red-500 text-red-400 hover:bg-red-500 hover:text-white rounded-xl text-sm font-bold transition shadow-sm">
      LOGOUT
    </button>
  </div>

  <div class="flex-1 p-6 md:p-10">
    <div class="flex justify-between items-center mb-8 border-b pb-4">
      <h1 class="text-2xl font-bold text-gray-800">Admin Home</h1>
      <button onclick="bukaModalUjian()" class="bg-blue-600 hover:bg-blue-700 text-white font-semibold px-4 py-2 rounded-xl shadow-md text-sm">
        <i class="fas fa-plus mr-1"></i> Buat Ujian
      </button>
    </div>

    <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
      <div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100 flex justify-between items-center">
        <div>
          <p class="text-gray-400 font-medium text-sm">Total Ujian Aktif</p>
          <h3 id="stat-ujian" class="text-3xl font-bold text-gray-800 mt-1">0</h3>
        </div>
        <div class="bg-blue-50 text-blue-600 p-4 rounded-xl text-xl"><i class="fas fa-file-alt"></i></div>
      </div>
      
      <div onclick="ubahTokenBypassSistem()" class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100 flex justify-between items-center cursor-pointer hover:border-amber-400 transition group duration-200">
        <div>
          <p class="text-gray-400 font-medium text-sm group-hover:text-amber-500 transition">Token Bypass Pengawas (Klik untuk Ubah)</p>
          <h3 id="display-token-bypass" class="text-xl font-mono font-bold text-emerald-600 mt-2 bg-gray-50 px-3 py-1 rounded-lg border border-gray-100 inline-block">MEMUAT...</h3>
        </div>
        <div class="bg-emerald-50 text-emerald-600 p-4 rounded-xl text-xl group-hover:bg-amber-50 group-hover:text-amber-600 transition"><i class="fas fa-edit"></i></div>
      </div>
    </div>

    <div class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
      <div class="p-5 border-b bg-gray-50">
        <h2 class="font-bold text-gray-700">Daftar Link Ujian & Kontrol Status</h2>
      </div>
      <div class="overflow-x-auto">
        <table class="w-full text-left border-collapse">
          <thead>
            <tr class="bg-gray-100 text-gray-600 text-xs font-semibold uppercase">
              <th class="p-4">Nama Ujian</th>
              <th class="p-4">Token Akses</th>
              <th class="p-4">Durasi Waktu</th>
              <th class="p-4">Link Google Form Target</th>
              <th class="p-4 text-center">Status</th>
              <th class="p-4 text-center">Aksi</th>
            </tr>
          </thead>
          <tbody id="tabel-ujian-body" class="text-sm text-gray-600 divide-y">
            <tr><td colspan="6" class="p-4 text-center text-gray-400">Memuat data ujian...</td></tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>

<div id="modal-ujian" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 hidden">
  <div class="bg-white w-full max-w-md rounded-2xl p-6 shadow-2xl space-y-4">
    <h3 class="text-lg font-bold text-gray-800">Buat Sesi Ujian Baru</h3>
    <div>
      <label class="block text-xs font-semibold text-gray-500 mb-1">Nama Ujian</label>
      <input type="text" id="adm-nama" class="w-full p-2.5 border rounded-xl text-sm" placeholder="Contoh: UTS Matematika Kelas X">
    </div>
    <div>
      <label class="block text-xs font-semibold text-gray-500 mb-1">Link Google Form</label>
      <input type="text" id="adm-link" class="w-full p-2.5 border rounded-xl text-sm" placeholder="https://docs.google.com/forms/.../viewform">
    </div>
    <div class="grid grid-cols-2 gap-4">
      <div>
        <label class="block text-xs font-semibold text-gray-500 mb-1">Token Acak</label>
        <input type="text" id="adm-token" class="w-full p-2.5 border rounded-xl text-sm font-mono font-bold" placeholder="Contoh: MATX01">
      </div>
      <div>
        <label class="block text-xs font-semibold text-gray-500 mb-1">Durasi (Menit)</label>
        <input type="number" id="adm-durasi" class="w-full p-2.5 border rounded-xl text-sm font-bold" placeholder="Contoh: 90" value="90">
      </div>
    </div>
    <div class="flex justify-end space-x-2 pt-2">
      <button onclick="tutupModalUjian()" class="px-4 py-2 bg-gray-200 text-gray-700 text-sm font-semibold rounded-xl">Batal</button>
      <button onclick="simpanUjianBaru()" class="px-4 py-2 bg-blue-600 text-white text-sm font-semibold rounded-xl shadow">Terbitkan</button>
    </div>
  </div>
</div>

<script>
  function bukaModalUjian() { document.getElementById('modal-ujian').classList.remove('hidden'); }
  function tutupModalUjian() { document.getElementById('modal-ujian').classList.add('hidden'); }

  // === 1. MEMUAT DATA DASHBOARD UTAMA (ALUR AMAN & SEPARASI) ===
  function loadDataAdmin() {
    // A. Ambil nilai token reset terlebih dahulu dari sheet Pengaturan
    google.script.run
      .withSuccessHandler(function(tokenBypass) {
        document.getElementById('display-token-bypass').innerText = tokenBypass;
      })
      .withFailureHandler(function(err) {
        document.getElementById('display-token-bypass').innerText = "LOCKRESET99";
        console.log("Gagal memuat token dari server, menggunakan default.");
      })
      .getTokenReset();

    // B. Ambil daftar ujian secara terpisah agar proses asinkron tidak macet
    google.script.run
      .withSuccessHandler(function(list) {
        var totalAktif = list.filter(u => u.status && u.status.toString().trim().toUpperCase() === "AKTIF").length;
        document.getElementById('stat-ujian').innerText = totalAktif;
        
        var tbody = document.getElementById('tabel-ujian-body');
        tbody.innerHTML = "";
        
        if(list.length === 0) {
          tbody.innerHTML = `<tr><td colspan="6" class="p-4 text-center text-gray-400">Belum ada ujian yang dibuat.</td></tr>`;
          return;
        }
        
        list.forEach(function(u) {
          var statusBersih = u.status ? u.status.toString().trim().toUpperCase() : "NON-AKTIF";
          var badgeClass = statusBersih === "AKTIF" ? "bg-emerald-100 text-emerald-800" : "bg-rose-100 text-rose-800";
          var btnAction = statusBersih === "AKTIF" ? 
            `<button onclick="ubahStatusKunci('${u.token}', 'Nonaktif')" class="bg-rose-600 hover:bg-rose-700 text-white text-xs font-bold px-3 py-1.5 rounded-lg shadow-sm transition"><i class="fas fa-power-off mr-1"></i> Matikan</button>` :
            `<button onclick="ubahStatusKunci('${u.token}', 'Aktif')" class="bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-bold px-3 py-1.5 rounded-lg shadow-sm transition"><i class="fas fa-play mr-1"></i> Aktifkan</button>`;

          tbody.innerHTML += `
            <tr class="hover:bg-gray-50 transition">
              <td class="p-4 font-medium text-gray-800">${u.nama}</td>
              <td class="p-4 font-mono font-bold text-blue-600">${u.token}</td>
              <td class="p-4 font-semibold text-gray-700">${u.durasi} Menit</td>
              <td class="p-4 text-xs text-gray-400 truncate max-w-xs">${u.link}</td>
              <td class="p-4 text-center">
                <span class="px-2.5 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${badgeClass}">${statusBersih}</span>
              </td>
              <td class="p-4 text-center">${btnAction}</td>
            </tr>`;
        });
        
        // Proteksi: Tutup popup loading modal jika masih menggantung di layar
        if (typeof Swal !== 'undefined' && Swal.isVisible()) {
          Swal.close();
        }
      })
      .withFailureHandler(function(err) {
        var tbody = document.getElementById('tabel-ujian-body');
        tbody.innerHTML = `<tr><td colspan="6" class="p-4 text-center text-red-500">Gagal memuat data dari database Google Sheets.</td></tr>`;
      })
      .getDaftarUjian();
  }

  // === 2. FUNGSI UBAH TOKEN BYPASS VIA POPUP SWEETALERT ===
  function ubahTokenBypassSistem() {
    var tokenLama = document.getElementById('display-token-bypass').innerText;
    
    Swal.fire({
      title: 'Ubah Token Bypass Guru',
      text: 'Masukkan kode kunci baru untuk membuka kunci siswa:',
      input: 'text',
      inputValue: tokenLama,
      inputPlaceholder: 'Masukkan token baru (Contoh: GURUAMAN77)',
      showCancelButton: true,
      confirmButtonText: 'Simpan Kode',
      cancelButtonText: 'Batal',
      inputValidator: (value) => {
        if (!value) {
          return 'Token tidak boleh kosong!';
        }
      }
    }).then((result) => {
      if (result.isConfirmed) {
        Swal.fire({ title: 'Menyimpan...', didOpen: () => { Swal.showLoading(); } });
        
        google.script.run.withSuccessHandler(function(res) {
          if(res.status === "success") {
            Swal.fire({ icon: 'success', title: 'Berhasil diubah!', text: res.message, timer: 2000, showConfirmButton: false });
            loadDataAdmin(); // Muat ulang data dashboard
          } else {
            Swal.fire({ icon: 'error', title: 'Gagal', text: res.message });
          }
        }).simpanTokenResetBaru(result.value);
      }
    });
  }

  function ubahStatusKunci(token, statusBaru) {
    Swal.fire({ title: 'Memproses...', allowOutsideClick: false, didOpen: () => { Swal.showLoading(); } });
    google.script.run.withSuccessHandler(function(res) {
      if(res.status === "success") {
        Swal.fire({ icon: 'success', title: 'Berhasil!', text: 'Status ujian diperbarui.', timer: 1200, showConfirmButton: false });
        loadDataAdmin();
      }
    }).toggleStatusUjian(token, statusBaru);
  }

  function simpanUjianBaru() {
    var nama = document.getElementById('adm-nama').value;
    var link = document.getElementById('adm-link').value;
    var token = document.getElementById('adm-token').value;
    var durasi = document.getElementById('adm-durasi').value;
    
    // GANTI BARIS FORM VALIDASI MENJADI SEPERTI INI:
if(!nama || !link || !token || !durasi) {
  Swal.fire({
    icon: 'warning',
    title: 'Form Belum Lengkap',
    text: 'Semua kolom formulir wajib diisi!'
  });
  return;
}

    Swal.fire({ title: 'Menerbitkan...', didOpen: () => { Swal.showLoading(); } });
    google.script.run.withSuccessHandler(function(res) {
      tutupModalUjian();
      Swal.fire({ icon: 'success', title: 'Sukses!', text: 'Ujian baru aktif.', timer: 1500, showConfirmButton: false });
      loadDataAdmin();
      document.getElementById('adm-nama').value = "";
      document.getElementById('adm-link').value = "";
      document.getElementById('adm-token').value = "";
    }).tambahUjian(nama, link, token, durasi);
  }
</script>
Selanjutnya SiswaView.html
SiswaView
<div class="min-h-screen flex flex-col bg-slate-900 text-white relative">
  <div class="bg-slate-800 px-6 py-4 flex justify-between items-center border-b border-slate-700 shadow-md">
    <div class="flex items-center space-x-3">
      <i class="fas fa-user-circle text-2xl text-blue-400"></i>
      <div>
        <p class="text-xs text-gray-400 font-medium">Peserta Ujian</p>
        <span id="nama-siswa-display" class="font-bold text-sm tracking-wide">Siswa</span>
      </div>
    </div>
    
    <div id="info-ujian-berjalan" class="text-center hidden flex items-center gap-4">
      <div>
        <p class="text-xs text-yellow-400 font-bold animate-pulse"><i class="fas fa-shield-alt"></i> MODE UJIAN DIKUNCI AKTIF</p>
        <h2 id="judul-ujian-berjalan" class="text-sm font-bold text-gray-200">Ujian</h2>
      </div>
    </div>
    
    <button onclick="konfirmasiSelesai()" class="bg-red-600 hover:bg-red-700 px-4 py-2 rounded-xl text-xs font-bold transition shadow-md">Selesai & Keluar</button>
  </div>

  <div id="box-token-siswa" class="flex-1 flex items-center justify-center p-4">
    <div class="bg-slate-800 border border-slate-700 p-6 rounded-2xl shadow-xl w-full max-w-sm text-center">
      <i class="fas fa-key text-3xl text-yellow-500 mb-2 animate-bounce"></i>
      <h3 class="text-lg font-bold mb-1">Verifikasi Token Ujian</h3>
      <p class="text-gray-400 text-xs mb-4">Dapatkan token verifikasi dari guru pengawas ruang.</p>
      <input type="text" id="siswa-token-input" class="w-full p-3 bg-slate-900 border border-slate-600 rounded-xl text-center font-mono font-bold text-lg tracking-widest text-yellow-400 focus:outline-none focus:border-yellow-400" placeholder="KODE TOKEN">
      <button onclick="mulaiUjianSiswa()" class="w-full mt-4 py-3 bg-blue-600 hover:bg-blue-700 rounded-xl font-bold text-sm uppercase transition shadow-lg">Mulai Kerjakan</button>
    </div>
  </div>

  <div id="container-iframe-ujian" class="flex-1 w-full h-full hidden bg-white">
    <iframe id="iframe-gform" src="" class="w-full h-full min-h-[85vh]" frameborder="0" marginheight="0" marginwidth="0">Memuat Soal Ujian...</iframe>
  </div>
</div>
Jangan Lupa untuk save Project 

Kemudian Develoy/Terapkan


Pilih Aplikasi Web


Pastikan memilih Siapa Saja/Anyone with Link kemudian terapkan

Izinkan akses _Klik Hide Advanced_Klik Go to ......... (unsafe)


Selanjutnya klik continue hingga muncul

Klik linknya 
Tampilan siswa dan admin



Untuk memulai Ujian Siswa maka isi terlebih spreadsheet dengan nama Sheet Siswa untuk masuk kedalam sistem admin boleh isi chat atau silahkan tinggalkan pesan dan gmai nanti akan dikirimkan gratis oleh admin.

Tampilan Admin



Untuk menambahkan ujian bisa labngsung klik tambahkan, jika ingin sekaligus bisa langsung via spreadsheet degan Sheet Ujian

Selamat Mencoba😃




sejarah31.com
sejarah31.com sejarah31.com adalah web yang dibuat untuk berbagi informasi seputar dunia pendidikan dan sejarah.

Posting Komentar untuk "SISTEM UJIAN BERBASIS ONLINE"