Featured

    Featured Posts

  • dimana kehidupan
  • disitulah jawaban

Nyoba Deploy Website ke Cloudflare Pages, Gratis dan Nggak Ribet

Ceritanya lagi bikin satu halaman sederhana berisi kumpulan tautan kantor. Satu file, isinya daftar link layanan yang sering dipakai, biar tidak usah mencari satu-satu lagi. Halaman ini saya sebut saja halaman portal.

Halaman itu saya taruh di GitHub dan saya tayangkan pakai GitHub Pages. Selama repo-nya publik, semua aman-aman saja. Alamatnya memang masih alamat bawaan GitHub, tapi yang penting bisa dibuka.

Masalahnya muncul dari keinginan yang kedengarannya sepele: saya pengin repo-nya saya privat saja. Ternyata keputusan kecil itu membuat websitenya mati. Catatan ini tentang bagaimana saya memindahkannya ke Cloudflare Pages, memasang domain sendiri, dan akhirnya repo bisa privat tanpa mematikan situsnya.

Daftar Isi

  • 1. Pas Diprivat, Malah Mati
  • 2. Bedanya Workers dan Pages
  • 3. Urutan Pindah yang Benar
  • 4. Langkah-langkah Pindahnya
  • 5. Pasang Domain Sendiri
  • 6. Baru Privat, Lalu Tes Ulang
  • 7. Setelah Pindah: Commit Langsung Tayang
  • 8. Hal yang Saya Pelajari
  • 9. Penutup

1. Pas Diprivat, Malah Mati

Suatu hari saya mengubah repo portal ini dari publik menjadi privat. Saya pikir urusannya selesai di situ, tinggal pengaturan tampil atau tidak tampilnya kode.

Ternyata tidak. Beberapa saat setelah saya privat, alamat GitHub Pages-nya tidak bisa dibuka lagi. Situsnya ikut mati.

Setelah saya cari tahu, sebabnya begini: di akun GitHub gratis, GitHub Pages hanya menerbitkan situs dari repo yang publik. Repo privat yang bisa ditayangkan itu fasilitas berbayar, masuknya ke paket Pro.

Jadi pilihannya waktu itu ada tiga. Repo tetap publik, atau bayar, atau pindah hosting ke tempat yang memperbolehkan repo privat. Saya pilih yang ketiga: pindah ke Cloudflare Pages. Gratisnya dapat, repo tetap bisa privat, dan domain sendiri juga bisa dipasang di tempat yang sama.

Pelajaran pertama dari kejadian ini: jangan privat dulu sebelum rumah barunya siap. Urutannya kebalik sedikit saja, situsnya mati beneran.

2. Bedanya Workers dan Pages

Sebelum lanjut, bagian ini yang sempat membuat saya bingung lumayan lama. Di Cloudflare ada dua nama yang mirip-mirip: Workers dan Pages. Keduanya ada di menu yang sama, tapi kerjaannya beda.

Cara gampang saya mengingatnya begini:

  • Pages itu etalase. Tempat menaruh file yang sudah jadi: HTML, CSS, JavaScript, gambar. Ada pengunjung datang, file itu diberikan apa adanya. Tidak ada yang berpikir, tidak ada yang mengolah.
  • Workers itu petugas yang berpikir dulu. Ini kode atau program yang berjalan setiap ada permintaan datang. Dia bisa meneruskan permintaan ke tempat lain, memeriksa sesuatu, mengubah jawaban, atau memanggil layanan lain sebelum menjawab.

Kalau dibuat tabel, kira-kira begini:

Pembanding Cloudflare Pages Cloudflare Workers
Isinya File statis yang sudah jadi Kode yang berjalan tiap ada permintaan
Bisa mengolah / meneruskan permintaan? Tidak, hanya mengirim file Bisa, termasuk jadi proxy atau perantara
Cocok untuk Halaman profil, landing page, kumpulan link, blog statis API, proxy ke Apps Script, aturan redirect, pemeriksaan akses
Contoh punya saya Halaman portal kantor di catatan ini Sistem antrean konsultasi di catatan saya yang lain

Dari tabel itu kesimpulannya jadi jelas buat saya. Halaman portal isinya cuma daftar link, file-nya sudah jadi, tidak ada yang perlu diolah di tengah jalan. Jadi Pages saja sudah cukup, memakai Workers malah berlebihan.

Sebaliknya, sistem antrean konsultasi saya yang lain memang butuh Workers, karena di sana ada perantara yang meneruskan data ke Apps Script. Beda kebutuhan, beda alatnya.

3. Urutan Pindah yang Benar

Ini bagian terpenting dari seluruh catatan ini. Urutannya tidak boleh ditukar:

Urutan Migrasi Custom Domain
1. Pindah hosting dulu ke Cloudflare Pages (repo masih publik)
↓
2. Tes alamat sementara dari Pages sampai benar-benar hidup
↓
3. Pasang domain sendiri dan tes lagi
↓
4. BARU privat-kan repo GitHub
↓
5. Tes ulang semuanya

Intinya satu: privat itu langkah terakhir. Kalau dilakukan di awal, yang terjadi ya seperti pengalaman saya sebelumnya, situsnya mati sebelum rumah barunya jadi.

4. Langkah-langkah Pindahnya

Langkah 1: Kasih Izin ke Repo Portal

Waktu pertama mencoba, repo portal saya tidak muncul di daftar pilihan Cloudflare. Sempat bingung, ternyata sebabnya sederhana: aplikasi GitHub milik Cloudflare itu hanya diizinkan mengakses repo tertentu saja.

Di GitHub, saya buka pengaturan aplikasi Cloudflare Workers and Pages, lalu di bagian akses repository saya pilih Only select repositories. Di daftarnya waktu itu baru ada repo konsultasi saya. Repo portalnya saya tambahkan di situ, sampai daftarnya jadi dua repo, lalu saya simpan.

Repo konsultasi jangan dicabut dari daftar. Kalau dicabut, proyek yang lama bisa ikut terganggu. Menambah saja, bukan mengganti.

Langkah 2: Masuk ke Jalur Pages

Di dashboard Cloudflare, menunya sekarang ada di Compute → Workers & Pages, lalu klik Create application.

Di halaman itu ada tombol besar bertuliskan Continue with GitHub. Nah, tombol besar itu bukan yang saya klik. Tombol itu jalur Workers yang baru, nanti langkah-langkah di dalamnya berbeda.

Yang saya klik adalah tautan kecil di bagian bawah halaman: Continue to Pages. Di dekatnya ada tulisan tentang legacy Pages workflow. Memang kecil dan gampang terlewat, saya sendiri awalnya hampir salah klik yang besar.

Setelah itu pilih Import an existing Git repository, lalu pilih repo portalnya dan klik Begin setup.

Langkah 3: Isi Build Settings

Karena halaman portal ini file statis polos tanpa framework apa pun, isiannya sangat sederhana:

Setting Cloudflare Pages
Project name portal-kantor (sesuai nama repo)
Production branch main
Framework preset None
Build command (kosong, tidak diisi apa pun)
Build output directory (kosong / root, karena file HTML ada di folder utama repo)

Bagian lanjutan seperti root directory dan environment variables saya biarkan saja, tidak saya buka-buka. Setelah sama semua, klik Save and Deploy dan tunggu satu sampai tiga menit.

Kalau berhasil, muncul tulisan Success dan satu alamat sementara berakhiran .pages.dev. Punya saya alamatnya tidak persis sama dengan nama proyek, ada tambahan huruf acak di belakangnya, karena nama polosnya ternyata sudah dipakai orang lain lebih dulu. Tidak masalah, itu wajar dan memang bukan alamat utamanya. Alamat itu cuma buat ngetes.

Alamat sementara itu saya klik dan saya buka. Halamannya muncul lengkap, judul dan daftar link-nya sama seperti aslinya. Berarti pindahan hostingnya sah, bukan cuma tulisan sukses saja.

5. Pasang Domain Sendiri

Masih di halaman proyek Pages yang sama, saya masuk ke tab Custom domains, lalu klik Set up a custom domain.

Di kolom domain saya isi subdomain yang saya mau, polos saja tanpa awalan apa pun. Contohnya di catatan ini saya tulis portal.contohdomain.my.id.

Setelah klik Continue, Cloudflare menampilkan satu record DNS yang akan dibuatkan. Isinya saya periksa dulu baik-baik sebelum lanjut:

Type CNAME
Name portal
Content / Target nama-project-abc.pages.dev
TTL Auto

Karena domain induknya memang berada di akun Cloudflare yang sama, record itu dibuatkan otomatis. Saya tinggal klik Activate domain.

Statusnya awalnya tertulis Verifying. Saya tunggu beberapa menit, tidak saya utak-atik. Setelah itu statusnya berubah menjadi Active, dan alamat domain sendiri itu sudah bisa dibuka dengan HTTPS seperti website biasa.

Saran dari pengalaman saya: periksa target CNAME-nya sebelum klik Activate. Salah satu huruf saja di target itu, alamatnya bisa nyasar ke tempat lain.

6. Baru Privat, Lalu Tes Ulang

Sampai di titik ini rumah barunya sudah jadi dan alamat barunya sudah hidup. Barulah sekarang repo-nya saya privat, dengan hati yang jauh lebih tenang dibanding percobaan pertama.

Caranya: buka repo di GitHub, masuk ke Settings pada repo itu, gulir sampai paling bawah ke bagian Danger Zone, lalu di Change repository visibility pilih Make private dan ikuti konfirmasinya sampai selesai.

Habis itu langsung saya tes tiga alamat:

  • Domain sendiri: hidup, terbuka normal. Ini yang paling penting.
  • Alamat .pages.dev: hidup juga, isinya sama.
  • Alamat lama .github.io: 404 alias mati.

Yang terakhir itu jangan panik. Justru 404 di alamat lama adalah bukti bahwa privatnya berhasil, dan alamat lama memang sudah tidak dipakai lagi. Bedanya dengan kejadian pertama: kali ini situs utamanya tidak ikut mati, karena yang menayangkan sudah Cloudflare Pages, bukan GitHub Pages.

7. Setelah Pindah: Commit Langsung Tayang

Satu hal yang saya tanyakan ke diri sendiri setelah pindah: kalau nanti saya mengubah isi link di repo, apakah saya harus deploy ulang secara manual?

Ternyata tidak. Setiap ada commit baru ke branch main, Cloudflare Pages langsung membangun dan menayangkan ulang secara otomatis. Biasanya satu sampai dua menit sudah berubah.

Enaknya jelas: edit file di GitHub, commit, selesai. Tapi konsekuensinya juga harus diingat: commit itu artinya langsung tayang ke publik. Jadi sebelum commit, periksa dulu isinya. Jangan sampai link yang belum jadi ikut terbit.

Data isi portalnya sendiri saya taruh di satu file data terpisah, isinya daftar link seperti ini. Ini contoh dummy:

{
  "links": [
    {
      "judul": "Contoh Layanan Satu",
      "url": "https://contoh-alamat-kantor.example",
      "keterangan": "Deskripsi singkat layanan"
    },
    {
      "judul": "Contoh Layanan Dua",
      "url": "https://contoh-alamat-lain.example",
      "keterangan": "Deskripsi singkat layanan"
    }
  ]
}

Dengan cara ini, kalau cuma mau menambah atau mengubah satu link, saya cukup mengubah file data itu saja. Tidak perlu mengutak-atik halaman utamanya.

8. Hal yang Saya Pelajari

  • Privat dan publik pada repo itu bukan cuma soal terlihat atau tidak. Di GitHub gratis, pilihan itu ikut menentukan apakah situs GitHub Pages-nya bisa tayang.
  • Urutan langkah sering lebih penting daripada langkahnya sendiri. Langkah yang sama, kalau urutannya ditukar, hasilnya bisa mati total.
  • Workers dan Pages itu dua alat berbeda. Memilih yang paling sederhana dan sesuai kebutuhan biasanya lebih enak dirawat.
  • Alamat sementara dari layanan hosting itu wajar kalau tidak persis sama dengan nama proyek. Alamat yang disebar ke orang tetap domain sendiri.
  • Setelah pindah, selalu tes ulang semua alamat. Jangan menganggap berhasil hanya dari tulisan Success di dashboard.

9. Penutup

Sekarang halaman portal kantor ini sudah tayang dari alamatnya sendiri, repo-nya sudah privat, dan saya tidak perlu waswas lagi setiap mau mengubah pengaturan repo.

Semoga catatan ini bermanfaat, terutama buat saya sendiri kalau nanti lupa dan harus memindahkan halaman statis yang lain suatu hari nanti.


Disclaimer

Tulisan ini adalah catatan belajar pribadi tentang memindahkan hosting halaman portal ke Cloudflare Pages dan memasang domain sendiri, bukan tutorial resmi dan bukan panduan baku dari pihak mana pun. Saya menulis ulang pengalaman saya supaya tidak lupa, bukan karena bisa.

Prosesnya sendiri banyak dibantu AI dan diskusi bolak-balik, apalagi saat mencari sebab situsnya mati setelah repo diprivat dan saat membedakan jalur Pages dengan Workers itu. Jadi jangan dibayangkan saya sudah paham semuanya. Saya masih belajar, dan sangat mungkin ada bagian di catatan ini yang kurang tepat atau bisa diperbaiki dengan cara yang lebih baik.

Kalau ada yang membaca dan menemukan cara yang lebih rapi, saya justru senang. Yang penting bagi saya, langkahnya tercatat dan bisa dibaca lagi saat lupa, dan semoga bermanfaat juga buat yang sedang belajar hal yang sama.

Belajar Apps Script: Pasang Domain Sendiri Pakai Cloudflare Worker

Sebelumnya sistem antrean konsultasi ini cuma jalan dari link bawaan Google Apps Script. Link-nya panjang, acak, dan jujur saja susah diingat. Saya sendiri saja kalau mau buka harus cari-cari lagi di catatan.

Dari situ muncul keinginan sederhana: pengin punya alamat sendiri yang pendek, misalnya bimbingan.contohdomain.my.id, biar gampang ditulis di pengumuman dan gampang diingat mahasiswa.

Ternyata perjalanan ke sana tidak sesederhana "tinggal ganti alamat". Catatan ini saya tulis supaya langkahnya tidak hilang, karena saya yakin suatu saat saya akan lupa dan butuh membaca ini lagi.

Daftar Isi

  • 1. Gambaran Alurnya Dulu
  • 2. Isi Dapurnya: Apps Script Jadi API
  • 3. Frontend dan Dua Worker
  • 4. Sempat Mentok: Error 405
  • 5. Sempat Bingung: Route /api yang 404
  • 6. Pasang Domain Sendiri
  • 7. Aturan Main Sistemnya
  • 8. Hal yang Saya Pelajari
  • 9. Penutup

1. Gambaran Alurnya Dulu

Sistem ini saya pecah jadi dua bagian. Bagian depan adalah halaman web yang dibuka orang. Bagian belakang adalah Apps Script yang mengurus data ke Google Sheets dan Google Drive.

Kenapa dipecah? Karena kalau halaman ikut menumpang di Apps Script, alamatnya ikut alamat Apps Script. Kalau halamannya dipisah dan ditaruh di tempat lain, alamatnya bisa pakai domain sendiri, sementara Apps Script cukup bekerja di belakang layar.

Arsitektur Sistem

Alurnya kalau ditulis pakai panah kira-kira begini:

Mahasiswa / Admin
↓ buka halaman
Frontend (domain sendiri)
↓ kirim data JSON
API Worker (proxy)
↓ teruskan permintaan
Apps Script Web App (/exec)
↓ baca / tulis
Google Sheets (database) + Google Drive (dokumen)

Halaman mahasiswanya dipakai untuk daftar antrean, cek status antrean, dan unggah dokumen. Halaman adminnya terpisah, masuknya pakai PIN, dipakai untuk memanggil antrean berikutnya, menandai selesai, mengatur jadwal, dan memeriksa dokumen yang masuk.

Alur Daftar Antrian

2. Isi Dapurnya: Apps Script Jadi API

Di Apps Script, fungsi doPost saya jadikan pintu masuk. Setiap permintaan datang membawa satu keterangan: mau melakukan apa. Di kode, keterangan itu dibaca dari isian bernama action.

Bentuk sederhananya begini:

function doPost(e) {
  var data = JSON.parse(e.postData.contents);
  var aksi = data.action;

  if (aksi === "daftar") { return daftarAntrean(data); }
  if (aksi === "cek")    { return cekAntrean(data); }
  if (aksi === "upload_dokumen") { return uploadDokumen(data); }
  if (aksi === "admin_next")     { return panggilBerikutnya(data); }

  return balasJson({ ok: false, pesan: "Action tidak dikenal" });
}

Action yang akhirnya kepakai cukup banyak. Untuk mahasiswa ada daftar, cek, upload_dokumen, dan cek_dokumen. Untuk admin ada admin_list, admin_next, admin_selesai, admin_set_jadwal, admin_list_dokumen, dan admin_update_dokumen.

Contoh permintaan cek antrean dari halaman depan bentuknya begini:

{
  "action": "cek",
  "nim": "0000000000"
}

Dan jawaban dari belakang kira-kira begini. Ini contoh dummy, bukan data asli:

{
  "ok": true,
  "pesan": "Data antrean ditemukan",
  "antrean": {
    "nomor": 7,
    "status": "menunggu"
  }
}

Databasenya bukan database beneran, melainkan Google Sheets. Isinya saya bagi jadi beberapa sheet:

ANTRIAN Isi daftar antrean: nama, NIM, waktu daftar, status.
DOKUMEN Catatan dokumen yang diunggah, file aslinya disimpan di Google Drive.
SETTING Pengaturan, termasuk PIN admin. PIN tersimpan di sheet SETTING, jangan tampilkan PIN asli di tulisan mana pun.
ADMIN dan TOKEN Pendukung untuk keperluan admin dan sesi.

Saat deploy Web App, pengaturannya saya isi: Execute as: Me dan Who has access: Anyone. Alasannya supaya mahasiswa bisa memakai halaman tanpa harus login akun Google dulu. Urusan admin tetap dijaga PIN, bukan dibuka begitu saja.

3. Frontend dan Dua Worker

Halaman depannya saya simpan di repo GitHub yang saya privat, lalu dihubungkan ke Cloudflare. Di Cloudflare, proyek ini jalan sebagai Worker, bukan sekadar tempat menaruh file.

Ada dua Worker yang bekerja:

  • Worker frontend — yang melayani halaman mahasiswa dan halaman admin saat domain dibuka.
  • Worker API — yang jadi perantara antara halaman dan Apps Script.

Kenapa perlu perantara segala? Karena halaman yang jalan di domain sendiri tidak bisa sembarangan memanggil alamat lain, ada aturan keamanan browser yang namanya CORS. Worker API ini yang menjembatani, sekaligus menyembunyikan alamat asli Apps Script dari tampilan luar.

Alamat Apps Script-nya sendiri di kode cukup ditulis seperti ini. Ini contoh yang sudah saya samarkan:

const GAS_URL = "https://script.google.com/macros/s/XXXX-contoh-xxxx/exec";

4. Sempat Mentok: Error 405

Bagian ini yang paling lama bikin saya berhenti. Halaman sudah jadi, Worker sudah jalan, tapi setiap mengirim data balasannya 405.

Setelah diurut pelan-pelan, ternyata masalahnya ada di cara Worker meneruskan kiriman ke Google. Apps Script itu kalau menerima POST, dia tidak langsung menjawab. Dia membalas dengan pengalihan, istilahnya redirect 302, ke alamat sementara.

Kesalahan saya waktu itu: Worker saya ikut-ikutan mengirim POST lagi ke alamat pengalihan itu. Di situlah Google menolak, keluarlah 405.

Solusinya ternyata sepele, tapi tidak kepikiran di awal:

POST cukup sekali saja ke alamat /exec. Setelah itu ikuti pengalihannya pakai GET, karena tiket pengalihan itu sudah membawa data POST yang kita kirim.

Potongan kodenya kira-kira begini:

async function fetchViaGas(body) {
  // 1. POST sekali saja ke /exec, jangan auto mengikuti redirect
  let res = await fetch(GAS_URL, {
    method: "POST",
    body: body,
    redirect: "manual"
  });

  // 2. Kalau dibalas redirect 302, ikuti pakai GET
  let lokasi = res.headers.get("Location");
  if (lokasi) {
    res = await fetch(lokasi, { method: "GET" });
  }

  return res;
}

Setelah bagian itu diperbaiki, kiriman cek antrean langsung dibalas 200 dengan jawaban JSON yang benar. Rasanya lega sekali, karena masalahnya bukan di Apps Script-nya, melainkan di cara meneruskannya.

5. Sempat Bingung: Route /api yang 404

Kejadian kedua tidak kalah aneh. Rencananya halaman dan API mau saya jadikan satu domain, jadi API saya pasang di route /api/* pada domain yang sama.

Dari hasil tes di luar, route itu menjawab 200 dan isinya benar. Tapi dari browser saya sendiri, yang muncul justru 404. Sudah dicoba pakai mode penyamaran, tetap sama.

Di titik itu saya berhenti memaksakan diri. Daripada sistemnya tidak jalan cuma gara-gara satu route, saya pakai jalan keluar yang lebih praktis: di kode halaman depan, alamat API-nya saya isi lengkap memakai alamat bawaan Worker API.

const CONFIG = {
  apiBase: "https://nama-api.username.workers.dev"
};

Kenapa ini aman dipakai? Dua hal. Pertama, Worker API saya beri header CORS yang membuka akses. Kedua, kiriman dari halaman saya buat memakai content type text/plain, jadi browser tidak ribet dengan pemeriksaan awal yang biasanya bikin gagal.

const headers = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type",
  "Content-Type": "application/json"
};

Sampai sekarang route satu domain itu belum saya sentuh lagi. Sistemnya sudah jalan dengan cara ini, jadi saya catat saja sebagai utang lama. Kalau nanti ada waktu dan kepala sedang dingin, baru saya tengok lagi.

6. Pasang Domain Sendiri

Domain yang saya pakai memang sudah saya tambahkan ke Cloudflare sejak awal, jadi urusan nama server tidak perlu diulang dari nol.

Langkah pasang alamatnya begini. Pertama, buka Worker frontend di dashboard Cloudflare. Kedua, masuk ke pengaturan domain dan route. Ketiga, tambahkan Custom Domain dengan subdomain yang diinginkan, contohnya bimbingan.contohdomain.my.id. Keempat, API-nya sekalian diatur jalurnya, dari pengaturan Worker API.

Setelah itu tinggal ditunggu sebentar sampai HTTPS-nya siap, lalu dicoba dibuka seperti membuka website biasa. Momen pertama kali alamat pendek itu kebuka dan halamannya muncul, itu yang bikin semua utak-atik sebelumnya terasa terbayar.

7. Aturan Main Sistemnya

Biar tidak cuma soal teknis, sekalian saya catat aturan main yang saya pasang di sistem ini:

  • Pendaftaran dibuka pukul 08.00 sampai 23.00, di luar itu formulir ditutup.
  • Satu mahasiswa hanya boleh konsultasi satu kali dalam satu minggu.
  • Antrean bisa dibuka atau ditutup dari pengaturan.
  • Dokumen yang diunggah masuk ke folder Drive yang berbeda sesuai jenis dokumennya.
  • Halaman admin hanya bisa dipakai setelah memasukkan PIN, dan PIN-nya tersimpan di sheet SETTING.

8. Hal yang Saya Pelajari

  • Alamat bawaan layanan itu tidak wajib jadi alamat yang dilihat orang. Halaman dan mesin di belakangnya bisa dipisah.
  • Redirect itu bukan error. Dalam kasus Apps Script, redirect 302 justru bagian dari alur normalnya, hanya cara mengikutinya harus benar: POST sekali, lalu GET.
  • Kalau satu jalan mentok terlalu lama, jalan keluar yang sederhana dan terbukti jalan itu lebih berharga daripada memaksakan cara yang paling rapi di atas kertas.
  • Mencatat pesan error dan langkah terakhir sebelum error itu jauh lebih membantu daripada sekadar mengingat "tadi gagal".
  • Sistem kecil seperti ini ternyata tetap butuh aturan main yang jelas, bukan cuma halaman dan tombol.

9. Penutup

Sekarang sistem antrean ini sudah bisa dibuka dari alamat sendiri yang pendek. Mahasiswa tidak perlu lagi menerima link panjang dari saya, dan saya tidak perlu lagi mencari-cari link itu tiap mau membagikan.

Semoga catatan ini bermanfaat, terutama buat saya sendiri kalau nanti lupa dan harus mengulang hal yang sama dari awal.


Disclaimer

Tulisan ini adalah catatan belajar pribadi tentang sistem antrean konsultasi dan domain sendiri, bukan tutorial resmi dan bukan panduan baku dari pihak mana pun. Saya menulis ulang pengalaman saya supaya tidak lupa, bukan karena saya sudah paham.

Prosesnya sendiri banyak dibantu AI dan diskusi bolak-balik, apalagi saat mengatasi error 405 dan route yang 404 itu. Jadi jangan dibayangkan saya sudah paham semuanya. Saya masih belajar, dan sangat mungkin ada bagian di catatan ini yang kurang tepat atau bisa diperbaiki dengan cara yang lebih baik.

Kalau ada yang membaca dan menemukan cara yang lebih rapi, saya justru senang. Yang penting bagi saya, langkahnya tercatat dan bisa dibaca lagi saat lupa, dan semoga bermanfaat juga buat yang sedang belajar hal yang sama.

Belajar n8n: Membuat Pengingat Absensi Otomatis ke Grup WhatsApp Kantor

Awalnya saya sudah pernah membuat workflow pengingat absensi menggunakan n8n. Tapi jujur saja, waktu itu proses bikinnya cukup banyak dibantu AI. Jadi meskipun workflow-nya jalan, saya tidak benar-benar paham alurnya.

Membuat Pengingat Absensi Otomatis ke Grup WhatsApp Kantor


Workflow lama yang saya buat kurang lebih seperti ini:

Cron
↓
Baca daftar pegawai
↓
Loop satu per satu
↓
Generate pesan
↓
Kirim WhatsApp personal

Karena pesannya dikirim ke masing-masing nomor pegawai, workflow-nya jadi lumayan panjang.

Nah, beberapa waktu lalu saya kepikiran untuk menyederhanakan semuanya.

Daripada kirim satu-satu ke seluruh pegawai, kenapa tidak kirim sekali saja ke grup WhatsApp kantor?

Selain lebih sederhana, proses maintenance-nya juga jauh lebih mudah.

Kondisi yang saya gunakan

Beberapa catatan setup yang saya pakai:

n8n

Self-hosted.

WhatsApp Gateway

Saya menggunakan:

GOWA

Repository:

https://github.com/aldinokemal/go-whatsapp-web-multidevice

Keamanan

Dashboard GOWA saya amankan menggunakan Cloudflare Access.

Jadi orang luar tidak bisa sembarangan membuka halaman login.

Sedangkan untuk n8n, saya tidak menggunakan URL Cloudflare.

Saya langsung mengakses GOWA menggunakan IP lokal:

http://IP_LOKAL_SERVER:PORT

Tujuannya supaya request dari n8n tidak terhalang proteksi Cloudflare.

Rencana Workflow

Targetnya sederhana:

Pagi

Senin - Jumat
07:45

Kirim ke grup kantor:

Jangan lupa absensi masuk

Sore

Senin - Kamis
16:45

Kirim:

Jangan lupa absensi pulang

Jumat

Jamnya berbeda.

Karena jam kerja kantor berbeda dengan hari biasa.

Membuat Schedule Trigger

Di node pertama saya menggunakan:

Schedule Trigger

Lalu memilih:

Trigger Interval
=
Weeks

Kemudian memilih hari:

Monday
Tuesday
Wednesday
Thursday
Friday

dan mengatur waktu sesuai kebutuhan.

Contoh:

Hour  : 07
Minute: 45

Awalnya saya sempat bingung antara menggunakan:

Days

atau

Weeks

Ternyata yang lebih pas adalah:

Weeks

karena kita bisa memilih hari tertentu dan mengecualikan Sabtu-Minggu.

Membuat Isi Pesan

Node kedua saya menggunakan:

Edit Fields

atau yang dulu dikenal sebagai:

Set

Saya membuat field baru bernama:

message

Isi contohnya:

🌞 Selamat pagi.

Jangan lupa melakukan absensi masuk.

Terima kasih.

Sederhana saja.

Yang penting pesannya tersampaikan.

Mengirim Pesan ke Grup WhatsApp

Bagian ini yang awalnya membuat saya agak bingung.

Karena sebelumnya saya mengirim ke nomor pribadi.

Sedangkan sekarang tujuannya grup WhatsApp.

Ternyata yang dibutuhkan adalah:

Group ID

Formatnya bukan nomor HP.

Kurang lebih seperti ini:

120363xxxxxxxxxxxx@g.us

Yang penting ada akhiran:

@g.us

HTTP Request ke GOWA

Node berikutnya:

HTTP Request

Konfigurasinya:

Method:
POST

URL:

http://IP_LOKAL_SERVER:PORT/send/message

Body JSON:

{
  "phone": "120363xxxxxxxxxxxx@g.us",
  "message": "{{ $json.message }}"
}

Untuk memilih device WhatsApp yang digunakan:

device_id = device_contoh

Karena di server saya ada beberapa device WhatsApp yang terhubung.

Saya ingin workflow ini menggunakan device:

device_contoh

Sempat Bingung Soal Basic Auth

Workflow lama saya menggunakan:

Basic Auth

Sedangkan workflow baru saya mencoba:

Authentication = None

Setelah dicoba ternyata tetap berjalan normal.

Karena request berasal dari jaringan lokal dan endpoint memang bisa diakses dari LAN.

Akhirnya saya memilih yang lebih sederhana:

None

Eksperimen Mention Semua Anggota Grup

Saya juga sempat mencoba:

@everyone

dengan harapan seluruh anggota grup akan terkena mention.

Pesan memang berhasil terkirim:

@everyone

tetapi tidak benar-benar menjadi mention WhatsApp.

Masih tampil sebagai teks biasa.

Untuk sementara saya biarkan saja karena tujuan utama workflow ini hanyalah pengingat absensi.

Tidak terlalu penting harus melakukan mention ke seluruh anggota grup.

Menambahkan Sistem "Hansip Lapor Error"

Sambil belajar, saya juga sekalian membuat workflow khusus untuk menangkap error.

Kalau ada workflow yang gagal, saya ingin mendapatkan laporan otomatis.

Kurang lebih seperti ini:

Error Trigger
↓
Kirim Telegram

Pesan yang saya kirim ke Telegram:

🚨 HANSIP JAGA MELAPOR!

Workflow:
...

Node:
...

Error:
...

Link:
...

Awalnya saya sempat bingung.

Workflow dibuat.

Sudah publish.

Tapi Telegram tidak masuk.

Ternyata ada satu hal yang saya pelajari:

Error Trigger tidak selalu aktif ketika saya hanya menekan tombol:

Execute Workflow

di editor.

Supaya benar-benar teruji, saya membuat workflow yang sengaja error menggunakan kode:

throw new Error("TEST HANSIP PRODUCTION")

Setelah workflow dijalankan sebagai production execution, akhirnya notifikasi Telegram berhasil masuk.

Lumayan puas juga waktu melihat "Hansip" pertama kali berhasil melapor. 😆

Hal Yang Saya Pelajari

Kalau dipikir-pikir, workflow pengingat absensi grup ternyata jauh lebih sederhana daripada workflow pengingat ke nomor personal.

Dari yang awalnya:

Cron
↓
Baca Data
↓
Loop
↓
Generate Pesan
↓
Send WA

menjadi:

Schedule Trigger
↓
Edit Fields
↓
HTTP Request

Lebih pendek.

Lebih mudah dipahami.

Dan lebih gampang dirawat kalau nanti suatu saat perlu diubah.

Penutup

Belajar n8n buat saya menarik karena sering kali yang sulit bukan menambahkan node, tetapi memahami alur datanya.

Kadang workflow bisa jalan, tetapi kita sendiri belum tentu paham kenapa bisa jalan.

Makanya saya mulai menulis catatan-catatan seperti ini.

Selain untuk dokumentasi pribadi, juga sebagai pengingat kalau beberapa bulan lagi saya lupa settingannya dan harus mengulang dari awal.


Disclaimer

Tulisan ini lebih merupakan catatan belajar daripada tutorial resmi.

Proses pembuatan workflow di atas banyak saya lakukan dengan bantuan Microsoft Copilot dan diskusi bolak-balik selama proses troubleshooting.

Jadi jangan dibayangkan saya sudah benar-benar paham semuanya. 😄

Saya masih belajar.

Kalau ada bagian yang menurut teman-teman kurang tepat, sangat mungkin memang ada yang bisa diperbaiki.

Yang penting buat saya, setiap kali mencoba sesuatu, ada catatan yang bisa dibaca lagi saat lupa, dan semoga bermanfaat juga untuk teman-teman yang sedang belajar n8n dari nol seperti saya.

Merapikan Data Inventaris Kantor dengan Google Spreadsheet

Beberapa waktu ini saya mulai belajar merapikan data inventaris kantor. Ceritanya saya kebetulan diminta untuk ikut membantu pendataan barang. Awalnya saya pikir pekerjaannya sederhana. Paling hanya mencocokkan data yang sudah ada dengan barang yang ada di ruangan.

Ternyata setelah mulai dikerjakan, kondisinya tidak sesederhana itu.

Data utama memang sudah ada. Tetapi ketika dibandingkan dengan kondisi nyata di ruangan, ternyata banyak yang belum sesuai. Ada barang yang di data tercatat berada di lokasi tertentu, tetapi fisiknya ada di ruangan lain. Ada barang yang labelnya masih ada, tetapi ketika dicari di database utama tidak langsung ditemukan. Ada juga barang yang secara fisik ada, tetapi informasinya belum cocok dengan data utama.

Merapikan Data Inventaris Kantor dengan Google Spreadsheet

Dari situ saya mulai berpikir, kalau langsung ingin merapikan semuanya sekaligus, sepertinya akan berat. Jadi saya coba mulai dari langkah yang paling sederhana dulu, yaitu melakukan pendataan per ruangan.

Alurnya saya buat pelan-pelan:

  1. Cek barang langsung ke tiap ruangan.
  2. Catat manual dulu di tabel/form pendataan.
  3. Masukkan hasil catatan ke Google Spreadsheet.
  4. Cocokkan dengan database utama.
  5. Tandai data yang sudah diproses.
  6. Catat juga data yang tidak ditemukan atau perlu dicek ulang.

Jadi tulisan ini saya buat sebagai catatan belajar saya sendiri. Supaya kalau nanti lupa, saya bisa buka lagi.

Gambaran Alur Kerja

Sebelum masuk ke Google Spreadsheet, proses awal yang saya lakukan sebenarnya masih manual.

Saya mengecek barang langsung ke tiap ruangan, lalu mencatatnya di tabel/form pendataan sederhana. Jadi sumber input awalnya bukan langsung dari database utama, tetapi dari hasil pengecekan fisik di ruangan.

Misalnya saya masuk ke satu ruangan, lalu mulai mencatat barang-barang yang ada di sana. Saya catat nomor atau identitas item yang masih terbaca, nama item jika perlu, kondisi item, dan keterangan lain yang dianggap penting.

Setelah hasil catatan dari ruangan terkumpul, barulah data tersebut saya masukkan ke Google Spreadsheet.

Tujuannya bukan hanya mengetik ulang data, tetapi untuk membandingkan antara:

  • item yang benar-benar ada di ruangan,
  • item yang tercatat di database utama,
  • dan apakah lokasi item di data sudah sesuai dengan kondisi nyata.

Dengan cara ini, saya tidak hanya bergantung pada data lama. Database utama tetap dipakai sebagai pembanding, tetapi kondisi nyata di ruangan tetap menjadi dasar pengecekan awal.

Penomoran Ruangan

Sebelum membuat sheet per ruangan, saya sudah lebih dulu memberi nomor untuk tiap ruangan.

Tujuannya sederhana, agar proses pendataan lebih mudah dilacak.

Misalnya:

  • Ruang nomor 1 saya beri nama 1RuangRapat
  • Ruang nomor 38 saya beri nama 38RuangServer

Jadi angka 1, 2, 38, dan seterusnya itu bukan berarti urutan saya menginput data di spreadsheet. Angka itu adalah nomor ruangan yang memang sudah saya buat sebelumnya untuk memudahkan pendataan.

Dalam praktiknya, input data tidak harus dimulai dari ruangan nomor 1.

Misalnya ruangan nomor 38 lebih dulu selesai dicek secara manual, maka saya bisa input dulu ke sheet 38RuangServer. Kalau ruangan nomor 1 selesai belakangan, baru nanti saya input ke sheet 1RuangRapat.

Jadi urutan input mengikuti ruangan mana yang sudah selesai didata di lapangan, bukan harus urut dari nomor ruangan terkecil.

Ini penting supaya pekerjaan lebih fleksibel. Karena dalam proses cek fisik, kadang ruangan tertentu lebih mudah diakses, sedangkan ruangan lain harus menunggu waktu kosong, menunggu kunci, atau menunggu penanggung jawab ruangannya.

Struktur Sheet yang Saya Buat

Di Google Spreadsheet, saya membuat beberapa sheet.

Kurang lebih strukturnya seperti ini:

  1. Master
  2. Daftar_Ruangan
  3. Cek_Fisik
  4. Sheet per ruangan, misalnya:
    • 1RuangRapat
    • 38RuangServer

Nama sheet saya buat tanpa spasi agar lebih mudah dipakai di rumus. Misalnya saya pakai 1RuangRapat, bukan 1 Ruang Rapat.

Sheet Master

Sheet Master berisi database utama item.

Header-nya bisa disesuaikan dengan kebutuhan. Saya buat dengan istilah umum saja, misalnya:

  • No
  • Kode Unit
  • Nama Unit
  • Kode Item
  • ID Item
  • Nama Item
  • Status
  • Brand
  • Model
  • Kondisi
  • Lokasi
  • Keterangan

Yang paling penting untuk proses awal ini adalah kolom-kolom berikut:

Kolom Isi
ANo
FID Item
GNama Item
IBrand
KKondisi

Kolom No menjadi kunci utama. Jadi ketika saya mengisi nomor di sheet ruangan, data lain seperti nama item, ID item, brand, dan kondisi bisa otomatis ditarik dari sheet Master.

Sheet Daftar_Ruangan

Sheet Daftar_Ruangan saya gunakan untuk menyimpan daftar kode dan nama ruangan.

Formatnya sederhana:

Kode Ruangan Nama Ruangan
R001Ruang Rapat
R002Ruang Server
R003Ruang Administrasi

Misalnya:

  • Kolom A = Kode Ruangan
  • Kolom B = Nama Ruangan

Data dimulai dari baris 2, karena baris 1 dipakai untuk header.

Contoh rentang datanya:

A2:B40

Sheet ini nanti dipakai sebagai sumber dropdown di sheet Cek_Fisik.

Sheet Cek_Fisik

Sheet Cek_Fisik saya gunakan untuk menampung hasil pengecekan fisik barang di ruangan.

Jadi sebelum data dimasukkan ke sheet ini, saya terlebih dahulu mengecek barang secara langsung di tiap ruangan dan mencatatnya secara manual di tabel/form pendataan.

Setelah itu, hasil catatan tersebut baru saya masukkan ke Google Spreadsheet.

Di sheet Cek_Fisik, saya ingin ketika memilih kode ruangan, maka nama ruangannya otomatis muncul. Ini supaya proses input lebih rapi dan saya tidak perlu mengetik nama ruangan berulang-ulang.

Contohnya seperti ini:

Kode Ruangan Nama Ruangan
pilih dari dropdownotomatis muncul

Dengan cara ini, saya bisa lebih fokus mencatat hasil pengecekan fisik, lalu setelah itu membandingkannya dengan data yang ada di sheet Master.

Bisa dibilang, sheet Cek_Fisik ini menjadi jembatan antara kondisi nyata di ruangan dengan database utama yang sudah ada.

Langkah pertama yang saya buat adalah dropdown untuk kode ruangan.

Tujuannya supaya kode ruangan tidak diketik manual. Kalau diketik manual, kadang ada risiko salah ketik, kelebihan spasi, atau formatnya tidak sama.

Langkahnya:

  1. Buka sheet Cek_Fisik.
  2. Blok kolom yang ingin diberi dropdown, misalnya:
A2:A500
  1. Klik menu:
Data → Validasi data
  1. Pada bagian kriteria, pilih:
Dropdown dari rentang
  1. Masukkan rentang sumber data:
Daftar_Ruangan!A2:A40
  1. Klik selesai.

Setelah itu, kolom kode ruangan di sheet Cek_Fisik sudah memiliki dropdown. Jadi tinggal pilih kode ruangan dari daftar yang tersedia.

Membuat Nama Ruangan Otomatis Muncul

Setelah dropdown kode ruangan berhasil, langkah berikutnya adalah membuat nama ruangan otomatis muncul.

Misalnya di sheet Cek_Fisik:

Kolom Isi
AKode Ruangan
BNama Ruangan

Maka di cell B2, saya isi rumus berikut:

=IF(A2="";"";IFERROR(VLOOKUP(A2;Daftar_Ruangan!A:B;2;FALSE);"Tidak ditemukan"))

Setelah rumus ini dipasang, ketika saya memilih kode ruangan di kolom A, nama ruangan akan otomatis muncul di kolom B.

Penjelasan sederhananya:

  • A2 adalah kode ruangan yang dipilih.
  • Daftar_Ruangan!A:B adalah sumber data kode dan nama ruangan.
  • Angka 2 berarti mengambil data dari kolom kedua, yaitu nama ruangan.
  • FALSE berarti pencarian harus cocok persis.
  • IFERROR dipakai supaya kalau data tidak ditemukan, tidak langsung muncul error.

Catatan Penting tentang Koma, Titik Koma, dan Setting Wilayah

Ini salah satu hal kecil yang sempat membuat saya bingung.

Awalnya saya mencoba memasukkan rumus seperti ini:

=IF(A2="","",IFERROR(VLOOKUP(A2,Daftar_Ruangan!A:B,2,FALSE),"Tidak ditemukan"))

Ternyata muncul error.

Setelah dicek, masalahnya bukan pada logika rumusnya, tetapi pada setting wilayah atau locale Google Spreadsheet yang saya gunakan.

Kalau Google Spreadsheet memakai setting Indonesia, biasanya pemisah antarbagian rumus menggunakan titik koma ;, bukan koma ,.

Jadi untuk setting Indonesia, rumusnya menjadi seperti ini:

=IF(A2="";"";IFERROR(VLOOKUP(A2;Daftar_Ruangan!A:B;2;FALSE);"Tidak ditemukan"))

Perbedaannya kecil, tapi cukup penting.

Versi setting Inggris/US biasanya memakai koma:

=VLOOKUP(B2,Master!A:G,7,FALSE)

Sedangkan versi setting Indonesia memakai titik koma:

=VLOOKUP(B2;Master!A:G;7;FALSE)

Jadi kalau rumus terasa sudah benar tetapi tetap error, coba periksa dulu tanda pemisahnya.

Setting SpreadsheetPemisah Rumus
Indonesiatitik koma ;
Inggris/USkoma ,

Hal yang sama juga berlaku untuk rumus lain seperti IF, IFERROR, VLOOKUP, ARRAYFORMULA, COUNTIF, dan OR.

Contoh rumus format bersyarat dengan setting Indonesia:

=COUNTIF(INDIRECT("'1RuangRapat'!B:B");$A2)>0

Kalau memakai setting Inggris/US, biasanya menjadi:

=COUNTIF(INDIRECT("'1RuangRapat'!B:B"),$A2)>0

Bagi saya, ini pelajaran kecil tapi penting. Kadang kita merasa rumusnya salah, padahal yang salah hanya tanda pemisahnya.

Membuat Nama Ruangan Otomatis untuk Semua Baris

Kalau tidak ingin menarik rumus ke bawah satu per satu, bisa memakai ARRAYFORMULA.

Misalnya di cell B2 sheet Cek_Fisik:

=ARRAYFORMULA(IF(A2:A="";"";IFERROR(VLOOKUP(A2:A;Daftar_Ruangan!A:B;2;FALSE);"Tidak ditemukan")))

Dengan rumus ini, cukup pasang sekali di baris 2. Nanti baris-baris di bawahnya akan otomatis mengikuti.

Tapi ada catatan penting.

Kalau memakai ARRAYFORMULA, maka kolom hasilnya tidak bisa diisi manual di tengah-tengah. Karena seluruh kolom sudah dikendalikan oleh rumus dari baris atas.

Ini nanti menjadi penting ketika ada data yang tidak ditemukan di database utama dan perlu diisi manual.

Membuat Sheet Per Ruangan

Setelah sheet Cek_Fisik mulai rapi, saya lanjut membuat sheet per ruangan.

Contohnya saya membuat sheet:

1RuangRapat

dan juga:

38RuangServer

Sekali lagi, angka di depan nama sheet adalah nomor ruangan yang sudah saya tentukan sebelumnya. Jadi 1RuangRapat artinya ruangan nomor 1, sedangkan 38RuangServer artinya ruangan nomor 38.

Header di sheet ruangan kurang lebih seperti ini:

NoNo Urut Data UtamaNama ItemID ItemBrandKondisi

Kolom pentingnya:

KolomIsi
BNo Urut Data Utama
CNama Item
DID Item
EBrand
FKondisi

Konsepnya begini.

Saya cukup mengisi kolom No Urut Data Utama. Nomor tersebut sama dengan kolom No yang ada di sheet Master.

Setelah nomor diisi, maka data lain akan otomatis muncul dari sheet Master.

Dalam proses input, saya tidak harus mengisi sheet ruangan secara berurutan dari nomor 1. Kalau hasil cek fisik ruangan nomor 38 sudah selesai duluan, saya bisa langsung mengisi sheet 38RuangServer. Nanti kalau ruangan nomor 1 baru selesai dicek, baru saya isi sheet 1RuangRapat.

Jadi spreadsheet ini mengikuti alur kerja nyata di lapangan, bukan memaksa harus urut dari awal.

Menarik Data dari Master

Menarik Nama Item dari Master

Di sheet 1RuangRapat, kolom B adalah No Urut Data Utama. Kolom C adalah Nama Item.

Karena di sheet Master, kolom A adalah No dan kolom G adalah Nama Item, maka di cell C2, saya isi rumus:

=IF(B2="";"";IFERROR(VLOOKUP(B2;Master!A:G;7;FALSE);""))

Menarik ID Item dari Master

Untuk kolom D atau ID Item, rumus di cell D2:

=IF(B2="";"";IFERROR(VLOOKUP(B2;Master!A:F;6;FALSE);""))

Menarik Brand dari Master

Untuk kolom E atau Brand, rumus di cell E2:

=IF(B2="";"";IFERROR(VLOOKUP(B2;Master!A:I;9;FALSE);""))

Menarik Kondisi dari Master

Untuk kolom F atau Kondisi, rumus di cell F2:

=IF(B2="";"";IFERROR(VLOOKUP(B2;Master!A:K;11;FALSE);""))

Setelah semua rumus dipasang di baris 2, rumus bisa ditarik ke bawah sesuai kebutuhan.

Versi Otomatis dengan ARRAYFORMULA

Awalnya saya mencoba memakai ARRAYFORMULA, supaya tidak perlu menarik rumus ke bawah.

Untuk Nama Item di C2:

=ARRAYFORMULA(IF(B2:B="";"";IFERROR(VLOOKUP(B2:B;Master!A:G;7;FALSE);"Tidak ditemukan")))

Untuk ID Item di D2:

=ARRAYFORMULA(IF(B2:B="";"";IFERROR(VLOOKUP(B2:B;Master!A:F;6;FALSE);"Tidak ditemukan")))

Untuk Brand di E2:

=ARRAYFORMULA(IF(B2:B="";"";IFERROR(VLOOKUP(B2:B;Master!A:I;9;FALSE);"Tidak ditemukan")))

Untuk Kondisi di F2:

=ARRAYFORMULA(IF(B2:B="";"";IFERROR(VLOOKUP(B2:B;Master!A:K;11;FALSE);"Tidak ditemukan")))

Dengan cara ini, semua baris otomatis ikut terisi ketika kolom No Urut Data Utama diisi. Tapi setelah dipakai, ternyata ada kasus khusus.

Ketika Data Tidak Ada di Master dan Perlu Diisi Manual

Saat mulai pendataan, saya menemukan kasus seperti ini.

Identitas item ada secara fisik, tetapi ketika dicari di database utama, datanya tidak ditemukan. Bisa jadi karena data belum diperbarui, lokasinya belum sesuai, atau memang perlu dicek ulang.

Masalahnya, kalau kolom memakai ARRAYFORMULA, saya tidak bisa mengetik manual di kolom tersebut. Ketika dipaksa diisi manual, muncul error karena cell itu bagian dari hasil array.

Dari sini saya belajar, untuk pekerjaan pendataan inventaris yang datanya belum sepenuhnya rapi, lebih fleksibel menggunakan rumus biasa per baris, bukan ARRAYFORMULA.

Jadi solusinya:

  1. Hapus rumus ARRAYFORMULA.
  2. Pakai rumus biasa di baris 2.
  3. Tarik rumus ke bawah.
  4. Jika ada data yang perlu diisi manual, hapus rumus di cell tersebut.
  5. Isi manual sesuai hasil pengecekan.

Rumus yang saya pakai akhirnya seperti ini:

C2 - Nama Item

=IF(B2="";"";IFERROR(VLOOKUP(B2;Master!A:G;7;FALSE);""))

D2 - ID Item

=IF(B2="";"";IFERROR(VLOOKUP(B2;Master!A:F;6;FALSE);""))

E2 - Brand

=IF(B2="";"";IFERROR(VLOOKUP(B2;Master!A:I;9;FALSE);""))

F2 - Kondisi

=IF(B2="";"";IFERROR(VLOOKUP(B2;Master!A:K;11;FALSE);""))

Dengan cara ini, kalau ada data yang tidak ditemukan di Master, saya masih bisa mengisi manual di baris tertentu.

Untuk kondisi data inventaris yang masih perlu dirapikan, cara ini terasa lebih aman.

Menandai Data di Master Jika Sudah Diproses

Setelah data dari Master ditarik ke sheet ruangan, saya ingin di sheet Master ada tanda otomatis.

Misalnya nomor 280 di Master adalah item tertentu. Kalau nomor 280 sudah dimasukkan ke sheet 1RuangRapat, maka cell nomor 280 di sheet Master otomatis berubah warna.

Tujuannya agar saya tahu item mana yang sudah diproses dan item mana yang belum.

Caranya menggunakan Format Bersyarat atau Conditional Formatting.

Format Bersyarat untuk Satu Sheet Ruangan

Misalnya sheet Master kolom A adalah No, dan sheet 1RuangRapat kolom B adalah No Urut Data Utama. Saya ingin jika Master!A2 ditemukan di 1RuangRapat!B:B, maka cell tersebut berubah warna.

Awalnya saya mencoba rumus seperti ini:

=COUNTIF('1RuangRapat'!B:B;$A2)>0

Tapi ternyata Google Sheets tidak mengizinkan format bersyarat langsung mengambil referensi dari sheet lain.

Solusinya adalah memakai INDIRECT.

Rumus yang berhasil:

=COUNTIF(INDIRECT("'1RuangRapat'!B:B");$A2)>0

Langkahnya:

  1. Buka sheet Master.
  2. Blok rentang data yang ingin diberi warna, misalnya:
A2:A620
  1. Klik menu:
Format → Format bersyarat
  1. Pada bagian aturan, pilih:
Formula khususnya adalah
  1. Masukkan rumus:
=COUNTIF(INDIRECT("'1RuangRapat'!B:B");$A2)>0
  1. Pilih warna, misalnya hijau.
  2. Klik selesai.

Sekarang, kalau nomor di Master sudah ada di sheet 1RuangRapat, maka cell nomor tersebut akan berubah warna.

Kenapa Saya Pakai A2:A620?

Awalnya saya ingin memakai rentang seperti ini:

A2:A

atau untuk satu baris penuh:

A2:CM

Tapi di bagian format bersyarat, kadang Google Sheets menolak dan muncul pesan:

Masukkan rentang yang valid

Saya juga sempat mencoba:

A2:CM9999

Tetapi tetap tidak bisa.

Kemungkinan karena kolom terakhir yang tersedia atau terbaca di sheet belum sampai CM, atau format rentangnya belum diterima di bagian format bersyarat.

Karena data saya saat ini hanya sekitar 600-an baris, akhirnya saya pakai rentang yang lebih aman:

A2:A620

Artinya, yang berubah warna hanya kolom No, dari baris 2 sampai 620.

Untuk kebutuhan awal, ini sudah cukup. Yang penting saya bisa melihat nomor mana saja yang sudah diproses.

Menambahkan Sheet Ruangan Lain

Setelah berhasil untuk 1RuangRapat, saya ingin menambahkan sheet lain, misalnya:

38RuangServer

Kalau semua sheet ingin diberi warna yang sama, rumusnya bisa digabung memakai OR.

Contohnya:

=OR(COUNTIF(INDIRECT("'1RuangRapat'!B:B");$A2)>0;COUNTIF(INDIRECT("'38RuangServer'!B:B");$A2)>0)

Artinya:

Jika nomor di Master!A2 ditemukan di sheet 1RuangRapat atau 38RuangServer, maka cell tersebut akan berubah warna.

Rumus ini cocok kalau semua ruangan ingin diberi warna yang sama.

Memberi Warna Berbeda untuk Tiap Sheet Ruangan

Setelah dicoba, saya merasa akan lebih enak kalau setiap sheet ruangan punya warna berbeda.

Misalnya:

  • 1RuangRapat = hijau
  • 38RuangServer = kuning atau warna lain

Kalau ingin warna berbeda, jangan memakai rumus OR.

Caranya adalah membuat beberapa aturan Format Bersyarat.

Aturan 1: 1RuangRapat Warna Hijau

Di sheet Master, buat aturan format bersyarat.

Terapkan ke rentang:

A2:A620

Formula khususnya:

=COUNTIF(INDIRECT("'1RuangRapat'!B:B");$A2)>0

Pilih warna hijau, lalu klik selesai.

Aturan 2: 38RuangServer Warna Lain

Tambahkan aturan baru.

Terapkan ke rentang:

A2:A620

Formula khususnya:

=COUNTIF(INDIRECT("'38RuangServer'!B:B");$A2)>0

Pilih warna lain, misalnya kuning, lalu klik selesai.

Dengan cara ini, nomor item di Master akan berubah warna sesuai sheet ruangan tempat nomor tersebut dipakai.

Kalau nanti ada sheet lain, tinggal tambah aturan baru lagi.

Misalnya:

=COUNTIF(INDIRECT("'NamaSheetLain'!B:B");$A2)>0

Lalu pilih warna lain sesuai kebutuhan.

Catatan tentang Nama Sheet

Saya memakai nama sheet tanpa spasi, misalnya:

1RuangRapat
38RuangServer

Menurut saya ini lebih aman dan mudah dipakai di rumus.

Tapi walaupun tanpa spasi, saya tetap menulis nama sheet di dalam tanda petik seperti ini:

INDIRECT("'1RuangRapat'!B:B")

Ini untuk menghindari error, terutama karena nama sheet diawali angka.

Jika Satu Nomor Muncul di Dua Sheet

Idealnya, satu nomor item hanya masuk ke satu sheet ruangan.

Tapi kalau ternyata satu nomor masuk di dua sheet sekaligus, warna yang tampil bisa mengikuti aturan format bersyarat yang posisinya lebih atas atau lebih dominan di daftar aturan.

Jadi untuk menjaga data tetap rapi, saya harus hati-hati agar satu nomor tidak tercatat di dua ruangan berbeda, kecuali memang sedang dalam proses pengecekan.

Alur Kerja yang Saya Pakai

Setelah beberapa kali coba dan error, alur kerja yang saya pakai kira-kira seperti ini:

  1. Siapkan sheet Master dari database item.
  2. Siapkan sheet Daftar_Ruangan.
  3. Beri nomor dan nama untuk tiap ruangan agar mudah dilacak.
  4. Lakukan pengecekan fisik ke tiap ruangan.
  5. Catat hasil pengecekan secara manual di tabel/form pendataan.
  6. Masukkan hasil catatan tersebut ke sheet Cek_Fisik atau sheet ruangan.
  7. Input data tidak harus urut dari ruangan nomor 1, tetapi mengikuti ruangan mana yang sudah selesai dicek lebih dulu.
  8. Buat dropdown kode ruangan di sheet Cek_Fisik.
  9. Buat nama ruangan otomatis muncul berdasarkan kode ruangan.
  10. Buat sheet per ruangan, misalnya 1RuangRapat dan 38RuangServer.
  11. Isi No Urut Data Utama di sheet ruangan.
  12. Nama item, ID item, brand, dan kondisi otomatis ditarik dari sheet Master.
  13. Jika data tidak ditemukan di Master, isi manual di cell tertentu.
  14. Di sheet Master, buat format bersyarat untuk menandai nomor yang sudah diproses.
  15. Jika ingin beda warna tiap ruangan, buat aturan format bersyarat masing-masing sheet.

Dengan alur ini, pekerjaan menjadi lebih masuk akal. Saya tidak langsung memaksa data di spreadsheet harus benar semua. Saya mulai dari kondisi nyata dulu, lalu membandingkannya dengan database utama.

Kesimpulan Sementara

Dari proses ini saya belajar bahwa merapikan data inventaris tidak cukup hanya dengan melihat data di sistem. Tetap harus dicocokkan dengan kondisi nyata di ruangan.

Google Spreadsheet cukup membantu untuk tahap awal, terutama untuk:

  • membuat dropdown kode ruangan,
  • menarik nama ruangan otomatis,
  • mengambil data item dari database utama,
  • menandai data yang sudah diproses,
  • dan memudahkan pengecekan per ruangan.

Penomoran ruangan juga cukup membantu. Jadi meskipun input data tidak harus urut dari ruangan nomor 1, setiap data tetap bisa dilacak berdasarkan nomor dan nama ruangan yang sudah dibuat sebelumnya.

Untuk data yang sudah rapi, ARRAYFORMULA memang enak karena otomatis. Tapi untuk data inventaris yang masih banyak kemungkinan tidak cocok antara database dan kondisi fisik, rumus biasa per baris lebih fleksibel karena masih bisa diisi manual jika diperlukan.

Ini baru tahap awal. Setidaknya saya mulai punya alur yang lebih jelas untuk merapikan data inventaris kantor pelan-pelan, dimulai dari pendataan per ruangan, lalu dicocokkan dengan database utama.

Semoga catatan ini bermanfaat, terutama buat saya sendiri kalau nanti lupa langkah-langkahnya.

Belajar Akses SSH Server dari Luar Jaringan

🎯 Akses SSH Server STB dari Luar Jaringan (Cloudflare Tunnel) + Tips Keamanan

🎯 Akses SSH Server STB dari Luar Jaringan (Cloudflare Tunnel) + Tips Keamanan

Saya masih belajar dan nyoba-nyoba. Dulu kalau bingung harus googling, baca-baca, tanya-tanya di forum. Sekarang alhamdulillah belajar lebih mudah karena bisa langsung dipandu sama GPT—jadi step by step-nya lebih jelas dan cepat dipraktekin.

Target saya di catatan ini: bisa remote SSH ke server STB Armbian + CasaOS dari luar rumah tanpa ribet buka port di modem/ISP. Solusinya pakai Cloudflare Tunnel + domain sendiri (contoh: ssh.domainkita.com).


1) Setting Cloudflare Tunnel di Server

Edit file /etc/cloudflared/config.yml. Gunakan TCP mode untuk SSH (penting!):

tunnel: 12345678-abcd-4321-efgh-9876543210ab
credentials-file: /root/.cloudflared/12345678-abcd-4321-efgh-9876543210ab.json

ingress:
  - hostname: casa.domainkita.com
    service: http://localhost:80
  - hostname: n8n.domainkita.com
    service: http://localhost:5678
  - hostname: ssh.domainkita.com
    service: tcp://localhost:22
  - service: http_status:404

Ganti 12345678-abcd-4321-efgh-9876543210ab dengan Tunnel ID kamu sendiri. Berlaku juga nanti saat mengisi “Target” di DNS (format: TUNNEL_ID.cfargotunnel.com).

Restart service biar config terbaca:

sudo systemctl restart cloudflared
sudo systemctl status cloudflared --no-pager -l

Contoh “screenshot” output (dummy):

● cloudflared.service - cloudflared
     Loaded: loaded (/etc/systemd/system/cloudflared.service; enabled)
     Active: active (running) since Wed 2025-09-10 04:42:44 UTC
   Main PID: 28406 (cloudflared)
      Tasks: 9
Sep 10 04:42:44 cloudflared[28406]: INF Registered tunnel connection ... protocol=quic location=sin13
Sep 10 04:42:45 cloudflared[28406]: INF Registered tunnel connection ... protocol=quic location=sin09

2) Tambahkan DNS Record di Cloudflare

Masuk dashboard Cloudflare → menu DNS → tambah CNAME:

  • Name: ssh
  • Target: 12345678-abcd-4321-efgh-9876543210ab.cfargotunnel.com
    (Ganti 12345678-... dengan Tunnel ID kamu → TUNNEL_ID.cfargotunnel.com)
  • Proxy status: Proxied (ikon awan oranye)

Cek dari server:

dig +short CNAME ssh.domainkita.com
dig +short A ssh.domainkita.com

Kalau keluar IP Cloudflare (104.xx, 172.xx), berarti DNS sudah resolve OK.


3) Install cloudflared di Laptop/PC Client

Kita butuh agent cloudflared di sisi client supaya koneksi SSH bisa “diterowongkan” lewat Cloudflare.

Windows

  1. Download cloudflared.exe dari situs resmi Cloudflare.
  2. Buat folder C:\cloudflared dan taruh cloudflared.exe di sana.
  3. Tambahkan ke PATH:
    • Buka Start → ketik Environment Variables → pilih Edit the system environment variables.
    • Klik Environment Variables….
    • Di System variables → pilih Path → Edit → New.
    • Isi: C:\cloudflared → OK semua.
    • Tutup semua CMD/PowerShell yang terbuka, lalu buka lagi.
  4. Cek versi:
cloudflared --version

Linux

sudo apt-get install cloudflared
cloudflared --version

4) Jalankan Tunnel dari Client

Di laptop/PC (Windows/Linux), jalankan ini untuk bikin “listener” di port lokal 2222:

cloudflared access tcp --hostname ssh.domainkita.com --url localhost:2222

Maksudnya: koneksi ke ssh.domainkita.com akan diteruskan ke localhost:2222 di laptop.


5) Akses SSH Seperti Biasa

Buka terminal baru, lalu:

ssh root@localhost -p 2222

Contoh “screenshot” output (dummy):

The authenticity of host '[localhost]:2222' can't be established.
ED25519 key fingerprint is SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[localhost]:2222' (ED25519) to the list of known hosts.
root@localhost's password:
Welcome to Armbian 25.5 (bookworm) on aml-s9xx-box
Last login: Wed Sep 10 06:28:56 2025 from 127.0.0.1

Kalau sudah masuk shell server, artinya akses SSH dari luar jaringan berhasil 🎉.


Diagram Alur

Client (Laptop)
   |
   | SSH -> localhost:2222
   v
cloudflared (client)
   |
   | Tunnel terenkripsi via Cloudflare
   v
Cloudflare Edge
   |
   | Forward TCP -> server
   v
cloudflared (server) -> sshd (localhost:22)

Tips Keamanan SSH (Wajib kalau Publik)

1) Ubah port SSH default

Misalnya ganti ke 22222. Edit /etc/ssh/sshd_config:

sudo nano /etc/ssh/sshd_config
# ubah/aktifkan baris:
Port 22222

Restart SSH:

sudo systemctl restart ssh

Jangan lupa sesuaikan config.yml:

  - hostname: ssh.domainkita.com
    service: tcp://localhost:22222

2) Gunakan SSH Key (disable password login)

Di laptop/PC, buat key (kalau belum ada):

ssh-keygen -t ed25519 -C "ssh-remote-tunnel"
# tekan Enter terus atau set passphrase sesuai kebutuhan

Kirim public key ke server:

ssh-copy-id -p 2222 root@localhost
# kalau port server diganti 22222:
# ssh-copy-id -p 2222 -o "ProxyCommand=none" root@localhost

Disable password login di server (/etc/ssh/sshd_config):

PermitRootLogin prohibit-password
PasswordAuthentication no

Restart SSH lagi:

sudo systemctl restart ssh

Setelah ini, login hanya bisa dengan key → jauh lebih aman.

3) (Opsional) Buat alias di ~/.ssh/config

Biar lebih simple, kita bisa bikin profile:

Host stb
  HostName localhost
  Port 2222
  User root
  IdentityFile ~/.ssh/id_ed25519

Setelah itu, cukup jalankan:

ssh stb

Penutup

Awalnya saya kira rumit, tapi ternyata setelah dipandu step-by-step sama GPT, semuanya jadi lebih jelas. Sekarang saya bisa remote server STB/Armbian dari mana saja dengan domain sendiri, tanpa buka port router—lebih aman dan rapi.

www.CodeNirvana.in

Online Sejak 2013 Dwi Yanto | Blogger Templates | Designed By Code Nirvana