#!/usr/bin/env python3
"""StudioSoft Healthcare — run cron hc65 (14 Okt 2026 02:00 WIB / 13 Okt 19:00 UTC).

Topik Threads : "Tips SEO Digital Marketing Surabaya: Optimasi Local untuk Klinik dan Bengkel
                Lokal" https://studiosoft.id/tips-seo-digital-marketing-surabaya-optimasi-local-untuk-klinik-dan-bengkel-lokal/
                (WP id 2931) RE-AIR (jejak Threads terakhir 30 Sep 2026 02:01Z / hc50 = PALING TUA
                di antara artikel HC hidup yang TIDAK punya jadwal Threads mendatang; HTTP 200).
                Slide BARU ss_hc65_lokalseo_*.
LinkedIn      : TEXT storytelling artikel `cara-bikin-prompt-ai-yang-bagus-bukan-generik`
                (WP id 7530, terbit 26 Sep 2026) — artikel TERBARU yang 0 hit di SEMUA jadwal LI
                (semua artikel 27 Sep-13 Okt sudah ada di antrean LI).
Facebook      : TEXT-only tanpa media, replies=[], artikel `10-channel-digital-marketing-rumah-
                sakit-yang-wajib-dikelola-di-era-online` — jejak FB sukses terakhir 3 Okt 2026
                13:00Z = PALING TUA di antara artikel HC hidup tanpa jadwal FB mendatang & BUKAN
                topik Threads hari ini (hc65 = `tips-seo-...-klinik-dan-bengkel-lokal`).

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
import os
import re
import subprocess
import sys
from datetime import datetime, timedelta, timezone

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
import repliz_openssl_transport

repliz_openssl_transport.install(verbose=False)
from diskusi_engine import tambahkan_diskusi

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

TITLE = "Tips SEO Digital Marketing Surabaya: Optimasi Local untuk Klinik dan Bengkel Lokal"
ARTICLE_URL = ("https://studiosoft.id/"
               "tips-seo-digital-marketing-surabaya-optimasi-local-untuk-klinik-dan-bengkel-lokal/")
LI_TITLE = "Cara Bikin Prompt AI yang Bagus (Bukan Generik)"
LI_URL = "https://studiosoft.id/cara-bikin-prompt-ai-yang-bagus-bukan-generik/"
FB_TITLE = "10 Channel Digital Marketing Rumah Sakit yang Wajib Dikelola di Era Online"
FB_URL = ("https://studiosoft.id/"
          "10-channel-digital-marketing-rumah-sakit-yang-wajib-dikelola-di-era-online/")
HASHTAG_LINE = "#studiosoft #rumahsakit #klinik #digitalmarketing #seolokal"

MEDIA_FILES = [
    "ss_hc65_lokalseo_01_cover.jpg",
    "ss_hc65_lokalseo_02_profil.jpg",
    "ss_hc65_lokalseo_03_konten.jpg",
    "ss_hc65_lokalseo_04_ukur.jpg",
    "ss_hc65_lokalseo_05_cta.jpg",
]
SRC_PNG = os.path.join(WS, "studiosoft_hc65_carousel")
MEDIA_URLS = [IMG_BASE_URL.rstrip("/") + "/openclaw-img/" + f for f in MEDIA_FILES]
INGRESS = ["103.84.155.153", "103.84.155.217"]

# Caption Threads dirangkai UTUH (tidak lewat build_safe) supaya tidak gagal diam-diam.
THREADS = (
    "Klinik dan bengkel di Surabaya sering kalah bukan karena kualitas, tetapi karena "
    "tidak muncul saat orang mencari di Google.\n\n"
    "Lima langkahnya: rapikan Google Business Profile, incar kata kunci long-tail, konten "
    "hyperlocal, citation dan backlink lokal, pantau lewat Analytics.\n\n"
    "Selengkapnya: " + ARTICLE_URL + "\n\n"
    "Menurut Anda, mana yang paling sering dilewatkan: profil Google atau konten? "
    "Yuk diskusi di komentar."
)

LI_CAPTION = (
    "Dua orang memakai alat AI yang sama. Yang satu mendapat lima caption yang bisa langsung "
    "dipakai. Yang satu lagi menunggu tiga kali revisi, lalu menyerah dan menulis sendiri.\n\n"
    "Bedanya bukan alatnya, tetapi instruksinya.\n\n"
    "Dari pekerjaan kami membantu klien memakai AI untuk produksi konten, penyebabnya hampir "
    "selalu sama: pertanyaannya terlalu umum. \u201cBuatkan caption Instagram\u201d tidak "
    "memberi petunjuk apa pun tentang siapa pembacanya, produk apa yang dijual, dan nada "
    "bicara yang diinginkan. AI lalu mengisi kekosongan itu dengan jawaban paling aman, dan "
    "jawaban paling aman selalu terdengar seragam.\n\n"
    "Empat unsur yang membuat prompt bekerja:\n\n"
    "Peran. Tentukan siapa yang Anda ajak bicara, misalnya copywriter yang menulis untuk "
    "pemilik bengkel di Surabaya.\n\n"
    "Konteks. Sebutkan produk, pasar, dan tujuan yang diinginkan.\n\n"
    "Batasan. Beri aturan jumlah kata, istilah yang dilarang, dan hal yang wajib ada.\n\n"
    "Format. Jelaskan bentuk keluaran, misalnya lima pilihan maksimal 120 karakter plus "
    "ajakan menghubungi.\n\n"
    "Satu kebiasaan yang dampaknya paling besar: simpan prompt yang berhasil sebagai templat. "
    "Catat nama tugas, prompt yang dipakai, dan tautan hasil terbaiknya. Setelah sebulan, "
    "pustaka prompt Anda sendiri jauh lebih berguna daripada kumpulan prompt umum di "
    "internet.\n\n"
    "Satu hal yang jarang disadari: keluaran pertama bukan hasil akhir. AI menyusun draft, "
    "Anda yang memutuskan.\n\n"
    "Bacaan lengkapnya: " + LI_URL
)

FB_BASE = (
    "Pasien yang baru pindah ke Surabaya dan butuh dokter anak biasanya membuka Google, "
    "mengetik \u201crumah sakit ibu dan anak terdekat\u201d, lalu memilih dari beberapa nama "
    "yang muncul. Rumah sakit yang konsisten merawat kanal digitalnya ada di daftar itu. "
    "Yang tidak, tidak.\n\n"
    "Di era online, pasien tidak lagi hanya mengandalkan rekomendasi tetangga atau brosur. "
    "Mereka mencari di Google, membaca ulasan, membuka media sosial, lalu baru memutuskan. "
    "Kanal digital bukan sekadar dimiliki, tetapi dikelola.\n\n"
    "Tiga yang paling sering diabaikan:\n\n"
    "Website resmi. Profil, layanan, jadwal dokter, dan tarif harus jelas, dan tetap nyaman "
    "dibuka dari ponsel.\n\n"
    "Google Business Profile. Alamat, jam operasional, foto fasilitas, serta balasan untuk "
    "setiap ulasan pasien.\n\n"
    "Media sosial. Konten edukasi kesehatan dan informasi jadwal dokter yang dibagikan "
    "konsisten, bukan hanya saat ada promo.\n\n"
    "Kanal lain seperti SEO, YouTube, dan email tetap penting, tetapi tiga di atas adalah "
    "fondasinya. Rumah sakit yang rapi di fondasinya biasanya lebih dipercaya sejak sebelum "
    "pasien datang.\n\n"
    "Bacaan lengkapnya: " + FB_URL
)

CTA_RE = re.compile(r"komentar|pendapat|diskusi|setuju|share|pengalaman|menurut|bagaimana|apa yang|komen|tanggapan|pandangan", re.I)
CTA_STRICT = re.compile(r"komentar|diskusi|pendapat|tulis di|share|pengalaman|menurut anda|setuju", re.I)
BAN_ANY = re.compile(r"sembuh total|dijamin sembuh", re.I)
CURATED_LI = {
    "netral": "Menurut Anda, bagian mana yang lebih menentukan hasil AI: pilihan alatnya atau "
              "kejelasan instruksi yang Anda berikan? Yuk diskusi di komentar.",
    "opini": "Bold statement: AI tidak membuat hasil generik, instruksi yang malas yang "
             "membuatnya. Setuju atau tidak?",
}
CURATED_FB = {
    "netral": "Menurut Anda, mana yang lebih dulu dibuka calon pasien: profil Google rumah "
              "sakit atau media sosialnya? Yuk diskusi di komentar.",
    "opini": "Bold statement: rumah sakit yang websitenya berat dibuka dari ponsel sudah "
             "kehilangan pasien sebelum mereka sempat menelepon. Setuju atau tidak?",
}


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(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_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
    return build(base, platform, industri, tones, limit=limit, ban=ban)


assert len(THREADS) <= 500, "caption Threads > 500 (%d)" % len(THREADS)
LI = build_safe(LI_CAPTION, "linkedin", "digital_marketing", ["netral", "opini"], curated=CURATED_LI)
FB = build_safe(FB_BASE, "facebook", "kesehatan", ["netral", "opini"], ban=BAN_ANY, curated=CURATED_FB)

REPLY1 = ("Baca selengkapnya: %s\n\nButuh partner digital marketing untuk klinik atau rumah sakit "
          "Anda?\nwww.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 re.search(r"[\u3000-\u9fff\uff00-\uffef]", txt):
        print("  \u274c %s: ada karakter CJK" % name)
        ok = False
    if require_cta and not CTA_STRICT.search(txt[-160:]):
        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():
    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", "success")):
    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_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 next_free_li():
    """7-slot grid LI (05/08/11/14/17/20/23 WIB)."""
    busy = busy_by_account(ACC.STUDIOSOFT_LINKEDIN)
    now = datetime.now(timezone.utc) + timedelta(minutes=30)
    now_naive = now.replace(tzinfo=None)
    today = datetime.now(timezone.utc)
    cands = []
    for day in range(0, 25):
        d0 = datetime(today.year, today.month, today.day, tzinfo=timezone.utc) + timedelta(days=day)
        for h in (1, 4, 7, 10, 13, 16):   # 08/11/14/17/20/23 WIB (lewati 05:00/22:00Z)
            dt = d0 + timedelta(hours=h)
            if dt.replace(tzinfo=None) <= now_naive:
                continue
            cand = dt.strftime("%Y-%m-%dT%H:%M:%S")
            if cand not in busy:
                cands.append(cand)
    cands.sort()
    if not cands:
        return None, None
    cand = cands[0]
    lab = (datetime.fromisoformat(cand) + timedelta(hours=7)).strftime("%d %b %Y %H:%M WIB")
    return cand + ".000Z", lab


def prep_media():
    ok = True
    for png in sorted(os.listdir(SRC_PNG)):
        if not (png.endswith(".png") and png.startswith("ss_hc65_lokalseo_")):
            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):
    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", "-4", "-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", "-4", "-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 append_catatan(wib, results, slot_label, fb_slot_label, li_slot_label):
    path = os.path.join(WS, "catatan", wib.strftime("%Y-%m-%d") + ".md")
    os.makedirs(os.path.dirname(path), exist_ok=True)
    block = ("\n\n## hc65 Healthcare StudioSoft (%s WIB)\n"
             "- Threads carousel (RE-AIR `tips-seo-digital-marketing-surabaya-optimasi-local...`): %s | album `%s` + 2 replies (`%s`, `%s`)\n"
             "- LinkedIn TEXT `%s` (%s) memakai artikel `%s`\n"
             "- FB TEXT `%s` (%s) memakai artikel `%s`\n"
             "- Artikel Threads: %s\n"
             % (wib.strftime("%Y-%m-%d %H:%M"), slot_label,
                results.get("threads"), results.get("reply1"), results.get("reply2"),
                results.get("linkedin"), li_slot_label, LI_URL,
                results.get("facebook"), fb_slot_label, FB_URL, ARTICLE_URL))
    with open(path, "a") as f:
        f.write(block)
    print("  catatan -> %s" % path)


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 lama sudah dihapus dari website):")
    for u in (ARTICLE_URL, FB_URL, LI_URL):
        if not verify_link(u):
            print("  \u274c link tidak 200 - 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 - batal.")
        return False
    print("  \u2705 carousel Threads: %s (%s)" % (SLOT_LABEL, SLOT_UTC))
    FB_SLOT_UTC, FB_SLOT_LABEL = next_free_fb()
    if not FB_SLOT_UTC:
        print("  \u274c slot FB tidak ditemukan - batal.")
        return False
    print("  \u2705 Facebook text (tanpa media, replies=[]): %s (%s)" % (FB_SLOT_LABEL, FB_SLOT_UTC))
    LI_SLOT_UTC, LI_SLOT_LABEL = next_free_li()
    if not LI_SLOT_UTC:
        print("  \u274c slot LinkedIn tidak ditemukan - batal.")
        return False
    print("  \u2705 LinkedIn text (tanpa media, replies=[]): %s (%s)" % (LI_SLOT_LABEL, LI_SLOT_UTC))

    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 - batal.")
        return False
    print("  --- Threads (%d) ---\n%s\n  --- LinkedIn (%d) ---\n%s\n  --- FB (%d) ---\n%s"
          % (len(THREADS), THREADS, len(LI), LI, len(FB), FB))

    print("\n3) IDEMPOTENSI (media hc65 sudah pernah dijadwalkan?):")
    hits = scan_media()
    if hits:
        print("  \u26a0\ufe0f sudah ada jadwal dgn media hc65 -> 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 - batal.")
        return False

    print("\n5) JALUR INTERNET (2 ingress):")
    if not verify_urls():
        print("  \u274c ada media tidak 200 - 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 - 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)
        results["threads"] = pid
        res1 = c.create_schedule(ACC.STUDIOSOFT_THREADS, REPLY1, r2, type_="text", replies=[pid])
        results["reply1"] = res1.get("scheduleId") or res1.get("_id") or res1.get("id")
        res2 = c.create_schedule(ACC.STUDIOSOFT_THREADS, REPLY2, r3, type_="text", replies=[pid])
        results["reply2"] = res2.get("scheduleId") or res2.get("_id") or res2.get("id")
        print("  2 replies OK (%s, %s)" % (results["reply1"], results["reply2"]))
    except Exception as e:
        print("  GAGAL %s" % e)
        results.setdefault("threads", None)

    print("\n   LINKEDIN (TEXT saja, TANPA media, replies=[], %s):" % LI_SLOT_LABEL)
    try:
        res = c.create_schedule(ACC.STUDIOSOFT_LINKEDIN, LI, LI_SLOT_UTC, title=LI_TITLE,
                                type_="text", medias=[], replies=[])
        sid = res.get("scheduleId") or res.get("_id") or res.get("id")
        print("  text -> %s" % sid)
        results["linkedin"] = sid
    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=[])
        sid = res.get("scheduleId") or res.get("_id") or res.get("id")
        print("  text -> %s" % sid)
        results["facebook"] = sid
    except Exception as e:
        print("  GAGAL %s" % e)
        results["facebook"] = None

    print("\n" + "=" * 55)
    ok = bool(results.get("threads")) and bool(results.get("facebook")) and bool(results.get("linkedin"))
    print("SEMUA PLATFORM TERJADWAL" if ok else "GAGAL")

    if ok:
        print("\n7) VERIFIKASI (fetch ULANG dari API per jadwal):")
        for name in ("threads", "reply1", "reply2", "linkedin", "facebook"):
            sid = results.get(name)
            if not sid:
                continue
            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 [])))

        st = json.loads(open(WS + "/studiosoft_healthcare_state.json").read())
        st["index"] = st.get("index", 60) + 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] hc65 (RE-AIR Threads: `tips-seo-digital-marketing-surabaya-optimasi-"
                "local-untuk-klinik-dan-bengkel-lokal` - jejak Threads terakhir 30 Sep 2026 "
                "02:01Z (hc50) = PALING TUA di antara artikel HC hidup yang TIDAK punya jadwal "
                "Threads mendatang; artikel HTTP 200; slide BARU ss_hc65_lokalseo_*) | %s | %s | "
                "Threads album %s (%s) + 2 replies (%s, %s) | LinkedIn TEXT \"%s\" %s memakai "
                "artikel `cara-bikin-prompt-ai-yang-bagus-bukan-generik` (non-HC, artikel "
                "TERBARU yang 0 hit di SEMUA jadwal LI) | FB TEXT \"%s\" (tanpa media, "
                "replies=[]) %s memakai artikel 10-channel-digital-marketing-rumah-sakit-yang-"
                "wajib-dikelola-di-era-online (jejak FB sukses terakhir 3 Okt 2026 13:00Z = "
                "PALING TUA di antara artikel HC hidup tanpa jadwal FB mendatang & BUKAN topik "
                "Threads hari ini) | catatan: TIDAK ada artikel healthcare baru (post terbaru "
                "13 Okt `analitik-properti` = non-HC); 7 artikel HC lama MASIH 404 -> hanya "
                "artikel terverifikasi 200 yang dipakai. state index -> %d.\n"
                % (wib.strftime("%Y-%m-%d %H:%M"), ARTICLE_URL, SLOT_LABEL, SLOT_UTC,
                   results["threads"], results["reply1"], results["reply2"],
                   LI_TITLE, LI_SLOT_LABEL, FB_TITLE, FB_SLOT_LABEL, st["index"]))

        append_catatan(wib, results, SLOT_LABEL, FB_SLOT_LABEL, LI_SLOT_LABEL)
    return ok


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