tail -100 /var/moodledata/logs/wa_debug.log
Tips Paket Internet Pomo Special
4G Ceria Telkomsel - Agar Tidak Habis Pulsa..
4G Ceria Telkomsel - Agar Tidak Habis Pulsa..
Cara Install driver printer Epson L321 di Ubuntu Linux
Cara Install driver Printer Canon di Ubuntu Linux
tail -100 /var/moodledata/logs/wa_debug.log
Karena yang muncul adalah HTTP 500 Internal Server Error, berarti ada fatal error PHP, bukan error routing. File izin_guru.php memang ada di plugin Anda, sehingga penyebabnya hampir pasti berasal dari kode yang baru diubah.
Langkah tercepat adalah melihat error sebenarnya.
Jalankan di server:
tail -50 /var/log/apache2/error.log
atau jika memakai PHP-FPM:
tail -50 /var/log/php8.2-fpm.log
atau jika menggunakan Nginx:
tail -50 /var/log/nginx/error.log
Kalau Moodle mengaktifkan debugging, Anda juga bisa:
$CFG->debug = (E_ALL | E_STRICT);
$CFG->debugdisplay = 1;
atau melalui Site administration → Development → Debugging.
Kemungkinan terbesar ada pada blok notifikasi WA yang baru ditambahkan, misalnya:
ada koma yang hilang pada array,
ada kurung } atau ) yang tidak seimbang,
memanggil fungsi yang belum ada,
atau ada syntax error setelah penambahan:
jm_kirim_template_auto(...)
atau
$datawa = [
...
];
Tolong kirim salah satu dari berikut:
Isi file izin_guru.php terbaru, atau minimal bagian yang baru Anda ubah (terutama blok notifikasi WA), atau
Hasil:
php -l /var/www/html/moodle/local/jurnalmengajar/izin_guru.php
Perintah php -l biasanya langsung menunjukkan:
Parse error:
Fatal error:
Unexpected ...
on line ...
sehingga kita bisa menemukan letak error dalam hitungan menit.
Menurut saya, pengembangan yang kita lakukan kali ini bukan sekadar menambah fitur, tetapi menambahkan satu modul baru yang terintegrasi dengan sistem reminder jurnal.
Dibuat tabel baru (tanpa upgrade plugin), misalnya:
mdl_local_jurnalmengajar_kehadiran
Fungsinya menyimpan data guru yang tidak hadir.
Status yang didukung:
Sakit
Izin
Cuti
Tugas Luar
Status Alpa sengaja tidak dibuat, karena guru yang alpa tetap harus masuk ke reminder jurnal.
Dibuat halaman administrasi:
guru_takhadir.php
beserta CRUD:
✅ guru_takhadir.php
✅ guru_takhadir_add.php
✅ guru_takhadir_edit.php
✅ guru_takhadir_delete.php
Fitur:
pilih guru (role local/jurnalmengajar:submit)
status
periode mulai
periode selesai
keterangan
lib.phpDibuat fungsi:
jurnalmengajar_get_status_takhadir($userid, $tanggal)
Fungsinya:
mengecek apakah guru sedang tidak hadir
mengembalikan:
false
sakit
izin
cuti
tugasluar
Sehingga logika pengecekan cukup dipanggil dari satu fungsi.
notif_jurnal.phpAlur lama:
Jadwal
↓
Jam terlewat
↓
Sudah isi?
↓
Reminder
Sekarang menjadi:
Jadwal
↓
Cutoff
↓
Jam terlewat
↓
Guru Tidak Hadir?
│
├── Ya
│ ↓
│ Rekap Guru Tidak Hadir
│
└── Tidak
↓
Sudah isi jurnal?
│
├── Ya
└── Belum → Reminder
Akibatnya:
Guru sakit tidak menerima reminder.
Guru izin tidak menerima reminder.
Guru cuti tidak menerima reminder.
Guru tugas luar tidak menerima reminder.
Guru alpa tetap menerima reminder.
Rekap yang sebelumnya hanya berisi:
Guru Belum Mengisi Jurnal
sekarang menjadi:
Rekap Jurnal Mengajar
Belum Mengisi Jurnal
...
Guru Tidak Hadir
...
Sehingga admin dapat membedakan:
siapa yang memang lupa mengisi jurnal,
siapa yang memang tidak masuk.
Ditambahkan placeholder baru:
{tidakhadir}
Template default juga diperbarui agar menampilkan dua bagian:
Belum Mengisi Jurnal
Guru Tidak Hadir
Ditambahkan debug baru:
FILLED:
TAKHADIR:
Contoh:
TAKHADIR: Ahmad | XI-A | Sakit
TAKHADIR: Ahmad | XI-B | Sakit
Sehingga proses pengecekan lebih mudah ditelusuri saat menjalankan:
php local/jurnalmengajar/cli/notif_jurnal.php
Sekarang sistem reminder jurnal mampu membedakan tiga kondisi guru:
| Kondisi Guru | Reminder WA | Rekap Admin |
|---|---|---|
| Sudah mengisi jurnal | ❌ Tidak | ❌ Tidak |
| Belum mengisi jurnal | ✅ Ya | ✅ Ya |
| Sakit / Izin / Cuti / Tugas Luar | ❌ Tidak | ✅ Ya |
Menurut saya, ini adalah peningkatan yang cukup signifikan karena sebelumnya sistem hanya mengenal dua kondisi:
Sudah mengisi jurnal.
Belum mengisi jurnal.
Sekarang sistem mengenal tiga kondisi operasional:
Sudah mengisi jurnal → tidak perlu tindakan.
Belum mengisi jurnal → dikirim reminder.
Guru tidak hadir (sakit, izin, cuti, tugas luar) → tidak dikirim reminder, tetapi tetap tercatat dalam rekap admin.
Dengan demikian, reminder menjadi lebih akurat dan rekap yang diterima admin lebih informatif serta sesuai dengan kondisi pembelajaran di sekolah.
<?php
define('CLI_SCRIPT', true);
require_once(__DIR__.'/../../../config.php');
require_once(__DIR__.'/../jam_pelajaran_lib.php');
require_once(__DIR__.'/../jadwal_acuan_lib.php');
require_once(__DIR__.'/../lib.php');
require_once(__DIR__.'/../lib_notifikasi.php');// fungsi kirim WA
global $DB;
$cohortmap = [];
$cohorts = $DB->get_records('cohort', null, '', 'id,name');
foreach ($cohorts as $c) {
$cohortmap[$c->id] = $c->name;
}
$today = date('Y-m-d');
$hariIndo = jurnalmengajar_get_hari_ini();
$current = time();
$todayLabel = tanggal_indo(time());
$jamrekap = '19:50';
$jamsekarang = date('H:i');
//$isrekap = ($jamsekarang >= $jamrekap);
$isrekap = true; //test mode rekap
// ===== Cek hari sekolah =====
$hariSekolah = get_config('local_jurnalmengajar', 'harisekolah');
if (empty($hariSekolah)) {
$hariSekolah = 'Senin,Selasa,Rabu,Kamis,Jumat';
}
$hariSekolah = array_map('trim', explode(',', $hariSekolah));
if (!in_array($hariIndo, $hariSekolah)) {
mtrace("Hari $hariIndo bukan hari sekolah.");
exit(0);
}
// ===== Cek tanggal libur =====
if (jurnalmengajar_cek_libur($today)) {
mtrace("Hari ini tanggal libur.");
exit(0);
}
// ===== Cek tanggal asesmen =====
$tanggalasesmen = trim(get_config('local_jurnalmengajar', 'tanggalasesmen'));
if (!empty($tanggalasesmen)) {
if (preg_match('/(\d{4}-\d{2}-\d{2})\s*s\/d\s*(\d{4}-\d{2}-\d{2})/i',
$tanggalasesmen,
$match)) {
$mulai = strtotime($match[1]);
$selesai = strtotime($match[2]);
$hariini = strtotime($today);
if ($hariini >= $mulai && $hariini <= $selesai) {
mtrace("Hari ini berada dalam rentang asesmen.");
exit(0);
}
}
}
mtrace("=== Notifikasi Jurnal Rekap ===");
mtrace("Hari: $hariIndo");
// ===== Ambil jam pelajaran =====
$jam_pelajaran = jurnalmengajar_generate_jam();
// ===== Tentukan jam yang sudah selesai =====
$jam_terlewat = [];
foreach ($jam_pelajaran as $jamke => $jam) {
$selesai = $jam['selesai'];
if ($current > strtotime("$today $selesai")) {
$jam_terlewat[] = $jamke;
}
}
if (empty($jam_terlewat)) {
mtrace("Belum ada jam pelajaran yang terlewat.");
exit(0);
}
mtrace("Jam terlewat: " . implode(',', $jam_terlewat));
// ===== Ambil jurnal hari ini =====
$starttoday = strtotime("$today 00:00:00");
$endtoday = strtotime("$today 23:59:59");
$jurnaltoday = $DB->get_records_sql("
SELECT id, userid, kelas, jamke
FROM {local_jurnalmengajar}
WHERE timecreated BETWEEN :starttoday AND :endtoday
", [
'starttoday' => $starttoday,
'endtoday' => $endtoday
]);
$filled = [];
foreach ($jurnaltoday as $row) {
foreach (explode(',', $row->jamke) as $j) {
$j = (int)trim($j);
// Samakan kelas dengan jadwal
$kelas = $row->kelas;
if (isset($cohortmap[$kelas])) {
$kelas = $cohortmap[$kelas];
}
$key = $row->userid . '-' . $kelas . '-' . $j;
$filled[$key] = true;
// Debug
mtrace("FILLED: " . $key);
}
}
//
// ===== Ambil jadwal dari database =====
$jadwal_db = $DB->get_records_sql("
SELECT j.id, j.userid, j.kelas, j.jamke, u.lastname
FROM {local_jurnalmengajar_jadwal} j
JOIN {user} u ON u.id = j.userid
WHERE j.hari = :hari
", [
'hari' => $hariIndo
]);
$jadwal = [];
foreach ($jadwal_db as $j) {
$jadwal[] = [
'userid' => $j->userid,
'lastname' => $j->lastname,
'kelas' => $j->kelas,
'jamke' => $j->jamke
];
}
if (empty($jadwal)) {
mtrace("Tidak ada jadwal di database untuk hari $hariIndo");
exit(0);
}
mtrace("=== JADWAL ===");
foreach ($jadwal as $j) {
$k = $j['userid'].'-'.$j['kelas'].'-'.$j['jamke'];
mtrace("JADWAL: " . $k);
}
// ===== Group jurnal yang belum diisi =====
$pending = [];
$tidakhadir = [];
$cutoff_cache = [];
foreach ($jadwal as $j) {
// 🔥 FILTER CUT OFF MULTI KELAS
$kelas_level = null;
// deteksi kelas (VI, IX, XII)
if (preg_match('/\b(VI|IX|XII)\b/i', $j['kelas'], $match)) {
$kelas_level = strtoupper($match[1]);
}
if ($kelas_level) {
if (!isset($cutoff_cache[$kelas_level])) {
$cutoff_cache[$kelas_level] = jurnalmengajar_get_cutoff_by_kelas($kelas_level, $current);
}
$cutoff = $cutoff_cache[$kelas_level];
if ($cutoff && $current >= $cutoff) {
continue;
}
}
// ===== Lewati jika jam belum selesai =====
if (!in_array((int)$j['jamke'], $jam_terlewat)) {
continue;
}
// ===== Cek Guru Tidak Hadir =====
$status = jurnalmengajar_get_status_takhadir(
$j['userid'],
$today
);
if ($status !== false) {
if (!isset($tidakhadir[$j['userid']])) {
$tidakhadir[$j['userid']] = [
'lastname' => $j['lastname'],
'status' => $status
];
}
// Debug: tampilkan semua kelas yang sudah terlewat
mtrace(
"TAKHADIR: {$j['lastname']} | {$j['kelas']} | " .
ucfirst($status)
);
continue;
}
$key = $j['userid'] . '-' . $j['kelas'] . '-' . (int)$j['jamke'];
if (isset($filled[$key])) {
continue;
}
if (!isset($pending[$j['userid']])) {
$pending[$j['userid']] = [
'lastname' => $j['lastname'],
'kelasjam' => []
];
}
if (!isset($pending[$j['userid']]['kelasjam'][$j['kelas']])) {
$pending[$j['userid']]['kelasjam'][$j['kelas']] = [];
}
$pending[$j['userid']]['kelasjam'][$j['kelas']][] = (int)$j['jamke'];
}
if (empty($pending) && empty($tidakhadir)) {
mtrace("Semua jurnal sudah diisi.");
exit(0);
}
// ===== Kirim WA per guru =====
$mengirim = 0;
if (!$isrekap) {
mtrace("Mode: Reminder Guru");
foreach ($pending as $userid => $info) {
// $user = $DB->get_record('user', ['id'=>$userid], 'id, firstname, lastname');
// Ambil nomor WA
$nowa = $DB->get_field_sql("
SELECT d.data
FROM {user_info_data} d
JOIN {user_info_field} f ON f.id = d.fieldid
WHERE d.userid = :userid AND f.shortname = 'nowa'
", ['userid' => $userid]);
if (empty($nowa)) {
mtrace("Tidak ada nomor WA untuk {$info['lastname']}");
continue;
}
$nomor = preg_replace('/[^0-9]/', '', $nowa);
// Urutkan berdasarkan jam pertama
$urut = [];
foreach ($info['kelasjam'] as $kelas => $jamlist) {
$jamlist = array_unique($jamlist);
sort($jamlist);
$urut[$kelas] = $jamlist;
}
// Sort berdasarkan jam pertama
uasort($urut, function($a, $b) {
return $a[0] <=> $b[0];
});
$listkelas = "";
$ringkasParts = [];
foreach ($urut as $kelas => $jamlist) {
$listkelas .= "$kelas jam ke " . implode(',', $jamlist) . "\n";
$ringkasParts[] = $kelas . ':' . implode(',', $jamlist);
}
$ringkas = implode('; ', $ringkasParts);
$datawa = [
'{guru}' => $info['lastname'],
'{tanggal}' => $todayLabel,
'{kelasjam}' => trim($listkelas)
];
$res = jm_kirim_template(
'reminder_jurnal',
$nomor,
$datawa
);
$pending[$userid]['ringkas'] = $ringkas;
mtrace("Kirim ke $nomor ({$info['lastname']}) -> $res");
if ($res) {
$mengirim++;
}
// ===== Log TXT =====
$logtxt = __DIR__ . '/notif_log_' . date('Y-m-d') . '.txt';
$logstatus = $res ? 'BERHASIL' : 'GAGAL';
$line = date('Y-m-d H:i:s')
. " | Guru: {$info['lastname']}"
. " | Nomor: $nomor"
. " | Kelas/Jam: $ringkas"
. " | Status: $logstatus"
. "\n";
file_put_contents($logtxt, $line, FILE_APPEND);
}
} else {
// Mode rekap: tidak kirim WA ke guru,
// hanya membuat ringkasan.
mtrace("Mode: Rekap Admin");
foreach ($pending as $userid => $info) {
$urut = [];
foreach ($info['kelasjam'] as $kelas => $jamlist) {
$jamlist = array_unique($jamlist);
sort($jamlist);
$urut[$kelas] = $jamlist;
}
uasort($urut, function($a, $b) {
return $a[0] <=> $b[0];
});
$ringkasParts = [];
foreach ($urut as $kelas => $jamlist) {
$ringkasParts[] = $kelas . ':' . implode(',', $jamlist);
}
$pending[$userid]['ringkas'] = implode('; ', $ringkasParts);
}
}
if ($isrekap) {
$daftar = '';
$daftartakhadir = '';
if (empty($tidakhadir)) {
$daftartakhadir = '-';
}
foreach ($pending as $info) {
$daftar .= "• {$info['lastname']} - {$info['ringkas']}\n";
}
foreach ($tidakhadir as $info) {
$daftartakhadir .=
"• {$info['lastname']} - " .
ucfirst($info['status']) .
"\n";
}
$datawa = [
'{tanggal}' => $todayLabel,
'{daftar}' => trim($daftar),
'{jumlah}' => count($pending),
'{tidakhadir}' => trim($daftartakhadir)
];
// DEBUG
$config = get_config(
'local_jurnalmengajar',
'tujuan_rekap_reminder'
);
mtrace("CONFIG TUJUAN : " . $config);
$nomor = jm_get_nomor_tujuan(
'rekap_reminder',
$datawa
);
mtrace("NOMOR TUJUAN:");
print_r($nomor);
$template = jm_get_template('rekap_reminder');
mtrace("TEMPLATE:");
mtrace(empty($template) ? "(KOSONG)" : "ADA");
$res = jm_kirim_template_auto(
'rekap_reminder',
$datawa
);
if ($res) {
mtrace("Rekap reminder dikirim.");
} else {
mtrace("Rekap reminder gagal.");
}
}
mtrace("Selesai. Total notifikasi dikirim: $mengirim");
Karena kolom program di database menggunakan ENUM, maka ada 2 langkah.
Cek sekarang:
DESC jamaah;
Lalu ubah menjadi:
ALTER TABLE jamaah
MODIFY program ENUM(
'3_hari',
'10_hari',
'15_hari',
'40_hari',
'4_bulan',
'lainnya'
);
<select name="program">Di tambah.php dan edit.php, cari bagian:
<option value="10_hari"
<?= (($_POST['program'] ?? '') == '10_hari') ? 'selected' : '' ?>>
10 Hari
</option>
<option value="40_hari"
<?= (($_POST['program'] ?? '') == '40_hari') ? 'selected' : '' ?>>
40 Hari
</option>
Sisipkan di tengah:
<option
value="15_hari"
<?= (($_POST['program'] ?? '') == '15_hari') ? 'selected' : '' ?>>
15 Hari
</option>
Sehingga urutannya menjadi:
3 Hari
10 Hari
15 Hari
40 Hari
4 Bulan
Lainnya
Kalau ada halaman lain yang menampilkan program (misalnya index.php, rute.php, rekap.php), tidak perlu diubah karena yang disimpan tetap string 15_hari. Yang berubah hanya pilihan pada form dan nilai ENUM di database.
Bagus, sekarang sudah jelas. Anda tidak menggunakan Chrome + Chromium, melainkan dua profil Google Chrome:
Default → kemungkinan WhatsApp pribadi
Profile_1 → kemungkinan WhatsApp Business
Ini justru lebih bagus daripada memakai Chromium.
Lihat isi masing-masing launcher:
grep '^Name=' ~/.local/share/applications/chrome-hnpfjngllnobngcgfapefoaidbinmjnm-Default.desktop
grep '^Name=' ~/.local/share/applications/chrome-hnpfjngllnobngcgfapefoaidbinmjnm-Profile_1.desktop
Sekarang ubah namanya.
nano ~/.local/share/applications/chrome-hnpfjngllnobngcgfapefoaidbinmjnm-Default.desktop
Ubah:
Name=WhatsApp
nano ~/.local/share/applications/chrome-hnpfjngllnobngcgfapefoaidbinmjnm-Profile_1.desktop
Ubah:
Name=WhatsApp Business
Simpan, lalu jalankan:
update-desktop-database ~/.local/share/applications
Logout-login, atau tekan Alt+F2, ketik r (jika menggunakan X11), atau cukup tutup dan buka kembali menu aplikasi.
Mari kita lihat isi launcher yang kedua:
cat ~/.local/share/applications/chrome-hnpfjngllnobngcgfapefoaidbinmjnm-Profile_1.desktop
Terutama saya ingin melihat baris:
Icon=
Kalau Chrome menggunakan ikon lokal, kita bisa menggantinya dengan logo WhatsApp Business sehingga nanti di Dock tampil seperti ini:
🟢🅱 WhatsApp Business
Jadi tidak akan pernah tertukar.
Menurut saya ini adalah konfigurasi yang paling rapi untuk penggunaan sehari-hari.
pkill 5050
dhwan@dhwan-Aspire-A514-51K:~$ mocp
Running the server...
It seems that the server is already running with pid 5050.
If it is not true, remove the pid file (/home/dhwan/.moc/pid) and try again.
FATAL_ERROR: Exiting!
FATAL_ERROR: Server exited!
Pesan itu berarti proses MOC server masih dianggap berjalan dengan PID 5050, tetapi kemungkinan prosesnya sudah mati (stale PID).
Coba periksa dulu apakah proses 5050 benar-benar masih ada:
ps -p 5050
PID TTY TIME CMD
atau tidak ada proses, hapus file PID:
rm ~/.moc/pid
Lalu jalankan lagi:
mocp
Matikan dengan benar:
kill 5050
Jika masih membandel:
kill -9 5050
Lalu jalankan lagi:
mocp
kill sudah dilakukan tetapi tetap munculCek apakah ada beberapa proses MOC:
pgrep -a mocp
pgrep -a moc
ps aux | grep moc
Tolong kirimkan hasil dari ketiga perintah berikut:
ps -p 5050
ls -l ~/.moc/pid
cat ~/.moc/pid
Dari situ bisa diketahui apakah ini proses yang masih hidup atau hanya file PID yang tertinggal.
Kurang Guru
1 Al Quran = 42 JP
2 Sejarah = 27 JP
3 Bahasa Inggris = 19 JP
4 PJOK = 19 JP
5 Bahasa Indonesia = 18 JP
6 KWU = 18 JP
7 Matematika = 13 JP
8 Biologi = 10 JP
9 Ekonomi = 8 JP
10 Fisika = 7 JP
11 Sosiologi = 6 JP
12 Seni = 4 JP
13 Geografi = 4 JP
14 PKN = 4 JP
15 Informatika = 2 JP
bila dipakai untuk wakasek
Seni 6
Matematika 6
Kewirausahaan (KWU) 7
Sosiologi 4
Ekonomi 6
menjadi
1 Al Quran = 42 JP
2 Sejarah = 27 JP
3 Bahasa Inggris = 19 JP
4 PJOK = 19 JP
5 Bahasa Indonesia = 18 JP
6 KWU = 18 JP - seni 2 = 16 JP - KWU 7 = 11 JP
7 Matematika = 13 JP - matematika 6 = 7 JP
8 Biologi = 10 JP
9 Ekonomi = 8 JP - ekonomi 6 = 2 JP
10 Fisika = 7 JP
11 Sosiologi = 6 JP - sosiologi 4 = 2 JP
12 Seni = 4 JP - seni 4 = 0
13 Geografi = 4 JP
14 PKN = 4 JP
15 Informatika = 2 JP
Kesimpulan
Kelebihan jam
1 Al Quran = 42 JP
2 Sejarah = 27 JP
3 Bahasa Inggris = 19 JP
4 PJOK = 19 JP
5 Bahasa Indonesia = 18 JP
6 KWU = 11 JP
7 Matematika = 7 JP
8 Biologi = 10 JP
9 Ekonomi = 2 JP
10 Fisika = 7 JP
11 Sosiologi = 2 JP
12 Geografi = 4 JP
13 PKN = 4 JP
14 Informatika = 2 JP
Menu Timetable → Locking digunakan setelah FET berhasil membuat jadwal.
Fungsinya untuk mengunci hasil jadwal supaya ketika Generate ulang, aktivitas tertentu tidak dipindah lagi.
Lock all activities of the current timetable
Mengunci seluruh jadwal yang sudah dihasilkan.
Contoh:
Senin JP1 Matematika X-A
Senin JP2 Fisika X-A
...
semuanya dikunci.
Saat Generate lagi, FET harus mempertahankan posisi tersebut.
Unlock all activities of the current timetable
Membuka semua kunci.
FET bebas menyusun ulang dari awal.
Lock all activities of a specified day
Mengunci seluruh aktivitas pada hari tertentu.
Misalnya:
Senin
maka semua jadwal Senin tetap.
FET hanya boleh mengubah Selasa–Jumat.
Kebalikan dari nomor 3.
Lock all activities which end students day
Mengunci aktivitas yang berada pada jam terakhir siswa.
Jarang dipakai.
Misalnya Anda punya tag:
Praktikum
Agama
Lab
Semua aktivitas dengan tag tersebut dikunci.
Ini yang paling berguna.
Misalnya Anda ingin mengunci semua:
Matematika
atau
Guru tertentu
atau
Kelas X-A
tanpa mengunci yang lain.
Misalnya:
Jadwal sudah bagus 95%.
Hanya guru Matematika yang perlu diperbaiki.
Maka:
Lock all activities of current timetable
lalu buka (unlock) hanya aktivitas Matematika.
Kemudian Generate lagi.
FET hanya mengubah Matematika, yang lain tetap.
Saya sarankan jangan gunakan Locking dulu.
Karena Anda masih:
Menambah constraint MGMP
Mengatur jam pengawas
Mengatur batas jam guru
Biarkan FET menyusun ulang secara bebas sampai semua constraint final.
Locking biasanya dipakai setelah jadwal hampir selesai 100% dan hanya ada sedikit perbaikan.
Jumlah keseluruhan = 42 guru.
Guru yang memiliki aktivitas 2 JP dan juga 3 JP (minimal satu aktivitas 2 JP dan minimal satu aktivitas 3 JP) adalah:
Total: 24 guru.
Guru yang hanya memiliki aktivitas 2 JP
Total: 12 guru.
Guru yang seluruh aktivitasnya hanya berdurasi 3 JP (tidak memiliki aktivitas 1 JP, 2 JP, atau durasi lain) adalah:
=======================================
Drs. Muhammad Rafi'i M.Pd 35
Muhammad Saubari S.Pd.I 35
Nini Rahmini S.Pd.I 35
Ainayya Almadiyati S.Pd 34
Fathah Arsyad S.Pd 33
Muhammad Wildan Ariandi S.Pd 33
Ahmad Hafie S.Pd 32
Fajri Wahyudi S.Pd 32
Indah Wiratesta Manik S.Th 30
Rakhmad Taufik S.Pd 30
Sri Muliyatini S.Pd 30
Hj. Silfiana Hani S.Pd 30
Virawati S.Pd 29
Dra. Hj. Marfuah M.Pd 28
Dwiyani Lestari M.Pd 28
Hartati Jum'ah S.Pd 28
M. Riduan S.Pd 28
Risyandi S.Pd 28
Muhammad Naufal Akbar M.Pd 27
Hj. Maliyani S.Pd 26
Hj. Samnah Hayati M.Pd 26
Mariatul Qibtiah S.Pd 26
Sri Heldawati S.Pd 26
Sri Hidayati S.Pd 26
Anna Herlina S.Pd 25
Noor Ridhwan S.Pd M.A 25
Dra. Hj. Tri Krisnawati M.Pd 24
Ema Susi Darmawati S.Pd 24
Faridra Ramadhan P.S.Pd 24
Hj. St. Faridah S.Pd 24
Yekti Hanani S.Pd MA 24
Mukhtar Fuaddi S.Pd 18
Fatia Elfira Luthfyari S.Pd 16
Hj. Rianawati S.Pd 16
Muhammad Supiani S.IP 16
Nadya Rahmi Sari S.Pd 16
Khusnul Khotimah S.Pd 15
Muhammad Noor S.Pd 3
Misan S.Pd 2
Syamdani S.Pd 1
Ari Deviyanti S.Pd 1
Namun saya menyarankan jangan langsung memasukkan semua constraint ini.
Mulailah dari yang berat dulu:
35 JP -> max 7
34 JP -> max 7
33 JP -> max 7
32 JP -> max 7
Generate.
Kalau masih sukses, lanjut kelompok 30 JP (max 6).
Kalau langsung diberi constraint ketat untuk 42 guru sekaligus, FET bisa menjadi sulit menemukan solusi. Biasanya saya menambahkan bertahap sampai ketemu batas yang masih feasible.
| Hari | Guru |
|---|---|
| Senin | guru Seni, PJOK, Ekonomi |
| Selasa | guru Sosiologi, Sejarah, PPKn, Bahasa Indonesia |
| Rabu | guru Inggris, Geografi, Matematika, Agama |
| Kamis | guru Fisika, Kimia, Biologi, BK |
Saat kenaikan kelas
simpan_riwayatkelas.php.firstname dan cohort sesuai kelas baru.
2026/2027.
simpan_riwayatkelas.php.
Ya, karena Anda sudah berhasil mengekspor 461 siswa aktif ke CSV, maka Anda sudah punya cadangan data keanggotaan kelas saat ini.
Urutan yang saya sarankan sekarang:
Jangan diubah dulu. Simpan sebagai arsip:
siswa_aktif_20260618.csv
Saat ini cohort yang masih berisi siswa adalah:
75 X-A
76 X-B
77 X-C
78 X-D
79 X-E
80 X-F
81 X-G
82 XI-A
83 XI-B
84 XI-C
85 XI-D
86 XI-E
87 XI-F
88 XI-G
Cek dulu jumlah yang akan dihapus:
SELECT COUNT(*)
FROM mdl_cohort_members
WHERE cohortid IN (
75,76,77,78,79,80,81,
82,83,84,85,86,87,88
);
Harusnya hasil:
461
Kalau benar, lanjut:
DELETE FROM mdl_cohort_members
WHERE cohortid IN (
75,76,77,78,79,80,81,
82,83,84,85,86,87,88
);
SELECT COUNT(*)
FROM mdl_cohort_members;
Target:
0
Artinya seluruh siswa sudah tidak punya cohort.
Misalnya:
X-A → XI-C
X-B → XI-A
XI-A → XII-B
XI-B → XII-D
Isi kolom:
firstname
cohort1
dengan kelas baru.
Moodle:
Site administration
→ Users
→ Upload users
Pilih:
Update existing users only
dan aktifkan:
Allow cohort assignments
Maka:
firstname berubah menjadi kelas baru.
User masuk ke cohort baru.
Tidak ada sisa cohort lama karena sudah dikosongkan.
Sebelum menghapus 461 anggota cohort, pastikan cohort X-A s.d XII-E tetap ada (yang dikosongkan hanya anggotanya).
Cek:
SELECT id,name
FROM mdl_cohort
ORDER BY name;
Kalau cohortnya masih ada, maka aman untuk mengosongkan mdl_cohort_members dan mengisi ulang melalui CSV.
find . -maxdepth 1 -name "*.pdf" -size +2M -print0 |
while IFS= read -r -d '' f; do
echo "Memproses: $f"
gs \
-sDEVICE=pdfwrite \
-dCompatibilityLevel=1.4 \
-dPDFSETTINGS=/screen \
-dNOPAUSE \
-dQUIET \
-dBATCH \
-sOutputFile="${f%.pdf}-2mb.pdf" \
"$f"
done
Kalau Firefox normal terus reload (loading berputar terus) tetapi Firefox Private Mode bisa membuka 192.168.8.1 dengan normal, hampir pasti masalahnya bukan di modem E3276s, melainkan di browser.
Penyebab yang paling sering:
Firefox menyimpan JavaScript dan file CSS lama dari WebUI Huawei.
Coba:
Buka:
about:preferences#privacy
Cari Cookies and Site Data
Klik Manage Data
Cari:
192.168.8.1
Hapus data situs tersebut.
Atau lebih cepat:
Tekan Ctrl+Shift+Delete
Pilih Cached Web Content
Clear.
WebUI Huawei suka menyimpan data lokal yang membuat halaman looping.
Tekan:
F12
→ Storage
→ Hapus:
Local Storage
Session Storage
IndexedDB
untuk 192.168.8.1
Karena Private Mode biasanya menonaktifkan sebagian add-on.
Coba:
about:addons
sementara nonaktifkan:
uBlock Origin
Privacy Badger
NoScript
AdGuard
Cookie AutoDelete
Lalu buka lagi 192.168.8.1.
Kadang Firefox mencoba melakukan hal aneh pada alamat lokal.
Buka:
about:preferences#general
Network Settings → Settings
Matikan:
Enable DNS over HTTPS
Jika hanya WebUI Huawei yang bermasalah:
Buka:
Ctrl+Shift+R
(Hard Reload)
atau
Ctrl+F5
Di Firefox normal, buka:
http://192.168.8.1/api/device/information
atau
http://192.168.8.1/html/home.html
Jika tetap looping tetapi Private Mode lancar, kemungkinan cache/cookie WebUI Huawei yang korup.
Karena tadi Guru sempat mengubah band dan modem menggunakan WebUI modifikasi (ada tombol WatchCat), saya paling curiga cache JavaScript WebUI lama tersimpan di Firefox.
Coba hapus data situs khusus 192.168.8.1 dulu, tidak perlu menghapus cache seluruh Firefox. Biasanya langsung normal kembali.
Jika Guru ingin melihat:
find /var/www/html/moodle/local/jurnalmengajar -type f -daystart -mtime 0
find /var/www/html/moodle/local/jurnalmengajar -type f -daystart -mtime 1
find /var/www/html/moodle/local/jurnalmengajar -type f -daystart -mtime -2
find /var/www/html/moodle/local/jurnalmengajar -type f -mtime -2
Untuk audit perubahan plugin Moodle, saya biasanya lebih memilih:
find /var/www/html/moodle/local/jurnalmengajar \
-type f \
-daystart \
-mtime -2 \
-printf '%TY-%Tm-%Td %TH:%TM %p\n' | sort
karena langsung terlihat file mana yang kita ubah hari ini dan kemarin berdasarkan tanggal kalender, yang biasanya lebih mudah dicocokkan dengan riwayat pekerjaan Guru Duan.
cd "/mnt/GURU/GURU ONE/Surat Berharga/Ijazah"
for f in *.jpg; do
convert "$f" \
-resize 1200x1200\> \
-density 100 \
-quality 40 \
"${f%.jpg}.pdf"
done
Tentu Guru Duan. Script tersebut akan mengubah setiap file JPG menjadi PDF secara otomatis.
cd "/mnt/GURU/GURU ONE/Surat Berharga/Ijazah"
for f in *.jpg; do
convert "$f" \
-resize 1200x1200\> \
-density 100 \
-quality 40 \
"${f%.jpg}.pdf"
done
cd "/mnt/GURU/GURU ONE/Surat Berharga/Ijazah"
Masuk ke folder tempat file JPG berada.
for f in *.jpg; do
Artinya:
Cari semua file yang berakhiran .jpg
Simpan nama file satu per satu ke variabel f
Contoh:
Putaran pertama:
f="IJAZAH.jpg"
Putaran kedua:
f="IJAZAH MTs.jpg"
Putaran ketiga:
f="nem_SD.jpg"
dan seterusnya sampai semua file selesai.
convert "$f"
Misalnya:
convert "IJAZAH.jpg"
ImageMagick membaca gambar tersebut.
-resize 1200x1200\>
Tanda > sangat penting.
Artinya:
Jika ukuran gambar lebih besar dari 1200×1200, perkecil.
Jika sudah lebih kecil, jangan dibesarkan.
Contoh:
| Ukuran asli | Hasil |
|---|---|
| 4000×3000 | 1200×900 |
| 2400×1700 | 1200×850 |
| 800×600 | tetap 800×600 |
Tujuannya agar ukuran PDF lebih kecil.
-density 100
Menentukan resolusi PDF.
Umumnya:
| Density | Kualitas |
|---|---|
| 72 | sangat kecil |
| 100 | cukup jelas |
| 150 | bagus |
| 300 | kualitas scan tinggi |
Untuk arsip dan upload online, 100 biasanya cukup.
-quality 40
Kompresi JPEG.
| Quality | Hasil |
|---|---|
| 100 | hampir tanpa kompresi |
| 80 | bagus |
| 60 | cukup |
| 40 | kecil |
| 20 | sangat kecil |
Semakin kecil angkanya, ukuran file semakin kecil tetapi tulisan bisa mulai buram.
"${f%.jpg}.pdf"
Bagian:
${f%.jpg}
berarti:
Hapus
.jpgdari nama file.
Contoh:
| File JPG | PDF hasil |
|---|---|
| IJAZAH.jpg | IJAZAH.pdf |
| nem_SD.jpg | nem_SD.pdf |
| IJAZAH MTs.jpg | IJAZAH MTs.pdf |
Saat:
f="IJAZAH.jpg"
Perintah yang dijalankan menjadi:
convert "IJAZAH.jpg" \
-resize 1200x1200\> \
-density 100 \
-quality 40 \
"IJAZAH.pdf"
Kemudian lanjut ke file berikutnya.
Misalnya:
| JPG Asli | PDF Hasil |
|---|---|
| 3.6 MB | 300–700 KB |
| 2.8 MB | 250–600 KB |
| 1.1 MB | 100–300 KB |
Jadi seluruh dokumen ijazah/transkrip kemungkinan akan berada jauh di bawah batas 2 MB per file setelah dikonversi dengan pengaturan tersebut.
nano /var/www/html/moodle/local/jurnalmengajar/classes/form/jurnal_form.php
Mudah Guru Duan.
Cari:
$mform->addElement(
'textarea',
'catatan_pembinaan',
'Catatan',
'rows="3" cols="60"'
);
Ganti menjadi:
$mform->addElement(
'textarea',
'catatan_pembinaan',
'Masalah',
'rows="3" cols="60"'
);
Cari:
$mform->addElement(
'textarea',
'tindaklanjut_pembinaan',
'Tindak Lanjut',
'rows="3" cols="60"'
);
Ganti menjadi:
$mform->addElement(
'textarea',
'tindaklanjut_pembinaan',
'Tindak Lanjut / Solusi',
'rows="3" cols="60"'
);
Sekarang kemungkinan seperti ini:
$jenispembinaan = [
'' => '-- Pilih Jenis --',
'disiplin' => 'Disiplin',
'sikap' => 'Sikap',
'akademik' => 'Akademik',
'kerapian' => 'Kerapian',
'lainnya' => 'Lainnya'
];
Tinggal tambah item baru:
$jenispembinaan = [
'' => '-- Pilih Jenis --',
'disiplin' => 'Disiplin',
'sikap' => 'Sikap',
'akademik' => 'Akademik',
'kerapian' => 'Kerapian',
'kehadiran' => 'Kehadiran',
'etika' => 'Etika',
'ibadah' => 'Ibadah',
'pergaulan' => 'Pergaulan',
'motivasi' => 'Motivasi Belajar',
'tugas' => 'Tidak Mengerjakan Tugas',
'gadged' => 'Penggunaan HP/Gadget',
'lainnya' => 'Lainnya'
];
Kalau saya melihat kebutuhan Log Perkembangan Murid di sekolah, dropdown yang lebih berguna:
$jenispembinaan = [
'' => '-- Pilih Jenis --',
'kehadiran' => 'Kehadiran',
'disiplin' => 'Disiplin',
'kerapian' => 'Kerapian',
'sikap' => 'Sikap',
'akademik' => 'Akademik',
'tugas' => 'Tugas',
'motivasi' => 'Motivasi Belajar',
'pergaulan' => 'Pergaulan',
'ibadah' => 'Ibadah',
'lainnya' => 'Lainnya'
];
Karena nanti saat direkap di Log Perkembangan akan lebih mudah melihat pola masalah siswa:
Kehadiran : 5 kali
Disiplin : 3 kali
Akademik : 2 kali
Pergaulan : 1 kali
dan ini sangat membantu wali kelas serta BK.
teks gelap background terang
<a class="btn fw-bold w-100" style="background:#7fc7f5;color:#000;"
EKSTRA BG KUNING
<a class="btn fw-bold w-100" style="background:#fde668;color:#000;"
teks putih background gelap
BG BIRU TUA TEKS PUTIH WALI KELAS
<a class="btn text-white fw-bold w-100" style="background:#0a1347;"
BG HIJAU TEKS PUTIH
<a class="btn text-white fw-bold w-100" style="background:#2e7d32;"
SURAT IZIN MURID DI PENGAWAS [MERAH BG]
<a class="btn btn-danger btn-block w-100"
href="izin_murid.php">
📝 Isi Surat Izin Murid
root@aml-s9xx-box:~# ls -lh /etc/nginx/sites-enabled/
total 0
lrwxrwxrwx 1 root root 37 Jun 4 20:33 data-murid -> /etc/nginx/sites-available/data-murid
lrwxrwxrwx 1 root root 34 May 10 11:25 situs_a -> /etc/nginx/sites-available/situs_a
lrwxrwxrwx 1 root root 34 May 10 18:00 situs_b -> /etc/nginx/sites-available/situs_b
root@aml-s9xx-box:~# cat /etc/nginx/sites-enabled/situs_a
server {
listen 80;
listen [::]:80;
server_name anjir.simsdn.my.id;
root /var/www/situs_a/moodle;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php(/|$) {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
}
root@aml-s9xx-box:~# cat /etc/nginx/sites-enabled/situs_b
server {
listen 8080;
server_name sman1wates.jurnaldo.my.id;
root /var/www/situs_b/moodle;
index index.php index.html;
# 🔥 WAJIB untuk Moodle
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# 🔥 FIX utama (support slasharguments)
location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
# 🔥 Optional (biar static file lebih cepat)
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|svg)$ {
expires max;
log_not_found off;
}
}
root@aml-s9xx-box:~# cat /etc/nginx/sites-available/data-murid
server {
listen 8081;
server_name murid_paduan.jurnaldo.my.id;
root /var/www/data-murid;
index index.html;
access_log /var/log/nginx/data-murid-access.log;
error_log /var/log/nginx/data-murid-error.log;
location / {
try_files $uri $uri/ =404;
}
}
Untuk kondisi Guru Duan saat ini, saya akan memilih tahapan bertahap, jangan langsung webhook.
Buat script:
nano /opt/update-data-murid.sh
Isi:
#!/bin/bash
echo "=== UPDATE DATA MURID ==="
cd /opt/data-murid || exit
git pull
rsync -av --delete \
--exclude='.git' \
/opt/data-murid/ \
/var/www/data-murid/
echo "=== UPDATE SELESAI ==="
Simpan:
chmod +x /opt/update-data-murid.sh
Saat ada perubahan di GitHub:
cd /opt/data-murid
git status
lalu:
/opt/update-data-murid.sh
Keuntungan:
Sangat sederhana.
Tidak ada service tambahan.
Tidak ada port tambahan.
Tidak ada risiko webhook disalahgunakan.
Karena ini data alumni yang berharga, saya justru lebih fokus ke backup daripada otomatisasi.
Buat:
mkdir -p /backup/data-murid
Backup harian:
cp /var/www/data-murid/alumni.json \
/backup/data-murid/alumni-$(date +%F).json
Atau pakai cron seminggu sekali.
Karena Guru Duan sudah memakai GoAccess, saya sarankan mempertahankan:
access_log /var/log/nginx/data-murid-access.log;
error_log /var/log/nginx/data-murid-error.log;
Nanti statistik hanya untuk situs alumni.
Baru pertimbangkan:
Misalnya tiap jam:
0 * * * * /opt/update-data-murid.sh
Ini cukup aman dan sederhana.
Saya baru akan memasang webhook jika:
Guru Duan mengubah data setiap hari.
Ada beberapa situs yang dikelola.
Ingin benar-benar otomatis setelah git push.
Untuk repository yang isinya hanya:
alumni.json
index.html
script.js
saya akan tetap memakai:
/opt/update-data-murid.sh
manual.
Alasannya:
Repository hanya ±500 KB.
Update tidak setiap jam.
HG680P sudah menjalankan beberapa situs.
Semakin sedikit service tambahan, semakin mudah dirawat.
Jadi alurnya:
Laptop Ubuntu
↓
git push
↓
Armbian
/opt/update-data-murid.sh
↓
Website langsung ter-update
Menurut saya itu sudah ideal untuk ukuran proyek data-murid saat ini. Jika nanti Guru Duan ingin menghubungkannya langsung dengan Moodle (misalnya ekspor JSON alumni otomatis dari database Moodle), barulah kita bisa membuat deploy yang lebih otomatis.
Baru install Composer di STB:
apt install composer
Setelah selesai:
cd /var/www/shared/local/jurnalmengajar
composer install
Kalau ingin menampilkan teks biasa, harus memakai:
echo
atau HTML.
Yang benar:
echo html_writer::start_div();
echo '<b>Pilih opsi berikut untuk Cetak Harian Bulanan</b><br><br>';
echo html_writer::label('Bulan', 'bulan') . ' ';
Atau lebih rapi:
echo html_writer::start_div(
null,
null,
['style' => 'margin-top:10px']
);
echo html_writer::tag(
'div',
'Pilih opsi berikut untuk Cetak Harian Bulanan',
[
'style' => '
font-weight:bold;
margin-bottom:8px;
color:#0f4c81;
'
]
);
echo html_writer::label('Bulan', 'bulan') . ' ';
Hasilnya nanti muncul seperti judul kecil di atas dropdown.
SELECT
u.username,
u.firstname,
u.lastname,
u.email,
nis.data AS profile_field_nis,
pw.data AS profile_field_pwexam,
c.name AS cohort1
INTO OUTFILE '/tmp/user_xi.csv'
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n'
FROM mdl_user u
JOIN mdl_cohort_members cm ON cm.userid = u.id
JOIN mdl_cohort c ON c.id = cm.cohortid
LEFT JOIN mdl_user_info_field fnis
ON fnis.shortname = 'nis'
LEFT JOIN mdl_user_info_data nis
ON nis.fieldid = fnis.id
AND nis.userid = u.id
LEFT JOIN mdl_user_info_field fpw
ON fpw.shortname = 'pwexam'
LEFT JOIN mdl_user_info_data pw
ON pw.fieldid = fpw.id
AND pw.userid = u.id
WHERE c.name IN (
'XI-A','XI-B','XI-C',
'XI-D','XI-E','XI-F','XI-G'
)
AND u.deleted = 0
AND u.suspended = 0
ORDER BY c.name, u.lastname;
SELECT
u.username,
u.firstname,
u.lastname,
u.email,
nis.data AS profile_field_nis,
pw.data AS profile_field_pwexam,
c.name AS cohort1
INTO OUTFILE '/tmp/user_x.csv'
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n'
FROM mdl_user u
JOIN mdl_cohort_members cm ON cm.userid = u.id
JOIN mdl_cohort c ON c.id = cm.cohortid
LEFT JOIN mdl_user_info_field fnis
ON fnis.shortname = 'nis'
LEFT JOIN mdl_user_info_data nis
ON nis.fieldid = fnis.id
AND nis.userid = u.id
LEFT JOIN mdl_user_info_field fpw
ON fpw.shortname = 'pwexam'
LEFT JOIN mdl_user_info_data pw
ON pw.fieldid = fpw.id
AND pw.userid = u.id
WHERE c.name IN (
'X-A','X-B','X-C',
'X-D','X-E','X-F','X-G'
)
AND u.deleted = 0
AND u.suspended = 0
ORDER BY c.name, u.lastname;
Kelas XI:
scp dhwan@192.168.200.20:/tmp/user_xi.csv ~/Downloads/
Kelas X:
scp dhwan@192.168.200.20:/tmp/user_x.csv ~/Downloads/
Kalau gagal karena secure_file_priv, cek:
SHOW VARIABLES LIKE 'secure_file_priv';
Lalu ganti path /tmp/ sesuai folder yang ditampilkan MariaDB, misalnya:
/var/lib/mysql-files/
ALTER TABLE mdl_local_jurnalmengajar_suratizinguru
ADD COLUMN userinput BIGINT(10) NULL AFTER nip;
root@cbt:~# while true; do echo "$(date '+%F %T') | $(mysql moodle -N -e "
SELECT COUNT(DISTINCT userid)
FROM mdl_logstore_standard_log
WHERE component='mod_quiz'
AND timecreated > UNIX_TIMESTAMP(NOW()) - 150;
")" >> quizlog.txt; sleep 10; done
lihatnya:
tail -f quizlog.txt
Ya Guru 👍
Itu berarti di server cbt user Linux root sudah bisa login ke MariaDB tanpa password eksplisit.
Biasanya karena salah satu dari ini:
Paling sering di Debian/Ubuntu.
Artinya:
kalau login Linux sebagai root
otomatis boleh masuk MariaDB sebagai root
tanpa password.
Jadi command:
mysql moodle
langsung berhasil.
Misalnya:
/root/.my.cnf
credential tersimpan
atau socket authentication
Masuk mysql:
mysql
Lalu:
SELECT user,host,plugin
FROM mysql.user;
Kalau hasilnya seperti:
| user | plugin |
|---|---|
| root | unix_socket |
atau:
| user | plugin |
|---|---|
| root | auth_socket |
berarti memang otomatis login lewat user Linux root.
Karena:
hanya user Linux root yang bisa
bukan semua orang
Jadi:
SSH root dulu
baru mysql otomatis boleh.
while true; do ...
karena:
mysql tidak perlu password
shell langsung bisa eksekusi query
Karena:
script otomatis lebih mudah
cron job lebih simpel
tidak perlu simpan password plaintext
Karena Guru sekarang punya logger realtime, saya sarankan nanti buat juga:
while true; do
echo "$(date '+%F %T') | $(uptime)"
sleep 10
done >> cpulog.txt
while true; do
echo "$(date '+%F %T') | $(free -m | awk '/Mem:/ {print $3 "/" $2 " MB"}')"
sleep 10
done >> ramlog.txt
Nanti bisa dibandingkan dengan:
quizlog.txt
jumlah siswa aktif
CPU
RAM
untuk mengetahui:
batas maksimum server CBT Guru
titik overload
waktu paling berat saat ujian berlangsung.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Tidak terlalu memberatkan Guru 👍
Untuk server Moodle/CBT, ketiga loop itu sangat ringan dibanding beban quiz siswa.
Mari kita hitung.
Command:
mysql moodle -N -e "SELECT COUNT(...) ..."
hanya:
1 query kecil
tiap 10 detik
hanya hitung DISTINCT userid
Itu sangat ringan untuk MariaDB.
uptime
hampir nol beban.
Karena cuma membaca:
load average
uptime server
dari kernel Linux.
free -m
juga hampir nol.
Hanya membaca statistik RAM dari kernel.
Guru menjalankan:
| Task | Interval |
|---|---|
| query mysql | 10 detik |
| uptime | 10 detik |
| free -m | 10 detik |
Artinya total:
sekitar 3 command / 10 detik
= 0.3 command per detik
Sangat kecil.
1 siswa quiz Moodle bisa menghasilkan:
autosave
AJAX request
render halaman
session write
DB write
puluhan request per menit.
Kalau 50 siswa:
ratusan request per menit.
Jadi logger Guru jauh lebih ringan dibanding trafik Moodle itu sendiri.
Kalau Guru menjalankan:
top
iotop
tcpdump
tail log sangat besar
query JOIN kompleks
monitoring tiap 1 detik
baru mulai terasa.
Untuk aman:
Tetap:
10 detik
Bisa:
30 detik
karena CPU/RAM tidak perlu sangat realtime.
Contoh lebih hemat:
while true; do
echo "$(date '+%F %T') | $(uptime)"
sleep 30
done >> cpulog.txt
while true; do
echo "$(date '+%F %T') | $(free -m | awk '/Mem:/ {print $3 "/" $2 " MB"}')"
sleep 30
done >> ramlog.txt
Dengan:
50–60 siswa aktif
Debian CLI
Nginx
MariaDB
RAM 6 GB
ketiga logger itu:
✅ aman dijalankan bersamaan
✅ tidak signifikan membebani server
✅ sangat berguna untuk analisis performa CBT Moodle.
Buat file:
nano ~/.my.cnfIsi:
[client]
user=root
password=PASSWORDMYSQLANDAContoh:[client]
user=root
password=abc123 Lalu amankan file:chmod 600 ~/.my.cnf
Setelah ituKalau Guru ingin melihat perkembangan jumlah siswa aktif selama 1 jam dalam bentuk baris-baris log, lebih bagus dibuat seperti ini:
WAKTU JUMLAH_AKTIF
2026-05-13 20:00:05 15
2026-05-13 20:00:10 22
2026-05-13 20:00:15 37
...
Jadi:
1 baris = 1 kali pengecekan
bisa dianalisis nanti
bisa tahu puncak trafik
Minimal 2 kolom:
| Kolom | Isi |
|---|---|
| waktu | timestamp pengecekan |
| jumlah_aktif | jumlah siswa aktif |
Kalau mau lebih lengkap:
| Kolom | Isi |
|---|---|
| waktu | waktu pengecekan |
| jumlah_aktif | user aktif quiz |
| koneksi | jumlah koneksi server |
| cpu_load | load server |
| ram_used | RAM terpakai |
Tapi untuk malam ini cukup:
waktu
jumlah_aktif
Jalankan ini di terminal:
while true; do
echo "$(date '+%F %T') $(mysql moodle2 -N -e "
SELECT COUNT(DISTINCT userid)
FROM mdl_logstore_standard_log
WHERE component='mod_quiz'
AND timecreated > UNIX_TIMESTAMP(NOW()) - 150;
")" >> quizlog.txt
sleep 10
done
datemengambil waktu sekarang.
mysql ...mengambil jumlah siswa aktif.
>> quizlog.txtmenyimpan ke file.
sleep 10ulang tiap 10 detik.
Isi quizlog.txt:
2026-05-13 20:00:01 12
2026-05-13 20:00:11 18
2026-05-13 20:00:21 25
2026-05-13 20:00:31 41
Karena interval 10 detik:
3600 / 10 = 360 baris
Jadi sekitar 360 baris log selama 1 jam.
Tekan:
CTRL + C
cat quizlog.txt
atau realtime:
tail -f quizlog.txt
Ini bagus sekali untuk analisis:
jam paling ramai
puncak siswa aktif
apakah server drop
kapan submit massal terjadi.
===========================
root@debian:~# while true; do
echo "$(date '+%F %T') | $(mysql moodle2 -N -e "
SELECT COUNT(DISTINCT userid)
FROM mdl_logstore_standard_log
WHERE component='mod_quiz'
AND timecreated > UNIX_TIMESTAMP(NOW()) - 150;
")" >> quizlog.txt
sleep 10
done
++++++++++++++++++++
pantau realtime
++++++++++++++++++++
root@debian:~# tail -f quizlog.txt
2026-05-14 04:15:43 | 0
2026-05-19 22:18:04 | 0
2026-05-19 22:18:14 | 0
2026-05-19 22:18:24 | 0
2026-05-19 22:18:34 | 0
2026-05-19 22:18:44 | 0
2026-05-19 22:18:54 | 0
2026-05-20 20:20:42 | 52
2026-05-20 20:20:52 | 52
2026-05-20 20:21:02 | 51
2026-05-20 20:21:12 | 52
2026-05-20 20:21:22 | 52
2026-05-20 20:21:32 | 51
2026-05-20 20:21:42 | 52
2026-05-20 20:21:52 | 51
2026-05-20 20:22:02 | 49
2026-05-20 20:22:12 | 46
2026-05-20 20:22:22 | 46
2026-05-20 20:22:32 | 46
2026-05-20 20:22:42 | 45
2026-05-20 20:22:52 | 45
2026-05-20 20:23:02 | 46
2026-05-20 20:23:12 | 48
2026-05-20 20:23:22 | 50
2026-05-20 20:23:32 | 49
2026-05-20 20:23:42 | 49
2026-05-20 20:23:52 | 50
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Ya Guru 👍
Command itu otomatis memakai ~/.my.cnf.
Karena saat Guru menjalankan:
mysql moodle2 -N -e "..."
tanpa:
-u
-p
maka client mysql akan mencari konfigurasi login dari:
~/.my.cnf
Isi file itu misalnya:
[client]
user=root
password=PASSWORDMYSQL
Jadi command loop tadi:
membaca username/password otomatis
tidak meminta password lagi
cocok untuk logging jangka panjang.
Cara memastikan benar memakai ~/.my.cnf:
Coba jalankan:
mysql moodle2 -e "SHOW DATABASES;"
Kalau langsung berhasil tanpa minta password:
✅ berarti ~/.my.cnf dipakai dengan benar.
Kalau file ~/.my.cnf salah:
biasanya muncul:
Access denied
atau mysql meminta password lagi.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%Bisa sekali Guru 👍
Karena quizlog.txt berada di server Moodle (Debian), Guru bisa menariknya ke laptop dengan beberapa cara.
Karena Guru biasa memakai SSH, paling mudah pakai scp.
Dari laptop Aspire jalankan:
scp root@IP_SERVER:/root/quizlog.txt .
Contoh:
scp root@192.168.1.113:/root/quizlog.txt .
Artinya:
ambil file dari server
simpan di folder laptop saat ini (.)
scp root@192.168.1.113:/root/quizlog.txt ~/Downloads/
Di laptop:
buka Files
Other Locations
Connect to Server
Isi:
sftp://root@192.168.1.113
Lalu login password server.
Nanti file server muncul seperti folder biasa.
Ambil:
/root/quizlog.txt
Di laptop:
ssh root@192.168.1.113 "tail -f /root/quizlog.txt"
Artinya:
melihat log realtime dari laptop
tanpa masuk server manual
Bisa juga nanti di server:
cp quizlog.txt quizlog.csv
Lalu dibuka di:
LibreOffice Calc
Excel
Karena formatnya sudah mirip CSV.
Biarkan logging berjalan di server:
while true; do ...
Lalu dari laptop Guru cukup:
tail -f quizlog.txt
atau tarik file setelah ujian selesai.
Kesimpulan sinkronisasi struktur tabel dari SiM → laptop Guru Duan:
mdl_local_jurnalmengajarTambahan kolom:
ALTER TABLE mdl_local_jurnalmengajar
ADD COLUMN absenid LONGTEXT NULL AFTER absen,
ADD COLUMN timemodified BIGINT(10) NOT NULL DEFAULT 0 AFTER timecreated,
ADD COLUMN modifiedby BIGINT(10) NOT NULL DEFAULT 0 AFTER timemodified;
Fungsi:
absenid → migrasi userid absensi
timemodified → tracking edit
modifiedby → siapa editor jurnal
mdl_local_jurnalpembinaanTambahan kolom:
ALTER TABLE mdl_local_jurnalpembinaan
ADD COLUMN pesertaid LONGTEXT NULL
AFTER peserta;
Fungsi:
menyimpan userid siswa BK/pembinaan
mdl_local_jurnallayananbkTambahan kolom:
ALTER TABLE mdl_local_jurnallayananbk
ADD COLUMN pesertaid LONGTEXT NULL
AFTER peserta;
Fungsi:
menyimpan userid layanan BK
mdl_local_jurnalguruwaliTambahan kolom:
ALTER TABLE mdl_local_jurnalguruwali
ADD COLUMN userid BIGINT(10) NULL AFTER muridid,
ADD COLUMN kelas VARCHAR(20) NULL AFTER userid;
Fungsi:
kompatibilitas log_perkembangan.php
filter guru wali
tampil nama kelas
mdl_local_jurnalmengajar_suratizinTambahan kolom:
ALTER TABLE mdl_local_jurnalmengajar_suratizin
ADD COLUMN kelas VARCHAR(20) NULL AFTER userid;
Fungsi:
snapshot nama kelas surat izin
dipakai timeline perkembangan
| Tabel | Status |
|---|---|
mdl_user | ✅ |
mdl_cohort | ✅ |
mdl_cohort_members | ✅ |
tipe bigint vs int | ✅ aman |
tipe longtext vs text | ✅ aman |
Lakukan:
php admin/cli/purge_caches.php
supaya Moodle laptop membaca schema & code terbaru dengan bersih.
Fitur berikut akan sinkron penuh dengan SiM:
| Fitur | Status |
|---|---|
| Jurnal Mengajar | ✅ |
| Absensi userid | ✅ |
| Surat Izin | ✅ |
| Layanan BK | ✅ |
| Pembinaan BK | ✅ |
| Guru Wali | ✅ |
| Log Perkembangan | ✅ |
| Filter card dashboard | ✅ |
| Timeline hybrid lama+baru | ✅ |
Siap, Guru Duan. Saya ringkas perintah Redshift yang paling penting — yang benar-benar kepakai sehari-hari saja 👇
redshift-gtk
👉 Jalankan versi GUI (yang ada tray/background)
redshift -O 2700
👉 Layar langsung jadi hangat (tanpa tunggu waktu)
redshift -x
👉 Reset ke warna default
killall redshift
redshift-gtk &
👉 Dipakai kalau terasa tidak jalan
ps aux | grep redshift
redshift -l -2.8:115.2
👉 Tanpa config file (langsung tes cepat)
redshift -O 3500 # agak hangat
redshift -O 4500 # ringan saja
redshift -x
👉 Cocok kalau lagi desain / warna harus akurat
Kalau mau praktis, ini yang biasanya dipakai:
Mode malam:
redshift -O 2700
Balik normal:
redshift -x
👉 Ini bisa dijadikan keyboard shortcut di Ubuntu
Kalau disingkat banget:
Jalankan → redshift-gtk
Malam paksa → redshift -O 2700
Normal → redshift -x
Error → restart (killall redshift)
Kalau Guru mau, saya bisa bantu:
👉 bikin shortcut keyboard (Ctrl+Alt+N = night mode)
👉 atau bikin toggle ON/OFF sekali tekan (lebih keren dari f.lux)
**buat folder
mkdir -p /var/www/shared/local
**pindah kode
mv /var/www/html/moodle/local/jurnalmengajar /var/www/shared/local/
**buat symlink kepada 2 moodle:
ln -s /var/www/shared/local/jurnalmengajar \
/var/www/situs_a/moodle/local/jurnalmengajar
ln -s /var/www/shared/local/jurnalmengajar \
/var/www/situs_b/moodle/local/jurnalmengajar
**atur permission
chown -R www-data:www-data /var/www/shared/local/jurnalmengajar
chmod -R 755 /var/www/shared/local/jurnalmengajar
**upgrade masing-masing
php /var/www/situs_a/moodle/admin/cli/upgrade.php
php /var/www/situs_b/moodle/admin/cli/upgrade.php
⚠️ HAL PENTING (JANGAN SAMPAI TERLEWAT)
1. Database tetap beda
a → database a
b → database b
👉 supaya tidak merusak data asli
2. moodledata beda
Contoh:
/var/moodledata_a
/var/moodledata_b
3. config.php beda
Cek:
situs_a ; /var/www/situs_a/moodle/config.php
$CFG->dirroot = '/var/www/situs_a/moodle';
situs_b ; /var/www/situs_b/moodle/config.php
$CFG->dirroot = '/var/www/situs_b/moodle';
** SET PERMISSION
chown -R www-data:www-data /var/moodledata_a
chown -R www-data:www-data /var/moodledata_b
chmod -R 755 /var/moodledata_a
chmod -R 755 /var/moodledata_b
** CONFIG situs_a**
# cat /var/www/situs_a/moodle/config.php
<?php // config.php
unset($CFG);
global $CFG;
$CFG = new stdClass();
// ----------------------
// Konfigurasi database
// ----------------------
$CFG->dbtype = 'mariadb';
$CFG->dblibrary = 'native';
$CFG->dbhost = 'localhost';
$CFG->dbname = 'moodle_a';
$CFG->dbuser = 'user_a';
$CFG->dbpass = 'password_a';
$CFG->prefix = 'mdl_';
$CFG->dboptions = array(
'dbpersist' => 0,
'dbport' => '',
'dbsocket' => '',
'dbcollation' => 'utf8mb4_unicode_ci',
);
// Jika Anda ingin paksa wwwroot tertentu, uncomment baris di bawah ini dan sesuaikan:
$CFG->wwwroot = 'https://situs_a';
$CFG->sslproxy = true;
//$CFG->reverseproxy = true;
// ----------------------
// Path Moodle
// ----------------------
//$CFG->dataroot = '/var/moodledata';
$CFG->dataroot = '/var/moodledata_a';
$CFG->admin = 'admin';
// Umum
$CFG->directorypermissions = 0777;
// Debugging (aktifkan sementara saat troubleshooting)
//@error_reporting(E_ALL | E_STRICT);
//@ini_set('display_errors', '1');
//$CFG->debug = (E_ALL | E_STRICT);
//$CFG->debugdisplay = 1;
require_once(__DIR__ . '/lib/setup.php');
==============================================================
** CONFIG situs-B **
==============================================================
# cat /var/www/situs_b/moodle/config.php
<?php // config.php
unset($CFG);
global $CFG;
$CFG = new stdClass();
// ----------------------
// Konfigurasi database
// ----------------------
$CFG->dbtype = 'mariadb';
$CFG->dblibrary = 'native';
$CFG->dbhost = 'localhost';
$CFG->dbname = 'moodle_b';
$CFG->dbuser = 'moodluser_b';
$CFG->dbpass = 'password_b';
$CFG->prefix = 'mdl_';
$CFG->dboptions = array(
'dbpersist' => 0,
'dbport' => '',
'dbsocket' => '',
'dbcollation' => 'utf8mb4_unicode_ci',
);
// Jika Anda ingin paksa wwwroot tertentu, uncomment baris di bawah ini dan sesuaikan:
$CFG->wwwroot = 'https://situs_b';
$CFG->sslproxy = true;
//$CFG->reverseproxy = true;
// 🔥 TAMBAHKAN INI
$CFG->sessioncookie = 'MoodleSessionDEV';
// ----------------------
// Path Moodle
// ----------------------
//$CFG->dataroot = '/var/moodledata';
$CFG->dataroot = '/var/moodledata_b';
$CFG->admin = 'admin';
// Umum
$CFG->directorypermissions = 0777;
// Debugging (aktifkan sementara saat troubleshooting)
//@error_reporting(E_ALL | E_STRICT);
//@ini_set('display_errors', '1');
//$CFG->debug = (E_ALL | E_STRICT);
//$CFG->debugdisplay = 1;
require_once(__DIR__ . '/lib/setup.php');
=======================================
nginx site availabel
=======================================
# cat /etc/nginx/sites-available/situs_a
server {
listen 80;
listen [::]:80;
server_name situs_a;
root /var/www/situs_a/moodle;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php(/|$) {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
}
=============================
# cat /etc/nginx/sites-available/situs_b
server {
listen 8080;
server_name situs_b;
root /var/www/situs_b/moodle;
index index.php index.html;
# 🔥 WAJIB untuk Moodle
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# 🔥 FIX utama (support slasharguments)
location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
# 🔥 Optional (biar static file lebih cepat)
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|svg)$ {
expires max;
log_not_found off;
}
}
==================================================================
ln -s /etc/nginx/sites-available/situs_a /etc/nginx/sites-enabled/
ln -s /etc/nginx/sites-available/situs_b /etc/nginx/sites-enabled/
======================
nginx -t
systemctl reload nginx
tail -100 /var/moodledata/logs/wa_debug.log