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

Topik Threads : "Panduan SEO untuk Klinik dan Rumah Sakit: Biar Mudah Ditemukan Pasien di Google"
                (artikel https://studiosoft.id/panduan-seo-klinik-rumah-sakit/ — audit 2.834
                 jadwal Repliz 21 Sep: TIDAK PERNAH ada di Threads SS -> topik BARU, bukan re-air)
                -> carousel BARU ss_hc42_seoklinik_*.
LinkedIn      : TEXT-only, artikel `iklan-facebook-rumah-sakit` (status FREE di LinkedIn SS —
                tidak ada jadwal apa pun, audit 21 Sep).
Facebook      : TEXT-only tanpa media, replies=[], artikel `google-business-profile-klinik`
                (di FB SS hanya pernah ERROR 29 Agu -> belum pernah tayang sebagai teks).

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, ada 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 = "Panduan SEO untuk Klinik dan Rumah Sakit: Biar Mudah Ditemukan Pasien di Google"
ARTICLE_URL = "https://studiosoft.id/panduan-seo-klinik-rumah-sakit/"
LI_TITLE = "Iklan Facebook & Instagram untuk Rumah Sakit: Etis, Efektif, dan Sesuai Regulasi"
LI_URL = "https://studiosoft.id/iklan-facebook-rumah-sakit/"
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 #seoklinik #rumahsakit #klinik #kesehatan"

MEDIA_FILES = [
    "ss_hc42_seoklinik_01_cover.jpg",
    "ss_hc42_seoklinik_02_keyword.jpg",
    "ss_hc42_seoklinik_03_local.jpg",
    "ss_hc42_seoklinik_04_teknis.jpg",
    "ss_hc42_seoklinik_05_cta.jpg",
]
SRC_PNG = os.path.join(WS, "studiosoft_hc42_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 baru pindah rumah dan butuh dokter kulit terdekat. Ia buka Google, ketik klinik kulit Surabaya, lalu pilih satu dari 10 hasil pertama.

SEO menentukan siapa yang muncul di situ. RS dan klinik yang rajin optimasi dapat pasien baru tiap hari \u2014 tanpa bayar per klik.

""" + ARTICLE_URL

LI_BASE = """Pertanyaan yang paling sering saya dengar dari manajemen RS dan klinik: \u201cboleh tidak sih pasang iklan layanan kesehatan di Facebook dan Instagram?\u201d

Boleh. Asal tahu aturan mainnya.

Saya sering mendampingi RS dan klinik menjalankan iklan Meta, dan tantangannya memang khas. Di satu sisi platform ini punya jangkauan luar biasa. Di sisi lain ada etika kedokteran dan kebijakan Meta yang ketat soal klaim kesehatan.

Empat hal yang tidak boleh dilakukan: klaim kesembuhan berlebihan, foto sebelum-sesudah yang sensasional tanpa disclaimer, testimoni tanpa izin pasien, dan promosi obat resep. Konten yang menakut-nakuti pasien juga masuk daftar ini.

Yang bisa dilakukan justru lebih banyak. Iklan edukasi yang informatif, promosi layanan dengan narasi netral seperti \u201clayanan konsultasi kulit dibuka setiap Sabtu\u201d, iklan brand awareness yang membangun citra RS sebagai institusi terpercaya, sampai retargeting untuk pasien yang sudah membuka website tapi belum booking.

Kekuatan Meta Ads ada di targeting yang spesifik. Iklan layanan kandungan bisa diarahkan ke perempuan usia 24-40 di radius 15 km dari RS. Iklan medical checkup ke usia 35-60 dengan minat kesehatan. Iklan layanan kulit ke usia 18-35 yang tertarik perawatan kulit.

Soal budget, tidak perlu langsung jutaan. Rp50.000 sampai Rp100.000 per hari sudah cukup untuk menguji kreatif mana yang paling efektif. Setelah terbukti, baru di-scale up, dan setiap rupiah dipertanggungjawabkan dengan data.

Bacaan lengkapnya: """ + LI_URL

FB_BASE = """Aset digital paling underrated untuk klinik: Google Business Profile.

Gratis, mudah dioptimasi, dan efeknya langsung terasa. Pasien yang mencari klinik gigi terdekat melihat profil klinik Anda lengkap dengan rating, jam buka, dan tombol petunjuk arah dalam satu klik.

Lima langkah optimasinya.

Pertama, verifikasi kepemilikan akun. Kedua, isi data dengan lengkap dan akurat: nama persis, alamat, jam operasional termasuk akhir pekan, nomor telepon, dan website. Ketiga, pilih kategori yang tepat, misal Hospital, Clinic, Dentist, atau Skin Care Clinic. Keempat, unggah minimal 20 foto berkualitas: eksterior, ruang tunggu, tim, dan peralatan. Kelima, posting rutin pengumuman layanan atau tips kesehatan.

Yang tak kalah penting: kelola review. Minta pasien yang puas meninggalkan ulasan, bisa lewat QR code di meja resepsionis. Tanggapi semua review dalam 24 jam, termasuk yang negatif \u2014 jawab sopan, akui keluhan, tawarkan solusi di luar publik. Dan jangan pernah membeli review palsu.

Satu klien klinik gigi di Surabaya mengalami kenaikan permintaan petunjuk arah hingga 40% dalam 3 bulan setelah profilnya dioptimasi. Review naik dari 12 menjadi 87 dengan rating terjaga 4.6.

GBP bukan aset pelengkap. Ia mesin penghasil pasien yang nyata. Bacaan lengkapnya: """ + FB_URL

CTA_RE = re.compile(r"komentar|pendapat|diskusi|setuju|share|pengalaman|menurut|bagaimana|apa yang|komen|tanggapan|pandangan", re.I)
BAN_RE = re.compile(r"kecantikan|skincare|jerawat|sembuh total", re.I)
# Pancingan diskusi KURASI (on-topic: sudut pandang manajemen RS/klinik). Bauran run ini:
# Threads netral + LinkedIn opini + FB netral (2 netral : 1 opini ~ 60/25).
CURATED_T = {
    "netral": "Menurut Anda, apa yang paling menentukan klinik muncul di halaman pertama Google? Yuk diskusi di komentar.",
    "opini": "Bold statement: RS yang rajin SEO lokal bisa mengalahkan RS besar di radius tertentu. Setuju atau tidak?",
    "sanjungan": "Buat yang websitenya sudah rapi dan mudah ditemukan, Anda selangkah lebih maju! \U0001f44f",
}
CURATED_LI = {
    "opini": "Bold statement: budget iklan kecil yang tepat sasaran mengalahkan budget besar tanpa data. Setuju atau tidak?",
    "netral": "Menurut Anda, sejauh mana RS atau klinik Anda memakai iklan Meta? Yuk diskusi di komentar.",
    "sanjungan": "Buat tim yang iklannya sudah jalan tanpa melanggar regulasi, Anda selangkah lebih maju! \U0001f44f",
}
CURATED_FB = {
    "netral": "Menurut Anda, langkah optimasi GBP mana yang paling sering dilewatkan klinik? Yuk diskusi di komentar.",
    "opini": "Bold statement: Google Business Profile itu mesin pasien, bukan aset pelengkap. Setuju atau tidak?",
    "sanjungan": "Buat yang review kliniknya sudah dikelola 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_RE,
                     curated=CURATED_T)
LI = build_safe(LI_BASE, "linkedin", "rumah_sakit", ["opini", "netral"], ban=BAN_RE, curated=CURATED_LI)
FB = build_safe(FB_BASE, "facebook", "rumah_sakit", ["netral", "opini"], ban=BAN_RE, curated=CURATED_FB)

REPLY1 = ("Baca selengkapnya: %s\n\nButuh partner SEO 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 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_hc42_seoklinik_")):
            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("\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 hc42 sudah pernah dijadwalkan?):")
    hits = scan_media(("pending", "scheduled", "draft", "success"))
    if hits:
        print("  \u26a0\ufe0f sudah ada jadwal dgn media hc42 -> 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

    print("\n6) LINK ARTIKEL:")
    for u in (ARTICLE_URL, LI_URL, FB_URL):
        if not verify_link(u):
            print("  \u274c link 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("\n7) 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("\n8) 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", 37) + 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] hc42 (TOPIK BARU di Threads: panduan SEO untuk klinik/RS \u2014 0 jadwal "
                    "di Threads SS, jadi bukan re-air) | %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 (WP REST 21 Sep: artikel "
                    "terbaru 20 Sep `jasa-branding-surabaya-membangun-identitas-brand-kuat` = "
                    "non-healthcare); audit 2.834 jadwal Repliz -> `panduan-seo-klinik-rumah-sakit` "
                    "BELUM PERNAH di Threads SS -> carousel BARU ss_hc42_seoklinik_* + engine "
                    "ss_carousel_lib (bg #122B63); LinkedIn pakai artikel iklan-facebook-rumah-sakit "
                    "(FREE di LI SS); FB pakai artikel google-business-profile-klinik (di FB SS hanya "
                    "pernah ERROR 29 Agu). CATATAN PENTING: album Threads hc41 (20 Sep, "
                    "ss_hc41_analytics_*) GAGAL lagi dgn 'media could not be fetched' walau media "
                    "200 di kedua ingress -> error intermiten Repliz, topik data-analytics masih "
                    "belum tayang bentuk carousel. 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)
