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

Topik Threads : "Kenapa Website Rumah Sakit Sering Gagal Menghasilkan Pasien Baru?"
                https://studiosoft.id/website-rumah-sakit-gagal-pasien/
                (WP id 9504, terbit ulang 14 Okt 2026 12:18 WIB, HTTP 200) RE-AIR —
                jejak Threads terakhir 17 Sep 2026 02:01Z = PALING TUA di antara artikel
                HC hidup tanpa jadwal Threads mendatang. Slide BARU ss_hc68_websigagal_*.
LinkedIn      : TEXT storytelling artikel `biaya-digital-agency-surabaya`
                ("Berapa Biaya Digital Agency di Surabaya? Rincian yang Perlu Anda Tahu",
                WP id 9288, terbit 16 Okt 2026 08:00 WIB) — artikel TERBARU yang
                0 hit di SEMUA jadwal THR/LI/FB/IG StudioSoft.
Facebook      : TEXT-only tanpa media, replies=[], artikel `cara-klinik-kecantikan-di-
                surabaya-bisa-ditemukan-lebih-cepat-di-google` — jejak FB sukses terakhir
                8 Okt 2026 12:30Z = PALING TUA di antara artikel HC hidup tanpa jadwal FB
                mendatang & BUKAN topik Threads hari ini.

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 = "Kenapa Website Rumah Sakit Sering Gagal Menghasilkan Pasien Baru?"
ARTICLE_URL = "https://studiosoft.id/website-rumah-sakit-gagal-pasien/"
LI_TITLE = "Berapa Biaya Digital Agency di Surabaya? Rincian yang Perlu Anda Tahu"
LI_URL = "https://studiosoft.id/biaya-digital-agency-surabaya/"
FB_TITLE = "Cara Klinik Kecantikan di Surabaya Bisa Ditemukan Lebih Cepat di Google"
FB_URL = ("https://studiosoft.id/"
          "cara-klinik-kecantikan-di-surabaya-bisa-ditemukan-lebih-cepat-di-google/")
HASHTAG_LINE = "#studiosoft #rumahsakit #klinik #digitalmarketingrumahsakit #websiterumahsakit"

MEDIA_FILES = [
    "ss_hc68_websigagal_01_cover.jpg",
    "ss_hc68_websigagal_02_keluhan.jpg",
    "ss_hc68_websigagal_03_sebab.jpg",
    "ss_hc68_websigagal_04_periksa.jpg",
    "ss_hc68_websigagal_05_cta.jpg",
]
SRC_PNG = os.path.join(WS, "studiosoft_hc68_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 = (
    "Banyak rumah sakit punya situs yang tampak rapi dan lengkap. Namun pasien baru dari "
    "situs itu tidak sebanding dengan pekerjaan yang dikeluarkan.\n\n"
    "Kegagalannya jarang karena desain. Yang paling sering mendasar: halaman layanan tidak "
    "menjelaskan apa pun, jalur menghubungi tersembunyi, dan situs lambat di ponsel.\n\n"
    "Selengkapnya: " + ARTICLE_URL + "\n\n"
    "Menurut Anda, mana yang lebih sering diabaikan: isi halaman layanan atau kemudahan "
    "menghubungi? Yuk diskusi."
)

LI_CAPTION = (
    "Pertanyaan pertama saat pemilik usaha menghubungi agensi hampir selalu soal biaya. "
    "Jawaban yang jujur justru terasa mengecewakan: angka itu tidak bisa diringkas jadi "
    "satu nilai.\n\n"
    "Agensi yang langsung menyebut angka tanpa bertanya apa pun belum tahu pekerjaan apa "
    "yang akan dikerjakan. Dua penawaran dengan tulisan pekerjaan yang sama persis bisa "
    "berbeda dua kali lipat.\n\n"
    "Pertama, siapa yang benar-benar mengerjakan. Satu orang yang sekaligus menulis, "
    "mengatur iklan, dan membuat laporan berbeda biayanya dengan tim yang berbagi tugas. "
    "Kedua, jumlah kanal yang digarap: Google saja jauh lebih ringan daripada Google "
    "ditambah Instagram, TikTok, YouTube, dan iklan berbayar.\n\n"
    "Ketiga, dan ini yang paling sering disalahpahami: uang belanja iklan selalu terpisah "
    "dari jasa pengelolaan. Menyetor Rp5 juta ke agensi bukan berarti seluruhnya menjadi "
    "tayangan iklan.\n\n"
    "Keempat, apa yang dipinjamkan dan apa yang jadi milik Anda. Akun Google Ads, "
    "Analytics, dan akses situs seharusnya tetap atas nama bisnis Anda. Kalau semuanya "
    "disimpan di akun agensi, Anda sulit pindah, dan itu berpengaruh ke harga saat kontrak "
    "diperpanjang.\n\n"
    "Kelima, lama kontrak dan isi laporan. Pekerjaan SEO yang jujur butuh tiga sampai enam "
    "bulan sebelum terlihat hasilnya, jadi tarif murah per bulan belum tentu lebih "
    "hemat.\n\n"
    "Yang paling sering luput justru pos kecil: langganan alat riset, produksi konten di "
    "luar kuota, perawatan situs, sampai ketentuan sebelum berhenti. Selisih harga "
    "penawaran dan uang yang keluar dari rekening biasanya dari sana.\n\n"
    "Bacaan lengkapnya: " + LI_URL
)

FB_BASE = (
    "Dua klinik kecantikan di Surabaya bisa punya layanan yang mirip, tetapi hanya satu "
    "yang muncul saat orang mengetik \"klinik kecantikan terdekat\".\n\n"
    "Kenapa bisa begitu? Karena yang menentukan bukan kecantikan situsnya, melainkan apakah "
    "klinik itu siap ditemukan: profil Google yang lengkap, kata kunci yang dipakai orang "
    "sungguhan, dan situs yang cepat dibuka dari ponsel.\n\n"
    "Yang paling sering terlewat justru langkah termurah: melengkapi profil bisnis dengan "
    "foto, jam buka, layanan, dan alamat yang sama persis dengan yang ada di situs. Ketika "
    "datanya cocok, mesin pencari tidak ragu menampilkan klinik Anda di peta.\n\n"
    "Bacaan lengkapnya: " + FB_URL
)

CTA_RE = re.compile(r"komentar|pendapat|diskusi|setuju|share|pengalaman|menurut|bagaimana|"
                    r"apa yang|komen|tanggapan|pandangan", re.I)
CTA_STRICT = re.compile(r"komentar|diskusi|pendapat|tulis di|share|pengalaman|menurut anda|"
                        r"setuju", re.I)
BAN_ANY = re.compile(r"sembuh total|dijamin sembuh|300%", re.I)
CURATED_LI = {
    "netral": "Menurut Anda, pos mana yang paling sering tidak disebut di penawaran agensi "
              "padahal ikut menambah pengeluaran? Yuk diskusi di komentar.",
    "opini": "Bold statement: agensi yang menolak memberi Anda akses ke akun dan data Anda "
             "sendiri sebaiknya tidak dipilih sejak awal. Setuju atau tidak?",
}
CURATED_FB = {
    "netral": "Menurut Anda, seberapa besar pengaruh profil Google yang lengkap terhadap "
              "keputusan pasien memilih klinik? Yuk diskusi di komentar.",
    "opini": "Bold statement: klinik yang layanannya bagus tetap kalah kalau informasinya "
             "sulit ditemukan di Google. 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_hc68_websigagal_")):
            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## hc68 Healthcare StudioSoft (%s WIB)\n"
             "- Threads carousel (RE-AIR `website-rumah-sakit-gagal-pasien`): %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):")
    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 hc68 sudah pernah dijadwalkan?):")
    hits = scan_media()
    if hits:
        print("  \u26a0\ufe0f sudah ada jadwal dgn media hc68 -> 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.")
        print("  slot Threads=%s FB=%s LI=%s" % (SLOT_UTC, FB_SLOT_UTC, LI_SLOT_UTC))
        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", 63) + 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] hc68 (RE-AIR Threads: `website-rumah-sakit-gagal-pasien` "
                "- jejak Threads terakhir 17 Sep 2026 02:01Z = PALING TUA di antara artikel HC "
                "hidup tanpa jadwal Threads mendatang; artikel HTTP 200 (WP id 9504, terbit ulang "
                "14 Okt 2026 12:18 WIB); slide BARU ss_hc68_websigagal_*) | %s | %s | Threads album "
                "%s (%s) + 2 replies (%s, %s) | LinkedIn TEXT \"%s\" %s memakai artikel "
                "`biaya-digital-agency-surabaya` (non-HC, artikel TERBARU 16 Okt 2026 "
                "yang 0 hit di SEMUA jadwal THR/LI/FB/IG) | FB TEXT \"%s\" (tanpa media, "
                "replies=[]) %s memakai artikel cara-klinik-kecantikan-di-surabaya-bisa-ditemukan-"
                "lebih-cepat-di-google (jejak FB sukses terakhir 8 Okt 2026 12:30Z = PALING TUA "
                "di antara artikel HC hidup tanpa jadwal FB mendatang & BUKAN topik Threads hari "
                "ini) | catatan: TIDAK ada artikel healthcare baru (post terbaru 16 Okt "
                "`biaya-digital-agency-surabaya` = non-HC) -> 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)
