#!/usr/bin/env python3
"""StudioSoft Healthcare — run cron hc45 (24 Sep 2026 02:00 WIB / 23 Sep 19:00 UTC).

Topik Threads : "9 Contoh Konten Digital Marketing Rumah Sakit yang Edukatif dan Tetap Etis"
                (artikel https://studiosoft.id/9-contoh-konten-digital-marketing-rumah-sakit-yang-edukatif-dan-tetap-etis/
                 — RE-AIR: audit 24 Sep 2026 -> terakhir tayang di Threads SS 10 Sep 2026,
                 paling tua di antara artikel HC yang MASIH hidup; nama file slide BARU
                 `ss_hc45_kontenhc_*`).
LinkedIn      : TEXT-only, artikel `10-channel-digital-marketing-rumah-sakit-yang-wajib-
                dikelola-di-era-online` (BELUM PERNAH tayang di LinkedIn SS — hanya pernah
                di Threads & FB 13 Sep 2026).
Facebook      : TEXT-only tanpa media, replies=[], artikel `rs-klinik-digital-marketing-2026`
                (di FB SS hanya pernah ERROR 27 Agu 2026 -> belum pernah tayang).

TEMUAN PENTING 24 Sep 2026: 7 artikel HC yang dipakai seri ini sudah DIHAPUS dari
studiosoft.id (HTTP 404): strategi-digital-marketing-rumah-sakit, website-rumah-sakit-
gagal-pasien, iklan-facebook-rumah-sakit, klinik-kecil-butuh-digital-marketing,
konten-edukasi-kesehatan-kepercayaan-pasien, social-media-rumah-sakit-efektif,
data-analytics-rumah-sakit-kebutuhan-pasien. Sejak run ini SCRIPT MEMVERIFIKASI semua
link artikel (Threads/LI/FB) wajib 200 sebelum create; kandidat mati otomatis dilewati.

ATURAN (FIXED 5 & 15 Sep 2026):
- HANYA THREADS SS yang boleh carousel/image. FB & LinkedIn SS: TEXT saja.
- IG SS tidak disentuh (punya desain sendiri).
- Carousel = 09:00 WIB. Threads <=500 char, hashtag huruf kecil, 0 mention, pancingan diskusi.
- Standar desain 16 Sep 2026: engine ss_carousel_lib.py, bg #122B63, rata kiri, blok tengah
  vertikal, tanpa separator, bullet dot, Poppins Bold 85 / Regular 36 / sub 30, auto-fit.
"""
import json, os, re, subprocess, sys
from datetime import datetime, timezone, timedelta

WS = "/home/mantapman/.hermes/profiles/atun/workspace"
sys.path.insert(0, WS)
from PIL import Image
from repliz_client import ReplizClient, ACC, IMG_DIR, IMG_BASE_URL
from diskusi_engine import tambahkan_diskusi

DRY = os.environ.get("DRY", "") == "1"
_DOCS = None
c = ReplizClient()

TITLE = "9 Contoh Konten Digital Marketing Rumah Sakit yang Edukatif dan Tetap Etis"
ARTICLE_URL = ("https://studiosoft.id/9-contoh-konten-digital-marketing-rumah-sakit-"
               "yang-edukatif-dan-tetap-etis/")
LI_TITLE = "10 Channel Digital Marketing Rumah Sakit yang Wajib Dikelola di Era Online"
LI_URL = "https://studiosoft.id/10-channel-digital-marketing-rumah-sakit-yang-wajib-dikelola-di-era-online/"
FB_TITLE = "Digital Marketing Rumah Sakit & Klinik: Panduan Lengkap 2026"
FB_URL = "https://studiosoft.id/rs-klinik-digital-marketing-2026/"
HASHTAG_LINE = "#studiosoft #digitalmarketingrumahsakit #kontenkesehatan #klinik #edukasikesehatan"

MEDIA_FILES = [
    "ss_hc45_kontenhc_01_cover.jpg",
    "ss_hc45_kontenhc_02_edukasi.jpg",
    "ss_hc45_kontenhc_03_trust.jpg",
    "ss_hc45_kontenhc_04_layanan.jpg",
    "ss_hc45_kontenhc_05_cta.jpg",
]
SRC_PNG = os.path.join(WS, "studiosoft_hc45_carousel")
MEDIA_URLS = [IMG_BASE_URL.rstrip("/") + "/openclaw-img/" + f for f in MEDIA_FILES]
INGRESS = ["103.84.155.153", "103.84.155.217"]

THREADS_BASE = """Pasien tidak lagi percaya brosur: mereka cari di Google, baca ulasan, dan menyimak konten edukasi sebelum memilih RS.

Promosi RS ada etikanya. 9 jenis konten aman: edukasi gejala, Tanya Dokter, tips gaya hidup, prosedur medis, profil dokter, testimoni berizin, program sosial, FAQ, pengingat kontrol.

""" + ARTICLE_URL

LI_BASE = """Pasien tidak lagi memilih rumah sakit dari brosur atau rekomendasi tetangga saja. Mereka mengetik di Google, membaca ulasan, membuka Instagram, lalu memutuskan.

Artinya RS dan klinik kini bersaing di ruang yang berbeda: layar ponsel. Ada 10 kanal digital yang sebaiknya dikelola serius \\u2014 website sebagai rumah utama, SEO agar ditemukan, Google Maps dan Google Business Profile untuk lokasi serta ulasan, media sosial untuk kedekatan dengan pasien, YouTube untuk video edukasi dokter, hingga email dan WhatsApp untuk pasien lama.

Kesalahan paling umum bukan kurang kanal, tapi mencoba hadir di semua tempat sekaligus sehingga semuanya setengah jalan. Dua sampai tiga kanal yang benar-benar dikelola biasanya mengalahkan sepuluh akun yang kosong.

Bacaan lengkapnya: """ + LI_URL

FB_BASE = """Digital marketing rumah sakit dan klinik tidak bisa disamakan dengan promosi bisnis biasa. Ada etika yang harus dijaga: jangan menjanjikan kesembuhan, jangan menakut-nakuti pembaca, dan jangan memakai foto atau testimoni pasien tanpa izin tertulis.

Yang paling sering terlewat justru hal mendasar. Nomor telepon dan WhatsApp sulit ditemukan, jadwal praktik dokter tidak diperbarui, dan halaman website lambat dibuka di ponsel. Padahal keputusan pasien dimulai dari layar ponsel, bukan dari meja pendaftaran.

Urutan yang biasanya bekerja: rapikan website dan halaman layanan, optimasi pencarian lokal, lengkapi Google Business Profile, baru bangun kehadiran media sosial secara konsisten.

Bacaan lengkapnya: """ + FB_URL

CTA_RE = re.compile(r"komentar|pendapat|diskusi|setuju|share|pengalaman|menurut|bagaimana|apa yang|komen|tanggapan|pandangan", re.I)
BAN_RE = re.compile(r"kecantikan|skincare|jerawat|sembuh total", re.I)
# Pancingan diskusi KURASI (on-topic). Bauran run ini: Threads opini + LinkedIn netral + FB netral.
CURATED_T = {
    "netral": "Menurut Anda, konten RS seperti apa yang paling dipercaya pasien? Yuk diskusi di komentar.",
    "opini": "Bold statement: RS yang kontennya edukatif lebih dipercaya daripada RS yang penuh promo. Setuju atau tidak?",
    "sanjungan": "Buat tim RS yang kontennya lebih banyak mengedukasi daripada berjualan, Anda selangkah lebih maju! \\U0001f44f",
}
CURATED_LI = {
    "netral": "Menurut Anda, kanal mana yang paling berdampak untuk RS di kota Anda: Google, website, atau media sosial? Yuk diskusi di komentar.",
    "opini": "Bold statement: RS yang mengelola website dan Google-nya serius akan mengalahkan RS yang hanya rajin posting di media sosial. Setuju atau tidak?",
    "sanjungan": "Buat tim RS yang memilih fokus pada dua-tiga kanal dan mengelolanya serius, Anda selangkah lebih maju! \\U0001f44f",
}
CURATED_FB = {
    "netral": "Menurut Anda, dari 10 kanal digital itu mana yang paling sering dilewatkan rumah sakit? Yuk diskusi di komentar.",
    "opini": "Bold statement: memperbaiki website lebih mendesak daripada menambah akun media sosial baru. Setuju atau tidak?",
    "sanjungan": "Buat yang sudah mengelola kanal digitalnya dengan rapi dan etis, Anda selangkah lebih maju! \\U0001f44f",
}


def build(base, platform, industri, tones, limit=None, ban=None, max_try=120):
    for tone in tones:
        for _ in range(max_try):
            txt = tambahkan_diskusi(base, platform=platform, industri=industri, tone=tone, force=True)
            if not CTA_RE.search(txt[-200:]):
                continue
            if limit is not None and len(txt) > limit:
                continue
            if ban is not None and ban.search(txt):
                continue
            if "..." in txt:
                continue
            return txt
    return None


def build_curated(base, curated, tones, limit=None):
    for tone in tones:
        hook = (curated or {}).get(tone)
        if not hook:
            continue
        cand = base.rstrip() + "\n\n" + hook
        if CTA_RE.search(cand[-200:]) and (limit is None or len(cand) <= limit) and "@" not in cand:
            return cand
    return None


def build_safe(base, platform, industri, tones, limit=None, ban=None, curated=None):
    txt = build_curated(base, curated, tones, limit=limit)
    if txt is not None:
        return txt
    txt = build(base, platform, industri, tones, limit=limit, ban=ban)
    if txt is not None:
        return txt
    for tone in tones:
        hook = (curated or {}).get(tone)
        if not hook:
            continue
        cand = base.rstrip() + "\n\n" + hook
        if CTA_RE.search(cand[-200:]) and (limit is None or len(cand) <= limit):
            return cand
    return None


THREADS = build_safe(THREADS_BASE, "threads", "rumah_sakit", ["netral"], limit=500, ban=BAN_RE,
                     curated=CURATED_T)
LI = build_safe(LI_BASE, "linkedin", "rumah_sakit", ["opini", "netral"], ban=BAN_RE, curated=CURATED_LI)
FB = build_safe(FB_BASE, "facebook", "rumah_sakit", ["netral", "opini"], ban=BAN_RE, curated=CURATED_FB)

REPLY1 = ("Baca selengkapnya: %s\n\nButuh partner digital marketing untuk RS atau klinik Anda?\n"
          "www.studiosoft.id" % ARTICLE_URL)
REPLY2 = HASHTAG_LINE


def check_caption(name, txt, limit=None, require_cta=True):
    ok = True
    if txt is None:
        print("  \u274c %s: gagal disusun" % name)
        return False
    if limit and len(txt) > limit:
        print("  \u274c %s: %d char > %d" % (name, len(txt), limit))
        ok = False
    m = re.findall(r"@\w+", txt)
    if m:
        print("  \u274c %s: ada mention %s" % (name, m))
        ok = False
    caps = [t for t in re.findall(r"#(\w+)", txt) if t != t.lower()]
    if caps:
        print("  \u274c %s: hashtag KAPITAL %s" % (name, caps))
        ok = False
    if "..." in txt:
        print("  \u274c %s: ada elipsis" % name)
        ok = False
    if require_cta and not CTA_RE.search(txt[-200:]):
        print("  \u274c %s: tidak ada pancingan diskusi di ekor caption" % name)
        ok = False
    print("  %s %s: %d char" % ("\u2705" if ok else "\u26a0\ufe0f", name, len(txt)))
    return ok


def get_retry(ep, tries=6):
    import time
    last = None
    for i in range(tries):
        try:
            return c.get(ep)
        except Exception as e:
            last = e
            time.sleep(3 + 3 * i)
    raise last


def all_docs():
    """Ambil semua jadwal SEKALI (cache) — dump berulang bikin run lambat/timeout."""
    global _DOCS
    if _DOCS is not None:
        return _DOCS
    docs, page = [], 1
    while True:
        d = get_retry("/public/schedule?page=%d&limit=100" % page)
        docs.extend(d.get("docs", []))
        if page >= (d.get("totalPages") or 1):
            break
        page += 1
    _DOCS = docs
    print("   (dump jadwal: %d docs)" % len(docs))
    return _DOCS


def scan_media(statuses=("pending", "scheduled", "draft")):
    hits = []
    for s in all_docs():
        if s.get("accountId") not in (ACC.STUDIOSOFT_THREADS, ACC.STUDIOSOFT_LINKEDIN,
                                      ACC.STUDIOSOFT_FB, ACC.STUDIOSOFT_IG):
            continue
        if (s.get("status") or "").lower() not in statuses:
            continue
        for m in (s.get("medias") or []):
            base = os.path.basename((m.get("url") or "").split("?")[0]).lower()
            if base in [f.lower() for f in MEDIA_FILES]:
                hits.append((s.get("accountId"), (s.get("scheduleAt") or "")[:16],
                             (s.get("title") or "")[:40], s.get("type"), s.get("status")))
                break
    return hits


def busy_by_account(aid):
    busy = set()
    for s in all_docs():
        if s.get("accountId") == aid and (s.get("status") or "").lower() in ("pending", "scheduled", "draft"):
            busy.add((s.get("scheduleAt") or "")[:19])
    return busy


def next_free_09():
    busy = busy_by_account(ACC.STUDIOSOFT_THREADS)
    now = datetime.now(timezone.utc)
    start = datetime(now.year, now.month, now.day, tzinfo=timezone.utc)
    if now.hour >= 2:
        start += timedelta(days=1)
    for day in range(0, 20):
        cand = (start + timedelta(days=day)).strftime("%Y-%m-%dT02:00:00")
        if cand not in busy:
            lab = (datetime.fromisoformat(cand) + timedelta(hours=7)).strftime("%d %b %Y 09:00 WIB")
            return cand + ".000Z", lab
    return None, None


def next_free_li():
    SLOTS = [("05:00 WIB", 22, -1), ("08:00 WIB", 1, 0), ("11:00 WIB", 4, 0), ("14:00 WIB", 7, 0),
             ("17:00 WIB", 10, 0), ("20:00 WIB", 13, 0), ("23:00 WIB", 16, 0)]
    busy = busy_by_account(ACC.STUDIOSOFT_LINKEDIN)
    now = datetime.now(timezone.utc) + timedelta(minutes=15)
    now_naive = now.replace(tzinfo=None)
    today = datetime.now(timezone.utc)
    for day in range(0, 60):
        for label, uh, shift in SLOTS:
            d0 = datetime(today.year, today.month, today.day, tzinfo=timezone.utc) + timedelta(days=day + shift)
            cand = "%sT%02d:00:00" % (d0.strftime("%Y-%m-%d"), uh)
            if datetime.fromisoformat(cand) <= now_naive:
                continue
            if cand not in busy:
                lab = (datetime.fromisoformat(cand) + timedelta(hours=7)).strftime("%d %b %Y %H:%M WIB")
                return cand + ".000Z", lab
    return None, None


def next_free_fb():
    busy = busy_by_account(ACC.STUDIOSOFT_FB)
    now = datetime.now(timezone.utc) + timedelta(minutes=30)
    now_naive = now.replace(tzinfo=None)
    today = datetime.now(timezone.utc)
    for day in range(0, 21):
        d0 = datetime(today.year, today.month, today.day, tzinfo=timezone.utc) + timedelta(days=day)
        cand = "%sT12:30:00" % d0.strftime("%Y-%m-%d")
        if datetime.fromisoformat(cand) <= now_naive:
            continue
        if cand not in busy:
            lab = (datetime.fromisoformat(cand) + timedelta(hours=7)).strftime("%d %b %Y 19:30 WIB")
            return cand + ".000Z", lab
    return None, None


def prep_media():
    ok = True
    for png in sorted(os.listdir(SRC_PNG)):
        if not (png.endswith(".png") and png.startswith("ss_hc45_kontenhc_")):
            continue
        jpg = png.replace(".png", ".jpg")
        src = Image.open(os.path.join(SRC_PNG, png))
        if src.mode != "RGB":
            src = src.convert("RGB")
        src.save(os.path.join(IMG_DIR, jpg), "JPEG", quality=85, optimize=True)
        print("  \u2705 %s -> %s %s" % (png, jpg, src.size))
    for f in MEDIA_FILES:
        try:
            im = Image.open(os.path.join(IMG_DIR, f))
            im.verify()
            im = Image.open(os.path.join(IMG_DIR, f))
            print("  \u2705 PIL %s %s %s %d bytes" % (f, im.size, im.mode,
                                                      os.path.getsize(os.path.join(IMG_DIR, f))))
        except Exception as e:
            print("  \u274c PIL %s: %s" % (f, e))
            ok = False
    return ok


def verify_urls(tries=4):
    """Ingress 103.84.155.217 kadang lambat (21-30s) -> timeout 60s + 4 percobaan."""
    ok = True
    for u in MEDIA_URLS:
        for ip in INGRESS:
            code = "000"
            for _ in range(tries):
                r = subprocess.run(["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", "60",
                                    "--resolve", "mantapserver.tail3947bb.ts.net:443:%s" % ip, u],
                                   capture_output=True, text=True)
                code = r.stdout.strip()
                if code == "200":
                    break
            print("  %s @%s: %s" % (u.split("/")[-1], ip, code))
            if code != "200":
                ok = False
    return ok


def verify_link(url):
    r = subprocess.run(["curl", "-sIL", "--http1.1", "--max-time", "200",
                        "-A", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", url,
                        "-o", "/dev/null", "-w", "%{http_code}"], capture_output=True, text=True)
    code = (r.stdout or "").strip()
    print("  langsung %s: %s" % (url, code))
    if code != "200":
        r2 = subprocess.run(["curl", "-s", "--max-time", "120", "-o", "/dev/null", "-w", "%{http_code}",
                             "https://r.jina.ai/" + url], capture_output=True, text=True)
        code = (r2.stdout or "").strip()
        print("  via r.jina.ai: %s" % code)
    return code == "200"


def main():
    wib = datetime.now(timezone(timedelta(hours=7)))
    print("Time WIB: %s" % wib.strftime("%Y-%m-%d %H:%M"))

    print("\n0) LINK ARTIKEL (WAJIB 200 — 7 artikel HC sudah dihapus dari website 24 Sep):")
    for u in (ARTICLE_URL, LI_URL, FB_URL):
        if not verify_link(u):
            print("  \u274c link tidak 200 \u2014 batal.")
            return False

    print("\n1) SLOT:")
    SLOT_UTC, SLOT_LABEL = next_free_09()
    if not SLOT_UTC:
        print("  \u274c tidak ada slot 09:00 WIB bebas \u2014 batal.")
        return False
    print("  \u2705 carousel Threads: %s (%s)" % (SLOT_LABEL, SLOT_UTC))
    LI_SLOT_UTC, LI_SLOT_LABEL = next_free_li()
    FB_SLOT_UTC, FB_SLOT_LABEL = next_free_fb()
    print("  \u2705 LinkedIn text: %s (%s)" % (LI_SLOT_LABEL, LI_SLOT_UTC))
    print("  \u2705 Facebook text (tanpa media, replies=[]): %s (%s)" % (FB_SLOT_LABEL, FB_SLOT_UTC))
    if not (LI_SLOT_UTC and FB_SLOT_UTC):
        print("  \u274c slot LI/FB tidak ditemukan \u2014 batal.")
        return False

    print("\n2) CAPTION:")
    ok = True
    ok &= check_caption("Threads", THREADS, 500)
    ok &= check_caption("LinkedIn", LI)
    ok &= check_caption("Facebook", FB)
    ok &= check_caption("Reply1", REPLY1, require_cta=False)
    ok &= check_caption("Reply2", REPLY2, require_cta=False)
    if not ok:
        print("\n\u274c caption gagal aturan \u2014 batal.")
        return False

    print("\n3) IDEMPOTENSI (media hc45 sudah pernah dijadwalkan?):")
    hits = scan_media(("pending", "scheduled", "draft", "success"))
    if hits:
        print("  \u26a0\ufe0f sudah ada jadwal dgn media hc45 -> SKIP create:")
        for h in hits:
            print("   ", h)
        return True
    print("  \u2705 belum ada")

    print("\n4) MEDIA: PNG standar baru -> JPEG openclaw-img + PIL:")
    if not prep_media():
        print("  \u274c media rusak \u2014 batal.")
        return False

    print("\n5) MEDIA JALUR INTERNET (2 ingress):")
    if not verify_urls():
        print("  \u274c ada media tidak 200 \u2014 batal.")
        return False

    if DRY:
        print("\n[DRY RUN] semua cek lolos; tidak ada create.")
        return True

    base_dt = datetime.fromisoformat(SLOT_UTC.replace("Z", "+00:00"))
    r2 = (base_dt + timedelta(minutes=1)).strftime("%Y-%m-%dT%H:%M:%S.000Z")
    r3 = (base_dt + timedelta(minutes=2)).strftime("%Y-%m-%dT%H:%M:%S.000Z")
    medias = [c.make_media(u, "image", "slide%d" % (i + 1)) for i, u in enumerate(MEDIA_URLS)]

    results = {}
    print("\n6) CREATE \u2014 THREADS (album + 2 replies):")
    try:
        alb = c.schedule_album(ACC.STUDIOSOFT_THREADS, THREADS, SLOT_UTC, medias, title=TITLE)
        pid = alb.get("scheduleId") or alb.get("_id") or alb.get("id")
        print("  album -> %s" % pid)
        c.create_schedule(ACC.STUDIOSOFT_THREADS, REPLY1, r2, type_="text", replies=[pid])
        c.create_schedule(ACC.STUDIOSOFT_THREADS, REPLY2, r3, type_="text", replies=[pid])
        print("  2 replies OK")
        results["threads"] = pid
    except Exception as e:
        print("  GAGAL %s" % e)
        results["threads"] = None

    print("\n   LINKEDIN (text-only, %s):" % LI_SLOT_LABEL)
    try:
        res = c.create_schedule(ACC.STUDIOSOFT_LINKEDIN, LI, LI_SLOT_UTC, title=LI_TITLE,
                                type_="text", medias=[], replies=[])
        print("  text -> %s" % (res.get("scheduleId") or res.get("_id") or res.get("id")))
        results["linkedin"] = res.get("scheduleId") or res.get("_id") or res.get("id")
    except Exception as e:
        print("  GAGAL %s" % e)
        results["linkedin"] = None

    print("\n   FACEBOOK (TEXT saja, TANPA media, replies=[], %s):" % FB_SLOT_LABEL)
    try:
        res = c.create_schedule(ACC.STUDIOSOFT_FB, FB, FB_SLOT_UTC, title=FB_TITLE,
                                type_="text", medias=[], replies=[])
        print("  text -> %s" % (res.get("scheduleId") or res.get("_id") or res.get("id")))
        results["facebook"] = res.get("scheduleId") or res.get("_id") or res.get("id")
    except Exception as e:
        print("  GAGAL %s" % e)
        results["facebook"] = None

    print("\n" + "=" * 55)
    ok = all(results.values())
    print("SEMUA PLATFORM TERJADWAL" if ok else "GAGAL: %s" % [k for k, v in results.items() if not v])

    if ok:
        print("\n7) VERIFIKASI (fetch ULANG dari API per jadwal):")
        for name, sid in results.items():
            d = get_retry("/public/schedule/%s" % sid)
            doc = d.get("doc") or d.get("data") or d
            print("  %s: status=%s at=%s type=%s medias=%d replies=%d" % (
                name, doc.get("status"), doc.get("scheduleAt"), doc.get("type"),
                len(doc.get("medias") or []), len(doc.get("replies") or [])))
        for h in scan_media(("pending",)):
            print("   ", h)

        st = json.loads(open(WS + "/studiosoft_healthcare_state.json").read())
        st["index"] = st.get("index", 40) + 1
        st["topic"] = TITLE
        st["last_post"] = wib.strftime("%Y-%m-%d %H:%M")
        json.dump(st, open(WS + "/studiosoft_healthcare_state.json", "w"))
        with open(WS + "/studiosoft_healthcare_log.txt", "a") as f:
            f.write("\n[%s WIB] hc45 (RE-AIR Threads: 9 konten edukatif RS \u2014 terakhir tayang "
                    "Threads 10 Sep 2026, paling tua di antara artikel HC yang MASIH hidup; slide BARU "
                    "ss_hc45_kontenhc_*) | %s | %s | Threads album %s (%s) + 2 replies | "
                    "LinkedIn TEXT \"%s\" %s (%s) | FB TEXT \"%s\" (tanpa media, replies=[]) %s | "
                    "catatan: stok artikel healthcare baru HABIS (WP REST 23 Sep, terbaru 20 Sep "
                    "`jasa-branding-surabaya...` = non-healthcare; kategori healthcare-digital hanya 4 post, "
                    "semua sdh dipakai). \u26a0\ufe0f TEMUAN: 7 artikel HC seri ini sudah DIHAPUS dari "
                    "studiosoft.id (404): strategi-digital-marketing-rumah-sakit, website-rumah-sakit-"
                    "gagal-pasien, iklan-facebook-rumah-sakit, klinik-kecil-butuh-digital-marketing, "
                    "konten-edukasi-kesehatan-kepercayaan-pasien, social-media-rumah-sakit-efektif, "
                    "data-analytics-rumah-sakit-kebutuhan-pasien -> run ini hanya pakai artikel yang "
                    "terverifikasi 200; LinkedIn: 10-channel (never di LI SS, cuma Threads+FB 13 Sep); "
                    "FB: rs-klinik-digital-marketing-2026 (di FB SS cuma pernah ERROR 27 Agu). "
                    "state index -> %s.\n"
                    % (wib.strftime("%Y-%m-%d %H:%M"), TITLE, ARTICLE_URL, SLOT_LABEL,
                       SLOT_UTC[11:16] + "Z", LI_TITLE, LI_SLOT_LABEL, LI_SLOT_UTC,
                       FB_TITLE, FB_SLOT_LABEL, st["index"]))
        print("Log + state ditulis.")
    return ok


if __name__ == "__main__":
    sys.exit(0 if main() else 1)
