11.8.26

SoccerNet Free Hotspot Login

 Berikut adalah login page untuk Hotspot Mikrotik sederhana, pembuatannya dibantu AI, khususnya untuk gambar dan efek bola. Simpan sebagai login.html menggantikan login.html halaman default.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SoccerNet</title>
    <style>
        *{margin:0;padding:0;box-sizing:border-box}
        body{
            min-height:100vh;
            display:flex;flex-direction:column;align-items:center;justify-content:center;
            background:#080c0a;
            background:radial-gradient(ellipse at 50% 100%,#101e16 0%,#080c0a 55%);
            font-family:-apple-system,'Segoe UI',Helvetica,sans-serif;
            color:rgba(255,255,255,.85);overflow:hidden;user-select:none;-webkit-user-select:none;
        }
        .brand{display:flex;flex-direction:column;align-items:center;margin-bottom:1.6rem}
        #ball{
            width:120px;height:120px;cursor:grab;
            filter:drop-shadow(0 6px 18px rgba(0,0,0,.5));
            margin-bottom:1rem;
        }
        #ball:active{cursor:grabbing}
        .title{font-size:1.65rem;font-weight:700;letter-spacing:.05em;color:#fff}
        .title b{color:#2d8a4e;font-weight:700}
        .card{
            width:100%;max-width:330px;
            padding:1.8rem 2rem 1.4rem;
            background:rgba(255,255,255,.025);
            border:1px solid rgba(255,255,255,.06);
            border-radius:14px;
            backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
        }
        .err{
            display:none;
            background:rgba(220,60,50,.08);
            border:1px solid rgba(220,60,50,.22);
            color:#d9604f;font-size:.8rem;
            padding:.55rem .85rem;border-radius:8px;
            margin-bottom:1.1rem;text-align:center;line-height:1.4;
        }
        .field{margin-bottom:.95rem}
        .field label{
            display:block;font-size:.7rem;font-weight:600;
            color:rgba(255,255,255,.38);text-transform:uppercase;
            letter-spacing:.12em;margin-bottom:.35rem;
        }
        .field input{
            width:100%;padding:.65rem 0;
            background:transparent;border:none;
            border-bottom:1.5px solid rgba(255,255,255,.1);
            color:#fff;font-size:.92rem;font-family:inherit;outline:none;
            transition:border-color .25s;
        }
        .field input:focus{border-bottom-color:#2d8a4e}
        .field input::placeholder{color:rgba(255,255,255,.18)}
        .btn{
            width:100%;padding:.72rem;margin-top:.5rem;
            background:#2d8a4e;color:#fff;border:none;border-radius:9px;
            font-size:.88rem;font-weight:600;font-family:inherit;
            letter-spacing:.04em;cursor:pointer;
            transition:background .2s,transform .12s;
        }
        .btn:hover{background:#35a15b}
        .btn:active{transform:scale(.98)}
        .foot{
            margin-top:1.8rem;text-align:center;
            font-size:.65rem;color:rgba(255,255,255,.16);line-height:1.7;
        }
        /* Subtle ground line */
        .gnd{
            width:100px;height:1px;margin-top:-4px;
            background:linear-gradient(90deg,transparent,rgba(255,255,255,.04),transparent);
        }
    </style>
</head>
<body>
<div class="brand">
    <svg id="ball" viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg"></svg>
    <div class="gnd"></div>
    <h1 class="title">Soccer<b>Net</b></h1>
</div>
<form name="sendin" action="$(link-login-only)" method="post" onsubmit="return doLogin()">
    <input type="hidden" name="dst" value="$(link-orig)" />
    <input type="hidden" name="popup" value="true" />
    <div class="card">
        <div class="err" $(if error)style="display:block"$(endif)>$(error)</div>
        <div class="field">
            <label for="un">Username</label>
            <input type="text" id="un" name="username" value="$(username)" placeholder="Enter username" autofocus />
        </div>
        <div class="field">
            <label for="pw">Password</label>
            <input type="password" id="pw" name="password" placeholder="Enter password" />
        </div>
        <button type="submit" class="btn">Login</button>
    </div>
</form>
<div class="foot">
    MAC: $(mac) &middot; IP: $(ip)
</div>
<script>
/* ===== Mikrotik hotspot submit ===== */
function doLogin(){document.sendin.submit();return false}
/* ===== Truncated Icosahedron Soccer Ball ===== */
(function(){
    var PHI=(1+Math.sqrt(5))/2, CX=200,CY=200,RAD=147;
    var raw=[];
    var a,b,c,i;
    for(a=-1;a<=1;a+=2)for(b=-1;b<=1;b+=2){
        raw.push([0,a,b*3*PHI]);
        raw.push([a,b*3*PHI,0]);
        raw.push([b*3*PHI,0,a]);
    }
    for(a=-1;a<=1;a+=2)for(b=-1;b<=1;b+=2)for(c=-1;c<=1;c+=2){
        raw.push([a*2,b*(1+2*PHI),c*PHI]);
        raw.push([b*(1+2*PHI),c*PHI,a*2]);
        raw.push([c*PHI,a*2,b*(1+2*PHI)]);
    }
    for(a=-1;a<=1;a+=2)for(b=-1;b<=1;b+=2)for(c=-1;c<=1;c+=2){
        raw.push([a,b*(2+PHI),c*2*PHI]);
        raw.push([b*(2+PHI),c*2*PHI,a]);
        raw.push([c*2*PHI,a,b*(2+PHI)]);
    }
    var NS=Math.sqrt(10+9*PHI);
    var V=raw.map(function(v){return[v[0]/NS,v[1]/NS,v[2]/NS]});
    /* Edges */
    var EL2=4/(NS*NS), NB=[];
    for(i=0;i<60;i++) NB[i]=[];
    for(i=0;i<60;i++)for(var j=i+1;j<60;j++){
        var dx=V[i][0]-V[j][0],dy=V[i][1]-V[j][1],dz=V[i][2]-V[j][2];
        if(Math.abs(dx*dx+dy*dy+dz*dz-EL2)<0.001){NB[i].push(j);NB[j].push(i)}
    }
    /* Helpers */
    function cr(a,b,c){
        var e1x=V[b][0]-V[a][0],e1y=V[b][1]-V[a][1],e1z=V[b][2]-V[a][2];
        var e2x=V[c][0]-V[b][0],e2y=V[c][1]-V[b][1],e2z=V[c][2]-V[b][2];
        return[e1y*e2z-e1z*e2y,e1z*e2x-e1x*e2z,e1x*e2y-e1y*e2x];
    }
    function dt(a,b){return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]}
    function cn(a,b,c){return[(V[a][0]+V[b][0]+V[c][0])/3,(V[a][1]+V[b][1]+V[c][1])/3,(V[a][2]+V[b][2]+V[c][2])/3]}
    /* Face tracing */
    var seen={}, faces=[];
    for(i=0;i<60;i++)for(var _j=0;_j<NB[i].length;_j++){
        var w=NB[i][_j];
        if(seen[i+'-'+w])continue;
        var face=[i],p=i,cc=w;
        while(true){
            seen[p+'-'+cc]=true;
            face.push(cc);
            var cands=NB[cc].filter(function(n){return n!==p});
            var best=-1,bestD=-1e9;
            for(var k=0;k<cands.length;k++){
                var d=dt(cr(p,cc,cands[k]),cn(p,cc,cands[k]));
                if(d>bestD){bestD=d;best=cands[k]}
            }
            if(best<0)break;
            p=cc;cc=best;
            if(cc===i)break;
            if(face.length>10)break;
        }
        if(face.length===5||face.length===6)faces.push(face);
    }
    /* Initial rotation: put a pentagon facing viewer */
    var topZ=-1e9,topC=null;
    for(i=0;i<faces.length;i++){
        var f=faces[i]; if(f.length!==5)continue;
        var cz=0;for(j=0;j<5;j++)cz+=V[f[j]][2]; cz/=5;
        if(cz>topZ){
            topZ=cz;
            topC=[0,0,0];
            for(j=0;j<5;j++){topC[0]+=V[f[j]][0];topC[1]+=V[f[j]][1];topC[2]+=V[f[j]][2]}
            topC[0]/=5;topC[1]/=5;topC[2]/=5;
        }
    }
    var iRY=Math.atan2(-topC[0],topC[2]);
    var iRX=Math.atan2(topC[1],Math.sqrt(topC[0]*topC[0]+topC[2]*topC[2]));
    function rot(x,y,z,rx,ry){
        var cY=Math.cos(ry),sY=Math.sin(ry);
        var x1=x*cY+z*sY,z1=-x*sY+z*cY;
        var cX=Math.cos(rx),sX=Math.sin(rx);
        return[x1,y*cX-z1*sX,y*sX+z1*cX];
    }
    var svg=document.getElementById('ball');
    function draw(rx,ry){
        var P=V.map(function(v){var t=rot(v[0],v[1],v[2],rx,ry);
            return{sx:CX+t[0]*RAD,sy:CY-t[1]*RAD,z:t[2]}});
        var rf=faces.map(function(f){
            var pts=f.map(function(ii){return P[ii]});
            var az=0;for(var k=0;k<pts.length;k++)az+=pts[k].z;az/=pts.length;
            return{pts:pts,az:az,pent:f.length===5};
        });
        rf.sort(function(a,b){return a.az-b.az});
        var s='<defs>';
        s+='<clipPath id="bc"><circle cx="'+CX+'" cy="'+CY+'" r="'+RAD+'"/></clipPath>';
        s+='<radialGradient id="sh" cx="34%" cy="28%" r="66%">';
        s+='<stop offset="0%" stop-color="rgba(255,255,255,0.12)"/>';
        s+='<stop offset="45%" stop-color="rgba(0,0,0,0)"/>';
        s+='<stop offset="100%" stop-color="rgba(0,0,0,0.55)"/>';
        s+='</radialGradient>';
        s+='<radialGradient id="sp" cx="35%" cy="26%" r="16%">';
        s+='<stop offset="0%" stop-color="rgba(255,255,255,0.5)"/>';
        s+='<stop offset="100%" stop-color="rgba(255,255,255,0)"/>';
        s+='</radialGradient></defs>';
        s+='<circle cx="'+CX+'" cy="'+CY+'" r="'+RAD+'" fill="#edecea"/>';
        s+='<g clip-path="url(#bc)">';
        for(i=0;i<rf.length;i++){
            var ff=rf[i],d='';
            for(var k=0;k<ff.pts.length;k++){
                d+=(k?'L':'M')+ff.pts[k].sx.toFixed(1)+','+ff.pts[k].sy.toFixed(1);
            }
            d+='Z';
            if(ff.pent){
                s+='<path d="'+d+'" fill="#1a1a1c" stroke="#0e0e10" stroke-width="0.8" stroke-linejoin="round"/>';
            }else{
                s+='<path d="'+d+'" fill="#edecea" stroke="#c8c5b9" stroke-width="0.5" stroke-linejoin="round"/>';
            }
        }
        s+='</g>';
        s+='<circle cx="'+CX+'" cy="'+CY+'" r="'+RAD+'" fill="url(#sh)"/>';
        s+='<circle cx="'+CX+'" cy="'+CY+'" r="'+RAD+'" fill="url(#sp)"/>';
        svg.innerHTML=s;
    }
    var rx=iRX,ry=iRY,drag=false,lx=0,ly=0,lastT=0,idleT=0,lastPh=-1;
    function pD(x,y){drag=true;lx=x;ly=y;lastT=performance.now()}
    function pM(x,y){if(!drag)return;ry+=(x-lx)*.007;rx+=(y-ly)*.007;
        rx=Math.max(-1.3,Math.min(1.3,rx));lx=x;ly=y;lastT=performance.now();draw(rx,ry)}
    function pU(){drag=false}
    svg.addEventListener('mousedown',function(e){e.preventDefault();pD(e.clientX,e.clientY)});
    window.addEventListener('mousemove',function(e){pM(e.clientX,e.clientY)});
    window.addEventListener('mouseup',pU);
    svg.addEventListener('touchstart',function(e){e.preventDefault();var t=e.touches[0];pD(t.clientX,t.clientY)},{passive:false});
    window.addEventListener('touchmove',function(e){if(!drag)return;var t=e.touches[0];pM(t.clientX,t.clientY)},{passive:true});
    window.addEventListener('touchend',pU);
    function loop(t){
        requestAnimationFrame(loop);
        if(drag)return;
        if(t-lastT<2500)return;
        idleT+=0.003;
        var ph=Math.round(idleT*60);
        if(ph===lastPh)return;lastPh=ph;
        draw(rx+Math.sin(idleT*.5)*.04, ry+idleT*.09);
    }
    draw(rx,ry);
    requestAnimationFrame(loop);
})();
</script>
</body>
</html>

Berikut adalah preview-nya:

 Preview

9.12.23

Script Mikrotik Menambahkan Monitoring Telegram Ke Netwatch Dari Queue

Netwatch adalah salah satu fitur Mikrotik yang biasa digunakan untuk monitoring. Netwatch umumnya menggunakan protokol ICMP untuk memeriksa alamat IP yang hendak dimonitor, namun pada RouterOS versi terbaru sudah ditambahkan beberapa metode selain ICMP. Terlepas dari metode yang digunakan, fungsi monitoring ini (bisa diakses dari Tools-Netwatch) akan memeriksa alamat IP pada host netwatch secara berkala sesuai interval yang diset, kemudian apabila terjadi perubahan status pada target, akan menjalankan script pada tab UP (saat target terhubung) atau DOWN (saat target terputus).

Telegram adalah salah satu aplikasi untuk komunikasi yang mendukung adanya BOT, dan dengan memanfaatkan fitur /tool fetch di mikrotik, bot ini bisa digunakan untuk mengirim pesan dari script Mikrotik, termasuk script pada Netwatch untuk mengirim perubahan status.

Di internet sudah banyak tersedia tutorial untuk mengirim perubahan status netwatch melalui telegram ini, misalnya di sini, Namun pada kenyataannya proses menyalin dan mengubah isi dari pesan telegram pada netwatch adalah proses yang sangat melelahkan, apalagi kalau host yang hendak dimonitor ada banyak. Artikel kali ini akan membahas tentang penyederhanaan proses menambah host netwatch secara otomatis menggunakan nama yang tercantum di queue simple menggunakan script sehingga tidak perlu repot mengubah isi pesan pada script Up dan Down pada netwatch sekaligus mengurangi kemungkinan kesalahan pada script.

Langkah pertama adalah membuat Bot Telegram. Untuk ini anda bisa mengikuti tutorial di sini, yang mana intinya antara lain:

  • Mengirim pesan /start ke @botfather di Telegram, kemudian perintah /newbot untuk membuat bot baru. Di sini kita disuruh memberi nama bot dan username bot yang mana harus berakhiran _bot.
  • Setelah berhasil membuat bot, kita akan diberikan token bot. Simpan token bot ini untuk kita gunakan pada script kita dan juga untuk mengambil info chat ID.
  • Buat sebuah grup di Telegram, undang Bot yang tadi dibuat ke grup tersebut, dan jadikan Bot sebagai Admin. Kirim sembarang pesan untuk mengambil chat ID
  • Buka API telegram di browser dengan format https://api.telegram.org/bot(TOKEN_BOT)/getUpdates yang akan membuka update terakhir dari bot kita. Dari pesan terakhir kita ambil Chat ID dari grup yang kita buat.

Setelah itu kita beralih ke Mikrotik. Masuk ke System->Script, klik tombol add (+), kasih nama misalnya add-netwatch, kemudian pada kotak source isikan script berikut ini:

:local noque <masukkan urutan yang akan ditambahkan di simple queue>
:local botToken "<masukkan Token Bot>"
:local chatID "<masukkan chat id>"
:local interval 20s 
:local nama [/queue simple get $noque name]
:local ipnya [/queue simple get $noque target]
:local ipne [put [:pick [:tostr $ipnya] 0 [:find [:tostr $ipnya] "/"]]]
:local namaencoded
:for i from=0 to=([:len $nama] - 1) do={ 
  :local char [:pick $nama $i]
  :if ($char = " ") do={
    :set $char "%20"
  }
  :if ($char = "-") do={
    :set $char "%2D"
  }
  :set namaencoded ($namaencoded . $char)
}
/tool netwatch add comment=$nama down-script="/tool fetch url=\"https://api.telegram.org/bot$botToken/sendMessage\\\?chat_id=\
    $chatID&text=$namaencoded%20DOWN%20%F0%9F%99%81\" keep-result=no" host=\
    $ipne interval=$interval up-script="/tool fetch url=\"https://api.telegram.org/bot$botToken/sendMessage\\\?chat_id=\
    $chatID&text=$namaencoded%20UP\" keep-result=no"

Setelah itu masukkan Token dari bot ke variabel di atas, menggantikan <masukkan Token Bot>, dan masukkan juga Chat ID menggantikan <masukkan chat ID> di script tersebut. Sesuaikan interval pengecekan sesuai yang dikehendaki, dan script kita siap digunakan.

Untuk menggunakan script kita, ubah nomor pada variabel noque dalam script menjadi nomor urut pada simple queue, klik apply, kemudian klik Run Script. Cek di tools->netwatch dan pastikan parameter sudah benar, cek juga di grup telegram kita, pastikan script berjalan dengan baik. Happy Scripting.

25.11.08

Posting Pertama

Blog ini dibuat sebagai blog pribadi untuk merekam kegiatan Yosan Pro khususnya yang berhubungan dengan jaringan dan MikroTik.