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

Topik Threads : "10 Channel Digital Marketing Rumah Sakit yang Wajib Dikelola di Era Online"
                (https://studiosoft.id/10-channel-digital-marketing-rumah-sakit-yang-wajib-
                 dikelola-di-era-online/ — RE-AIR: audit 27 Sep 2026 -> terakhir tayang di
                 Threads SS 13 Sep 2026 04:01Z (text) dan BELUM PERNAH jadi topik carousel
                 seri ini = PALING TUA di antara artikel HC yang MASIH hidup, setelah
                 7-strategi yang sudah dipakai hc47 27 Sep 02:00Z).
LinkedIn      : TEXT-only, artikel `cara-klinik-kecantikan-di-surabaya-bisa-ditemukan-lebih-
                cepat-di-google` (terakhir tayang di LinkedIn SS 18 Sep 2026 13:00Z = PALING
                TUA di antara artikel HC hidup di LI & TIDAK pending).
Facebook      : TEXT-only tanpa media, replies=[], artikel `tips-seo-digital-marketing-
                surabaya-optimasi-local-untuk-klinik-dan-bengkel-lokal` (terakhir tayang di
                FB SS 13 Sep 2026 07:00Z = PALING TUA di antara artikel HC hidup di FB yang
                TIDAK sedang tayang sbg topik Threads).

TEMUAN: stok artikel healthcare BARU masih HABIS (artikel terbaru studiosoft.id
non-healthcare) dan 7 artikel HC seri ini MASIH 404 (dihapus): 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 MEMVERIFIKASI semua
link artikel (Threads/LI/FB) wajib HTTP 200.

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 = "10 Channel Digital Marketing Rumah Sakit yang Wajib Dikelola di Era Online"
ARTICLE_URL = ("https://studiosoft.id/10-channel-digital-marketing-rumah-sakit-yang-wajib-"
               "dikelola-di-era-online/")
LI_TITLE = "Cara Klinik Kecantikan di Surabaya Bisa Ditemukan Lebih Cepat di Google"
LI_URL = ("https://studiosoft.id/cara-klinik-kecantikan-di-surabaya-bisa-ditemukan-lebih-"
          "cepat-di-google/")
FB_TITLE = "Tips SEO Digital Marketing Surabaya: Optimasi Local untuk Klinik dan Bengkel Lokal"
FB_URL = ("https://studiosoft.id/tips-seo-digital-marketing-surabaya-optimasi-local-untuk-"
          "klinik-dan-bengkel-lokal/")
HASHTAG_LINE = "#studiosoft #digitalmarketingrumahsakit #kontenkesehatan #seolokal #rumahsakit"

MEDIA_FILES = [
    "ss_hc48_channel10_01_cover.jpg",
    "ss_hc48_channel10_02_awal.jpg",
    "ss_hc48_channel10_03_medsos.jpg",
    "ss_hc48_channel10_04_iklan.jpg",
    "ss_hc48_channel10_05_cta.jpg",
]
SRC_PNG = os.path.join(WS, "studiosoft_hc48_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 memilih rumah sakit dari brosur atau rekomendasi tetangga. Mereka mengetik di Google, membaca ulasan, lalu memutuskan.

Sepuluh channel wajib dikelola: website, SEO, Google Maps, medsos, video, email, WhatsApp, iklan, portal kesehatan, dan blog edukasi.

""" + ARTICLE_URL

LI_BASE = """Di Surabaya, klinik kecantikan bersaing ketat. Setiap hari ratusan orang mencari layanan facial, laser, atau anti-aging lewat Google. Namun hanya klinik yang muncul di lima hasil teratas yang benar-benar ditemukan lalu dipilih.

Visibilitas di Google bukan sekadar urusan teknis. Muncul di halaman pertama membentuk kesan kredibilitas sebelum calon pasien membaca satu kata pun soal treatment Anda. Klinik yang belum dioptimasi kehilangan peluang itu setiap hari.

Jalurnya cukup jelas dan tidak perlu dikerjakan sekaligus: rapikan Google Business Profile, riset kata kunci lokal yang tepat, pastikan website cepat dan nyaman di ponsel, lalu rawat dengan konten edukatif serta backlink dari sumber lokal.

Yang membedakan klinik yang tumbuh dan yang jalan di tempat biasanya bukan anggaran, tapi konsistensi merawat kanal digitalnya.

Bacaan lengkapnya: """ + LI_URL

FB_BASE = """Pelanggan di sekitar kita hampir selalu mulai dari Google: "klinik terdekat", "bengkel mobil terdekat". Sayangnya banyak usaha lokal masih kalah hanya karena informasi lokasinya tidak rapi di mata Google.

Local SEO bukan trik rumit. Pastikan nama, alamat, dan nomor telepon konsisten di semua tempat, isi Google Business Profile selengkap mungkin, buat halaman khusus per layanan, lalu minta ulasan pelanggan dengan cara yang sopan.

Bagian yang sering dilewatkan justru yang paling murah: membalas setiap ulasan dan menyebut area layanan secara spesifik, misalnya Krian, Kendangsari, atau HR Muhammad. Google membaca sinyal itu sebagai relevansi lokal.

Bacaan lengkapnya: """ + FB_URL

CTA_RE = re.compile(r"komentar|pendapat|diskusi|setuju|share|pengalaman|menurut|bagaimana|apa yang|komen|tanggapan|pandangan", re.I)
# Threads run ini bertopik rumah sakit -> "kecantikan" dilarang di caption Threads.
BAN_T = re.compile(r"kecantikan|sembuh total|dijamin sembuh", re.I)
BAN_ANY = re.compile(r"sembuh total|dijamin sembuh", re.I)
# Pancingan diskusi KURASI (on-topic). Bauran run ini: Threads netral + LinkedIn opini + FB netral.
CURATED_T = {
    "netral": "Menurut Anda, dari sepuluh channel itu mana yang paling sering diabaikan rumah sakit? Yuk diskusi di komentar.",
    "opini": "Bold statement: website dan profil Google lebih menentukan pasien baru daripada iklan berbayar. Setuju atau tidak?",
    "sanjungan": "Buat rumah sakit yang website dan konten edukasinya sudah rapi, Anda selangkah lebih maju! \U0001f44f",
}
CURATED_LI = {
    "opini": "Bold statement: bagi klinik kecantikan, tampil di halaman pertama Google lebih menentukan daripada besarnya anggaran iklan. Setuju atau tidak?",
    "netral": "Menurut Anda, klinik kecantikan lebih cepat tumbuh lewat SEO organik atau lewat iklan berbayar? Yuk diskusi di komentar.",
    "sanjungan": "Buat klinik yang halaman dan kontennya sudah menempel pada identitas brand, Anda selangkah lebih maju! \U0001f44f",
}
CURATED_FB = {
    "netral": "Menurut Anda, local SEO atau iklan berbayar yang lebih dulu mendatangkan pelanggan baru? Yuk diskusi di komentar.",
    "opini": "Bold statement: membalas ulasan pelanggan lebih mendatangkan pelanggan baru daripada menambah anggaran iklan. Setuju atau tidak?",
    "sanjungan": "Buat usaha lokal yang profil Google dan ulasannya sudah rapi, 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", "opini"], limit=500,
                     ban=BAN_T, curated=CURATED_T)
LI = build_safe(LI_BASE, "linkedin", "klinik", ["opini", "netral"], ban=BAN_ANY, curated=CURATED_LI)
FB = build_safe(FB_BASE, "facebook", "klinik", ["netral", "opini"], ban=BAN_ANY, 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_hc48_channel10_")):
            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):")
    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 hc48 sudah pernah dijadwalkan?):")
    hits = scan_media(("pending", "scheduled", "draft", "success"))
    if hits:
        print("  \u26a0\ufe0f sudah ada jadwal dgn media hc48 -> 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", 43) + 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] hc48 (RE-AIR Threads: 10 channel digital marketing rumah sakit "
                    "\u2014 terakhir tayang Threads 13 Sep 2026 04:01Z (text) & BELUM PERNAH jadi "
                    "topik carousel seri ini = PALING TUA di antara artikel HC yang MASIH hidup "
                    "setelah 7-strategi dipakai hc47; slide BARU ss_hc48_channel10_*) | %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 "
                    "masih HABIS & 7 artikel HC seri ini 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: "
                    "cara-klinik-kecantikan (terakhir di LI SS 18 Sep 2026 13:00Z, paling tua & "
                    "tidak pending). FB: tips-seo-digital-marketing-surabaya-local (terakhir di FB "
                    "SS 13 Sep 2026 07:00Z, paling tua & tidak sedang jadi topik Threads). 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)
