Panel yang Tidak Mengambil Alih Server
Ditulis oleh Gustama Dimas
Server Valheim itu sudah berjalan berbulan-bulan sebelum saya sentuh. Ada dunia, beberapa pemain, skrip backup, dan kode join yang harus dicari manual tiap kali ada orang baru mau masuk.
Semua panel web yang saya temukan menyelesaikan masalah itu dengan mengambil alih servernya. Pasang ini, dan ia memiliki duniamu, compose-mu, mod-mu, update-mu. Itu desain yang wajar. Bukan desain yang saya mau. Saya tidak butuh server baru. Saya butuh jendela ke server yang sudah ada.
Jadi batasannya ditentukan dulu, sebelum satu baris kode: menempel, bukan memiliki.

Tiga kanal yang sebenarnya sudah ada
Server itu sudah membuka semua yang dibutuhkan panel. Cuma tidak diberi label begitu.
- Supervisor XML-RPC di
:8088— start, stop, status proses, baca log. Sudah terautentikasi, sudah jalan. - RCON di
:25575— perintah admin dan daftar pemain, lewat TCP di luar jalur utama, tetap jalan dengan crossplay. - Agent ValheimUI di
:2456— JSON terstruktur: pemain beserta posisinya, status dunia, kematian, layer peta.
Tanpa Docker socket. Tanpa container privileged. Tanpa menyisipkan string ke shell. Panel jalan sebagai user biasa, membuang semua capability, dan hanya bisa melakukan apa yang diizinkan tiga kanal itu — dan justru itu intinya. Panel kontrol yang bisa menjalankan perintah apa pun sebagai root bukan panel kontrol; itu shell jarak jauh dengan font yang lebih enak dilihat.
Dua kata kerja yang bohong
Supervisor tidak punya restartProcess. Di form HTML-nya ada tombol restart, di supervisorctl ada verb restart, jadi wajar kalau saya mengira API XML-RPC-nya juga punya. Ternyata tidak. supervisor.restart me-restart supervisord-nya sendiri, dan memanggil restartProcess menjawab UNKNOWN_METHOD.
Perbaikannya membosankan: stop, tunggu STOPPED, start. Tapi justru lebih baik, karena jeda itulah yang memberi waktu game menjalankan handler SIGTERM dan menyimpan dunia. Restart yang selesai sebelum save kelar adalah restart yang kehilangan progres.
Jebakan yang sama muncul lagi. /v1/commands/time milik agent terlihat seperti pembaca nilai. Ia penulis nilai — dipanggil tanpa argumen, jawabannya "fraction, skip=morning or seconds is required". Pembacaannya ada di RCON. Entri katalognya saya pindah.
Dua bug itu bentuknya sama: nama perintahnya menyarankan perilaku yang belum saya buktikan. Menyambungkan tombol ke endpoint tidak sama dengan tahu apa yang endpoint itu lakukan.

Mod yang terpasang sempurna dan tidak pernah dimuat
Yang ini paling saya sukai, karena panel tidak salah melaporkan apa pun.
Panel menulis plugin ke /config/bepinex/plugins. Game memuatnya dari /opt/valheim/bepinex/BepInEx/plugins. Dua direktori berbeda, dan penyalinan di antaranya terjadi di dalam valheim-bootstrap — program supervisor sekali jalan yang berjalan saat container start lalu duduk di status EXITED sepanjang umur container.
Jadi stop/start biasa menghidupkan lagi server dengan set plugin lama. Panel bilang "terpasang". Pemasangannya memang berhasil. Log game bilang tiga plugin dimuat, bukan lima.
Untuk pengelola mod, diam-diam mengabaikan mod baru adalah kegagalan terburuk. Lebih buruk daripada error, karena error ada yang memperbaiki. Solusinya: jalankan bootstrap di antara stop dan start, di setiap restart, dan buat start juga menyinkronkan. Bootstrap idempoten, jadi menjalankannya saat tidak ada perubahan cuma makan satu detik. Setelah itu log bilang lima dimuat, nol gagal.
Ada pelajaran kecil di dalamnya: pesan sukses panel itu menggambarkan kerja panel, bukan keadaan server. Dua klaim yang berbeda, dan hanya satu yang layak ditampilkan ke pengguna.

Petanya bukan gambar
Saya kira /v1/map/layers mengembalikan gambar. Yang dikembalikan PNG RGBA 2048×2048 berisi field mentah: merah membawa mask terrain dan kode biome, hijau dan biru membawa ketinggian, alpha membawa kerapatan hutan. POST /v1/map/render tidak merender — ia mulai mengambil sampel. Styling sebenarnya ada di kode Go milik agent, yang tidak akan saya porting.
Jadi panel menata petanya sendiri, di browser, di atas canvas. Ternyata arsitektur itu justru lebih baik: 4,2 juta piksel ditata di Python butuh beberapa detik per frame, sedangkan canvas selesai dalam milidetik lalu bisa digeser tanpa bertanya ke server sama sekali. Pekerjaan server menyusut jadi cache, autentikasi, dan penggabungan objek.
Saya memverifikasinya dengan merender pratinjau 512 piksel lalu melihatnya. Deep North putih di atas, Ashlands merah-cokelat di bawah, laut biru, padang rumput hijau, gunung kelabu bersalju. Utara di atas sudah benar, biome ada di tempatnya, garis pantai bersih. Review kode tidak akan menangkap sumbu yang terbalik; gambar menangkapnya.

Fog of war sekarang 100% belum dijelajahi — agent baru mulai mencatat posisi pemain hari ini. Kalau mask itu dipatuhi, yang muncul kotak hitam. Jadi fog mematikan dirinya selama datanya kosong, menjelaskan alasannya di UI, dan akan menyalakan diri lagi begitu ada yang perlu disembunyikan.
Kesalahan keamanan yang saya buat sendiri
Jembatan Discord membuat bot bisa membaca dan mengendalikan server. Versi pertama saya menyuruh bot mengirim ID role pemanggil ke skrip relay, dan skrip itu yang memutuskan boleh atau tidak.
Itu batas kepercayaan di tempat yang salah. Bot-nya menjalankan model kecil gratis. Prompt injection bisa membuatnya mengaku sebagai Admin, dan skripnya akan percaya.
Sekarang relay bertanya langsung ke Discord: untuk guild ini dan user ini, role apa yang benar-benar dimiliki anggota itu? Klaimnya diverifikasi ke guild, bukan diterima.
Dan karena satu akun relay akan membuat pengecekan skrip jadi satu-satunya batas, sekarang ada tiga — relay-user, relay-mod, relay-admin — dan skrip login sesuai role yang diberikan. Panel menegakkan batas yang sama secara mandiri. Saya mengujinya dengan berpura-pura skripnya bug: sesi relay-user dapat 403 saat restart, saat mengubah daftar, saat memasang mod, dan 200 untuk bacaan.

Dua kunci, dan satu tidak mempercayai yang lain. Itu bentuk yang saya mau untuk apa pun yang bisa me-restart server.
Satu aturan CSS yang layak diingat
Geser dan zoom tidak jalan. Peta tergambar, tombolnya bereaksi, tidak ada yang bergerak.
Overlay pesan membawa atribut hidden. Class-nya juga menyetel display: grid. CSS penulis menang atas [hidden] { display: none } bawaan browser, jadi overlay tetap menempati ruang — posisi absolut, inset nol, latar semi-transparan — dan menelan setiap event mouse sebelum sampai ke canvas.
Geser dan zoom-nya tidak pernah rusak. Ia tidak pernah bisa dijangkau.
Perbaikannya satu baris: [hidden] { display: none !important }. Pola ini muncul di seluruh panel, jadi sekarang global, bukan lokal. Dua bug peta lain ketemu di putaran yang sama: layer fog dibangun ulang piksel demi piksel di setiap gerakan mouse, dan canvas mengambil ukuran dari tab yang tersembunyi lalu keluar 1×1.
Yang masih jujur-jujur kosong
- Papan kematian masih kosong. Belum ada yang mati sejak pelacak menyala, dan riwayat sebelum hari ini tidak bisa dipulihkan.
- Fog tetap mati sampai data penjelajahan ada.
- Tiga plugin dipasang manual sebelum panel ini ada, jadi panel mendaftarnya sebagai unmanaged, bukan mengklaimnya.
- Ganti dunia tidak disediakan. Dunia aktif datang dari environment variable, jadi menggantinya butuh edit compose yang privileged. UI-nya bilang begitu, bukan pura-pura bisa.
Panelnya sekitar 9.000 baris — 5.600 Python, 2.000 JavaScript, sisanya markup dan CSS. Tanpa Docker socket, dan berjalan di samping server yang bukan ia bangun. Dirilis MIT.
Alasan panel ini ada adalah batasan yang saya pilih di awal. Memiliki servernya akan lebih mudah ditulis dan lebih sulit dipercaya.