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

Topik Threads : "Jasa Digital Marketing RS: Cara Memilih Agensi Tepat"
                https://studiosoft.id/jasa-digital-marketing-rs/
                ARTIKEL BARU (publish 1 Okt 2026 08:00Z, WP id 9160).
                Audit hc53_audit.py: jejak Threads/LinkedIn/Facebook SS untuk artikel ini = 0
                di ketiga platform (belum pernah dipakai). Slide BARU ss_hc53_jasars_*.
LinkedIn      : TEXT-only, artikel YANG SAMA (baru, belum ada di antrean LI pending 68 jadwal).
Facebook      : TEXT-only tanpa media, replies=[], artikel `google-business-profile-klinik`
                (terakhir SUKSES di FB SS 21 Sep 2026 12:30Z = jejak FB paling tua di antara
                artikel HC hidup yang TIDAK punya jadwal FB mendatang).

TEMUAN: 7 artikel HC lama MASIH 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). Semua link artikel run ini DIVERIFIKASI HTTP 200 sebelum create.

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
from diskusi_engine import tambahkan_diskusi

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

TITLE = "Jasa Digital Marketing RS: Cara Memilih Agensi Tepat"
ARTICLE_URL = "https://studiosoft.id/jasa-digital-marketing-rs/"
LI_TITLE = "Jasa Digital Marketing RS: Cara Memilih Agensi Tepat"
LI_URL = ARTICLE_URL
FB_TITLE = "Google Business Profile untuk Klinik: Cara Biar Pasien Gampang Nemu Lokasi Anda"
FB_URL = "https://studiosoft.id/google-business-profile-klinik/"
HASHTAG_LINE = "#studiosoft #digitalmarketingrs #rumahsakit #kesehatan #marketingkesehatan"

MEDIA_FILES = [
    "ss_hc53_jasars_01_cover.jpg",
    "ss_hc53_jasars_02_tugas.jpg",
    "ss_hc53_jasars_03_tanda.jpg",
    "ss_hc53_jasars_04_pertanyaan.jpg",
    "ss_hc53_jasars_05_cta.jpg",
]
SRC_PNG = os.path.join(WS, "studiosoft_hc53_carousel")
MEDIA_URLS = [IMG_BASE_URL.rstrip("/") + "/openclaw-img/" + f for f in MEDIA_FILES]
INGRESS = ["103.84.155.153", "103.84.155.217"]

CURATED_T_HOOK = "Menurut Anda, mana yang paling penting? Yuk diskusi di komentar."

THREADS_BASE = """Memilih jasa digital marketing untuk rumah sakit tidak sama dengan memilih agensi untuk bisnis biasa. Ada batas etika promosi kesehatan dan data pasien yang wajib dilindungi.

Tugas agensi bukan membuat rumah sakit viral, tapi memastikan orang yang mencari layanan menemukan RS yang tepat.

Pertanyaan penting: siapa yang memverifikasi kebenaran medis sebelum konten tayang?

""" + ARTICLE_URL + "\n\n" + CURATED_T_HOOK

LI_BASE = """Saya beberapa kali diminta menilai kerja agensi digital untuk klien di sektor kesehatan, dan polanya selalu mirip: masalahnya jarang soal kreativitas, lebih sering soal pemahaman batasan.

Memilih jasa digital marketing untuk rumah sakit tidak bisa disamakan dengan memilih agensi untuk bisnis biasa. Ada etika promosi kesehatan, ada data pasien yang harus dilindungi, dan ada istilah medis yang tidak boleh disederhanakan sembarangan.

Hal pertama yang perlu diluruskan: tugas agensi rumah sakit bukan membuat konten viral. Tugasnya memastikan orang yang sedang mencari layanan kesehatan menemukan rumah sakit yang tepat, lalu memudahkan mereka mengambil langkah berikutnya. Riset kata kunci kesehatan, optimasi halaman layanan, konten edukasi yang tetap etis, pengelolaan reputasi, dan pengukuran yang menghubungkan aktivitas digital dengan jumlah pasien. Tidak ada satu pun bagian itu yang berbunyi bikin konten viral.

Cara paling cepat menilai agensi adalah mendengar pertanyaannya. Agensi yang paham industri kesehatan akan bertanya soal verifikasi medis sejak awal: siapa yang memeriksa kebenaran isi sebelum tayang, dan bagaimana kalau ada koreksi. Mereka juga berani menyebut batasan, misalnya tidak menjanjikan kesembuhan dan tidak memakai foto pasien tanpa izin tertulis.

Sebelum menandatangani, pastikan domain, akun iklan, dan properti analitik tetap atas nama rumah sakit. Itu pertanyaan yang paling sering terlupa, padahal paling melindungi.

""" + LI_URL

FB_BASE = """Banyak klinik masih menganggap pasien datang dari iklan atau dari orang ke orang. Padahal salah satu pintu masuk paling murah justru peta: orang mengetik nama layanan ditambah kata dekat saya, lalu memilih yang muncul paling lengkap.

Di situ letak pentingnya Google Business Profile untuk klinik. Foto yang terbaru, jam operasional yang benar, nomor yang bisa langsung diklik, dan lokasi yang akurat sering menentukan apakah calon pasien jadi menelepon atau lanjut mencari yang lain. Ulasan juga berpengaruh: satu keluhan yang dibiarkan menggantung bisa lebih merugikan daripada tidak punya profil sama sekali.

Yang saya sarankan sebelum menambah anggaran iklan: rapikan dulu profil lokasinya. Pastikan alamat dan jam praktik konsisten di semua kanal, minta pasien yang puas memberi ulasan, dan jawab setiap masukan dengan sopan. Perbaikan ini sering terasa lebih cepat daripada kampanye berbayar.

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_RE punya FALSE POSITIVE: "siapa yang" mengandung "apa yang" (kejadian hc53, 2 Okt 2026).
# Untuk VALIDASI caption final pakai CTA_STRICT (tidak memuat pola `apa yang`).
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_T = {
    "netral": "Menurut Anda, bagian mana yang paling sering terlupa saat memilih agensi untuk rumah sakit? Yuk diskusi di komentar.",
    "opini": "Bold statement: agensi yang tidak pernah menyebut batasan etika kesehatan biasanya belum pernah menangani klien rumah sakit. Setuju atau tidak?",
    "sanjungan": "Buat tim rumah sakit yang sudah punya alur verifikasi konten yang rapi, Anda selangkah lebih maju! \U0001f44f",
}
CURATED_LI = {
    "netral": "Menurut Anda, apa yang lebih menentukan saat memilih agensi untuk rumah sakit: portofolio visualnya, atau pemahaman mereka soal batasan etika? Yuk diskusi di komentar.",
    "opini": "Bold statement: agensi yang hebat untuk produk konsumen belum tentu cocok menangani rumah sakit, karena aturan mainnya berbeda. Setuju atau tidak?",
}
CURATED_FB = {
    "netral": "Menurut Anda, apa bagian profil lokasi klinik yang paling sering diabaikan? Yuk diskusi di komentar.",
    "opini": "Bold statement: memperbaiki profil lokasi di peta sering lebih cepat mendatangkan pasien daripada menambah anggaran iklan. Setuju atau tidak?",
}


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 = THREADS_BASE   # caption final sudah memuat pancingan diskusi + link (490 char)
assert len(THREADS) <= 500, "caption Threads > 500"
# ⚠️ JANGAN lewat build_safe(): base sudah 490 char -> tambahkan_diskusi() mengembalikan caption
# APA ADANYA (menolak nambah hook) -> caption lolos tanpa ajakan komentar (kejadian 2 Okt 2026).
LI = build_safe(LI_BASE, "linkedin", "rumah_sakit", ["netral", "opini"], ban=BAN_ANY,
                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")):
    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_hc53_jasars_")):
            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", "-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 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):
        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))
    LI_SLOT_UTC, LI_SLOT_LABEL = next_free_li()
    if not LI_SLOT_UTC:
        print("  \u274c tidak ada slot LinkedIn bebas - batal.")
        return False
    print("  \u2705 LinkedIn text: %s (%s)" % (LI_SLOT_LABEL, LI_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))

    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("\n3) IDEMPOTENSI (media hc53 sudah pernah dijadwalkan?):")
    hits = scan_media(("pending", "scheduled", "draft", "success"))
    if hits:
        print("  \u26a0\ufe0f sudah ada jadwal dgn media hc53 -> 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)
        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=[])
        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 = 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", 48) + 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] hc53 (TOPIK BARU Threads: artikel healthcare BARU `jasa-digital-"
                    "marketing-rs` (publish 1 Okt 2026 08:00Z, WP id 9160) - jejak Threads/"
                    "LinkedIn/FB SS = 0 di ketiga platform; slide BARU ss_hc53_jasars_*) | %s | %s | "
                    "Threads album %s (%s) + 2 replies | LinkedIn TEXT \"%s\" %s (%s) | FB TEXT "
                    "\"%s\" (tanpa media, replies=[]) %s | catatan: 2 hari berturut ada artikel "
                    "healthcare BARU (30 Sep `pembuatan-website-rumah-sakit`, 1 Okt `jasa-digital-"
                    "marketing-rs`); 7 artikel HC lama MASIH 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) "
                    "-> hanya artikel terverifikasi 200 yang dipakai. LinkedIn TIDAK SKIP: artikel "
                    "baru ini belum ada di antrean LI pending (68 jadwal, penuh s/d 11 Okt 07:00Z). "
                    "FB: %s (terakhir SUKSES di FB SS 21 Sep 2026 12:30Z = jejak FB paling tua di "
                    "antara artikel HC hidup yang tidak punya jadwal FB mendatang). 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[11:16] + "Z",
                       FB_TITLE, FB_SLOT_LABEL, FB_TITLE, st["index"]))
        print("Log + state ditulis.")
    return ok


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