"""Bersihkan konten contoh dan isi blog dengan tulisan seputar AI automation & Python.

    python manage.py setup_content

Profil, foto, dan social links TIDAK disentuh — itu data asli pemilik situs.
Yang dihapus: proyek, pengalaman kerja, pendidikan, sertifikat, komentar,
pesan masuk, dan seluruh postingan lama.
"""

from django.core.management.base import BaseCommand
from django.utils import timezone

from blog.models import Category, Comment, Post, Tag
from careers.models import Certificate, Education, Experience, Project, Skill
from core.models import ContactMessage

# --------------------------------------------------------------------- skill
SKILLS = [
    # (nama, kategori, level, unggulan)
    ("Python", "language", 93, True),
    ("JavaScript", "language", 80, True),
    ("SQL", "language", 85, True),
    ("HTML & CSS", "language", 88, False),
    ("Django", "framework", 94, True),
    ("Django REST Framework", "framework", 88, True),
    ("FastAPI", "framework", 86, True),
    ("Celery", "framework", 78, False),
    ("Tailwind CSS", "framework", 85, True),
    ("PostgreSQL", "database", 84, True),
    ("MySQL", "database", 86, True),
    ("Redis", "database", 76, False),
    ("pgvector", "database", 72, False),
    ("n8n", "ai", 87, True),
    ("Claude API", "ai", 85, True),
    ("LangChain", "ai", 74, False),
    ("RAG / Vector Search", "ai", 79, True),
    ("Prompt Engineering", "ai", 82, True),
    ("Docker", "tool", 78, True),
    ("Git & GitHub", "tool", 88, False),
    ("Linux Server", "tool", 76, False),
]

CATEGORIES = [
    ("AI & Automation", "#f97316", "Otomasi, LLM, dan agent yang benar-benar dipakai"),
    ("Python & Django", "#facc15", "Catatan teknis seputar backend Python"),
    ("Tutorial", "#fb923c", "Panduan langkah demi langkah"),
    ("Catatan", "#eab308", "Pemikiran dan pengalaman pribadi"),
]

TAGS = [
    "python", "django", "fastapi", "n8n", "ai", "llm", "automation",
    "api", "async", "rag", "deployment", "database", "celery", "docker",
]


# --------------------------------------------------------------------- artikel
ARTICLES = [
    {
        "title": "Django di 2026: Kenapa Saya Masih Memilihnya",
        "category": "Catatan",
        "tags": ["django", "python"],
        "pinned": True,
        "excerpt": "Framework baru bermunculan tiap tahun. Tapi ketika harus mengirim produk yang benar-benar dipakai orang, saya selalu kembali ke sini.",
        "content": """
<p>Setiap tahun ada framework baru yang menjanjikan kecepatan dan kemudahan. Saya sudah mencoba banyak di antaranya. Tapi ketika harus mengirim produk yang benar-benar dipakai orang — bukan demo, bukan proyek akhir pekan — saya selalu kembali ke Django.</p>

<h2>Baterainya sudah termasuk</h2>
<p>Autentikasi, ORM, migrasi, admin panel, proteksi CSRF, manajemen sesi. Semuanya sudah ada sejak menit pertama. Waktu yang biasanya habis untuk merakit potongan-potongan itu bisa langsung dipakai membangun fitur yang sebenarnya.</p>
<p>Ini terdengar sepele sampai kamu pernah menghabiskan tiga hari hanya untuk menyambungkan sistem login yang aman di framework minimalis.</p>

<h2>ORM yang bisa ditebak</h2>
<p>ORM Django bukan yang paling canggih. Tapi setelah bertahun-tahun memakainya, saya hampir selalu bisa menebak query SQL apa yang akan dihasilkan sebuah baris kode. Itu jauh lebih berharga daripada kepintaran yang tidak terduga.</p>
<blockquote>Teknologi yang membosankan adalah teknologi yang bisa kamu andalkan jam tiga pagi.</blockquote>

<h2>Async sudah matang</h2>
<p>Keberatan lama soal Django — "tidak bisa async" — sudah tidak berlaku. View async, ORM async, dan dukungan ASGI penuh sudah tersedia sejak beberapa versi lalu. Untuk aplikasi yang banyak menunggu I/O, seperti memanggil API LLM, ini penting.</p>
<pre><code>async def ringkas(request):
    async for artikel in Artikel.objects.filter(status="draft"):
        await proses(artikel)
    return JsonResponse({"ok": True})</code></pre>

<h2>Ekosistemnya sudah dewasa</h2>
<p>Hampir setiap masalah umum sudah ada paketnya, dan paket itu biasanya sudah teruji bertahun-tahun di lingkungan produksi orang lain. Ketika ada masalah pukul dua pagi, kemungkinan besar sudah ada orang yang mengalaminya dan menulis solusinya.</p>

<h2>Bukan berarti cocok untuk semua</h2>
<p>Kalau yang kamu bangun murni API tanpa halaman HTML, tanpa admin, tanpa form — FastAPI kemungkinan lebih pas. Kalau butuh WebSocket sebagai inti produk, ada pilihan yang lebih tepat.</p>
<p>Tapi untuk aplikasi web yang punya database, pengguna, dan aturan bisnis, Django masih sulit dikalahkan.</p>
""",
    },
    {
        "title": "FastAPI atau Django? Cara Saya Memutuskan",
        "category": "Python & Django",
        "tags": ["fastapi", "django", "python", "api"],
        "excerpt": "Bukan soal mana yang lebih baik. Ini soal bentuk masalah yang sedang kamu selesaikan.",
        "content": """
<p>Pertanyaan ini muncul hampir di setiap proyek baru, dan jawabannya jarang sesederhana "pakai yang lebih cepat".</p>

<h2>Pertanyaan yang sebenarnya</h2>
<p>Bukan "mana yang lebih cepat" — keduanya cukup cepat untuk 99% kasus nyata. Pertanyaan yang benar adalah: <strong>berapa banyak yang harus saya bangun sendiri?</strong></p>

<h2>Pilih Django kalau kamu butuh</h2>
<ul>
  <li>Halaman HTML yang dirender server, bukan hanya JSON</li>
  <li>Panel admin untuk mengelola data</li>
  <li>Sistem pengguna dengan login, permission, dan grup</li>
  <li>Form dengan validasi yang rumit</li>
  <li>Migrasi database yang terkelola rapi</li>
</ul>
<p>Kalau tiga dari lima poin di atas kamu butuhkan, Django menghemat berminggu-minggu kerja.</p>

<h2>Pilih FastAPI kalau</h2>
<ul>
  <li>Yang kamu bangun murni API — frontend-nya terpisah</li>
  <li>Dokumentasi OpenAPI otomatis itu penting bagi tim</li>
  <li>Beban kerjanya banyak menunggu I/O: memanggil API lain, streaming respons LLM</li>
  <li>Kamu ingin validasi berbasis type hint lewat Pydantic</li>
</ul>

<pre><code>from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Permintaan(BaseModel):
    teks: str
    bahasa: str = "id"

@app.post("/ringkas")
async def ringkas(req: Permintaan):
    hasil = await panggil_llm(req.teks, req.bahasa)
    return {"ringkasan": hasil}</code></pre>

<p>Validasi, dokumentasi, dan type safety — semuanya dari satu deklarasi class. Untuk API, ini memang menyenangkan.</p>

<h2>Yang jarang dibahas: pakai keduanya</h2>
<p>Di beberapa proyek saya menjalankan Django untuk aplikasi utama — pengguna, data, admin — lalu FastAPI terpisah untuk beban kerja AI yang butuh streaming dan konkurensi tinggi. Keduanya berbagi database yang sama.</p>
<p>Ini bukan kompromi. Ini memakai alat sesuai bentuk pekerjaannya.</p>

<h2>Jebakan yang sering saya lihat</h2>
<p>Orang memilih FastAPI karena "lebih modern", lalu tiga bulan kemudian menghabiskan waktu membangun sistem admin, autentikasi, dan migrasi — persis yang sudah gratis di Django.</p>
<p>Kecepatan framework hampir tidak pernah jadi hambatan. Yang jadi hambatan biasanya query database yang buruk dan panggilan API eksternal yang lambat.</p>
""",
    },
    {
        "title": "Otomasi Tanpa Coding dengan n8n: Panduan Mulai",
        "category": "AI & Automation",
        "tags": ["n8n", "automation", "ai"],
        "excerpt": "Tidak semua otomasi layak ditulis dengan kode. Kadang seret-lepas sudah cukup — dan jauh lebih mudah dirawat.",
        "content": """
<p>Sebagai developer, refleks pertama saya selalu menulis kode. Tapi setelah beberapa kali membangun skrip integrasi yang akhirnya tidak ada yang berani menyentuh, saya belajar: <strong>tidak semua otomasi layak jadi kode.</strong></p>

<h2>Apa itu n8n</h2>
<p>n8n adalah alat otomasi berbasis alur visual. Kamu menyusun node — pemicu, aksi, kondisi — lalu menyambungkannya. Mirip Zapier, dengan tiga perbedaan penting: bisa di-<em>self-host</em>, bisa menjalankan kode JavaScript atau Python di tengah alur, dan tidak menagih per eksekusi.</p>

<h2>Kapan n8n lebih tepat daripada kode</h2>
<ul>
  <li>Menyambungkan beberapa layanan yang sudah punya API (Sheets, Slack, WhatsApp, database)</li>
  <li>Alurnya sering berubah mengikuti kebutuhan bisnis</li>
  <li>Orang non-teknis perlu melihat atau mengubah alurnya</li>
  <li>Kamu butuh retry, logging, dan riwayat eksekusi tanpa membangunnya sendiri</li>
</ul>

<h2>Kapan tetap tulis kode</h2>
<ul>
  <li>Logikanya rumit dan bercabang banyak</li>
  <li>Butuh dites otomatis</li>
  <li>Performanya kritis</li>
  <li>Sudah jadi bagian inti dari produk</li>
</ul>

<h2>Self-host dalam satu perintah</h2>
<pre><code>docker run -d --name n8n \\
  -p 5678:5678 \\
  -v n8n_data:/home/node/.n8n \\
  -e GENERIC_TIMEZONE="Asia/Jakarta" \\
  n8nio/n8n</code></pre>
<p>Buka <code>http://localhost:5678</code> dan alurmu sudah bisa dibuat.</p>

<h2>Alur pertama yang berguna</h2>
<p>Contoh nyata yang saya pakai: setiap ada pesan masuk di form kontak website, ringkas isinya dengan LLM, tentukan tingkat prioritasnya, lalu kirim ke Slack — hanya kalau prioritasnya tinggi.</p>
<pre><code>Webhook (pesan masuk)
   ↓
HTTP Request (kirim ke LLM untuk diringkas + dinilai)
   ↓
IF (prioritas == "tinggi")
   ↓            ↘
Slack        Simpan ke Sheets</code></pre>
<p>Membangun ini dengan kode butuh sekitar 150 baris plus penanganan error. Di n8n: sepuluh menit, dan siapa pun di tim bisa mengubah ambang prioritasnya sendiri.</p>

<h2>Menyambungkannya ke Django</h2>
<p>Pola yang paling sering saya pakai: Django menangani data dan aturan bisnis, lalu mengirim webhook ke n8n untuk urusan integrasi.</p>
<pre><code>import httpx

async def kirim_ke_n8n(pesan):
    async with httpx.AsyncClient() as client:
        await client.post(
            settings.N8N_WEBHOOK_URL,
            json={"nama": pesan.name, "isi": pesan.message},
            timeout=10,
        )</code></pre>
<p>Django tetap bersih. Integrasi yang berubah-ubah tinggal diatur di n8n tanpa deploy ulang.</p>
""",
    },
    {
        "title": "Menyambungkan LLM ke Aplikasi Django",
        "category": "Tutorial",
        "tags": ["django", "llm", "ai", "python"],
        "excerpt": "Pola praktis untuk memanggil model bahasa dari Django tanpa membuat aplikasimu jadi lambat.",
        "content": """
<p>Menambahkan LLM ke aplikasi Django itu mudah. Menambahkannya <em>tanpa merusak pengalaman pengguna</em> — itu bagian yang perlu dipikirkan.</p>

<h2>Masalah utamanya: waktu tunggu</h2>
<p>Panggilan ke LLM bisa memakan 2 sampai 30 detik. Kalau kamu melakukannya di dalam view biasa, pengguna menatap layar kosong dan koneksi HTTP-mu tertahan selama itu.</p>
<p>Ada tiga pola untuk mengatasinya.</p>

<h2>Pola 1: Async view untuk respons cepat</h2>
<p>Untuk tugas ringan seperti klasifikasi atau ekstraksi, async view sudah cukup.</p>
<pre><code>from anthropic import AsyncAnthropic
from django.http import JsonResponse

client = AsyncAnthropic()

async def klasifikasi(request):
    pesan = request.POST.get("pesan", "")
    respons = await client.messages.create(
        model="claude-opus-5",
        max_tokens=1024,
        messages=[{
            "role": "user",
            "content": f"Klasifikasikan pesan ini sebagai keluhan/pertanyaan/pujian. "
                       f"Jawab satu kata saja.\\n\\n{pesan}",
        }],
    )
    teks = next(b.text for b in respons.content if b.type == "text")
    return JsonResponse({"kategori": teks.strip()})</code></pre>

<h2>Pola 2: Streaming untuk jawaban panjang</h2>
<p>Kalau hasilnya panjang, jangan buat pengguna menunggu sampai selesai. Alirkan per potongan.</p>
<pre><code>from django.http import StreamingHttpResponse

async def tulis_draf(request):
    async def aliran():
        async with client.messages.stream(
            model="claude-opus-5",
            max_tokens=8000,
            messages=[{"role": "user", "content": request.GET["topik"]}],
        ) as stream:
            async for teks in stream.text_stream:
                yield teks

    return StreamingHttpResponse(aliran(), content_type="text/plain")</code></pre>
<p>Teks mulai muncul dalam hitungan detik, bukan puluhan detik. Perbedaan yang dirasakan pengguna sangat besar.</p>

<h2>Pola 3: Antrean untuk tugas berat</h2>
<p>Untuk pekerjaan yang butuh menit — menganalisis dokumen, memproses banyak baris — jangan tahan permintaan HTTP sama sekali. Lempar ke antrean.</p>
<pre><code>@shared_task
def analisis_dokumen(dokumen_id):
    dok = Dokumen.objects.get(pk=dokumen_id)
    respons = client.messages.create(
        model="claude-opus-5",
        max_tokens=16000,
        thinking={"type": "adaptive"},
        output_config={"effort": "high"},
        messages=[{"role": "user", "content": dok.isi}],
    )
    dok.ringkasan = next(b.text for b in respons.content if b.type == "text")
    dok.save(update_fields=["ringkasan"])</code></pre>
<p>View-nya cukup memanggil <code>analisis_dokumen.delay(dok.pk)</code> dan langsung membalas. Pengguna dapat notifikasi ketika selesai.</p>

<h2>Tiga hal yang mudah terlupa</h2>
<p><strong>Simpan hasilnya.</strong> Panggilan LLM tidak gratis dan tidak cepat. Kalau pertanyaan yang sama bisa muncul lagi, simpan jawabannya di database atau Redis.</p>
<p><strong>Batasi lajunya.</strong> Tanpa rate limit, satu pengguna iseng bisa menghabiskan kuota API-mu dalam hitungan menit.</p>
<p><strong>Siapkan jalur gagal.</strong> API eksternal bisa lambat, penuh, atau menolak permintaan. Tentukan apa yang terjadi pada pengguna ketika itu terjadi — jangan biarkan halamannya sekadar error.</p>
<pre><code>try:
    hasil = panggil_llm(teks)
except Exception:
    logger.exception("LLM gagal")
    hasil = None  # tampilkan versi tanpa AI, jangan matikan fiturnya</code></pre>
""",
    },
    {
        "title": "RAG: Membuat AI Menjawab dari Dokumenmu Sendiri",
        "category": "AI & Automation",
        "tags": ["rag", "ai", "llm", "database"],
        "excerpt": "Model bahasa tidak tahu isi dokumen perusahaanmu. RAG adalah cara memberitahunya — tanpa melatih ulang apa pun.",
        "content": """
<p>Pertanyaan yang paling sering saya dengar dari klien: <em>"Bisa nggak AI-nya menjawab berdasarkan dokumen kami sendiri?"</em></p>
<p>Bisa. Namanya RAG — <em>Retrieval-Augmented Generation</em>. Dan ini jauh lebih sederhana daripada kedengarannya.</p>

<h2>Idenya dalam satu kalimat</h2>
<p>Sebelum bertanya ke model, cari dulu potongan dokumen yang paling relevan, lalu selipkan potongan itu ke dalam pertanyaan.</p>
<p>Itu saja. Tidak ada pelatihan ulang, tidak ada fine-tuning, tidak ada GPU.</p>

<h2>Alurnya</h2>
<pre><code>Dokumen → dipotong → jadi embedding → disimpan di database
                                              ↓
Pertanyaan → jadi embedding → cari yang mirip →┘
                                              ↓
                          Potongan relevan + pertanyaan → LLM → Jawaban</code></pre>

<h2>Kenapa harus dipotong</h2>
<p>Menyuapkan seluruh dokumen 200 halaman ke setiap pertanyaan itu mahal dan justru menurunkan kualitas jawaban — informasi pentingnya tenggelam di antara yang tidak relevan.</p>
<p>Potong per bagian yang bermakna. Untuk dokumen berstruktur, potong per heading. Untuk teks mengalir, sekitar 500–1000 kata dengan sedikit tumpang tindih supaya konteks di batas potongan tidak hilang.</p>

<h2>Embedding: mengubah makna jadi angka</h2>
<p>Embedding mengubah teks jadi deretan angka, di mana teks yang maknanya mirip menghasilkan angka yang berdekatan. Ini yang membuat pencarian bekerja berdasarkan makna, bukan kecocokan kata.</p>
<p>Pertanyaan "bagaimana cara refund" akan menemukan bagian berjudul "Pengembalian Dana" — meski tidak ada satu kata pun yang sama.</p>

<h2>Menyimpannya: tidak perlu database baru</h2>
<p>Banyak orang langsung memasang Pinecone atau Weaviate. Untuk kebanyakan kasus, itu berlebihan. PostgreSQL dengan ekstensi <code>pgvector</code> sudah lebih dari cukup.</p>
<pre><code>CREATE EXTENSION vector;

CREATE TABLE potongan (
    id SERIAL PRIMARY KEY,
    dokumen_id INT,
    isi TEXT,
    embedding vector(1536)
);

CREATE INDEX ON potongan
USING hnsw (embedding vector_cosine_ops);</code></pre>

<p>Mencari yang paling mirip:</p>
<pre><code>SELECT isi
FROM potongan
ORDER BY embedding &lt;=&gt; %s
LIMIT 5;</code></pre>

<h2>Merangkainya</h2>
<pre><code>def jawab(pertanyaan):
    vektor = buat_embedding(pertanyaan)
    potongan = cari_terdekat(vektor, limit=5)
    konteks = "\\n\\n---\\n\\n".join(p.isi for p in potongan)

    respons = client.messages.create(
        model="claude-opus-5",
        max_tokens=4000,
        messages=[{
            "role": "user",
            "content": (
                "Jawab pertanyaan berikut HANYA berdasarkan konteks di bawah. "
                "Kalau jawabannya tidak ada di konteks, katakan tidak tahu.\\n\\n"
                f"KONTEKS:\\n{konteks}\\n\\nPERTANYAAN: {pertanyaan}"
            ),
        }],
    )
    return next(b.text for b in respons.content if b.type == "text")</code></pre>

<h2>Yang menentukan berhasil atau tidak</h2>
<p>Kualitas RAG hampir seluruhnya ditentukan oleh <strong>tahap pencarian</strong>, bukan modelnya. Kalau potongan yang diambil salah, model sepintar apa pun akan menjawab salah.</p>
<p>Jadi ketika hasilnya mengecewakan, jangan langsung ganti model. Cek dulu: potongan apa yang sebenarnya diambil? Sembilan dari sepuluh kali, masalahnya ada di sana.</p>
<p>Satu instruksi kecil yang sangat penting: <em>"kalau jawabannya tidak ada di konteks, katakan tidak tahu"</em>. Tanpa itu, model akan mengarang dengan percaya diri.</p>
""",
    },
    {
        "title": "Async Python: Kapan Benar-benar Berguna",
        "category": "Python & Django",
        "tags": ["python", "async", "django"],
        "excerpt": "Async bukan tombol ajaib pembuat cepat. Ini alat untuk satu masalah spesifik — dan sering dipakai di tempat yang salah.",
        "content": """
<p>Ada kesalahpahaman yang sangat umum: menambahkan <code>async</code> membuat kode jadi lebih cepat. Tidak begitu cara kerjanya.</p>

<h2>Yang sebenarnya dilakukan async</h2>
<p>Async tidak membuat satu tugas selesai lebih cepat. Async membuat programmu bisa <strong>mengerjakan hal lain sambil menunggu</strong>.</p>
<p>Kalau programmu menghabiskan waktu <em>menghitung</em>, async tidak menolong sama sekali — malah menambah beban. Kalau programmu menghabiskan waktu <em>menunggu</em>, async bisa mengubah segalanya.</p>

<h2>Bedanya menunggu dan menghitung</h2>
<p><strong>Menunggu (async menolong):</strong> panggilan API, query database, baca-tulis file, permintaan HTTP.</p>
<p><strong>Menghitung (async tidak menolong):</strong> mengolah gambar, kalkulasi berat, parsing data besar, kompresi.</p>

<h2>Contoh yang membuatnya jelas</h2>
<p>Memanggil lima API secara berurutan:</p>
<pre><code>import httpx, time

def ambil_semua(urls):
    hasil = []
    with httpx.Client() as client:
        for url in urls:
            hasil.append(client.get(url).json())
    return hasil

# 5 API × 2 detik = 10 detik</code></pre>

<p>Versi async:</p>
<pre><code>import asyncio, httpx

async def ambil_semua(urls):
    async with httpx.AsyncClient() as client:
        tugas = [client.get(url) for url in urls]
        respons = await asyncio.gather(*tugas)
    return [r.json() for r in respons]

# 5 API bersamaan = ~2 detik</code></pre>
<p>Lima kali lebih cepat. Bukan karena Python jadi lebih kencang, tapi karena waktu tunggunya ditumpuk, bukan diantre.</p>

<h2>Di Django</h2>
<p>Django mendukung async view dan async ORM. Tapi jangan asal mengubah semua view jadi async.</p>
<pre><code># Berguna: view yang memanggil layanan eksternal
async def dashboard(request):
    cuaca, kurs, berita = await asyncio.gather(
        ambil_cuaca(),
        ambil_kurs(),
        ambil_berita(),
    )
    return render(request, "dashboard.html", {...})</code></pre>

<pre><code># Tidak berguna: view yang cuma query database sederhana
async def daftar_produk(request):
    produk = [p async for p in Produk.objects.all()]
    return render(request, "produk.html", {"produk": produk})
    # Versi sync-nya sama cepat, dan lebih mudah dibaca</code></pre>

<h2>Jebakan paling berbahaya</h2>
<p>Memanggil kode <em>sync</em> yang lambat di dalam fungsi async. Ini memblokir seluruh event loop — bukan cuma satu permintaan, tapi <strong>semua permintaan</strong> yang sedang ditangani proses itu.</p>
<pre><code># BAHAYA — memblokir semuanya
async def view(request):
    time.sleep(5)          # salah
    requests.get(url)      # salah, requests itu sync
    hasil = query_berat()  # salah

# BENAR
async def view(request):
    await asyncio.sleep(5)
    await httpx_client.get(url)
    hasil = await sync_to_async(query_berat)()</code></pre>

<h2>Kesimpulan sederhana</h2>
<p>Pakai async kalau kode kamu banyak menunggu jaringan. Lewati kalau tidak. Kompleksitas tambahannya nyata, dan hanya sepadan kalau kamu benar-benar mendapat manfaatnya.</p>
""",
    },
    {
        "title": "Celery: Memindahkan Pekerjaan Berat ke Belakang Layar",
        "category": "Tutorial",
        "tags": ["celery", "django", "python", "automation"],
        "excerpt": "Kalau ada pekerjaan yang butuh lebih dari beberapa detik, jangan biarkan pengguna menunggunya.",
        "content": """
<p>Aturan sederhana: kalau sebuah pekerjaan butuh lebih dari dua atau tiga detik, jangan kerjakan di dalam permintaan HTTP.</p>
<p>Kirim email, olah gambar, buat laporan, panggil LLM — semuanya masuk kategori ini.</p>

<h2>Kenapa penting</h2>
<p>Permintaan HTTP yang lama menahan worker server. Kalau semua worker tertahan, situsmu berhenti melayani siapa pun. Browser juga punya batas waktu sendiri — pengguna melihat error meski pekerjaannya sebenarnya berhasil.</p>

<h2>Menyiapkan Celery</h2>
<pre><code>pip install celery redis</code></pre>

<p><code>config/celery.py</code>:</p>
<pre><code>import os
from celery import Celery

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")

app = Celery("config")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()</code></pre>

<p><code>config/__init__.py</code>:</p>
<pre><code>from .celery import app as celery_app

__all__ = ("celery_app",)</code></pre>

<p><code>settings.py</code>:</p>
<pre><code>CELERY_BROKER_URL = "redis://localhost:6379/0"
CELERY_RESULT_BACKEND = "redis://localhost:6379/1"
CELERY_TASK_TIME_LIMIT = 30 * 60
CELERY_TASK_ACKS_LATE = True</code></pre>

<h2>Tugas pertama</h2>
<pre><code>from celery import shared_task

@shared_task(bind=True, max_retries=3)
def kirim_laporan(self, pengguna_id):
    try:
        pengguna = Pengguna.objects.get(pk=pengguna_id)
        berkas = susun_laporan(pengguna)
        kirim_email(pengguna.email, berkas)
    except Exception as exc:
        raise self.retry(exc=exc, countdown=60)</code></pre>

<p>Memanggilnya dari view:</p>
<pre><code>def minta_laporan(request):
    kirim_laporan.delay(request.user.pk)
    return JsonResponse({"pesan": "Laporan sedang dibuat, kami kabari lewat email."})</code></pre>
<p>View-nya membalas seketika. Pekerjaannya jalan di belakang layar.</p>

<h2>Tiga aturan yang saya pegang</h2>

<p><strong>1. Kirim ID, bukan objek.</strong></p>
<pre><code>kirim_laporan.delay(pengguna.pk)      # benar
kirim_laporan.delay(pengguna)         # salah — objek harus diserialisasi</code></pre>
<p>Objek yang diserialisasi bisa jadi basi sebelum tugasnya jalan.</p>

<p><strong>2. Buat tugasnya aman diulang.</strong> Celery bisa menjalankan ulang tugas yang gagal. Kalau tugasmu mengirim email, pastikan tidak mengirim dua kali.</p>

<p><strong>3. Selalu pasang batas waktu.</strong> Tanpa itu, satu tugas yang macet bisa menahan worker selamanya.</p>

<h2>Tugas terjadwal</h2>
<pre><code>from celery.schedules import crontab

app.conf.beat_schedule = {
    "bersihkan-sesi-harian": {
        "task": "core.tasks.bersihkan_sesi",
        "schedule": crontab(hour=3, minute=0),
    },
}</code></pre>

<h2>Alternatif yang lebih ringan</h2>
<p>Celery butuh Redis atau RabbitMQ, plus proses worker terpisah. Untuk kebutuhan sederhana, itu berlebihan.</p>
<p>Kalau tugasmu sedikit dan tidak kritis, <strong>django-q2</strong> atau <strong>huey</strong> lebih ringan. Kalau cuma butuh satu hal jalan tiap malam, cron biasa memanggil <code>manage.py</code> sudah cukup — tidak semua hal butuh antrean.</p>
""",
    },
    {
        "title": "pgvector: Pencarian Semantik Tanpa Menambah Infrastruktur",
        "category": "AI & Automation",
        "tags": ["database", "rag", "ai", "python"],
        "excerpt": "Sebelum memasang vector database khusus, cek dulu — PostgreSQL yang sudah kamu punya mungkin sudah cukup.",
        "content": """
<p>Setiap kali topik pencarian semantik muncul, saran pertama yang keluar selalu: pasang Pinecone, Weaviate, atau Qdrant.</p>
<p>Untuk kebanyakan proyek, itu menambah satu layanan lagi untuk dirawat, dibayar, dan dimonitor — padahal PostgreSQL yang sudah jalan bisa melakukannya.</p>

<h2>Memasang</h2>
<pre><code>CREATE EXTENSION IF NOT EXISTS vector;</code></pre>
<p>Sebagian besar layanan Postgres terkelola sudah menyediakannya. Untuk server sendiri, satu perintah instalasi paket.</p>

<h2>Memakainya dari Django</h2>
<pre><code>pip install pgvector django</code></pre>

<pre><code>from django.db import models
from pgvector.django import VectorField, HnswIndex

class Potongan(models.Model):
    dokumen = models.ForeignKey("Dokumen", on_delete=models.CASCADE)
    isi = models.TextField()
    embedding = VectorField(dimensions=1536)

    class Meta:
        indexes = [
            HnswIndex(
                name="potongan_embedding_idx",
                fields=["embedding"],
                m=16,
                ef_construction=64,
                opclasses=["vector_cosine_ops"],
            ),
        ]</code></pre>

<h2>Mencari</h2>
<pre><code>from pgvector.django import CosineDistance

def cari_mirip(vektor_pertanyaan, jumlah=5):
    return (
        Potongan.objects
        .annotate(jarak=CosineDistance("embedding", vektor_pertanyaan))
        .order_by("jarak")[:jumlah]
    )</code></pre>

<h2>Keunggulan yang sering diremehkan</h2>
<p><strong>Bisa digabung dengan filter biasa.</strong> Ini kelebihan terbesarnya. Kamu bisa mencari berdasarkan makna <em>sekaligus</em> menyaring dengan kolom biasa, dalam satu query.</p>
<pre><code>Potongan.objects
    .filter(dokumen__pemilik=request.user)
    .filter(dokumen__dibuat__gte=bulan_lalu)
    .annotate(jarak=CosineDistance("embedding", vektor))
    .order_by("jarak")[:5]</code></pre>
<p>Di vector database terpisah, hal ini biasanya berarti dua query dan penggabungan manual di sisi aplikasi.</p>

<p><strong>Ikut transaksi database.</strong> Kalau penyimpanan gagal, semuanya di-rollback bersama. Tidak ada kondisi di mana dokumen tersimpan tapi embedding-nya hilang.</p>

<p><strong>Ikut backup yang sudah ada.</strong> Tidak ada layanan tambahan yang perlu dicadangkan terpisah.</p>

<h2>Kapan pgvector tidak cukup</h2>
<p>Jujur soal batasannya: kalau kamu punya puluhan juta vektor dan butuh latensi di bawah sepuluh milidetik, vector database khusus memang lebih unggul. Mereka dibangun untuk itu.</p>
<p>Tapi kalau jumlahnya di bawah satu juta — dan itu mencakup hampir semua aplikasi internal, dokumentasi produk, dan basis pengetahuan perusahaan — pgvector cepat dan jauh lebih sederhana.</p>

<h2>Saran praktis</h2>
<p>Mulai dengan pgvector. Ukur. Pindah kalau memang terbukti tidak cukup. Jangan menambah layanan untuk masalah yang belum kamu miliki.</p>
""",
    },
    {
        "title": "Membangun AI Agent yang Benar-benar Dipakai",
        "category": "AI & Automation",
        "tags": ["ai", "llm", "automation", "python"],
        "excerpt": "Kebanyakan proyek AI agent gagal bukan karena modelnya kurang pintar, tapi karena masalahnya salah pilih.",
        "content": """
<p>Saya sudah membangun beberapa AI agent. Sebagian dipakai setiap hari. Sebagian lagi ditinggalkan dalam dua minggu.</p>
<p>Perbedaannya hampir tidak pernah soal model.</p>

<h2>Pertanyaan sebelum menulis kode</h2>
<p>Sebelum membangun agent, jawab empat hal ini dengan jujur:</p>
<ul>
  <li><strong>Apakah tugasnya memang sulit dijabarkan?</strong> Kalau langkahnya bisa ditulis lengkap sebagai <code>if-else</code>, tulis saja sebagai kode. Lebih murah, lebih cepat, dan bisa dites.</li>
  <li><strong>Apakah hasilnya sepadan?</strong> Agent lebih lambat dan lebih mahal daripada kode biasa.</li>
  <li><strong>Apakah kesalahan bisa dideteksi?</strong> Kalau agent salah dan tidak ada yang tahu, kamu sedang membangun mesin pembuat masalah diam-diam.</li>
  <li><strong>Apakah kesalahan bisa dibatalkan?</strong> Agent yang bisa menghapus data tanpa konfirmasi adalah kecelakaan yang menunggu waktu.</li>
</ul>
<p>Kalau ada satu saja yang jawabannya "tidak", pertimbangkan ulang.</p>

<h2>Tiga tingkat, dari yang paling sederhana</h2>

<p><strong>1. Satu panggilan.</strong> Klasifikasi, ringkasan, ekstraksi. Satu permintaan, satu jawaban. Ini menyelesaikan lebih banyak masalah nyata daripada yang orang kira.</p>

<p><strong>2. Alur terkendali.</strong> Beberapa langkah, tapi <em>kamu</em> yang menentukan urutannya. Model mengerjakan bagian yang butuh pemahaman bahasa; kode mengurus alurnya.</p>
<pre><code>def proses_tiket(tiket):
    kategori = klasifikasi(tiket.isi)              # LLM
    if kategori == "teknis":
        konteks = cari_dokumentasi(tiket.isi)      # kode
        balasan = susun_balasan(tiket, konteks)    # LLM
    else:
        balasan = teruskan_ke_manusia(tiket)       # kode
    return balasan</code></pre>
<p><strong>Sebagian besar yang orang sebut "AI agent" sebenarnya cukup di tingkat ini.</strong> Bisa diprediksi, bisa dites, bisa di-debug.</p>

<p><strong>3. Agent sebenarnya.</strong> Model yang memutuskan sendiri langkah apa berikutnya, memakai alat yang kamu sediakan, sampai tugasnya selesai. Paling fleksibel, paling sulit dikendalikan.</p>

<h2>Merancang alat yang baik</h2>
<p>Kualitas agent sangat ditentukan oleh alat yang kamu berikan. Beberapa prinsip yang saya pegang:</p>
<p><strong>Deskripsinya harus menjelaskan <em>kapan</em> dipakai</strong>, bukan cuma apa fungsinya. "Cari data pelanggan berdasarkan email — gunakan ketika pengguna menyebut alamat email atau menanyakan riwayat akun" jauh lebih efektif daripada "Cari pelanggan".</p>
<p><strong>Alat yang berbahaya harus dipisah dan dijaga.</strong> Jangan beri satu alat <code>jalankan_sql</code>. Buat alat spesifik: <code>cari_pesanan</code>, <code>ubah_status</code> — masing-masing dengan validasi sendiri.</p>
<p><strong>Sedikit tapi jelas lebih baik daripada banyak.</strong> Dua puluh alat membuat model bingung memilih.</p>

<h2>Contoh sederhana</h2>
<pre><code>from anthropic import Anthropic, beta_tool

client = Anthropic()

@beta_tool
def cari_pesanan(email: str) -> str:
    \"\"\"Cari pesanan terakhir seorang pelanggan.

    Gunakan ketika pengguna menanyakan status pesanan
    dan menyebutkan alamat email.

    Args:
        email: Alamat email pelanggan.
    \"\"\"
    pesanan = Pesanan.objects.filter(email=email).order_by("-dibuat")[:5]
    if not pesanan:
        return "Tidak ada pesanan untuk email tersebut."
    return "\\n".join(f"{p.kode}: {p.get_status_display()}" for p in pesanan)

runner = client.beta.messages.tool_runner(
    model="claude-opus-5",
    max_tokens=8000,
    tools=[cari_pesanan],
    messages=[{"role": "user", "content": pertanyaan_pengguna}],
)

for pesan in runner:
    ...</code></pre>

<h2>Pelajaran termahal saya</h2>
<p>Agent pertama saya diberi akses tulis ke database sejak hari pertama. Berhasil sembilan dari sepuluh kali. Yang kesepuluh menghabiskan satu hari penuh untuk membereskannya.</p>
<p>Sekarang aturannya tetap: <strong>mulai dari hanya-baca.</strong> Amati keputusannya selama beberapa minggu. Baru berikan izin menulis satu per satu, masing-masing dengan konfirmasi.</p>
<p>Kecerdasan model bukan bagian yang sulit. Yang sulit adalah membangun pagar pengaman di sekelilingnya.</p>
""",
    },
    {
        "title": "Deploy Aplikasi Python: Dari Laptop ke Server",
        "category": "Tutorial",
        "tags": ["deployment", "python", "django", "docker"],
        "excerpt": "Jalan di laptop bukan berarti jalan di server. Ini yang berubah, dan kenapa.",
        "content": """
<p>Perbedaan terbesar antara aplikasi Python dan aplikasi PHP terlihat justru saat deploy. PHP tinggal ditaruh; Python butuh proses yang hidup terus.</p>

<h2>Kenapa runserver tidak boleh dipakai</h2>
<p><code>manage.py runserver</code> dirancang untuk pengembangan. Satu proses, tanpa optimasi, dengan peringatan keamanan yang jelas di dokumentasi Django.</p>
<p>Untuk produksi kamu butuh <strong>server WSGI</strong> — biasanya Gunicorn — dan di depannya <strong>reverse proxy</strong> seperti Nginx.</p>

<h2>Susunannya</h2>
<pre><code>Pengunjung → Nginx → Gunicorn → Django
              │
              └─ melayani /static/ dan /media/ langsung</code></pre>
<p>Nginx menangani koneksi lambat, HTTPS, dan berkas statis. Gunicorn menjalankan kode Python-mu di beberapa proses sekaligus.</p>

<h2>Menjalankan Gunicorn</h2>
<pre><code>pip install gunicorn

gunicorn config.wsgi:application \\
  --bind 127.0.0.1:8000 \\
  --workers 3 \\
  --timeout 60 \\
  --access-logfile -</code></pre>
<p>Aturan umum jumlah worker: <code>(2 × jumlah CPU) + 1</code>. Untuk VPS 1 CPU, tiga sudah pas.</p>

<h2>Supaya tetap hidup setelah restart</h2>
<p>Proses yang dijalankan manual akan mati saat kamu keluar dari SSH. Serahkan ke systemd.</p>
<pre><code>[Unit]
Description=Gunicorn untuk situs saya
After=network.target

[Service]
User=www-data
WorkingDirectory=/var/www/situs
Environment="PATH=/var/www/situs/venv/bin"
ExecStart=/var/www/situs/venv/bin/gunicorn \\
    config.wsgi:application --bind 127.0.0.1:8000 --workers 3
Restart=always

[Install]
WantedBy=multi-user.target</code></pre>
<pre><code>sudo systemctl enable --now situs
sudo systemctl status situs</code></pre>

<h2>Yang berubah dari lingkungan lokal</h2>
<ul>
  <li><code>DEBUG=False</code> — wajib. Dengan <code>True</code>, pesan error menampilkan isi settings-mu ke publik</li>
  <li><code>SECRET_KEY</code> baru, berbeda dari yang di laptop</li>
  <li><code>ALLOWED_HOSTS</code> diisi domain aslimu</li>
  <li>Berkas statis dikumpulkan dengan <code>collectstatic</code></li>
  <li>Kredensial database mengikuti server, bukan lokal</li>
</ul>
<p>Satu hal yang sering mengejutkan: begitu <code>DEBUG=False</code>, Django <strong>berhenti melayani berkas statis</strong>. Kalau Nginx belum diatur, situsmu tampil tanpa CSS sama sekali. WhiteNoise adalah jalan pintas yang baik untuk ini.</p>

<h2>Daftar periksa sebelum go-live</h2>
<pre><code>python manage.py check --deploy
python manage.py migrate
python manage.py collectstatic --noinput
python manage.py createsuperuser</code></pre>
<p>Perintah pertama akan menyebutkan pengaturan keamanan yang belum aktif. Perhatikan semuanya.</p>

<h2>Docker: kapan sepadan</h2>
<p>Docker menyelesaikan masalah "jalan di laptop saya". Tapi menambah satu lapisan lagi untuk dipelajari dan di-debug.</p>
<p>Untuk satu aplikasi di satu VPS, deploy langsung sering kali lebih sederhana. Docker mulai sepadan ketika kamu punya beberapa layanan, beberapa lingkungan, atau tim yang perlu setup identik.</p>

<h2>Yang paling sering terlupa</h2>
<p><strong>Backup database.</strong> Bukan "nanti", tapi sebelum pengguna pertama masuk. Satu cron harian yang menyimpan dump ke tempat lain sudah menyelamatkan banyak orang.</p>
<p><strong>Pemantauan.</strong> Kamu perlu tahu situsmu mati sebelum penggunamu yang memberitahu.</p>
<p><strong>Rencana rollback.</strong> Suatu saat deploy akan gagal. Tahu cara kembali ke versi sebelumnya dalam dua menit, bukan dua jam.</p>
""",
    },
]


class Command(BaseCommand):
    help = "Bersihkan konten contoh dan isi blog dengan tulisan AI automation & Python."

    def handle(self, *args, **options):
        # ------------------------------------------------------------ bersihkan
        hapus = [
            ("Komentar", Comment),
            ("Postingan", Post),
            ("Tag", Tag),
            ("Kategori", Category),
            ("Proyek", Project),
            ("Pengalaman kerja", Experience),
            ("Pendidikan", Education),
            ("Sertifikat", Certificate),
            ("Skill", Skill),
            ("Pesan masuk", ContactMessage),
        ]
        self.stdout.write(self.style.WARNING("Membersihkan konten lama..."))
        for label, model in hapus:
            jumlah = model.objects.count()
            model.objects.all().delete()
            if jumlah:
                self.stdout.write(f"  - {label}: {jumlah} dihapus")

        # ---------------------------------------------------------------- skill
        for urutan, (nama, kategori, level, unggulan) in enumerate(SKILLS):
            Skill.objects.create(
                name=nama,
                category=kategori,
                level=level,
                order=urutan,
                is_featured=unggulan,
            )

        # ------------------------------------------------------- kategori & tag
        kategori_map = {}
        for nama, warna, deskripsi in CATEGORIES:
            kategori_map[nama] = Category.objects.create(
                name=nama, color=warna, description=deskripsi
            )

        tag_map = {nama: Tag.objects.create(name=nama) for nama in TAGS}

        # -------------------------------------------------------------- artikel
        sekarang = timezone.now()
        total = len(ARTICLES)
        for i, data in enumerate(ARTICLES):
            post = Post.objects.create(
                title=data["title"],
                post_type="article",
                excerpt=data["excerpt"],
                content=data["content"].strip(),
                category=kategori_map[data["category"]],
                status="published",
                # Terbit mundur beberapa hari supaya urutannya wajar.
                published_at=sekarang - timezone.timedelta(days=(total - i) * 4),
                is_pinned=data.get("pinned", False),
                allow_comments=True,
            )
            post.tags.set([tag_map[t] for t in data["tags"] if t in tag_map])

        # ------------------------------------------------------------- ringkasan
        self.stdout.write(self.style.SUCCESS("\nKonten baru siap:"))
        self.stdout.write(f"  Skill      : {Skill.objects.count()}")
        self.stdout.write(f"  Kategori   : {Category.objects.count()}")
        self.stdout.write(f"  Tag        : {Tag.objects.count()}")
        self.stdout.write(f"  Artikel    : {Post.objects.count()}")
        self.stdout.write(
            self.style.SUCCESS(
                "\nProyek, pengalaman, pendidikan, dan sertifikat sengaja dikosongkan.\n"
                "Profil dan social links tidak disentuh."
            )
        )
