// FILE: _St/IG_StYellow/side.js | ROLE: Side UI·정렬·자동실행·다운로드 조립
(() => {

  // IGblue DEBUG v36: set IG_DBG_ON=0 or put `return;` at top of dbg() to disable all logs.
  const IG_DBG_ON = 1;
  function dbg(fn, loc, line, value) {
    // return; // DEBUG OFF: uncomment this one line later.
    if (!IG_DBG_ON) return;
    try {
      const clip = (v) => {
        const seen = new WeakSet();
        let s = JSON.stringify(v, (k, val) => {
          if (typeof val === "string") return val.length > 500 ? val.slice(0, 500) + `...<${val.length}>` : val;
          if (val && typeof val === "object") {
            if (seen.has(val)) return "[Circular]";
            seen.add(val);
          }
          return val;
        });
        if (s && s.length > 2200) s = s.slice(0, 2200) + `...<${s.length}>`;
        return s || "";
      };
      const payload = { ts: new Date().toISOString(), file: "side.js", fn, loc, line, href: location.href, value };
      const out = clip(payload);
      const key = `IG_dbg/side.js/${fn}/${loc}/${line}`.replace(/\s+/g, "_");
      localStorage.setItem(key, out);
      localStorage.setItem("IG_dbg/side.js/_last", out);
      console.log(`[IG_DBG][side.js][${fn}][${loc}:${line}]`, value);
    } catch (e) {}
  }
  function dbgMedia(m){
    return m ? {
      code:m.code, username:m.username, media_type:m.media_type, has_img_origin:!!m.img_origin,
      has_video_url:!!m.video_url, has_carousel:!!(typeof m.carousel_media === "string" && m.carousel_media.trim()),
      carousel_count: typeof m.carousel_media === "string" ? m.carousel_media.split("\n").filter(Boolean).length : 0,
      like_count:m.like_count, comment_count:m.comment_count, media_repost_count:m.media_repost_count,
      share_count:m.share_count, play_count:m.play_count, created_at:m.created_at,
      video_url:m.video_url ? String(m.video_url).slice(0,180) : "",
      img_origin:m.img_origin ? String(m.img_origin).slice(0,180) : ""
    } : null;
  }

  const IGBLUE_BUILD_VERSION = (() => { try { const m = chrome.runtime.getManifest(); return String(m.version_name || m.version || "2.3.8.170"); } catch (_) { return "2.3.8.170"; } })();
  const IGBLUE_BUILD_TAG = (() => { const m = String(IGBLUE_BUILD_VERSION || "").match(/(\d+(?:a\d*)?)$/i); return m ? ("v" + m[1]) : String(IGBLUE_BUILD_VERSION || "v170"); })();
  const IGBLUE_PACKAGE_VERSION = "v288";
  const IGBLUE_CYCLE_SCHEMA = "ig_auto_cycle_state_v127";
  function teBuildCycleId(suffix) { return `${IGBLUE_BUILD_TAG}_${Date.now()}_${String(suffix || "cycle")}`; }

  (() => {
    "use strict";
    var e = window.IGBLUE_EXCELJS || window.ExcelJS;
    var t = () => e;
    if (!e) console.warn("[IG] side_xlsx_export.js not loaded; export_xlsx unavailable");
    function teCopyTextTripleEnter(text) {
        const payload = String(text == null ? "" : text).trimEnd() + "\n\n\n";
        try {
          if (navigator.clipboard && navigator.clipboard.writeText) {
            navigator.clipboard.writeText(payload).catch(()=>{});
            return true;
          }
        } catch (_) {}
        try {
          const ta = document.createElement("textarea");
          ta.value = payload;
          ta.style.position = "fixed";
          ta.style.left = "-9999px";
          document.body.appendChild(ta);
          ta.focus();
          ta.select();
          document.execCommand("copy");
          document.body.removeChild(ta);
          return true;
        } catch (_) { return false; }
      }
    function n(e) {
        const msg = String(e == null ? "" : e);
        if (!msg.trim()) return;
        if (/상태\s*전송은\s*최신값만\s*유지하여\s*중복전송을\s*생략/.test(msg)) return;
        var t = document.createElement("div");
        ((t.textContent = msg),
          (t.title = "클릭하면 클립보드에 복사됩니다."),
          (t.style.visibility = "hidden"),
          (t.style.minWidth = "220px"),
          (t.style.maxWidth = "calc(100vw - 24px)"),
          (t.style.backgroundColor = "#1f2937"),
          (t.style.color = "#fff"),
          (t.style.textAlign = "center"),
          (t.style.borderRadius = "10px"),
          (t.style.padding = "10px 12px"),
          (t.style.position = "fixed"),
          (t.style.zIndex = "2147483647"),
          (t.style.left = "50%"),
          (t.style.bottom = "8px"),
          (t.style.transform = "translateX(-50%)"),
          (t.style.fontSize = "13px"),
          (t.style.lineHeight = "1.35"),
          (t.style.cursor = "copy"),
          (t.style.boxShadow = "0 8px 18px rgba(0,0,0,0.22)"),
          (t.style.transition = "all 0.25s ease"),
          t.addEventListener("click", function(){ teCopyTextTripleEnter(msg); }),
          document.body.appendChild(t),
          setTimeout(function () {
            ((t.style.visibility = "visible"),
              (t.style.opacity = "1"),
              (t.style.bottom = "12px"));
          }, 50),
          setTimeout(function () {
            ((t.style.opacity = "0"),
              (t.style.visibility = "hidden"),
              setTimeout(function () {
                try { document.body.removeChild(t); } catch (_) {}
              }, 300));
          }, 3200));
      }
      const i = ["auto_swipe", "auto_swipe_10", "auto_swipe_30", "auto_top"],
        s = ["export_xlsx", "refresh"],
        o = ["down_all_medias", "down_filtered_medias"],
        a = "bulk-download",
        l = new Set();
      let c = null,
        u = !1,
        h = 0,
        f = 0,
        d = 0,
        p = null,
        teProgressState = { visible: !1, done: !1, file: "", file_path: "", file_paths: [], no_label: "", loaded: 0, total: 0, percent: 0, message: "", owner_key: "" },
        teDownloadDisplayAwaitingStart = !0;
      let teAutoPyStats = { active: false, run_seq: 0, start_ts: 0, current_no: 0, current_no_label: "", max_rank: 0, target_count: 0, completed_post_count: 0, completed_shortcodes: [], mp4_count: 0, jpg_count: 0, mp4_total_ms: 0, jpg_total_ms: 0, mp4_max_ms: 0, jpg_max_ms: 0, mp4_min_ms: 0, jpg_min_ms: 0, cycle_count: 0, cycle_total_ms: 0, cycle_max_ms: 0, cycle_min_ms: 0, last_reason: "" };
      let teAutoPyRunTotals = { completed_post_count: 0, completed_shortcodes: [], mp4_count: 0, jpg_count: 0, expected_asset_keys: [], completed_asset_keys: [] };
      let teAutoPyUiState = { phase: "idle", target_count: 0, completed_count: 0, completed_shortcodes: [], current_no: 0, asset_kind: "", wait_remaining: 0, exhausted: false, exhausted_reason: "", automatic_end: false, message: "", account_index: 0, account_total: 0 };
      let teAutoPyLastDoneText = "";
      let teAutoPyStatsTimer = 0;
      let teAutoPyStatsLastMainPush = 0;
      const m = "ndy_ig_overlay_mode",
        b = ["detail", "download", "none"],
        g = "detail",
        y = "ndy_ig_er_weights",
        v = Object.freeze(["like", "comment", "repost"]),
        w = Object.freeze({ like: 1, comment: 4, repost: 4 });
      let _ = g,
        x = { ...w };
      function k(e) {
        return b.includes(e);
      }
      function S() {
        return new Promise((e) => {
          chrome.storage.local.get(m, (t = {}) => {
            const r = t[m],
              n = k(r) ? r : g;
            ((_ = n),
              k(t[m]) ||
                chrome.storage.local.set({ [m]: n }, () => {
                  chrome.runtime.lastError;
                }),
              e(n));
          });
        });
      }
      function C(e) {
        const t = { ...w };
        return e && "object" == typeof e
          ? (v.forEach((r) => {
              const n = Number(e[r]);
              Number.isFinite(n) && n >= 0 && (t[r] = n);
            }),
            t)
          : t;
      }
      function E(e) {
        return (
          !(!e || "object" != typeof e) &&
          v.every((t) => {
            const r = Number(e[t]);
            return Number.isFinite(r) && r >= 0;
          })
        );
      }
      function M() {
        return new Promise((e) => {
          chrome.storage.local.get(y, (t = {}) => {
            const r = t[y],
              n = E(r) ? C(r) : { ...w };
            ((x = n),
              E(r) ||
                chrome.storage.local.set({ [y]: n }, () => {
                  chrome.runtime.lastError;
                }),
              e(n));
          });
        });
      }

      // IGblue local patch v21: editable 5-step ER emoji badges.
      const teErEmojiStoreKey = "visual/er_emoji_settings";
      function teErEmojiDefaults() {
        return {
          enabled: true,
          minViews: 10000,
          levels: [
            { enabled: true, pct: 0, emoji: "😭", label: "약함" },
            { enabled: true, pct: 1, emoji: "😐", label: "보통" },
            { enabled: true, pct: 3, emoji: "🙂", label: "괜찮음" },
            { enabled: true, pct: 5, emoji: "❤️", label: "충성도 좋음" },
            { enabled: true, pct: 10, emoji: "🔥", label: "팬덤급" }
          ]
        };
      }
      function teErEmojiSettings() {
        const e = teErEmojiDefaults();
        try {
          const t = JSON.parse(teLocalGet(teErEmojiStoreKey, ""));
          if (t && typeof t === "object") {
            e.enabled = t.enabled !== false;
            const r = Number(t.minViews);
            Number.isFinite(r) && r >= 0 && (e.minViews = r);
            Array.isArray(t.levels) && t.levels.forEach((t, r) => {
              if (!e.levels[r]) return;
              "enabled" in t && (e.levels[r].enabled = t.enabled !== false);
              const n = Number(t.pct);
              Number.isFinite(n) && n >= 0 && (e.levels[r].pct = n);
              null != t.emoji && (e.levels[r].emoji = String(t.emoji).slice(0, 8) || e.levels[r].emoji);
              null != t.label && (e.levels[r].label = String(t.label).slice(0, 24) || e.levels[r].label);
            });
          }
        } catch {}
        return e;
      }
      function teSaveErEmojiSettings(e) {
        const t = teErEmojiDefaults();
        try {
          const r = {
            enabled: e?.enabled !== false,
            minViews: Math.max(0, Number(e?.minViews) || 0),
            levels: (Array.isArray(e?.levels) ? e.levels : t.levels).slice(0, 5).map((e, r) => ({
              enabled: e?.enabled !== false,
              pct: Math.max(0, Number(e?.pct) || 0),
              emoji: String(e?.emoji || t.levels[r]?.emoji || "").slice(0, 8),
              label: String(e?.label || t.levels[r]?.label || "").slice(0, 24)
            }))
          };
          teLocalSet(teErEmojiStoreKey, JSON.stringify(r));
        } catch {}
      }
      function teErPercent(e) {
        if (null != e?.engagement_rate) {
          const t = 100 * Number(e.engagement_rate);
          return Number.isFinite(t) ? t : null;
        }
        const t = Number(e?.play_count);
        if (!Number.isFinite(t) || t <= 0) return null;
        const r = (Number(e?.like_count) || 0) * (Number(x?.like) || 1) +
          (Number(e?.comment_count) || 0) * (Number(x?.comment) || 4) +
          (Number(e?.media_repost_count) || 0) * (Number(x?.repost) || 4);
        return 100 * r / t;
      }
      function teErEmojiForMedia(e) {
        const t = teErEmojiSettings();
        if (!t.enabled) return null;
        const r = Number(e?.play_count);
        if (!Number.isFinite(r) || r < Number(t.minViews || 0)) return null;
        const n = teErPercent(e);
        if (null == n) return null;
        const i = (t.levels || []).filter((e) => e && e.enabled !== false).sort((e, t) => Number(t.pct) - Number(e.pct));
        for (const t of i) {
          if (n >= Number(t.pct || 0)) return { emoji: t.emoji || "", percent: n, label: t.label || "", threshold: Number(t.pct || 0) };
        }
        return null;
      }
      function teCreateErEmojiSettings() {
        const e = teErEmojiSettings();
        const t = document.createElement("div");
        t.style.display = "flex";
        t.style.flexDirection = "column";
        t.style.gap = "10px";
        t.style.borderTop = "1px solid #e5e7eb";
        t.style.paddingTop = "12px";
        const r = document.createElement("div");
        r.textContent = "ER emoji 5-step";
        r.style.fontSize = "14px";
        r.style.fontWeight = "700";
        const n = document.createElement("label");
        n.style.display = "flex";
        n.style.alignItems = "center";
        n.style.gap = "6px";
        n.style.fontSize = "12px";
        const i = document.createElement("input");
        i.type = "checkbox";
        i.checked = e.enabled !== false;
        n.appendChild(i);
        n.appendChild(document.createTextNode("Show ER emoji badge"));
        n.appendChild(teMakeDefaultButton("ER emoji", teErEmojiStoreKey));
        const s = document.createElement("label");
        s.style.display = "flex";
        s.style.alignItems = "center";
        s.style.gap = "6px";
        s.style.fontSize = "12px";
        s.textContent = "Min views";
        const o = document.createElement("input");
        o.type = "number";
        o.min = "0";
        o.step = "1000";
        o.value = e.minViews;
        o.style.width = "90px";
        o.style.textAlign = "right";
        o.style.padding = "4px 6px";
        o.style.border = "1px solid #ccc";
        o.style.borderRadius = "4px";
        s.appendChild(o);
        const a = document.createElement("div");
        a.style.display = "grid";
        a.style.gridTemplateColumns = "42px 74px 52px 1fr";
        a.style.gap = "5px";
        a.style.alignItems = "center";
        a.style.fontSize = "12px";
        ["Use", "ER %", "Emoji", "Label"].forEach((e) => {
          const t = document.createElement("div");
          t.textContent = e;
          t.style.fontWeight = "700";
          a.appendChild(t);
        });
        const l = [];
        (e.levels || teErEmojiDefaults().levels).slice(0, 5).forEach((e) => {
          const t = document.createElement("input");
          t.type = "checkbox";
          t.checked = e.enabled !== false;
          const r = document.createElement("input");
          r.type = "number";
          r.min = "0";
          r.step = "0.1";
          r.value = e.pct;
          r.style.width = "62px";
          r.style.textAlign = "right";
          r.style.padding = "4px 5px";
          const n = document.createElement("input");
          n.type = "text";
          n.value = e.emoji || "";
          n.style.width = "42px";
          n.style.textAlign = "center";
          n.style.padding = "4px 5px";
          const i = document.createElement("input");
          i.type = "text";
          i.value = e.label || "";
          i.style.padding = "4px 5px";
          [r, n, i].forEach((e) => { e.style.border = "1px solid #ccc"; e.style.borderRadius = "4px"; });
          a.appendChild(t);
          a.appendChild(r);
          a.appendChild(n);
          a.appendChild(i);
          l.push({ enabled: t, pct: r, emoji: n, label: i });
        });
        const c = document.createElement("div");
        c.style.fontSize = "12px";
        c.style.padding = "8px";
        c.style.borderRadius = "8px";
        c.style.background = "#f4faff";
        c.style.border = "1px solid #dbeafe";
        const u = () => {
          const e = l.map((e) => ({ enabled: e.enabled.checked, pct: Number(e.pct.value) || 0, emoji: e.emoji.value || "", label: e.label.value || "" }))
            .filter((e) => e.enabled)
            .sort((e, t) => Number(t.pct) - Number(e.pct));
          const t = e[0] || { emoji: "🔥", pct: 10, label: "팬덤급" };
          c.textContent = `Sample: ${t.emoji} ${Number(t.pct).toFixed(1).replace(/\.0$/, "")}% · ${t.label}`;
        };
        [i, o, ...l.flatMap((e) => [e.enabled, e.pct, e.emoji, e.label])].forEach((e) => e.addEventListener("input", u));
        u();
        t.appendChild(r);
        t.appendChild(n);
        t.appendChild(s);
        t.appendChild(a);
        t.appendChild(c);
        return {
          element: t,
          getValues: () => ({
            enabled: i.checked,
            minViews: Number(o.value) || 0,
            levels: l.map((e) => ({ enabled: e.enabled.checked, pct: Number(e.pct.value) || 0, emoji: e.emoji.value || "", label: e.label.value || "" }))
          })
        };
      }

      // IGblue local patch v27: simple metric underline bars + configurable date borders.
      const teMetricBarStoreKey = "visual/metric_bar_settings";
      const teDateBorderStoreKey = "visual/date_border_settings";
      const teRankBadgeStoreKey = "visual/rank_badge_settings";
      const teActiveDownloadStoreKey = "visual/active_download_settings";
      const teThumbWidthStoreKey = "visual/thumb_min_width";
      function teThumbWidthDefault() { return 200; }
      function teThumbWidthValue() {
        const e = Number(teLocalGet(teThumbWidthStoreKey, teThumbWidthDefault()));
        return Math.max(90, Math.min(420, Number.isFinite(e) ? Math.floor(e) : teThumbWidthDefault()));
      }
      function teApplyThumbWidth(e) {
        const t = Math.max(90, Math.min(420, Number(e) || teThumbWidthDefault()));
        document.documentElement.style.setProperty("--igblue-thumb-min-width", `${Math.floor(t)}px`);
        teLocalSet(teThumbWidthStoreKey, Math.floor(t));
        return Math.floor(t);
      }
      function teMetricBarDefaults() {
        return {
          enabled: true,
          barHeight: 3,
          itemGap: 4,
          textBarGap: 2,
          itemHeight: 18,
          items: {
            views: { enabled: true, side: true, main: true, label: "Views", max: 1000000, color: "#0d6efd" },
            likes: { enabled: true, side: true, main: true, label: "Likes", max: 100000, color: "#ef4444" },
            comments: { enabled: true, side: true, main: true, label: "Comments", max: 10000, color: "#22c55e" },
            shares: { enabled: true, side: true, main: true, label: "Reposts", max: 10000, color: "#f97316" }
          }
        };
      }
      function teSafeHexColor(e, t) {
        const r = String(e || "").trim();
        return /^#[0-9a-fA-F]{6}$/.test(r) ? r : t;
      }
      function teHexToRgba(e, a, fallback) {
        const h = teSafeHexColor(e, fallback || "#ff2d2d").replace("#", "");
        const r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
        const o = Math.max(0, Math.min(1, Number(a)));
        return `rgba(${r}, ${g}, ${b}, ${Number.isFinite(o) ? o : 1})`;
      }
      function teSampleGradientBackground() {
        return "linear-gradient(135deg,#f8fafc 0%,#facc15 18%,#22c55e 42%,#2563eb 68%,#111827 100%)";
      }
      function teSampleCardWidthValue(e) {
        const t = Number(e);
        return Math.max(160, Math.min(420, Number.isFinite(t) ? Math.floor(t) : teThumbWidthValue()));
      }
      function teSampleMetricIcon(e) {
        const common = 'width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"';
        if (e === "likes") return `<svg ${common}><title>Likes</title><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>`;
        if (e === "comments") return `<svg ${common}><title>Comments</title><path d="M3 11.5a8.38 8.38 0 0 0 .9 3.8 8.5 8.5 0 0 0 7.6 4.7 8.38 8.38 0 0 0 3.8-.9L21 21l-1.9-5.7a8.38 8.38 0 0 0 .9-3.8 8.5 8.5 0 0 0-4.7-7.6 8.38 8.38 0 0 0-3.8-.9h-.5a8.48 8.48 0 0 0-8 8v.5z"></path></svg>`;
        if (e === "shares") return `<svg width="16" height="16" viewBox="0 0 24 24"><title>Repost</title><path d="M4.51617 6.9986C6.13179 4.58593 8.88099 2.99979 11.9995 2.99979C15.7267 2.99979 18.9259 5.26459 20.2927 8.49676C20.5079 9.00543 21.0946 9.24341 21.6033 9.0283C22.1119 8.81318 22.3499 8.22644 22.1348 7.71777C20.466 3.7716 16.5582 0.999786 11.9995 0.999786C8.27776 0.999786 4.9897 2.84823 2.99988 5.67416V2.9986C2.99988 2.44631 2.55216 1.9986 1.99988 1.9986C1.44759 1.9986 0.999878 2.44631 0.999878 2.9986V7.9986C0.999878 8.55088 1.44759 8.9986 1.99988 8.9986H6.99988C7.55216 8.9986 7.99988 8.55088 7.99988 7.9986C7.99988 7.44631 7.55216 6.9986 6.99988 6.9986H4.51617Z" fill="currentColor"></path><path d="M2.39572 14.9713C2.90439 14.7562 3.49113 14.9942 3.70625 15.5029C5.07309 18.735 8.27228 20.9998 11.9995 20.9998C15.118 20.9998 17.8672 19.4137 19.4828 17.001H16.9991C16.4468 17.001 15.9991 16.5533 15.9991 16.001C15.9991 15.4487 16.4468 15.001 16.9991 15.001H21.9991C22.5514 15.001 22.9991 15.4487 22.9991 16.001V21.001C22.9991 21.5533 22.5514 22.001 21.9991 22.001C21.4468 22.001 20.9991 21.5533 20.9991 21.001V18.3255C19.0093 21.1514 15.7212 22.9998 11.9995 22.9998C7.44077 22.9998 3.53298 20.228 1.86419 16.2818C1.64908 15.7732 1.88705 15.1864 2.39572 14.9713Z" fill="currentColor"></path></svg>`;
        if (e === "date") return `<svg ${common}><title>Date</title><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>`;
        if (e === "views") return `<svg ${common}><title>Views</title><path d="M23.441 11.819C23.413 11.74 20.542 4 12 4S.587 11.74.559 11.819a1 1 0 0 0 1.881.677 10.282 10.282 0 0 1 19.12 0 1 1 0 0 0 1.881-.677Zm-7.124 2.368a3.359 3.359 0 0 1-1.54-.1 3.56 3.56 0 0 1-2.365-2.362 3.35 3.35 0 0 1-.103-1.542.99.99 0 0 0-1.134-1.107 5.427 5.427 0 0 0-3.733 2.34 5.5 5.5 0 0 0 8.446 6.97 5.402 5.402 0 0 0 1.536-3.09.983.983 0 0 0-1.107-1.109Z" fill-rule="evenodd"></path></svg>`;
        return `<span></span>`;
      }
      function teSampleCardHtml(inner, width) {
        const w = teSampleCardWidthValue(width);
        return `<div style="font-weight:700;margin-bottom:7px">Sample thumbnail preview</div><div style="width:${w}px;max-width:100%;aspect-ratio:1/1.2;border-radius:12px;overflow:hidden;background:${teSampleGradientBackground()};color:#fff;position:relative;box-shadow:0 4px 16px rgba(15,23,42,.18);margin-bottom:6px"><div class="overlay">${inner}</div></div>`;
      }
      function teMetricBarSettings() {
        const e = teMetricBarDefaults();
        try {
          const t = JSON.parse(teLocalGet(teMetricBarStoreKey, ""));
          if (t && typeof t === "object") {
            "enabled" in t && (e.enabled = t.enabled !== false);
            ["barHeight", "itemGap", "textBarGap", "itemHeight"].forEach((r) => {
              const n = Number(t[r]);
              Number.isFinite(n) && n >= 0 && (e[r] = n);
            });
            if (t.items && typeof t.items === "object") {
              Object.keys(e.items).forEach((r) => {
                const n = t.items[r];
                if (!n || typeof n !== "object") return;
                "enabled" in n && (e.items[r].enabled = n.enabled !== false);
                "side" in n && (e.items[r].side = n.side !== false);
                "main" in n && (e.items[r].main = n.main !== false);
                const i = Number(n.max);
                Number.isFinite(i) && i >= 1 && (e.items[r].max = Math.floor(i));
                e.items[r].color = teSafeHexColor(n.color, e.items[r].color);
                null != n.label && (e.items[r].label = String(n.label).slice(0, 20));
              });
            }
          }
        } catch {}
        e.barHeight = Math.max(1, Math.min(12, Number(e.barHeight) || 3));
        e.itemGap = Math.max(0, Math.min(20, Number(e.itemGap) || 4));
        e.textBarGap = Math.max(0, Math.min(20, Number(e.textBarGap) || 2));
        e.itemHeight = Math.max(0, Math.min(60, Number(e.itemHeight) || 18));
        return e;
      }
      function teSaveMetricBarSettings(e) {
        const t = teMetricBarDefaults(), r = { enabled: e?.enabled !== false, barHeight: 3, itemGap: 4, textBarGap: 2, itemHeight: 18, items: {} };
        r.barHeight = Math.max(1, Math.min(12, Math.floor(Number(e?.barHeight) || t.barHeight)));
        r.itemGap = Math.max(0, Math.min(20, Math.floor(Number(e?.itemGap) || t.itemGap)));
        r.textBarGap = Math.max(0, Math.min(20, Math.floor(Number(e?.textBarGap) || t.textBarGap)));
        r.itemHeight = Math.max(0, Math.min(60, Math.floor(Number(e?.itemHeight) || t.itemHeight)));
        Object.keys(t.items).forEach((n) => {
          const i = e?.items?.[n] || t.items[n];
          r.items[n] = {
            enabled: i?.enabled !== false,
            side: i?.side !== false,
            main: i?.main !== false,
            label: String(i?.label || t.items[n].label).slice(0, 20),
            max: Math.max(1, Math.floor(Number(i?.max) || t.items[n].max)),
            color: teSafeHexColor(i?.color, t.items[n].color)
          };
        });
        teLocalSet(teMetricBarStoreKey, JSON.stringify(r));
      }
      function teMetricBarPercent(e, t) {
        const r = Math.max(0, Number(e) || 0), n = Math.max(1, Number(t) || 1);
        if (r <= 0) return 0;
        return Math.max(0, Math.min(100, (r / n) * 100));
      }
      function teClearMetricBar(e) {
        if (!e) return;
        e.classList.remove("igblue-metric-bar-on");
        ["--igblue-stat-bar-color", "--igblue-stat-bar-width", "--igblue-stat-bar-height", "--igblue-stat-bar-gap", "--igblue-stat-item-gap", "--igblue-stat-item-height"].forEach((t) => e.style.removeProperty(t));
      }
      function teApplyMetricBarToStat(e, t, r, n) {
        if (!e) return;
        teClearMetricBar(e);
        if (!t.enabled || !t.side || !Number.isFinite(Number(r)) || Number(r) <= 0) return;
        const i = teMetricBarPercent(r, t.max);
        e.style.setProperty("--igblue-stat-bar-color", teSafeHexColor(t.color, "#0d6efd"));
        e.style.setProperty("--igblue-stat-bar-width", `${i.toFixed(2)}%`);
        e.style.setProperty("--igblue-stat-bar-height", `${Math.max(1, Math.min(12, Number(n.barHeight) || 3))}px`);
        e.style.setProperty("--igblue-stat-bar-gap", `${Math.max(0, Math.min(20, Number(n.textBarGap) || 2))}px`);
        e.style.setProperty("--igblue-stat-item-gap", `${Math.max(0, Math.min(20, Number(n.itemGap) || 4))}px`);
        e.style.setProperty("--igblue-stat-item-height", `${Math.max(0, Math.min(60, Number(n.itemHeight) || 18))}px`);
        e.classList.add("igblue-metric-bar-on");
        e.title = `${t.label || "metric"}: ${Number(r).toLocaleString()} / max ${Number(t.max || 0).toLocaleString()}`;
      }
      function teApplyMetricBars(e, t) {
        if (!e) return;
        const r = teMetricBarSettings(), n = r.items || {}, overlay = e[0]?.closest?.(".overlay");
        overlay && overlay.style.setProperty("--igblue-overlay-item-gap", `${Math.max(0, Math.min(20, Number(r.itemGap) || 4))}px`);
        [e[1], e[2], e[3], e[5]].forEach(teClearMetricBar);
        if (!r.enabled) return;
        teApplyMetricBarToStat(e[5], n.views || {}, teMetricNumber(t?.play_count), r);
        teApplyMetricBarToStat(e[1], n.likes || {}, teMetricNumber(t?.like_count), r);
        teApplyMetricBarToStat(e[2], n.comments || {}, teMetricNumber(t?.comment_count), r);
        teApplyMetricBarToStat(e[3], n.shares || {}, teMetricNumber(t?.media_repost_count), r);
      }
      function teDateBorderDefaults() {
        return {
          enabled: true,
          borderWidth: 2,
          levels: {
            today: { enabled: true, side: true, main: true, days: 0, label: "오늘", color: "#ff2d2d" },
            week: { enabled: true, side: true, main: true, days: 7, label: "1주", color: "#22c55e" },
            month: { enabled: true, side: true, main: true, days: 31, label: "1개월", color: "#eab308" },
            quarter: { enabled: false, side: true, main: true, days: 90, label: "3개월", color: "#9ca3af" }
          }
        };
      }
      function teDateBorderSettings() {
        const e = teDateBorderDefaults();
        try {
          const t = JSON.parse(teLocalGet(teDateBorderStoreKey, ""));
          if (t && typeof t === "object") {
            "enabled" in t && (e.enabled = t.enabled !== false);
            const r = Number(t.borderWidth);
            Number.isFinite(r) && r >= 1 && (e.borderWidth = r);
            if (t.levels && typeof t.levels === "object") {
              Object.keys(e.levels).forEach((r) => {
                const n = t.levels[r];
                if (!n || typeof n !== "object") return;
                "enabled" in n && (e.levels[r].enabled = n.enabled !== false);
                "side" in n && (e.levels[r].side = n.side !== false);
                "main" in n && (e.levels[r].main = n.main !== false);
                e.levels[r].color = teSafeHexColor(n.color, e.levels[r].color);
              });
            }
          }
        } catch {}
        e.borderWidth = Math.max(1, Math.min(9, Number(e.borderWidth) || 2));
        return e;
      }
      function teSaveDateBorderSettings(e) {
        const t = teDateBorderDefaults(), r = { enabled: e?.enabled !== false, borderWidth: Math.max(1, Math.min(9, Math.floor(Number(e?.borderWidth) || t.borderWidth))), levels: {} };
        Object.keys(t.levels).forEach((n) => {
          const i = e?.levels?.[n] || t.levels[n];
          r.levels[n] = {
            enabled: i?.enabled !== false,
            side: i?.side !== false,
            main: i?.main !== false,
            color: teSafeHexColor(i?.color, t.levels[n].color)
          };
        });
        teLocalSet(teDateBorderStoreKey, JSON.stringify(r));
      }
      function teRankBadgeDefaults() {
        return { enabled: true, side: true, borderColor: "#ff6666", borderWidth: 4, fontSize: 11, bold: false, borderOpacity: 0.55, textOpacity: 0.82 };
      }
      function teRankBadgeSettings() {
        const e = teRankBadgeDefaults();
        try {
          const t = JSON.parse(teLocalGet(teRankBadgeStoreKey, ""));
          if (t && typeof t === "object") {
            "enabled" in t && (e.enabled = t.enabled !== false);
            "side" in t && (e.side = t.side !== false);
            e.borderColor = teSafeHexColor(t.borderColor, e.borderColor);
            const r = Number(t.borderWidth); Number.isFinite(r) && (e.borderWidth = r);
            const n = Number(t.fontSize); Number.isFinite(n) && (e.fontSize = n);
            "bold" in t && (e.bold = t.bold === true);
            const oldOpacity = Number(t.opacity);
            const bo = Number(t.borderOpacity); Number.isFinite(bo) ? (e.borderOpacity = bo) : Number.isFinite(oldOpacity) && (e.borderOpacity = oldOpacity);
            const to = Number(t.textOpacity); Number.isFinite(to) ? (e.textOpacity = to) : Number.isFinite(oldOpacity) && (e.textOpacity = oldOpacity);
          }
        } catch {}
        e.borderWidth = Math.max(0, Math.min(8, Math.floor(Number(e.borderWidth) || 0)));
        e.fontSize = Math.max(8, Math.min(24, Math.floor(Number(e.fontSize) || 11)));
        { const v = Number(e.borderOpacity); e.borderOpacity = Math.max(0, Math.min(1, Number.isFinite(v) ? v : 0.55)); }
        { const v = Number(e.textOpacity); e.textOpacity = Math.max(0, Math.min(1, Number.isFinite(v) ? v : 0.82)); }
        return e;
      }
      function teSaveRankBadgeSettings(e) {
        const t = teRankBadgeDefaults();
        const r = {
          enabled: e?.enabled !== false,
          side: e?.side !== false,
          borderColor: teSafeHexColor(e?.borderColor, t.borderColor),
          borderWidth: Math.max(0, Math.min(8, Math.floor(Number(e?.borderWidth) || t.borderWidth))),
          fontSize: Math.max(8, Math.min(24, Math.floor(Number(e?.fontSize) || t.fontSize))),
          bold: e?.bold === true,
          borderOpacity: Math.max(0, Math.min(1, Number.isFinite(Number(e?.borderOpacity)) ? Number(e?.borderOpacity) : t.borderOpacity)),
          textOpacity: Math.max(0, Math.min(1, Number.isFinite(Number(e?.textOpacity)) ? Number(e?.textOpacity) : t.textOpacity))
        };
        teLocalSet(teRankBadgeStoreKey, JSON.stringify(r));
      }
      function teApplyRankBadgeVars(e, t) {
        if (!e) return;
        const boRaw = Number(t.borderOpacity), toRaw = Number(t.textOpacity);
        const bo = Math.max(0, Math.min(1, Number.isFinite(boRaw) ? boRaw : 0.55));
        const to = Math.max(0, Math.min(1, Number.isFinite(toRaw) ? toRaw : 0.82));
        e.style.setProperty("--igblue-rank-border-color", teSafeHexColor(t.borderColor, "#ff6666"));
        e.style.setProperty("--igblue-rank-border-rgba", teHexToRgba(t.borderColor, bo, "#ff6666"));
        e.style.setProperty("--igblue-rank-border-width", `${Math.max(0, Math.min(8, Number(t.borderWidth) || 0))}px`);
        e.style.setProperty("--igblue-rank-font-size", `${Math.max(8, Math.min(24, Number(t.fontSize) || 11))}px`);
        e.style.setProperty("--igblue-rank-font-weight", t.bold ? "800" : "500");
        e.style.setProperty("--igblue-rank-text-opacity", String(to));
        e.style.setProperty("--igblue-rank-opacity", String(to));
      }
      function teClearRankBadgeVars(e) {
        if (!e) return;
        ["--igblue-rank-border-color", "--igblue-rank-border-rgba", "--igblue-rank-border-width", "--igblue-rank-font-size", "--igblue-rank-font-weight", "--igblue-rank-opacity", "--igblue-rank-text-opacity"].forEach((t) => e.style.removeProperty(t));
      }
      function teActiveDownloadDefaults() {
        // v179a37: 기본은 인스타 화면을 덜 가리도록 확대/강조 OFF. 자동 클릭 좌표는 DOM rect payload로 유지한다.
        return { enabled: false, color: "#ff0000", borderWidth: 4, radius: 3, glow: false, glowOpacity: 0.45, scale: 1.0 };
      }
      function teActiveDownloadSettings() {
        const e = teActiveDownloadDefaults();
        try {
          const t = JSON.parse(teLocalGet(teActiveDownloadStoreKey, ""));
          if (t && typeof t === "object") {
            "enabled" in t && (e.enabled = t.enabled !== false);
            e.color = teSafeHexColor(t.color, e.color);
            const bw = Number(t.borderWidth); Number.isFinite(bw) && (e.borderWidth = bw);
            const rr = Number(t.radius); Number.isFinite(rr) && (e.radius = rr);
            "glow" in t && (e.glow = t.glow !== false);
            const go = Number(t.glowOpacity); Number.isFinite(go) && (e.glowOpacity = go);
            const sc = Number(t.scale); Number.isFinite(sc) && (e.scale = sc);
          }
        } catch {}
        e.borderWidth = Math.max(0, Math.min(12, Math.floor(Number(e.borderWidth) || 0)));
        e.radius = Math.max(0, Math.min(999, Math.floor(Number(e.radius) || 0)));
        e.glowOpacity = Math.max(0, Math.min(1, Number(e.glowOpacity) || 0));
        e.scale = Math.max(1, Math.min(1.6, Number(e.scale) || 1));
        return e;
      }
      function teSaveActiveDownloadSettings(e) {
        const t = teActiveDownloadDefaults();
        const r = {
          enabled: e?.enabled !== false,
          color: teSafeHexColor(e?.color, t.color),
          borderWidth: Math.max(0, Math.min(12, Math.floor(Number(e?.borderWidth) || t.borderWidth))),
          radius: Math.max(0, Math.min(999, Math.floor(Number(e?.radius) || t.radius))),
          glow: e?.glow !== false,
          glowOpacity: Math.max(0, Math.min(1, Number.isFinite(Number(e?.glowOpacity)) ? Number(e?.glowOpacity) : t.glowOpacity)),
          scale: Math.max(1, Math.min(1.6, Number.isFinite(Number(e?.scale)) ? Number(e?.scale) : t.scale))
        };
        teLocalSet(teActiveDownloadStoreKey, JSON.stringify(r));
      }
      function teLocalRemove(e) {
        try { localStorage.removeItem(teLocalKey(e)); } catch {}
      }
      function teResetSettingsSection(e, t) {
        if (!confirm(`${e} 기본값으로 되돌릴까요?`)) return;
        (Array.isArray(t) ? t : [t]).forEach(teLocalRemove);
        n(`${e} 기본값 적용`);
        const r = document.getElementById("settingsModal");
        r && r.remove();
        Array.isArray(oe) && ee(oe);
        setTimeout(T, 0);
      }
      function teMakeDefaultButton(e, t) {
        const r = document.createElement("button");
        r.type = "button";
        r.textContent = "기본값";
        r.style.padding = "3px 8px";
        r.style.border = "1px solid #cfd8e3";
        r.style.borderRadius = "999px";
        r.style.background = "#f4faff";
        r.style.cursor = "pointer";
        r.style.fontSize = "12px";
        r.addEventListener("click", () => teResetSettingsSection(e, t));
        return r;
      }
      function teCreateSettingsTabs(e) {
        const t = document.createElement("div"), r = document.createElement("div"), n = document.createElement("div");
        t.style.display = "flex"; t.style.flexDirection = "column"; t.style.gap = "12px";
        r.style.display = "flex"; r.style.flexWrap = "wrap"; r.style.gap = "6px"; r.style.borderBottom = "1px solid #e5e7eb"; r.style.padding = "8px 0 7px"; r.style.position = "sticky"; r.style.top = "0"; r.style.background = "#fff"; r.style.zIndex = "3";
        n.style.display = "flex"; n.style.flexDirection = "column"; n.style.gap = "12px";
        e.forEach((e, i) => {
          const s = document.createElement("button");
          s.type = "button"; s.textContent = e.title;
          s.style.padding = "6px 10px"; s.style.border = "1px solid #cfd8e3"; s.style.borderRadius = "999px"; s.style.background = i ? "#fff" : "#e7f1ff"; s.style.cursor = "pointer";
          const o = document.createElement("div");
          o.style.display = i ? "none" : "flex"; o.style.flexDirection = "column"; o.style.gap = "12px";
          o.appendChild(e.element);
          s.addEventListener("click", () => {
            Array.from(r.children).forEach((e) => e.style.background = "#fff");
            Array.from(n.children).forEach((e) => e.style.display = "none");
            s.style.background = "#e7f1ff"; o.style.display = "flex";
          });
          r.appendChild(s); n.appendChild(o);
        });
        t.appendChild(r); t.appendChild(n);
        return { element: t };
      }

      function teDateLevelByAge(e, t) {
        if (e === 0) return t.today;
        if (e >= 1 && e <= 7) return t.week;
        if (e >= 8 && e <= 31) return t.month;
        if (e >= 32 && e <= 90) return t.quarter;
        return null;
      }
      function teApplyDateFreshClass(e, t) {
        if (!e) return;
        e.classList.remove("igblue-date-today", "igblue-date-week", "igblue-date-month", "igblue-date-quarter", "igblue-date-border");
        e.style.removeProperty("--igblue-date-color");
        e.style.removeProperty("--igblue-date-border-width");
        const r = Number(t?.created_at || 0);
        if (!Number.isFinite(r) || r <= 0) return;
        const n = teDateBorderSettings();
        if (!n.enabled) return;
        const i = new Date(), s = new Date(1e3 * r);
        i.setHours(0, 0, 0, 0);
        s.setHours(0, 0, 0, 0);
        const o = Math.floor((i.getTime() - s.getTime()) / 86400000), a = teDateLevelByAge(o, n.levels || {});
        if (!a || !a.enabled || !a.side) return;
        e.classList.add("igblue-date-border");
        e.style.setProperty("--igblue-date-color", teSafeHexColor(a.color, "#22c55e"));
        e.style.setProperty("--igblue-date-border-width", `${Math.max(1, Math.min(9, Number(n.borderWidth) || 2))}px`);
      }
      // Old top-card view highlight is intentionally disabled in v27.
      function teViewVisualSettings() { return { enabled: false, applySide: false, applyMain: false, border: false, bar: false, thumbWidth: teThumbWidthValue() }; }
      function teViewLevelForMedia() { return null; }
      function teViewBarPercent() { return 0; }
      function teEnsureViewBar(e) {
        let t = e?.querySelector?.(".igblue-view-bar-wrap");
        if (!t && e) {
          t = document.createElement("div");
          t.className = "igblue-view-bar-wrap";
          const r = document.createElement("div");
          r.className = "igblue-view-bar";
          t.appendChild(r);
          e.appendChild(t);
        }
        return t;
      }
      function teApplyViewVisual(e) {
        if (!e) return;
        e.classList.remove("igblue-view-border", "igblue-view-bar-on");
        ["--igblue-view-color", "--igblue-view-glow", "--igblue-view-bar-width", "--igblue-view-bar-height", "--igblue-view-border-width"].forEach((t) => e.style.removeProperty(t));
      }
      function teSaveViewVisualSettings(e) {
        teSaveMetricBarSettings(e?.metric || {});
        teSaveDateBorderSettings(e?.date || {});
      }
      function teCreateMetricBarSettings() {
        const e = teMetricBarSettings(), t = document.createElement("div");
        t.style.display = "flex"; t.style.flexDirection = "column"; t.style.gap = "10px";
        const r = document.createElement("div");
        r.textContent = "Metric underline bars"; r.style.fontSize = "14px"; r.style.fontWeight = "700";
        const n = document.createElement("div");
        n.style.display = "grid"; n.style.gridTemplateColumns = "repeat(auto-fit, minmax(95px, 1fr))"; n.style.gap = "8px";
        const mkCheck = (label, checked) => { const e = document.createElement("label"), t = document.createElement("input"); e.style.fontSize = "12px"; e.style.display = "flex"; e.style.gap = "5px"; e.style.alignItems = "center"; t.type = "checkbox"; t.checked = checked !== false; e.appendChild(t); e.appendChild(document.createTextNode(label)); return { label: e, input: t }; };
        const enabled = mkCheck("사용", e.enabled);
        n.appendChild(enabled.label);
        n.appendChild(teMakeDefaultButton("Metric bars", [teMetricBarStoreKey, teThumbWidthStoreKey]));
        const o = document.createElement("div");
        o.style.display = "grid"; o.style.gridTemplateColumns = "80px 80px 92px 98px 92px"; o.style.gap = "6px"; o.style.alignItems = "center"; o.style.fontSize = "12px";
        const mkNum = (label, value, min, max) => { const e = document.createElement("label"), t = document.createElement("input"); e.textContent = label; e.style.display = "flex"; e.style.flexDirection = "column"; e.style.gap = "3px"; t.type = "number"; t.min = String(min); t.max = String(max); t.step = "1"; t.value = value; t.style.width = label.includes("최대") ? "98px" : "70px"; t.style.textAlign = "right"; t.style.padding = "4px 5px"; t.style.border = "1px solid #ccc"; t.style.borderRadius = "4px"; e.appendChild(t); return { label: e, input: t }; };
        const barHeight = mkNum("막대 px", e.barHeight, 1, 12), itemGap = mkNum("항목간 px", e.itemGap, 0, 20), textBarGap = mkNum("글자-바 px", e.textBarGap, 0, 20), itemHeight = mkNum("항목높이 px", e.itemHeight, 0, 60), thumbWidth = mkNum("썸네일 px", teThumbWidthValue(), 90, 420);
        [barHeight, itemGap, textBarGap, itemHeight, thumbWidth].forEach((e) => o.appendChild(e.label));
        const a = document.createElement("div");
        a.style.display = "grid"; a.style.gridTemplateColumns = "38px 38px 42px 82px 112px 70px"; a.style.gap = "5px"; a.style.alignItems = "center"; a.style.fontSize = "12px";
        ["Use", "Side", "Main", "색상", "최대값", "항목"].forEach((e) => { const t = document.createElement("div"); t.textContent = e; t.style.fontWeight = "700"; a.appendChild(t); });
        const rows = {}, labels = { views: "Views", likes: "Likes", comments: "Comments", shares: "Repost" };
        Object.keys(labels).forEach((t) => {
          const r = e.items[t];
          const n = document.createElement("input"); n.type = "checkbox"; n.checked = r.enabled !== false;
          const i = document.createElement("input"); i.type = "checkbox"; i.checked = r.side !== false;
          const s = document.createElement("input"); s.type = "checkbox"; s.checked = r.main === true;
          const o = document.createElement("input"); o.type = "color"; o.value = teSafeHexColor(r.color, "#0d6efd"); o.style.width = "72px";
          const l = document.createElement("input"); l.type = "number"; l.min = "1"; l.step = "100"; l.value = r.max; l.style.width = "104px"; l.style.textAlign = "right"; l.style.padding = "4px 5px";
          const c = document.createElement("div"); c.textContent = labels[t]; c.style.fontWeight = "600";
          [l].forEach((e) => { e.style.border = "1px solid #ccc"; e.style.borderRadius = "4px"; });
          a.appendChild(n); a.appendChild(i); a.appendChild(s); a.appendChild(o); a.appendChild(l); a.appendChild(c);
          rows[t] = { enabled: n, side: i, main: s, color: o, max: l, label: labels[t] };
        });
        const sample = document.createElement("div"); sample.style.border = "1px solid #dbeafe"; sample.style.borderRadius = "10px"; sample.style.background = "#f4faff"; sample.style.padding = "8px"; sample.style.fontSize = "12px";
        const updateSample = () => {
          const bh = Math.max(1, Math.min(12, Number(barHeight.input.value) || 3)), gap = Math.max(0, Math.min(20, Number(textBarGap.input.value) || 2)), itemH = Math.max(0, Math.min(60, Number(itemHeight.input.value) || 18));
          const sampleValues = { views: 72000000, likes: 380000, comments: 1800, shares: 2500 };
          const rowHtml = Object.keys(rows).map((k) => {
            const row = rows[k], max = Math.max(1, Number(row.max.value) || 1), val = sampleValues[k] || Math.round(max * .4);
            const pct = Math.max(0, Math.min(100, val / max * 100));
            const disabled = !row.enabled.checked || !row.side.checked;
            return `<div class="stat igblue-metric-bar-on" style="--igblue-stat-bar-height:${bh}px;--igblue-stat-bar-gap:${gap}px;--igblue-stat-item-height:${itemH}px;--igblue-stat-item-gap:${Number(itemGap.input.value)||0}px;--igblue-stat-bar-color:${row.color.value};--igblue-stat-bar-width:${disabled ? 0 : pct}%;opacity:${disabled ? .38 : 1}">${teSampleMetricIcon(k)}<span>${val.toLocaleString()}</span></div>`;
          }).join("\n");
          sample.innerHTML = teSampleCardHtml(rowHtml, thumbWidth.input.value);
        };
        [enabled.input, barHeight.input, itemGap.input, textBarGap.input, itemHeight.input, thumbWidth.input, ...Object.values(rows).flatMap((e) => [e.enabled, e.side, e.main, e.color, e.max])].forEach((e) => e.addEventListener("input", updateSample));
        const applyThumbNow = () => { thumbWidth.input.value = String(teApplyThumbWidth(thumbWidth.input.value)); };
        thumbWidth.input.addEventListener("blur", applyThumbNow); thumbWidth.input.addEventListener("change", applyThumbNow);
        updateSample();
        t.appendChild(r); t.appendChild(n); t.appendChild(o); t.appendChild(a); t.appendChild(sample);
        return { element: t, getValues: () => ({ enabled: enabled.input.checked, barHeight: Number(barHeight.input.value) || 3, itemGap: Number(itemGap.input.value) || 0, textBarGap: Number(textBarGap.input.value) || 0, itemHeight: Number(itemHeight.input.value) || 0, thumbWidth: Number(thumbWidth.input.value) || teThumbWidthDefault(), items: Object.fromEntries(Object.entries(rows).map(([k, v]) => [k, { enabled: v.enabled.checked, side: v.side.checked, main: v.main.checked, color: v.color.value, max: Number(v.max.value) || 1, label: v.label }])) }) };
      }
      function teCreateDateBorderSettings() {
        const e = teDateBorderSettings(), t = document.createElement("div");
        t.style.display = "flex"; t.style.flexDirection = "column"; t.style.gap = "10px"; t.style.borderTop = "1px solid #e5e7eb"; t.style.paddingTop = "12px";
        const r = document.createElement("div"); r.textContent = "Date border"; r.style.fontSize = "14px"; r.style.fontWeight = "700";
        const n = document.createElement("div"); n.style.display = "grid"; n.style.gridTemplateColumns = "repeat(auto-fit, minmax(95px, 1fr))"; n.style.gap = "8px";
        const mkCheck = (label, checked) => { const e = document.createElement("label"), t = document.createElement("input"); e.style.fontSize = "12px"; e.style.display = "flex"; e.style.gap = "5px"; e.style.alignItems = "center"; t.type = "checkbox"; t.checked = checked !== false; e.appendChild(t); e.appendChild(document.createTextNode(label)); return { label: e, input: t }; };
        const enabled = mkCheck("사용", e.enabled); n.appendChild(enabled.label); n.appendChild(teMakeDefaultButton("Date border", teDateBorderStoreKey));
        const borderWidth = document.createElement("input"); borderWidth.type = "number"; borderWidth.min = "1"; borderWidth.max = "9"; borderWidth.value = e.borderWidth; borderWidth.style.width = "70px"; borderWidth.style.textAlign = "right"; borderWidth.style.padding = "4px 5px"; borderWidth.style.border = "1px solid #ccc"; borderWidth.style.borderRadius = "4px";
        const bwLabel = document.createElement("label"); bwLabel.textContent = "테두리 px"; bwLabel.style.fontSize = "12px"; bwLabel.style.display = "flex"; bwLabel.style.flexDirection = "column"; bwLabel.style.gap = "3px"; bwLabel.appendChild(borderWidth); n.appendChild(bwLabel);
        const a = document.createElement("div"); a.style.display = "grid"; a.style.gridTemplateColumns = "38px 38px 42px 82px 1fr"; a.style.gap = "5px"; a.style.alignItems = "center"; a.style.fontSize = "12px";
        ["Use", "Side", "Main", "색상", "기간"].forEach((e) => { const t = document.createElement("div"); t.textContent = e; t.style.fontWeight = "700"; a.appendChild(t); });
        const labels = { today: "오늘", week: "1주", month: "1개월", quarter: "3개월" }, rows = {};
        Object.keys(labels).forEach((t) => {
          const r = e.levels[t];
          const n = document.createElement("input"); n.type = "checkbox"; n.checked = r.enabled !== false;
          const i = document.createElement("input"); i.type = "checkbox"; i.checked = r.side !== false;
          const s = document.createElement("input"); s.type = "checkbox"; s.checked = r.main === true;
          const o = document.createElement("input"); o.type = "color"; o.value = teSafeHexColor(r.color, "#22c55e"); o.style.width = "72px";
          const l = document.createElement("div"); l.textContent = labels[t]; l.style.fontWeight = "600";
          a.appendChild(n); a.appendChild(i); a.appendChild(s); a.appendChild(o); a.appendChild(l);
          rows[t] = { enabled: n, side: i, main: s, color: o, label: labels[t] };
        });
        const sample = document.createElement("div"); sample.style.border = "1px solid #dbeafe"; sample.style.borderRadius = "10px"; sample.style.background = "#f4faff"; sample.style.padding = "8px"; sample.style.fontSize = "12px";
        const updateSample = () => {
          sample.innerHTML = `<div style="font-weight:700;margin-bottom:5px">Sample date border</div>` + Object.keys(rows).map((k) => `<span style="display:inline-block;margin:3px;border:${Number(borderWidth.value)||2}px solid ${rows[k].color.value};border-radius:999px;padding:1px 6px;background:#111;color:#fff">${rows[k].label}</span>`).join("\n");
        };
        [enabled.input, borderWidth, ...Object.values(rows).flatMap((e) => [e.enabled, e.side, e.main, e.color])].forEach((e) => e.addEventListener("input", updateSample));
        updateSample();
        t.appendChild(r); t.appendChild(n); t.appendChild(a); t.appendChild(sample);
        return { element: t, getValues: () => ({ enabled: enabled.input.checked, borderWidth: Number(borderWidth.value) || 2, levels: Object.fromEntries(Object.entries(rows).map(([k, v]) => [k, { enabled: v.enabled.checked, side: v.side.checked, main: v.main.checked, color: v.color.value }])) }) };
      }
      function teCreateRankBadgeSettings() {
        const e = teRankBadgeSettings(), t = document.createElement("div");
        t.style.display = "flex"; t.style.flexDirection = "column"; t.style.gap = "10px"; t.style.borderTop = "1px solid #e5e7eb"; t.style.paddingTop = "12px";
        const r = document.createElement("div"); r.textContent = "Side Rank badge / No 000"; r.style.fontSize = "14px"; r.style.fontWeight = "700";
        const desc = document.createElement("div"); desc.textContent = "Side 카드에만 순위를 표시합니다. Main DOM에는 순위 UI를 만들지 않습니다."; desc.style.fontSize = "12px"; desc.style.color = "#64748b";
        const n = document.createElement("div"); n.style.display = "grid"; n.style.gridTemplateColumns = "repeat(auto-fit, minmax(95px, 1fr))"; n.style.gap = "8px";
        const mkCheck = (label, checked) => { const e = document.createElement("label"), t = document.createElement("input"); e.style.fontSize = "12px"; e.style.display = "flex"; e.style.gap = "5px"; e.style.alignItems = "center"; t.type = "checkbox"; t.checked = checked !== false; e.appendChild(t); e.appendChild(document.createTextNode(label)); return { label: e, input: t }; };
        const enabled = mkCheck("사용", e.enabled), side = mkCheck("Side", e.side), bold = mkCheck("Bold", e.bold);
        [enabled.label, teMakeDefaultButton("Side Rank badge", teRankBadgeStoreKey), side.label, bold.label].forEach((e) => n.appendChild(e));
        const grid = document.createElement("div"); grid.style.display = "grid"; grid.style.gridTemplateColumns = "110px 80px 80px 96px 96px"; grid.style.gap = "8px"; grid.style.alignItems = "end"; grid.style.fontSize = "12px";
        const mkNum = (label, value, min, max, step) => { const e = document.createElement("label"), t = document.createElement("input"); e.textContent = label; e.style.display = "flex"; e.style.flexDirection = "column"; e.style.gap = "3px"; t.type = "number"; t.min = String(min); t.max = String(max); t.step = String(step || 1); t.value = value; t.style.width = "76px"; t.style.textAlign = "right"; t.style.padding = "4px 5px"; t.style.border = "1px solid #ccc"; t.style.borderRadius = "4px"; e.appendChild(t); return { label: e, input: t }; };
        const colorLabel = document.createElement("label"); colorLabel.textContent = "테두리색"; colorLabel.style.display = "flex"; colorLabel.style.flexDirection = "column"; colorLabel.style.gap = "3px";
        const borderColor = document.createElement("input"); borderColor.type = "color"; borderColor.value = teSafeHexColor(e.borderColor, "#ff2d2d"); borderColor.style.width = "90px"; colorLabel.appendChild(borderColor);
        const borderWidth = mkNum("테두리 px", e.borderWidth, 0, 8, 1), fontSize = mkNum("글자 px", e.fontSize, 8, 24, 1), borderOpacity = mkNum("테두리 투명", e.borderOpacity, 0, 1, 0.05), textOpacity = mkNum("글자 투명", e.textOpacity, 0, 1, 0.05);
        [colorLabel, borderWidth.label, fontSize.label, borderOpacity.label, textOpacity.label].forEach((e) => grid.appendChild(e));
        const sample = document.createElement("div"); sample.style.border = "1px solid #dbeafe"; sample.style.borderRadius = "10px"; sample.style.background = "#f4faff"; sample.style.padding = "8px"; sample.style.fontSize = "12px";
        const updateSample = () => {
          const fw = bold.input.checked ? 800 : 500;
          const bo = Math.max(0, Math.min(1, Number(borderOpacity.input.value) || 0));
          const to = Math.max(0, Math.min(1, Number(textOpacity.input.value) || 0));
          const badge = `<div class="stat igblue-rank-badge" style="--igblue-rank-border-rgba:${teHexToRgba(borderColor.value, bo, "#ff2d2d")};--igblue-rank-border-width:${Number(borderWidth.input.value)||0}px;--igblue-rank-font-size:${Number(fontSize.input.value)||11}px;--igblue-rank-font-weight:${fw};--igblue-rank-text-opacity:${to}"><span>No 001</span></div>`;
          const body = `${enabled.input.checked && side.input.checked ? badge : ""}<div class="stat">${teSampleMetricIcon("likes")}<span>118</span></div><div class="stat">${teSampleMetricIcon("comments")}<span>23</span></div><div class="stat">${teSampleMetricIcon("shares")}<span>6</span></div><div class="stat">${teSampleMetricIcon("date")}<span>2026-06-15</span></div><div class="stat">${teSampleMetricIcon("views")}<span>37,819</span></div><div class="stat"><span>ER 0.62%</span></div>`;
          sample.innerHTML = teSampleCardHtml(body, teThumbWidthValue());
        };
        [enabled.input, side.input, bold.input, borderColor, borderWidth.input, fontSize.input, borderOpacity.input, textOpacity.input].forEach((e) => e.addEventListener("input", updateSample));
        updateSample();
        t.appendChild(r); t.appendChild(desc); t.appendChild(n); t.appendChild(grid); t.appendChild(sample);
        return { element: t, getValues: () => ({ enabled: enabled.input.checked, side: side.input.checked, bold: bold.input.checked, borderColor: borderColor.value, borderWidth: Number(borderWidth.input.value) || 0, fontSize: Number(fontSize.input.value) || 11, borderOpacity: Number(borderOpacity.input.value) || 0, textOpacity: Number(textOpacity.input.value) || 0 }) };
      }

      function teCreateActiveDownloadSettings() {
        const e = teActiveDownloadSettings(), t = document.createElement("div");
        t.style.display = "flex"; t.style.flexDirection = "column"; t.style.gap = "10px"; t.style.borderTop = "1px solid #e5e7eb"; t.style.paddingTop = "12px";
        const r = document.createElement("div"); r.textContent = "다운로드 상태 강조"; r.style.fontSize = "14px"; r.style.fontWeight = "700";
        const desc = document.createElement("div"); desc.textContent = "체크하면 Instagram Main DOM에서 실제 다운로드 중인 게시물과 MP4/JPG 버튼을 Glow·테두리·확대 값으로 강조합니다. 완료·실패 상태와 파일 형식을 해당 게시물에 유지해 표시합니다."; desc.style.fontSize = "12px"; desc.style.color = "#64748b";
        const n = document.createElement("div"); n.style.display = "grid"; n.style.gridTemplateColumns = "repeat(auto-fit, minmax(92px, 1fr))"; n.style.gap = "8px"; n.style.alignItems = "end"; n.style.fontSize = "12px";
        const mkCheck = (label, checked) => { const e = document.createElement("label"), t = document.createElement("input"); e.style.fontSize = "12px"; e.style.display = "flex"; e.style.gap = "5px"; e.style.alignItems = "center"; t.type = "checkbox"; t.checked = checked !== false; e.appendChild(t); e.appendChild(document.createTextNode(label)); return { label: e, input: t }; };
        const mkNum = (label, value, min, max, step) => { const e = document.createElement("label"), t = document.createElement("input"); e.textContent = label; e.style.display = "flex"; e.style.flexDirection = "column"; e.style.gap = "3px"; t.type = "number"; t.min = String(min); t.max = String(max); t.step = String(step || 1); t.value = value; t.style.width = "76px"; t.style.textAlign = "right"; t.style.padding = "4px 5px"; t.style.border = "1px solid #ccc"; t.style.borderRadius = "4px"; e.appendChild(t); return { label: e, input: t }; };
        const enabled = mkCheck("다운로드 상태 강조", e.enabled), glow = mkCheck("Glow", e.glow);
        const colorLabel = document.createElement("label"); colorLabel.textContent = "색상"; colorLabel.style.display = "flex"; colorLabel.style.flexDirection = "column"; colorLabel.style.gap = "3px";
        const color = document.createElement("input"); color.type = "color"; color.value = teSafeHexColor(e.color, "#ffffff"); color.style.width = "90px"; colorLabel.appendChild(color);
        const borderWidth = mkNum("테두리 px", e.borderWidth, 0, 12, 1), radius = mkNum("radius", e.radius, 0, 999, 1), glowOpacity = mkNum("Glow 투명", e.glowOpacity, 0, 1, 0.05), scale = mkNum("확대", e.scale, 1, 1.6, 0.01);
        [enabled.label, teMakeDefaultButton("다운로드 상태 강조", teActiveDownloadStoreKey), glow.label, colorLabel, borderWidth.label, radius.label, glowOpacity.label, scale.label].forEach((e) => n.appendChild(e));
        const sample = document.createElement("div"); sample.style.border = "1px solid #dbeafe"; sample.style.borderRadius = "10px"; sample.style.background = "#0f172a"; sample.style.padding = "14px"; sample.style.fontSize = "12px"; sample.style.minHeight = "52px"; sample.style.display = "flex"; sample.style.alignItems = "center"; sample.style.justifyContent = "center";
        const updateSample = () => {
          const c = teSafeHexColor(color.value, "#ffffff");
          const bo = Math.max(0, Math.min(12, Number(borderWidth.input.value) || 0));
          const rr = Math.max(0, Math.min(999, Number(radius.input.value) || 0));
          const go = Math.max(0, Math.min(1, Number(glowOpacity.input.value) || 0));
          const sc = Math.max(1, Math.min(1.6, Number(scale.input.value) || 1));
          const gl = glow.input.checked ? `0 0 18px 8px ${teHexToRgba(c, go, "#ffffff")}` : "none";
          sample.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;gap:8px"><span style="padding:3px 8px;border-radius:999px;background:rgba(37,99,235,.92);color:#fff;font-weight:800">MP4 다운로드 중</span><span style="display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border:${bo}px solid ${c};border-radius:${rr}px;box-shadow:0 0 0 4px rgba(0,0,0,.55),${gl};transform:scale(${sc});color:#fff;background:rgba(255,255,255,.08)">▶</span></div>`;
        };
        [enabled.input, glow.input, color, borderWidth.input, radius.input, glowOpacity.input, scale.input].forEach((e) => e.addEventListener("input", updateSample));
        updateSample();
        t.appendChild(r); t.appendChild(desc); t.appendChild(n); t.appendChild(sample);
        return { element: t, getValues: () => ({ enabled: enabled.input.checked, glow: glow.input.checked, color: color.value, borderWidth: Number(borderWidth.input.value) || 0, radius: Number(radius.input.value) || 0, glowOpacity: Number(glowOpacity.input.value) || 0, scale: Number(scale.input.value) || 1 }) };
      }

      function teCreateViewVisualSettings() {
        const e = document.createElement("div");
        e.style.display = "flex"; e.style.flexDirection = "column"; e.style.gap = "12px"; e.style.borderTop = "1px solid #e5e7eb"; e.style.paddingTop = "12px";
        const t = document.createElement("div"); t.textContent = "Visual settings"; t.style.fontSize = "15px"; t.style.fontWeight = "800";
        const r = document.createElement("div"); r.style.fontSize = "12px"; r.style.color = "#475569"; r.textContent = "Side/Main 체크는 사이드패널과 인스타 메인 오버레이 표시 대상을 나눕니다.";
        const n = teCreateMetricBarSettings(), i = teCreateDateBorderSettings();
        e.appendChild(t); e.appendChild(r); e.appendChild(n.element); e.appendChild(i.element);
        return { element: e, getValues: () => ({ metric: n.getValues(), date: i.getValues(), thumbWidth: Number(n.getValues().thumbWidth) || teThumbWidthDefault() }) };
      }
      function teVisualPayload(metricValue, dateValue, activeValue) {
        return { metric: metricValue || teMetricBarSettings(), date: dateValue || teDateBorderSettings(), active: activeValue || teActiveDownloadSettings() };
      }
      function teSendVisualSettingsToMain(metricValue, dateValue, activeValue) {
        const payload = teVisualPayload(metricValue, dateValue, activeValue);
        try { chrome.runtime.sendMessage({ type: "IGBLUE_SAVE_VISUAL_SETTINGS", settings: payload }, () => { chrome.runtime.lastError; }); } catch {}
        try { teSendToTargetTab({ type: "ndy_ig_visual_settings_changed", value: payload }, ""); } catch {}
      }
      function T() {
        const e = document.getElementById("settingsModal");
        e && e.remove();
        const { modal: t, dialog: r } = (function () {
            const e = document.createElement("div");
            ((e.id = "settingsModal"),
              (e.style.position = "fixed"),
              (e.style.inset = "0"),
              (e.style.display = "flex"),
              (e.style.alignItems = "center"),
              (e.style.justifyContent = "center"),
              (e.style.backgroundColor = "rgba(0, 0, 0, 0.4)"),
              (e.style.zIndex = "2100"));
            const t = document.createElement("div");
            return (
              (t.style.backgroundColor = "#fff"),
              (t.style.borderRadius = "8px"),
              (t.style.boxShadow = "0 10px 30px rgba(0,0,0,0.2)"),
              (t.style.padding = "20px 24px"),
              (t.style.width = "760px"),
              (t.style.maxWidth = "96%"),
              (t.style.maxHeight = "90vh"),
              (t.style.overflow = "auto"),
              (t.style.display = "flex"),
              (t.style.flexDirection = "column"),
              (t.style.gap = "20px"),
              { modal: e, dialog: t }
            );
          })(),
          { header: i, closeButton: s } = (function (e) {
            const t = document.createElement("div");
            ((t.style.display = "flex"),
              (t.style.justifyContent = "space-between"),
              (t.style.alignItems = "center"),
              (t.style.position = "sticky"),
              (t.style.top = "0"),
              (t.style.background = "#fff"),
              (t.style.zIndex = "4"),
              (t.style.paddingBottom = "8px"));
            const r = document.createElement("h3");
            ((r.textContent = "Settings"),
              (r.style.margin = "0"),
              (r.style.fontSize = "18px"),
              (r.style.fontWeight = "600"));
            const n = document.createElement("button");
            return (
              (n.type = "button"),
              (n.textContent = "×"),
              (n.style.border = "none"),
              (n.style.background = "transparent"),
              (n.style.fontSize = "24px"),
              (n.style.cursor = "pointer"),
              (n.style.lineHeight = "1"),
              n.setAttribute("aria-label", "Close"),
              n.addEventListener("click", e),
              t.appendChild(r),
              t.appendChild(n),
              { header: t, closeButton: n }
            );
          })(() => t.remove()),
          o = (function (e) {
            const t = document.createElement("label");
            ((t.style.display = "flex"),
              (t.style.flexDirection = "column"),
              (t.style.gap = "6px"),
              (t.style.fontSize = "14px"),
              (t.style.fontWeight = "600"),
              (t.textContent = "Overlay display"));
            const r = document.createElement("select");
            ((r.style.padding = "6px 8px"),
              (r.style.borderRadius = "4px"),
              (r.style.border = "1px solid #ccc"),
              (r.style.fontSize = "14px"));
            const n = {
              detail: "Detail (stats + downloads)",
              download: "Down icon only",
              none: "None (hide overlay)",
            };
            return (
              b.forEach((e) => {
                const t = document.createElement("option");
                ((t.value = e), (t.textContent = n[e] || e), r.appendChild(t));
              }),
              (r.value = e),
              t.appendChild(r),
              {
                element: t,
                getValue: () => r.value,
                setValue: (e) => {
                  k(e) && (r.value = e);
                },
              }
            );
          })(_),
          a = (function (e) {
            const t = document.createElement("div");
            ((t.style.display = "flex"),
              (t.style.flexDirection = "column"),
              (t.style.gap = "12px"));
            const r = document.createElement("div");
            ((r.textContent = "Engagement formula"),
              (r.style.fontSize = "14px"),
              (r.style.fontWeight = "600"));
            const n = document.createElement("div");
            ((n.textContent =
              "Adjust how likes and comments contribute to ER."),
              (n.style.fontSize = "12px"),
              (n.style.color = "#555"));
            const i = document.createElement("div");
            ((i.style.display = "grid"),
              (i.style.gridTemplateColumns =
                "repeat(auto-fit, minmax(140px, 1fr))"),
              (i.style.gap = "10px"));
            const s = {},
              o = (e, t, r) => {
                const n = document.createElement("label");
                ((n.style.display = "flex"),
                  (n.style.flexDirection = "column"),
                  (n.style.gap = "4px"),
                  (n.style.fontSize = "12px"),
                  (n.textContent = e));
                const i = document.createElement("input");
                return (
                  (i.type = "number"),
                  (i.min = "0"),
                  (i.step = "0.1"),
                  (i.placeholder = r),
                  (i.style.padding = "6px 8px"),
                  (i.style.borderRadius = "4px"),
                  (i.style.border = "1px solid #ccc"),
                  n.appendChild(i),
                  (s[t] = i),
                  n
                );
              };
            (i.appendChild(o("Like weight", "like", "1")),
              i.appendChild(o("Comment weight", "comment", "4")),
              i.appendChild(o("Repost weight", "repost", "4")));
            const a = document.createElement("div");
            ((a.style.fontSize = "12px"),
              (a.style.color = "#333"),
              (a.style.backgroundColor = "#f5f5f5"),
              (a.style.borderRadius = "4px"),
              (a.style.padding = "8px"));
            const l = () => ({
                like: s.like.valueAsNumber,
                comment: s.comment.valueAsNumber,
                repost: s.repost.valueAsNumber,
              }),
              c = () => {
                const e = l(),
                  t = Number.isFinite(e.like) ? e.like : "?",
                  r = Number.isFinite(e.comment) ? e.comment : "?",
                  n = Number.isFinite(e.repost) ? e.repost : "?";
                a.textContent = `ER = (Likes × ${t} + Comments × ${r} + Reposts × ${n}) ÷ Plays count`;
              };
            Object.values(s).forEach((e) => {
              e.addEventListener("input", c);
            });
            const u = (e) => {
              const t = C(e);
              ((s.like.value = t.like),
                (s.comment.value = t.comment),
                (s.repost.value = t.repost),
                c());
            };
            return (
              u(e),
              t.appendChild(r),
              t.appendChild(n),
              t.appendChild(i),
              t.appendChild(a),
              { element: t, getValues: l, setValues: u }
            );
          })(x),
          teErUi = teCreateErEmojiSettings(),
          teViewUi = teCreateViewVisualSettings(),
          teRankUi = teCreateRankBadgeSettings(),
          teActiveUi = teCreateActiveDownloadSettings(),
          teStatsUi = teCreateStatsModeSettings();
        let l;
        const tePreviewCurrentSettings = () => {
          const teErValue = teErUi.getValues(), teViewValue = teViewUi.getValues(), teRankValue = teRankUi.getValues(), teActiveValue = teActiveUi.getValues();
          teSaveErEmojiSettings(teErValue);
          teSaveViewVisualSettings(teViewValue);
          teSaveRankBadgeSettings(teRankValue);
          teSaveActiveDownloadSettings(teActiveValue);
          teViewValue && teApplyThumbWidth(teViewValue.thumbWidth);
          teSendVisualSettingsToMain(teViewValue.metric, teViewValue.date, teActiveValue);
          Array.isArray(oe) && ee(oe);
          n("미리보기 적용");
        };
        const c = (function (onCancel, onPreview, onSave) {
          const r = document.createElement("div");
          ((r.style.display = "flex"),
            (r.style.justifyContent = "flex-end"),
            (r.style.gap = "8px"),
            (r.style.position = "sticky"),
            (r.style.bottom = "0"),
            (r.style.background = "#fff"),
            (r.style.paddingTop = "8px"),
            (r.style.zIndex = "4"));
          const n = document.createElement("button");
          ((n.type = "button"),
            (n.textContent = "Cancel"),
            (n.style.padding = "6px 12px"),
            (n.style.border = "1px solid #ccc"),
            (n.style.borderRadius = "4px"),
            (n.style.backgroundColor = "#f4f4f4"),
            n.addEventListener("click", onCancel));
          const pBtn = document.createElement("button");
          ((pBtn.type = "button"),
            (pBtn.textContent = "미리보기"),
            (pBtn.style.padding = "6px 12px"),
            (pBtn.style.border = "1px solid #93c5fd"),
            (pBtn.style.borderRadius = "4px"),
            (pBtn.style.backgroundColor = "#e7f1ff"),
            (pBtn.style.color = "#1d4ed8"),
            (pBtn.style.cursor = "pointer"),
            pBtn.addEventListener("click", (ev) => { ev.preventDefault(); ev.stopPropagation(); onPreview(); }));
          const i = document.createElement("button");
          return (
            (i.type = "button"),
            (i.textContent = "Save"),
            (i.style.padding = "6px 12px"),
            (i.style.border = "none"),
            (i.style.borderRadius = "4px"),
            (i.style.backgroundColor = "#f7941e"),
            (i.style.color = "#fff"),
            (i.style.cursor = "pointer"),
            i.addEventListener("click", onSave),
            r.appendChild(n),
            r.appendChild(pBtn),
            r.appendChild(i),
            { element: r, saveButton: i, previewButton: pBtn }
          );
        })(
          () => t.remove(),
          tePreviewCurrentSettings,
          () => {
            const e = o.getValue(),
              r = a.getValues(),
              teErValue = teErUi.getValues(),
              teViewValue = teViewUi.getValues(),
              teRankValue = teRankUi.getValues(),
              teActiveValue = teActiveUi.getValues(),
              teStatsModeValue = teStatsUi.getValue();
            if (!k(e)) return void n("Invalid overlay mode");
            if (!E(r)) return void n("Invalid ER weights");
            const i = C(r);
            var s, c;
            ((l.disabled = !0),
              Promise.all([
                ((c = e),
                new Promise((e, t) => {
                  chrome.storage.local.set({ [m]: c }, () => {
                    chrome.runtime.lastError
                      ? t(new Error(chrome.runtime.lastError.message))
                      : ((_ = c), e());
                  });
                })),
                ((s = i),
                new Promise((e, t) => {
                  const r = C(s);
                  chrome.storage.local.set({ [y]: r }, () => {
                    chrome.runtime.lastError
                      ? t(new Error(chrome.runtime.lastError.message))
                      : ((x = r), e());
                  });
                })),
              ])
                .then(() => {
                  (!(function (e) {
                    chrome.tabs.query(
                      { url: ["*://www.instagram.com/*"] },
                      (t) => {
                        chrome.runtime.lastError ||
                          (t &&
                            0 !== t.length &&
                            t.forEach((t) => {
                              chrome.tabs.sendMessage(
                                t.id,
                                {
                                  type: "ndy_ig_overlay_mode_changed",
                                  value: e,
                                },
                                () => {
                                  chrome.runtime.lastError &&
                                    chrome.runtime.lastError.message?.includes(
                                      "Receiving end does not exist",
                                    );
                                },
                              );
                            }));
                      },
                    );
                  })(e),
                    (function (e) {
                      const t = C(e || x);
                      chrome.tabs.query(
                        { url: ["*://www.instagram.com/*"] },
                        (e) => {
                          chrome.runtime.lastError ||
                            (e &&
                              0 !== e.length &&
                              e.forEach((e) => {
                                chrome.tabs.sendMessage(
                                  e.id,
                                  {
                                    type: "ndy_ig_er_weights_changed",
                                    value: t,
                                  },
                                  () => {
                                    chrome.runtime.lastError &&
                                      chrome.runtime.lastError.message?.includes(
                                        "Receiving end does not exist",
                                      );
                                  },
                                );
                              }));
                        },
                      );
                    })(i),
                    teSaveErEmojiSettings(teErValue),
                    teSaveViewVisualSettings(teViewValue),
                    teSaveRankBadgeSettings(teRankValue),
                    teSaveActiveDownloadSettings(teActiveValue),
                    teSaveStatsMode(teStatsModeValue),
                    teViewValue && teApplyThumbWidth(teViewValue.thumbWidth),
                    teSendVisualSettingsToMain(teViewValue.metric, teViewValue.date, teActiveValue),
                    Array.isArray(oe) && ee(oe),
                    n("Settings updated"),
                    t.remove());
                })
                .catch((e) => {
                  n("Failed to save, please retry");
                })
                .finally(() => {
                  l.disabled = !1;
                }));
          },
        );
        const teTabs = teCreateSettingsTabs([
          { title: "기본", element: o.element },
          { title: "ER", element: (() => { const e = document.createElement("div"); e.style.display = "flex"; e.style.flexDirection = "column"; e.style.gap = "12px"; e.appendChild(a.element); e.appendChild(teErUi.element); return e; })() },
          { title: "막대/날짜", element: (() => { const e = document.createElement("div"); e.style.display = "flex"; e.style.flexDirection = "column"; e.style.gap = "12px"; e.appendChild(teViewUi.element); e.appendChild(teActiveUi.element); return e; })() },
          { title: "순위", element: teRankUi.element },
          { title: "통계", element: teStatsUi.element }
        ]);
        ((l = c.saveButton),
          r.appendChild(i),
          r.appendChild(teTabs.element),
          r.appendChild(c.element),
          t.appendChild(r),
          document.body.appendChild(t),
          S().then((e) => {
            o.setValue(e);
          }),
          M().then((e) => {
            a.setValues(e);
          }),
          setTimeout(() => {
            s.focus();
          }, 0));
      }
      function A() {
        n("Down limit reached");
        const e = document.getElementById("down_progress");
        e && (e.textContent = "Status: Down limit reached");
        teProgressState.visible = !1;
        teProgressState.done = !1;
        teUpdateProgressBox();
      }
      function R(e) {
        return new Promise((t) => setTimeout(t, e));
      }
      function O() {
        return {
          all: Array.isArray(se) ? se.length : 0,
          filtered: Array.isArray(oe) ? oe.length : 0,
        };
      }
      function j(e, t, r) {
        const n = document.getElementById(e);
        return n
          ? t !== r
            ? ((n.textContent = r),
              n.classList.add("highlight"),
              setTimeout(() => n.classList.remove("highlight"), 500),
              r)
            : (n.textContent !== String(r) && (n.textContent = r), t)
          : r;
      }
      function I(e, t) {
        const r = {
          filtered: Number.isFinite(e) ? e : O().filtered,
          all: Number.isFinite(t) ? t : O().all,
        };
        ((h = j("page_info_all", h, r.all)),
          (f = j("page_info_filtered", f, r.filtered)),
          N(),
          P());
      }
      // IGblue local patch v15: side-panel progress box for bulk downloads.
      function teFormatBytes(e) {
        const t = Number(e) || 0;
        return t >= 1073741824 ? `${(t / 1073741824).toFixed(2)} GB` : t >= 1048576 ? `${(t / 1048576).toFixed(1)} MB` : t >= 1024 ? `${(t / 1024).toFixed(1)} KB` : `${t} B`;
      }
      function teExtractNoLabelFromText(...values) {
        try {
          const joined = values.map((v) => String(v || "")).filter(Boolean).join("\n");
          if (!joined) return "";
          const m = joined.match(/(?:^|[^A-Za-z0-9])No\s*0*(\d{1,6})(?=$|[^A-Za-z0-9])/i);
          if (!m) return "";
          const n = Number(m[1] || 0) || 0;
          return n > 0 ? ("No" + String(n).padStart(3, "0")) : "";
        } catch (_) { return ""; }
      }
      function teProgressCurrentNoLabel(extra) {
        return teProgressState.no_label || teExtractNoLabelFromText(extra || "", teProgressState.file_path, teProgressState.file, teProgressState.message, teProgressState.shortcode);
      }
      function tePad2(n) { return String(Math.max(0, Number(n) || 0)).padStart(2, "0"); }
      function teFormatShortDateTime(ts) {
        if (!ts) return "-- --:--";
        const d = new Date(Number(ts));
        const ap = d.getHours() >= 12 ? "PM" : "AM";
        const hh = d.getHours() % 12 || 12;
        return `${tePad2(d.getMonth() + 1)}-${tePad2(d.getDate())} ${ap}${tePad2(hh)}:${tePad2(d.getMinutes())}`;
      }
      function teFormatElapsed(ms) {
        const s = Math.max(0, Math.floor((Number(ms) || 0) / 1000));
        const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
        return `${tePad2(h)}:${tePad2(m)}:${tePad2(sec)}`;
      }
      function teFormatSec(ms) {
        const v = Math.max(0, Number(ms) || 0) / 1000;
        return v >= 10 ? `${Math.round(v)}초` : `${v.toFixed(1)}초`;
      }
      function teFormatSecRange(minMs, maxMs) {
        return `${teFormatSec(minMs || 0).replace(/초$/, "")}~${teFormatSec(maxMs || 0)}`;
      }
      function teProgressAllPaths() {
        try {
          const raw = [];
          if (teProgressState.file_path) raw.push(teProgressState.file_path);
          if (teProgressState.file) raw.push(teProgressState.file);
          if (Array.isArray(teProgressState.file_paths)) raw.push.apply(raw, teProgressState.file_paths);
          const seen = new Set();
          const out = [];
          for (const pth of raw) {
            const v = String(pth || "").trim();
            if (!v || seen.has(v)) continue;
            seen.add(v);
            out.push(v);
          }
          return out;
        } catch (_) { return []; }
      }
      function teProgressPrimaryPath() {
        const paths = teProgressAllPaths();
        return paths.length ? paths[0] : "";
      }
      function teProgressMp4Path() {
        try {
          for (const pth of teProgressAllPaths()) {
            const v = String(pth || "");
            if (/\.mp4(?:$|[?#])/i.test(v)) return v;
          }
        } catch (_) {}
        return "";
      }
      function teProgressAssetLabel() {
        const k = String(teProgressState.asset_kind || "").toLowerCase();
        if (k === "video" || /\.mp4(?:$|[?#])/i.test(teProgressPrimaryPath())) return "mp4";
        if (k === "image" || /\.(jpg|jpeg|png|webp)(?:$|[?#])/i.test(teProgressPrimaryPath())) return "jpg";
        return k || "파일";
      }
      function teAutoPyRoundedDecilePct(done, target) {
        const t = Math.max(0, Number(target || 0) || 0);
        const d = Math.max(0, Number(done || 0) || 0);
        if (!t) return 0;
        return Math.max(0, Math.min(100, Math.round(((d / t) * 100) / 10) * 10));
      }
      function teAutoPyUiVisible() {
        return !!(teAutoPyUiState && teAutoPyUiState.phase && teAutoPyUiState.phase !== "idle");
      }
      function teAutoPyUiReset(targetCount, reason, options) {
        const opts = options && typeof options === "object" ? options : {};
        const target = Math.max(1, Number(targetCount || teAutoPyTargetCount && teAutoPyTargetCount() || 1) || 1);
        teAutoPyUiState = { phase: "prepare", target_count: target, completed_count: 0, completed_shortcodes: [], current_no: 0, asset_kind: "", wait_remaining: 0, exhausted: false, exhausted_reason: "", automatic_end: false, message: String(reason || "자동 준비"), account_index: 0, account_total: 0 };
        if (opts.reset_run_totals !== false) teAutoPyRunTotals = { completed_post_count: 0, completed_shortcodes: [], mp4_count: 0, jpg_count: 0, expected_asset_keys: [], completed_asset_keys: [] };
        try { teUpdateProgressBox(); teRenderUnifiedStatus("auto_ui_reset"); } catch (_) {}
      }
      function teAutoPySetUiPhase(phase, extra) {
        const next = Object.assign({}, teAutoPyUiState || {}, extra || {});
        next.phase = String(phase || next.phase || "idle");
        next.target_count = Math.max(1, Number(next.target_count || teAutoPyTargetCount && teAutoPyTargetCount() || 1) || 1);
        next.completed_count = Math.max(0, Number(next.completed_count || 0) || 0);
        if (!Array.isArray(next.completed_shortcodes)) next.completed_shortcodes = [];
        next.wait_remaining = Math.max(0, Number(next.wait_remaining || 0) || 0);
        next.automatic_end = !!next.automatic_end;
        next.exhausted = !!next.exhausted;
        teAutoPyUiState = next;
        try { teUpdateProgressBox(); teRenderUnifiedStatus("auto_ui_" + next.phase); } catch (_) {}
        return next;
      }
      function teAutoPyCompletedPostCount() {
        return Math.max(0, Number(teAutoPyStats && teAutoPyStats.completed_post_count || teAutoPyUiState && teAutoPyUiState.completed_count || 0) || 0);
      }
      function teAutoPyAssetProgressKey(shortcode, kind) {
        const code = String(shortcode || "").trim();
        const asset = String(kind || "").toLowerCase() === "jpg" ? "jpg" : "mp4";
        return code ? `${code}:${asset}` : "";
      }
      function teAutoPyRegisterExpectedAsset(shortcode, kind) {
        const key = teAutoPyAssetProgressKey(shortcode, kind);
        if (!key) return false;
        const list = Array.isArray(teAutoPyRunTotals.expected_asset_keys) ? teAutoPyRunTotals.expected_asset_keys : [];
        if (!list.includes(key)) list.push(key);
        teAutoPyRunTotals.expected_asset_keys = list;
        return true;
      }
      function teAutoPyRegisterCompletedAsset(shortcode, kind) {
        const key = teAutoPyAssetProgressKey(shortcode, kind);
        if (!key) return false;
        teAutoPyRegisterExpectedAsset(shortcode, kind);
        const list = Array.isArray(teAutoPyRunTotals.completed_asset_keys) ? teAutoPyRunTotals.completed_asset_keys : [];
        if (!list.includes(key)) list.push(key);
        teAutoPyRunTotals.completed_asset_keys = list;
        return true;
      }
      function teAutoPyRunFileProgress() {
        const expected = new Set((Array.isArray(teAutoPyRunTotals.expected_asset_keys) ? teAutoPyRunTotals.expected_asset_keys : []).map((v) => String(v || "")).filter(Boolean));
        const completed = new Set((Array.isArray(teAutoPyRunTotals.completed_asset_keys) ? teAutoPyRunTotals.completed_asset_keys : []).map((v) => String(v || "")).filter((v) => expected.has(v)));
        return { completed: completed.size, total: expected.size };
      }
      function teAutoPyOverallRunPct() {
        const acct = typeof teAutoPyAccountQueueStatus === "function" ? teAutoPyAccountQueueStatus() : null;
        const target = Math.max(1, Number(teAutoPyUiState && teAutoPyUiState.target_count || teAutoPyStats && teAutoPyStats.target_count || teAutoPyTargetCount && teAutoPyTargetCount() || 1) || 1);
        const totalPosts = acct && acct.has && Number(acct.total || 0) > 0 ? Math.max(1, Number(acct.total || 0) * target) : target;
        const completedPosts = Math.max(0, Number(teAutoPyRunTotals.completed_post_count || 0) || 0);
        const raw = teFmtProgressPct(completedPosts, totalPosts);
        return teAutoPyUiState && teAutoPyUiState.automatic_end ? 100 : Math.min(99, raw);
      }
      function teAutoPyCompletedShortcodeSet() {
        const list = Array.isArray(teAutoPyStats && teAutoPyStats.completed_shortcodes) ? teAutoPyStats.completed_shortcodes : [];
        return new Set(list.map((code) => String(code || "").trim()).filter(Boolean));
      }
      function teAutoPyFirstPendingRankNo(list) {
        const arr = Array.isArray(list) ? list : [];
        const completed = teAutoPyCompletedShortcodeSet();
        for (let i = 0; i < arr.length; i++) {
          const media = arr[i] || {};
          const code = String(media.code || media.shortcode || "").trim();
          if (code && !completed.has(code)) return i + 1;
        }
        return 0;
      }
      function teAutoPyRecordCompletedPost(shortcode, reason) {
        const code = String(shortcode || "").trim();
        if (!code) return teAutoPyCompletedPostCount();
        const list = Array.isArray(teAutoPyStats.completed_shortcodes) ? teAutoPyStats.completed_shortcodes : [];
        if (!list.includes(code)) list.push(code);
        teAutoPyStats.completed_shortcodes = list;
        teAutoPyStats.completed_post_count = list.length;
        const runList = Array.isArray(teAutoPyRunTotals.completed_shortcodes) ? teAutoPyRunTotals.completed_shortcodes : [];
        if (!runList.includes(code)) runList.push(code);
        teAutoPyRunTotals.completed_shortcodes = runList;
        teAutoPyRunTotals.completed_post_count = runList.length;
        teAutoPyStats.last_reason = String(reason || "post_complete");
        teAutoPySetUiPhase("post_complete", {
          completed_count: list.length,
          completed_shortcodes: list.slice(),
          current_no: Number(teAutoPyStats.current_no || 0) || 0,
          message: String(reason || "실제 저장 완료")
        });
        teAutoPyStatsTouch("post_complete", true);
        teProgressCommitCompletedUnifiedSnapshot("post_complete");
        return list.length;
      }
      function teAutoPyUiTitleText() {
        const st = teAutoPyUiState || {};
        const target = Math.max(1, Number(st.target_count || teAutoPyTargetCount && teAutoPyTargetCount() || 1) || 1);
        const completed = Math.max(0, Number(st.completed_count || teAutoPyCompletedPostCount() || 0) || 0);
        const noLabel = Number(st.current_no || 0) > 0 ? teAutoPyNoLabel(st.current_no) : "";
        const assetRaw = String(st.asset_kind || "").toLowerCase();
        const asset = assetRaw === "video" || assetRaw === "mp4" ? "MP4" : assetRaw === "image" || assetRaw === "jpg" || assetRaw === "jpeg" ? "JPG" : String(st.asset_kind || "").toUpperCase();
        if (st.phase === "auto_end") {
          if (st.exhausted && completed < target) return `다운로드 완료 · ${teAutoPyRoundedDecilePct(completed, target)}% 진행 · 더이상없음 · 자동종료`;
          return `다운로드 완료 · 목표 ${completed}/${target} · 자동종료`;
        }
        if (st.phase === "user_stopped") return `다운로드 중단 · 사용자 중단 · 완료 ${completed}/${target}`;
        if (st.phase === "failed") return `다운로드 실패 · ${String(st.message || "오류")} · 완료 ${completed}/${target}`;
        if (st.phase === "account_next") return `다운로드 완료 · ${completed}/${target} · 다음 계정 대기`;
        if (st.phase === "paused") return `자동 대기중 · ${String(st.message || "정상화 대기")} · 완료 ${completed}/${target}`;
        if (st.phase === "waiting") return `다운로드 · ${String(Math.max(0, Number(st.wait_remaining || 0) || 0)).padStart(2, "0")}초 대기중 · 완료 ${completed}/${target}`;
        if (st.phase === "swipe") return `다운로드 · Swipe 중 · 완료 ${completed}/${target}`;
        if (st.phase === "searching" || st.phase === "loading") return `다운로드 · 게시물 탐색중 · 완료 ${completed}/${target}`;
        if (st.phase === "downloading") return `다운로드 중${noLabel ? ` · ${noLabel}` : ""}${asset ? ` · ${asset}` : ""} · 완료 ${completed}/${target}`;
        if (st.phase === "post_complete") return `다운로드 진행 · 완료 ${completed}/${target}`;
        if (st.phase === "prepare") return `다운로드 준비 · 목표 ${target}개 · 완료 ${completed}개`;
        return `다운로드 진행${noLabel ? ` · ${noLabel}` : ""}`;
      }
      async function teAutoPyWaitCountdown(seconds, context) {
        const sec = Math.max(0, Math.floor(Number(seconds || 0) || 0));
        for (let remain = sec; remain > 0; remain--) {
          if (teAutoPyStopping) return false;
          teAutoPySetUiPhase("waiting", Object.assign({}, context || {}, { wait_remaining: remain }));
          await teSleep(1000);
        }
        teAutoPySetUiPhase("searching", Object.assign({}, context || {}, { wait_remaining: 0 }));
        return true;
      }
      function teProgressPreserveUntilNextDownload(reason) {
        try {
          if (!teDownloadDisplayAwaitingStart) {
            const snapshot = teUnifiedProgressText();
            if (snapshot) teAutoPyLastDoneText = snapshot;
          }
          teDownloadDisplayAwaitingStart = true;
          return String(reason || "");
        } catch (_) { teDownloadDisplayAwaitingStart = true; return ""; }
      }
      function teProgressBeginActualDownload(event) {
        const e = event && typeof event === "object" ? event : {};
        const priorMode = teProgressState && teProgressState.mode || "";
        teDownloadDisplayAwaitingStart = false;
        teAutoPyLastDoneText = "";
        teProgressState = {
          visible: true,
          done: false,
          file: "",
          file_path: "",
          file_paths: [],
          no_label: "",
          loaded: 0,
          total: 0,
          percent: 0,
          message: "",
          owner_key: teActiveFolderAccountName ? teActiveFolderAccountName() : "",
          shortcode: "",
          asset_kind: "",
          media_index: 0,
          json_path: "",
          start_ts: Date.now(),
          mode: priorMode,
          actual_start_status: String(e.status || "start")
        };
      }
      function teProgressCommitCompletedUnifiedSnapshot(reason) {
        try {
          const held = teDownloadDisplayAwaitingStart;
          teDownloadDisplayAwaitingStart = false;
          const text = teUnifiedProgressText();
          if (text) {
            teAutoPyLastDoneText = text;
            const el = document.getElementById("igblue_auto_py_stats");
            if (el) {
              el.textContent = text;
              el.title = text + "\n현재 주소 진행률 / 전체 주소 목록 진행률 / 종료예상입니다.";
            }
          }
          teDownloadDisplayAwaitingStart = held;
          return String(reason || "");
        } catch (_) { return ""; }
      }
      function teRecentDownloadSummaryText() {
        try {
          const no = teProgressCurrentNoLabel ? teProgressCurrentNoLabel() : "";
          const pct = Math.max(0, Math.min(100, Number(teProgressState.percent || 0) || 0));
          const bytes = teProgressState.total ? ` · ${teFormatBytes(teProgressState.total)}` : "";
          const file = String(teProgressState.file_path || teProgressState.file || "");
          const name = file ? file.split(/[\/]/).pop() : "";
          const sc = teProgressState.shortcode ? ` · ${teProgressState.shortcode}` : "";
          if (!no && !file && !teProgressState.shortcode) return "";
          return `최근다운${no ? " " + no : ""} ${pct}%${bytes}${sc}${name ? " · " + name : ""}`;
        } catch (_) { return ""; }
      }
      function teFmtProgressPct(done, total) {
        const t = Math.max(0, Number(total || 0) || 0);
        const d = Math.max(0, Number(done || 0) || 0);
        if (!t) return 0;
        return Math.max(0, Math.min(100, Math.round((d / t) * 100)));
      }
      function teFormatEtaHhMm(ms) {
        if (ms == null || !Number.isFinite(Number(ms))) return "계산중";
        const totalSec = Math.max(0, Math.ceil(Number(ms) / 1000));
        const h = Math.floor(totalSec / 3600);
        const m = Math.floor((totalSec % 3600) / 60);
        const sec = totalSec % 60;
        return `${String(h).padStart(2, "0")}시간 ${String(m).padStart(2, "0")}분 ${String(sec).padStart(2, "0")}초`;
      }
      const TE_AUTO_ETA_STATS_KEY = "download/auto_eta_stats_v274";
      function teAutoEtaDefaultStats() {
        return { schema: "ig_auto_eta_v279ccc", sample_limit: 20, samples_ms: [], post_count: 0, post_total_ms: 0, mp4_count: 0, mp4_total_ms: 0, jpg_count: 0, jpg_total_ms: 0, updated_at: "", last_reason: "" };
      }
      function teAutoEtaLoadStats() {
        try {
          const parsed = JSON.parse(teLocalGet(TE_AUTO_ETA_STATS_KEY, "{}") || "{}");
          const out = Object.assign(teAutoEtaDefaultStats(), parsed && typeof parsed === "object" ? parsed : {});
          out.samples_ms = (Array.isArray(out.samples_ms) ? out.samples_ms : []).map((v) => Math.max(0, Number(v || 0) || 0)).filter((v) => v > 0).slice(-20);
          out.sample_limit = 20;
          return out;
        } catch (_) { return teAutoEtaDefaultStats(); }
      }
      function teAutoEtaSaveStats(stats, reason) {
        try {
          const next = Object.assign(teAutoEtaDefaultStats(), stats && typeof stats === "object" ? stats : {});
          next.updated_at = new Date().toISOString();
          next.last_reason = String(reason || next.last_reason || "save");
          teLocalSet(TE_AUTO_ETA_STATS_KEY, JSON.stringify(next));
          return next;
        } catch (_) { return teAutoEtaDefaultStats(); }
      }
      function teAutoEtaAverageMs(stats) {
        const st = stats && typeof stats === "object" ? stats : {};
        const samples = (Array.isArray(st.samples_ms) ? st.samples_ms : []).map((v) => Math.max(0, Number(v || 0) || 0)).filter((v) => v > 0).slice(-20);
        if (samples.length) return samples.reduce((sum, value) => sum + value, 0) / samples.length;
        const count = Math.max(0, Number(st.post_count || 0) || 0);
        const total = Math.max(0, Number(st.post_total_ms || 0) || 0);
        return count > 0 && total > 0 ? total / count : 0;
      }
      function teAutoEtaRecordCompletedPost(results, reason, setElapsedMs) {
        try {
          const arr = Array.isArray(results) ? results : [];
          let postMs = 0, mp4Ms = 0, jpgMs = 0, mp4Count = 0, jpgCount = 0;
          arr.forEach((item) => {
            if (!(item && item.ok) || item.skipped) return;
            const ms = Math.max(0, Number(item.elapsed_ms || 0) || 0);
            if (!ms) return;
            const kind = String(item.media_type || item.asset_kind || "").toLowerCase();
            postMs += ms;
            if (kind === "jpg" || kind === "jpeg" || kind === "image") { jpgMs += ms; jpgCount += 1; }
            else { mp4Ms += ms; mp4Count += 1; }
          });
          if (!(postMs > 0)) return false;
          const stats = teAutoEtaLoadStats();
          stats.post_count = Math.max(0, Number(stats.post_count || 0) || 0) + 1;
          stats.post_total_ms = Math.max(0, Number(stats.post_total_ms || 0) || 0) + postMs;
          stats.mp4_count = Math.max(0, Number(stats.mp4_count || 0) || 0) + mp4Count;
          stats.mp4_total_ms = Math.max(0, Number(stats.mp4_total_ms || 0) || 0) + mp4Ms;
          stats.jpg_count = Math.max(0, Number(stats.jpg_count || 0) || 0) + jpgCount;
          stats.jpg_total_ms = Math.max(0, Number(stats.jpg_total_ms || 0) || 0) + jpgMs;
          const sampleMs = Math.max(0, Number(setElapsedMs || 0) || 0) || postMs;
          const samples = Array.isArray(stats.samples_ms) ? stats.samples_ms : [];
          samples.push(sampleMs);
          stats.samples_ms = samples.slice(-20);
          stats.sample_limit = 20;
          stats.schema = "ig_auto_eta_v279ccc";
          teAutoEtaSaveStats(stats, reason || "post_download_complete");
          return true;
        } catch (_) { return false; }
      }
      function teAutoEtaPersist(reason) {
        try { teAutoEtaSaveStats(teAutoEtaLoadStats(), reason || "persist"); } catch (_) {}
      }
      function teAutoPyRemainingPostsForEta(accountIndex, accountTotal, targetCount, currentCompleted) {
        const total = Math.max(0, Number(accountTotal || 0) || 0);
        const index = Math.max(0, Number(accountIndex || 0) || 0);
        const target = Math.max(1, Number(targetCount || 1) || 1);
        const completed = Math.max(0, Math.min(target, Number(currentCompleted || 0) || 0));
        if (!(total > 0)) return Math.max(0, target - completed);
        if (index <= 0) return Math.max(0, total * target);
        const currentRemain = Math.max(0, target - completed);
        const futureAccounts = Math.max(0, total - Math.min(total, index));
        return currentRemain + futureAccounts * target;
      }
      function teAutoPyAveragePostDownloadMs() {
        const saved = teAutoEtaAverageMs(teAutoEtaLoadStats());
        if (saved > 0) return saved;
        const st = teAutoPyStats && typeof teAutoPyStats === "object" ? teAutoPyStats : {};
        const completed = Math.max(0, Number(st.completed_post_count || 0) || 0);
        const currentTotal = Math.max(0, Number(st.mp4_total_ms || 0) || 0) + Math.max(0, Number(st.jpg_total_ms || 0) || 0);
        return completed > 0 && currentTotal > 0 ? currentTotal / completed : 0;
      }
      function teAutoPyOverallEtaMs(accountIndex, accountTotal, targetCount, currentCompleted) {
        const remain = teAutoPyRemainingPostsForEta(accountIndex, accountTotal, targetCount, currentCompleted);
        if (!remain) return 0;
        const averageDownloadMs = Math.max(0, teAutoPyAveragePostDownloadMs());
        if (!(averageDownloadMs > 0)) return null;
        const delayMs = Math.max(0, Number(teDownloadDelaySec && teDownloadDelaySec() || 0) || 0) * 1000;
        return remain * (averageDownloadMs + delayMs);
      }
      function teEstimateEtaMs(done, total, startTs) {
        const t = Math.max(0, Number(total || 0) || 0);
        const d = Math.max(0, Number(done || 0) || 0);
        const remain = Math.max(0, t - d);
        if (!remain) return 0;
        if (!(d > 0)) return null;
        const elapsed = Math.max(0, Date.now() - (Number(startTs || 0) || Date.now()));
        const avg = Math.max(500, elapsed / d);
        const delay = Math.max(0, Number(teDownloadDelaySec && teDownloadDelaySec() || 0) || 0) * 1000;
        return remain * (avg + delay);
      }
      function teUnifiedCurrentAccountName() {
        try {
          const acct = typeof teAutoPyAccountQueueStatus === "function" ? teAutoPyAccountQueueStatus() : null;
          const fromQueue = String(acct && acct.account || "").replace(/^@/, "").trim();
          if (fromQueue) return fromQueue;
          const fromUrl = String(teAccountNameFromAccountUrl && teAccountNameFromAccountUrl(acct && acct.url || teCurrentTargetUrl()) || "").replace(/^@/, "").trim();
          return fromUrl || "계정미확인";
        } catch (_) { return "계정미확인"; }
      }
      function teUnifiedProgressText() {
        try {
          const ui = teAutoPyUiState || {};
          if (teDownloadDisplayAwaitingStart && !ui.automatic_end) return teAutoPyLastDoneText || "";
          let currentLine = "현재 주소  계정미확인  0%";
          let overallLine = "전체 주소 목록 -  0%  종료예상 계산중";
          const countLine = (label, stats) => {
            const value = stats || {};
            const posts = Math.max(0, Number(value.completed_post_count || 0) || 0);
            const mp4 = Math.max(0, Number(value.mp4_count || 0) || 0);
            const jpg = Math.max(0, Number(value.jpg_count || 0) || 0);
            return `${label}: 게시물 ${posts}개 · MP4 ${mp4}개 · JPG ${jpg}개`;
          };
          const currentCountLine = () => countLine("현재 계정", teAutoPyStats || {});
          const runCountLine = () => countLine("전체 실행", teAutoPyRunTotals || {});
          const acct = (typeof teAutoPyAccountQueueStatus === "function" ? teAutoPyAccountQueueStatus() : null);
          const acctTotal = acct && acct.has ? Math.max(0, Number(acct.total || 0) || 0) : 0;
          const acctIndex = acct && acct.has ? Math.max(0, Number(acct.index || 0) || 0) : 0;
          const currentAccountName = teUnifiedCurrentAccountName();
          if (ui.automatic_end) {
            const target = Math.max(1, Number(ui.target_count || teAutoPyTargetCount && teAutoPyTargetCount() || 1) || 1);
            const completed = Math.max(0, Number(ui.completed_count || teAutoPyCompletedPostCount() || 0) || 0);
            currentLine = ui.exhausted && completed < target
              ? `자동종료 · ${teAutoPyRoundedDecilePct(completed, target)}% 진행 · 더이상없음`
              : `자동종료 · 목표 ${completed}/${target} 완료`;
            const finalTotal = Math.max(0, Number(ui.account_total || acctTotal || 0) || 0);
            const finalIndex = finalTotal > 0 ? Math.min(finalTotal, Math.max(0, Number(ui.account_index || acctIndex || 0) || 0)) : Math.max(0, Number(ui.account_index || acctIndex || 0) || 0);
            overallLine = finalTotal > 0
              ? `전체 주소 목록 ${finalIndex}/${finalTotal}  100%  종료예상 00시간 00분`
              : `전체 주소 목록 -  100%  종료예상 00시간 00분`;
            const downLine = currentCountLine() + "\n" + runCountLine();
            teAutoPyLastDoneText = currentLine + "\n" + overallLine + "\n" + downLine;
            return teAutoPyLastDoneText;
          }
          if (p) {
            const total = Math.max(0, Number(p.totalFiles || p.totalPosts || 0) || 0);
            const done = Math.max(0, Number(p.completedFiles || 0) || 0);
            const etaTarget = Math.max(1, Number(teAutoPyStats && teAutoPyStats.target_count || teAutoPyUiState && teAutoPyUiState.target_count || teAutoPyTargetCount && teAutoPyTargetCount() || 1) || 1);
            const etaCompleted = Math.max(0, Number(teAutoPyStats && teAutoPyStats.completed_post_count || teAutoPyUiState && teAutoPyUiState.completed_count || 0) || 0);
            const autoActive = !!(teAutoPyStats && teAutoPyStats.active || teAutoPyUiVisible());
            const pct = autoActive ? teFmtProgressPct(etaCompleted, etaTarget) : teFmtProgressPct(done, total);
            const noLabel = teProgressCurrentNoLabel(p.currentFileLabel || teProgressState.no_label || "");
            const currentIdentity = noLabel || (autoActive ? "" : `${done}/${total || 0}`);
            currentLine = [`현재 주소`, currentAccountName, currentIdentity, `${pct}%`].filter(Boolean).join("  ");
            const eta = acctTotal > 0
              ? teAutoPyOverallEtaMs(acctIndex, acctTotal, etaTarget, etaCompleted)
              : teEstimateEtaMs(done, total, p.start_ts || teProgressState.start_ts || 0);
            if (acctTotal > 0) {
              const base = Math.max(0, acctIndex - 1);
              const overallPct = autoActive ? teAutoPyOverallRunPct() : Math.max(0, Math.min(100, Math.round(((base + pct / 100) / acctTotal) * 100)));
              overallLine = `전체 주소 목록 ${acctIndex}/${acctTotal}  ${overallPct}%  종료예상 ${teFormatEtaHhMm(eta)}`;
            } else {
              overallLine = `전체 주소 목록 -  ${pct}%  종료예상 ${teFormatEtaHhMm(eta)}`;
            }
            const downLine = currentCountLine() + "\n" + runCountLine();
            if ((p.statusLabel === "Completed" || teProgressState.done) && total > 0) {
              teAutoPyLastDoneText = "자동다운로드 완료\n" + currentLine + "\n" + overallLine + "\n" + downLine;
              return teAutoPyLastDoneText;
            }
            return currentLine + "\n" + overallLine + "\n" + downLine;
          }
          const st = teAutoPyStats || {};
          if (st.start_ts || st.active) {
            const target = Math.max(1, Number(st.target_count || teAutoPyTargetCount && teAutoPyTargetCount() || 1) || 1);
            const curNo = Math.max(0, Number(st.current_no || teAutoPyReadLastRankTextValue && teAutoPyReadLastRankTextValue() || 0) || 0);
            const done = Math.max(0, Math.min(target, Number(st.completed_post_count || 0) || 0));
            const pct = teFmtProgressPct(done, target);
            const currentNoPart = curNo ? `  ${teAutoPyNoLabel(curNo)}` : "";
            currentLine = `현재 주소  ${currentAccountName}${currentNoPart}  ${pct}%`;
            const eta = acctTotal > 0
              ? teAutoPyOverallEtaMs(acctIndex, acctTotal, target, done)
              : teEstimateEtaMs(done, target, st.start_ts || Date.now());
            if (acctTotal > 0) {
              const overallPct = teAutoPyOverallRunPct();
              overallLine = `전체 주소 목록 ${acctIndex}/${acctTotal}  ${overallPct}%  종료예상 ${teFormatEtaHhMm(eta)}`;
            } else {
              overallLine = `전체 주소 목록 -  ${pct}%  종료예상 ${teFormatEtaHhMm(eta)}`;
            }
            const downLine = currentCountLine() + "\n" + runCountLine();
            return currentLine + "\n" + overallLine + "\n" + downLine;
          }
          if (teAutoPyLastDoneText) return teAutoPyLastDoneText;
          return "";
        } catch (_) { return ""; }
      }
      function teRenderUnifiedStatus(reason) {
        try {
          const el = document.getElementById("igblue_auto_py_stats");
          if (!el) return;
          if (teDownloadDisplayAwaitingStart && !(teAutoPyUiState && teAutoPyUiState.automatic_end)) return;
          const text = teUnifiedProgressText();
          el.textContent = text;
          el.title = text + "\n현재 주소 진행률 / 전체 주소 목록 진행률 / 종료예상입니다.";
          el.classList.toggle("is-idle", !p && !(teAutoPyStats && (teAutoPyStats.start_ts || teAutoPyStats.active)));
        } catch (_) {}
      }
      function teAutoPyStatsText() {
        return teUnifiedProgressText();
      }
      function teAutoPyStatsRender() {
        teRenderUnifiedStatus("auto_stats");
      }
      function teAutoPyStatsPushToMain(reason, force) {
        try {
          const now = Date.now();
          if (!force && now - teAutoPyStatsLastMainPush < 900) return;
          teAutoPyStatsLastMainPush = now;
          const text = teAutoPyStatsText();
          try { localStorage.setItem("IGBLUE/auto_py_stats_last", JSON.stringify(Object.assign({}, teAutoPyStats, { text, reason: String(reason || ""), updated_at: new Date().toISOString() }))); } catch (_) {}
          const accountState = teAutoPyAccountQueueStatus ? teAutoPyAccountQueueStatus() : null;
          teSendToTargetTab({ type: "igblue_auto_status_update", value: {
            text,
            reason: String(reason || ""),
            active: !!(teAutoPyStats && teAutoPyStats.active),
            phase: String(teAutoPyUiState && teAutoPyUiState.phase || "idle"),
            run_seq: Number(teAutoPyRunSeq || teAutoPyStats && teAutoPyStats.run_seq || 0) || 0,
            no_label: teAutoPyStats && teAutoPyStats.current_no_label || "",
            target_count: Number(teAutoPyStats && teAutoPyStats.target_count || teAutoPyUiState && teAutoPyUiState.target_count || 0) || 0,
            completed_count: Number(teAutoPyStats && teAutoPyStats.completed_post_count || teAutoPyUiState && teAutoPyUiState.completed_count || 0) || 0,
            account_index: accountState && accountState.has ? Number(accountState.index || 0) : 0,
            account_total: accountState && accountState.has ? Number(accountState.total || 0) : 0,
            account_username: accountState && accountState.has ? String(accountState.account || "").replace(/^@/, "") : "",
            ts: now
          } }, "").catch(() => {});
        } catch (_) {}
      }
      function teAutoPyStatsTouch(reason, force) {
        teAutoPyStatsRender();
        teAutoPyStatsPushToMain(reason, !!force);
      }
      function teAutoPyStatsStart(seq, startNo, maxRank, reason) {
        const n = Number(startNo || 0) || 0;
        if (!(teAutoPyStats && teAutoPyStats.active && Number(teAutoPyStats.run_seq || 0) === Number(seq || 0))) {
          teAutoPyStats = { active: true, run_seq: Number(seq || 0) || 0, start_ts: Date.now(), current_no: n, current_no_label: n > 0 ? teAutoPyNoLabel(n) : "No---", max_rank: Number(maxRank || 0) || 0, target_count: Math.max(1, Number(teAutoPyTargetCount && teAutoPyTargetCount() || maxRank || 1) || 1), completed_post_count: 0, completed_shortcodes: [], mp4_count: 0, jpg_count: 0, mp4_total_ms: 0, jpg_total_ms: 0, mp4_max_ms: 0, jpg_max_ms: 0, mp4_min_ms: 0, jpg_min_ms: 0, cycle_count: 0, cycle_total_ms: 0, cycle_max_ms: 0, cycle_min_ms: 0, last_reason: String(reason || "start") };
        } else {
          teAutoPyStats.current_no = n || teAutoPyStats.current_no || 0;
          teAutoPyStats.current_no_label = n > 0 ? teAutoPyNoLabel(n) : (teAutoPyStats.current_no_label || "No---");
          teAutoPyStats.max_rank = Number(maxRank || teAutoPyStats.max_rank || 0) || 0;
          teAutoPyStats.target_count = Math.max(1, Number(teAutoPyStats.target_count || teAutoPyTargetCount && teAutoPyTargetCount() || maxRank || 1) || 1);
          teAutoPyStats.last_reason = String(reason || teAutoPyStats.last_reason || "update");
        }
        if (!teAutoPyStatsTimer) teAutoPyStatsTimer = setInterval(() => { try { if (teAutoPyStats && teAutoPyStats.start_ts) teAutoPyStatsTouch("tick", false); } catch (_) {} }, 1000);
        teAutoPyStatsTouch(reason || "start", true);
      }
      function teAutoPyStatsSetRank(no, reason) {
        const n = Number(no || 0) || 0;
        if (n > 0) {
          teAutoPyStats.current_no = n;
          teAutoPyStats.current_no_label = teAutoPyNoLabel(n);
        }
        teAutoPyStats.last_reason = String(reason || teAutoPyStats.last_reason || "rank");
        teAutoPyStatsTouch(reason || "rank", false);
      }
      function teAutoPyStatsRecordAsset(kind, elapsedMs) {
        const k = String(kind || "").toLowerCase() === "jpg" ? "jpg" : "mp4";
        const ms = Math.max(0, Number(elapsedMs) || 0);
        if (k === "jpg") {
          teAutoPyStats.jpg_count += 1;
          teAutoPyRunTotals.jpg_count = Math.max(0, Number(teAutoPyRunTotals.jpg_count || 0) || 0) + 1;
          teAutoPyStats.jpg_total_ms += ms;
          teAutoPyStats.jpg_max_ms = Math.max(teAutoPyStats.jpg_max_ms || 0, ms);
          teAutoPyStats.jpg_min_ms = teAutoPyStats.jpg_min_ms ? Math.min(teAutoPyStats.jpg_min_ms, ms) : ms;
        } else {
          teAutoPyStats.mp4_count += 1;
          teAutoPyRunTotals.mp4_count = Math.max(0, Number(teAutoPyRunTotals.mp4_count || 0) || 0) + 1;
          teAutoPyStats.mp4_total_ms += ms;
          teAutoPyStats.mp4_max_ms = Math.max(teAutoPyStats.mp4_max_ms || 0, ms);
          teAutoPyStats.mp4_min_ms = teAutoPyStats.mp4_min_ms ? Math.min(teAutoPyStats.mp4_min_ms, ms) : ms;
        }
        teAutoPyStatsTouch("asset_" + k, true);
      }
      function teAutoPyStatsRecordCycle(startMs, reason) {
        const ms = Math.max(0, Date.now() - (Number(startMs) || Date.now()));
        if (!ms) return;
        teAutoPyStats.cycle_count += 1;
        teAutoPyStats.cycle_total_ms += ms;
        teAutoPyStats.cycle_max_ms = Math.max(teAutoPyStats.cycle_max_ms || 0, ms);
        teAutoPyStats.cycle_min_ms = teAutoPyStats.cycle_min_ms ? Math.min(teAutoPyStats.cycle_min_ms, ms) : ms;
        teAutoPyStatsTouch(reason || "cycle", true);
      }
      function teAutoPyStatsFinish(reason) {
        try {
          if (teAutoPyStats && teAutoPyStats.start_ts) {
            teAutoPyStats.active = false;
            teAutoPyStats.last_reason = String(reason || "finished");
            if (teAutoPyUiState && teAutoPyUiState.automatic_end) {
              teAutoPyLastDoneText = teUnifiedProgressText();
              try { n("자동종료"); } catch (_) {}
            } else if (teAutoPyUiState && ["failed", "user_stopped", "account_next"].includes(teAutoPyUiState.phase)) {
              teAutoPyLastDoneText = teUnifiedProgressText();
            }
            teAutoPyStatsTouch(reason || "finished", true);
          }
          teAutoEtaPersist(reason || "stats_finish");
          if (teAutoPyStatsTimer) { clearInterval(teAutoPyStatsTimer); teAutoPyStatsTimer = 0; }
        } catch (_) {}
      }
      function teProgressEls() {
        return {
          box: document.getElementById("igblue_download_progress"),
          bar: document.getElementById("igblue_progress_bar"),
          summary: document.getElementById("igblue_progress_summary"),
          detail: document.getElementById("igblue_progress_detail"),
          ok: document.getElementById("igblue_progress_ok"),
          stop: document.getElementById("igblue_progress_stop"),
          resume: document.getElementById("igblue_progress_resume"),
          openFile: document.getElementById("igblue_progress_open_file"),
          openFolder: document.getElementById("igblue_progress_open_folder"),
          title: document.getElementById("igblue_progress_title")
        };
      }
      function teIsDownloadRunning() {
        return !!(p && p.statusLabel && !["Completed", "Stopped", "No downloadable medias"].includes(p.statusLabel));
      }
      function teUpdateDownloadControlButtons() {
        const e = teProgressEls();
        const running = teIsDownloadRunning();
        if (e.stop) e.stop.disabled = !running || teDownloadStopRequested;
        if (e.resume) e.resume.disabled = !(teDownloadResumeState && !running);
      }
      function teRequestStopDownload() {
        if (!teIsDownloadRunning()) return;
        teDownloadStopRequested = !0;
        teProgressState.message = "Stop requested. Current file will finish first.";
        p && (p.statusLabel = "Stopping...");
        N();
      }
      function teResumeStoppedDownload() {
        if (!teDownloadResumeState || teIsDownloadRunning()) return;
        const e = teDownloadResumeState;
        teDownloadResumeState = null;
        X(e.scope, [], e);
      }
      function teCloseProgressBox() {
        teProgressState.visible = !1;
        teProgressState.done = !1;
        teProgressState.file = "";
        teProgressState.loaded = 0;
        teProgressState.total = 0;
        teProgressState.percent = 0;
        teProgressState.message = "";
        teProgressState.file_path = "";
        teProgressState.file_paths = [];
        teProgressState.shortcode = "";
        teProgressState.asset_kind = "";
        teProgressState.media_index = 0;
        teProgressState.json_path = "";
        teProgressState.start_ts = 0;
        p = null;
        teDownloadStopRequested = !1;
        teUpdateProgressBox();
      }
      function teUpdateProgressBox() {
        const e = teProgressEls();
        if (!e.box) return;
        if (e.ok && !e.ok.dataset.igblueBound) {
          e.ok.dataset.igblueBound = "1";
          e.ok.addEventListener("click", teCloseProgressBox);
        }
        if (e.stop && !e.stop.dataset.igblueBound) {
          e.stop.dataset.igblueBound = "1";
          e.stop.addEventListener("click", () => {
            teRequestStopDownload();
            try { if (teAutoPyStats && teAutoPyStats.active) teAutoPyStop("progress_stop"); } catch (_) {}
          });
        }
        if (e.resume && !e.resume.dataset.igblueBound) {
          e.resume.dataset.igblueBound = "1";
          e.resume.addEventListener("click", () => { teResumeStoppedDownload(); });
        }
        const openLocal = (kind) => {
          const path = kind === "folder" ? (teProgressMp4Path() || teProgressState.file_path || teProgressState.file || "") : teProgressMp4Path();
          if (!path) return;
          try {
            chrome.runtime.sendMessage({type: kind === "folder" ? "IGBLUE_OPEN_LOCAL_FOLDER" : "IGBLUE_OPEN_LOCAL_FILE", path}, (res) => {
              if (chrome.runtime.lastError) {
                teAiwkSetupNotice(true, chrome.runtime.lastError.message || "로컬 열기 실패");
                n("로컬 열기 실패");
              } else if (!res || res.ok === false) {
                teAiwkTrackWsResult(res || {ok:false, connected:false, error_code:"AIWK_OPEN_EMPTY"});
                n((res && (res.message || res.error)) || "로컬 열기 실패");
              } else {
                teAiwkTrackWsResult(res);
                n(kind === "folder" ? "폴더 열기 요청" : "mp4 열기 요청");
              }
            });
          } catch (_) { n("로컬 열기 예외"); }
        };
        if (e.openFile && !e.openFile.dataset.igblueBound) {
          e.openFile.dataset.igblueBound = "1";
          e.openFile.addEventListener("click", () => openLocal("file"));
        }
        if (e.openFolder && !e.openFolder.dataset.igblueBound) {
          e.openFolder.dataset.igblueBound = "1";
          e.openFolder.addEventListener("click", () => openLocal("folder"));
        }
        const mp4Path = teProgressMp4Path();
        const hasMp4 = !!mp4Path;
        const hasAny = !!(teProgressState.file_path || teProgressState.file || (Array.isArray(teProgressState.file_paths) && teProgressState.file_paths.length));
        const hasProgressIdentity = !!(teProgressState.shortcode || teProgressState.asset_kind || teProgressState.no_label);
        const autoTerminal = !!(teAutoPyUiState && teAutoPyUiState.automatic_end);
        if (teDownloadDisplayAwaitingStart && !autoTerminal) return;
        if (e.openFile) { e.openFile.disabled = !hasMp4; e.openFile.textContent = "mp4열기"; }
        if (e.openFolder) e.openFolder.disabled = !(hasMp4 || hasAny);
        const hasAutoUi = teAutoPyUiVisible();
        if (!p && !teProgressState.visible && !hasAny && !hasProgressIdentity) return;
        e.box.classList.remove("igblue-progress-hidden");
        const t = p || { statusLabel: teProgressState.done ? "Completed" : "Downing...", currentFileLabel: teProgressState.file };
        const i = Math.max(0, Math.min(100, Number(teProgressState.percent) || 0));
        const done = t.statusLabel === "Completed" || teProgressState.done || autoTerminal;
        const autoTarget = Math.max(1, Number(teAutoPyUiState && teAutoPyUiState.target_count || 1) || 1);
        const autoCompleted = Math.max(0, Number(teAutoPyUiState && teAutoPyUiState.completed_count || 0) || 0);
        const pct = hasAutoUi ? (autoTerminal ? 100 : teAutoPyOverallRunPct()) : (done ? 100 : i);
        const progressNoLabel = teProgressCurrentNoLabel(t.currentFileLabel || teProgressState.no_label || "");
        const size = teProgressState.total > 0 ? teFormatBytes(teProgressState.total) : (teProgressState.loaded > 0 ? teFormatBytes(teProgressState.loaded) : "");
        const sc = String(teProgressState.shortcode || "");
        if (e.title) e.title.textContent = autoTerminal ? teAutoPyUiTitleText() : (done ? `다운로드 완료${progressNoLabel ? ` · ${progressNoLabel}` : ""}` : (hasAutoUi ? teAutoPyUiTitleText() : `다운로드 진행${progressNoLabel ? ` · ${progressNoLabel}` : ""}`));
        if (e.bar) e.bar.style.width = `${pct}%`;
        if (e.ok) e.ok.style.display = "none";
        teUpdateDownloadControlButtons();
        const runFileProgress = teAutoPyRunFileProgress();
        const runFileLabel = hasAutoUi && runFileProgress.total > 0 ? `파일 ${runFileProgress.completed}/${runFileProgress.total}` : "";
        if (e.summary) e.summary.textContent = [progressNoLabel, runFileLabel, `${pct}%`, sc].filter(Boolean).join(" · ");
        if (e.detail) {
          const allPaths = teProgressAllPaths();
          const primaryPath = teProgressPrimaryPath();
          const assetLabel = teProgressAssetLabel();
          const pathLines = allPaths.length
            ? allPaths.slice(0, 6).map((pth, idx) => `${idx === 0 ? "전체 저장 주소" : "추가 저장 주소 " + (idx + 1)}: ${pth}`)
            : [`${assetLabel}: 대기중`];
          e.detail.textContent = [
            progressNoLabel ? `No: ${progressNoLabel}` : "",
            sc ? `shortcode: ${sc}` : "",
            assetLabel ? `asset: ${assetLabel}${Number(teProgressState.media_index || 0) ? `[${teProgressState.media_index}]` : ""}` : "",
            primaryPath ? `${assetLabel}: ${primaryPath}` : "",
            ...pathLines,
            teProgressState.json_path ? `json: ${teProgressState.json_path}` : "",
            teProgressState.message ? `상태: ${teProgressState.message}` : ""
          ].filter(Boolean).join("\n");
        }
        teRenderUnifiedStatus("progress_box");
      }
      function teHandleDownProgress(e) {
        const status = String(e && e.status || "").toLowerCase();
        if (e && e.feature_check_id) {
          teAutoPyFeatureCheck(
            e.feature_check_id,
            e.feature_check_status || "[체크완료]",
            e.feature_check_message_ko || e.message || "",
            {
              cycle_id: e.feature_cycle_id || e.cycle_id || "",
              rank_no: Number(e.rank_no || 0) || 0,
              rank_label: String(e.rank_label || e.no_label || ""),
              shortcode: String(e.shortcode || ""),
              asset_kind: String(e.asset_kind || ""),
              error_code: String(e.error_code || ""),
              next_check: String(e.next_check || "")
            }
          );
          if (String(e.status || "") === "feature_check") return;
        }
        if (status === "start") teProgressBeginActualDownload(e);
        teProgressState.visible = !0;
        teProgressState.file = e.file || teProgressState.file || "";
        teProgressState.file_path = e.file_path || teProgressState.file_path || (String(e.file || "").includes(":\\") ? e.file : "");
        teProgressState.file_paths = Array.isArray(e.file_paths) ? e.file_paths : teProgressState.file_paths || [];
        teProgressState.shortcode = e.shortcode || teProgressState.shortcode || (e.meta && e.meta.shortcode) || "";
        teProgressState.no_label = e.no_label || teExtractNoLabelFromText(e.file_path, e.file, e.prefix, e.filename, e.message, teProgressState.file_path, teProgressState.file) || teProgressState.no_label || "";
        teProgressState.asset_kind = e.asset_kind || teProgressState.asset_kind || "";
        teProgressState.media_index = e.media_index != null ? e.media_index : (teProgressState.media_index || 0);
        teProgressState.json_path = e.json_path || teProgressState.json_path || "";
        teProgressState.loaded = Number(e.loaded) || 0;
        teProgressState.total = Number(e.total) || 0;
        teProgressState.percent = Math.max(0, Math.min(100, Number(e.percent) || 0));
        teProgressState.message = e.message || "";
        teProgressState.owner_key = teActiveFolderAccountName ? teActiveFolderAccountName() : teProgressState.owner_key || "";
        const terminal = status === "done" || status === "already_done" || status === "error";
        teProgressState.done = status === "done" || status === "already_done";
        if (teProgressState.done) teProgressState.percent = 100;
        if (status === "error") teProgressState.message = e.message || "Down error";
        try {
          const recent = {
            ts: new Date().toISOString(),
            status: e.status || "",
            instagram_url: teCurrentTargetUrl(),
            shortcode: teProgressState.shortcode || "",
            no_label: teProgressState.no_label || teProgressCurrentNoLabel(),
            asset_kind: teProgressState.asset_kind || "",
            media_index: teProgressState.media_index || 0,
            file_path: teProgressState.file_path || teProgressState.file || "",
            file_paths: teProgressState.file_paths || [],
            json_path: teProgressState.json_path || "",
            message: teProgressState.message || ""
          };
          localStorage.setItem("IGBLUE/recent_download_state", JSON.stringify(recent));
        } catch (_) {}
        teUpdateProgressBox();
        if (terminal) {
          const snapshot = teUnifiedProgressText();
          if (snapshot) teAutoPyLastDoneText = snapshot;
          teDownloadDisplayAwaitingStart = true;
        }
      }
      function N() {
        const e = document.getElementById("down_progress");
        if (!e) return;
        if (!p) {
          e.textContent = d > 0 ? `Status: Queue ${d}` : "Status: Idle";
          teUpdateProgressBox();
          teRenderUnifiedStatus("status_idle");
          return;
        }
        e.textContent = p.statusLabel ? `Status: ${p.statusLabel}` : "Status: Running";
        if (p.statusLabel === "Completed") {
          teProgressState.visible = !0;
          teProgressState.done = !0;
          teProgressState.percent = 100;
        }
        teUpdateProgressBox();
        teRenderUnifiedStatus("status_running");
      }
      function P() {
        const hasIgTarget = !!u || (typeof teHasInstagramTarget !== "undefined" && !!teHasInstagramTarget);
        const e = document.getElementById("goto");
        // IGblue v86: Goto Instagram은 인스타 대상 존재 여부와 무관하게 항상 누를 수 있어야 한다.
        // 인스타 페이지가 아니거나 대상 탭이 끊긴 상태에서도 사용자가 직접 Instagram으로 이동해야 한다.
        e && (e.disabled = false);
        const t = (e, t) => {
          e.forEach((e) => {
            const r = document.getElementById(e);
            r && (r.disabled = t);
          });
        };
        if (!hasIgTarget) return (t([...i, ...s, ...o], !0), void N());
        const busy = l.size > 0;
        t(["refresh"], !1);
        t(["export_xlsx"], !u);
        t(i, busy);
        const n = O();
        (o.forEach((e) => {
          const t = document.getElementById(e);
          t &&
            (busy
              ? (t.disabled = !0)
              : "down_all_medias" === e
                ? (t.disabled = n.all <= 0)
                : "down_filtered_medias" === e &&
                  (t.disabled = n.filtered <= 0));
        }),
          N());
      }
      function B(e) {
        (l.add(e), P());
      }
      function D(e) {
        l.delete(e) && P();
      }
      function F() {
        ((c = null),
          // IGblue local patch v9: do not clear the UI merely because Instagram changed URL inside the SPA.
          // Clear only when there is no active Instagram target.
          (typeof teHasInstagramTarget !== "undefined" && teHasInstagramTarget)
            ? P()
            : u &&
              ((u = !1),
              void 0 !== se && (se = null),
              void 0 !== oe && (oe = []),
              ee([]),
              ie(0, 0),
              (p = null),
              (d = 0),
              D(a),
              I(0, 0),
              P()));
      }
      function L(e, t, r) {
        let n = { type: "getl" };
        try {
          chrome.runtime.sendMessage(n, function (i) {
            chrome.runtime.lastError ||
              (-1 == i.status
                ? e(r ? -1 : t)
                : ((n = { type: "down" }),
                  chrome.runtime.sendMessage(n, function (r) {
                    chrome.runtime.lastError || (r.status < 4 ? e(t) : A());
                  })));
          });
        } catch {}
      }
      function z() {
        n("Upgrade link disabled in local build");
      }
      function U() {
        L($, 3, !0);
      }
      function $(e) {
        oe && oe.length > 0
          ? (async function (e) {
              const r = new (t().Workbook)(),
                n = r.addWorksheet("Posts"),
                i = Array.from(
                  new Set(
                    e.flatMap((e) =>
                      Object.keys(e).filter(
                        (e) =>
                          "img_b64" !== e && "code" !== e && "username" !== e,
                      ),
                    ),
                  ),
                ).sort((e, t) => e.localeCompare(t)),
                s = ["Image", "Code", "username", ...i];
              (n.addRow(s), (n.views = [{ state: "frozen", ySplit: 1 }]));
              for (let t = 0; t < e.length; t++) {
                const s = e[t],
                  o = [
                    "",
                    "",
                    "",
                    ...i.map((e) =>
                      "created_at" === e && s[e]
                        ? ce(new Date(1e3 * s[e]))
                        : (s[e] ?? ""),
                    ),
                  ],
                  a = n.addRow(o);
                a.height = 78;
                const l = n.getCell(`B${t + 2}`);
                if (
                  ((l.value = {
                    text: s.code,
                    hyperlink: `https://www.instagram.com/p/${s.code}`,
                  }),
                  (l.font = { color: { argb: "FF0000FF" }, underline: !0 }),
                  s?.username)
                ) {
                  const e = n.getCell(`C${t + 2}`);
                  ((e.value = {
                    text: s.username,
                    hyperlink: `https://www.instagram.com/${s.username}`,
                  }),
                    (e.font = { color: { argb: "FF0000FF" }, underline: !0 }));
                }
                if (
                  (i.forEach((e, t) => {
                    const r = a.getCell(4 + t),
                      n = s[e];
                    n &&
                      ("audio_collection" === e &&
                        ((r.value = {
                          text: n,
                          hyperlink: `https://www.instagram.com/reels/audio/${n}/`,
                        }),
                        (r.font = {
                          color: { argb: "FF0000FF" },
                          underline: !0,
                        })),
                      "original_sound_username" === e &&
                        ((r.value = {
                          text: n,
                          hyperlink: `https://www.instagram.com/${n}`,
                        }),
                        (r.font = {
                          color: { argb: "FF0000FF" },
                          underline: !0,
                        })));
                  }),
                  s.img_b64?.startsWith("data:image"))
                ) {
                  const { width: e, height: i } = await le(s.img_b64),
                    o = 100,
                    a = o * (e / i),
                    l = r.addImage({
                      base64: s.img_b64,
                      extension: s.img_b64.includes("image/png")
                        ? "png"
                        : "jpeg",
                      hyperlink: {
                        hyperlink: `https://www.instagram.com/p/${s.code}`,
                        tooltip: "View on Instagram",
                      },
                    });
                  n.addImage(l, {
                    tl: { col: 0, row: t + 1 },
                    ext: { width: a, height: o },
                  });
                }
              }
              n.columns = [
                { width: 13 },
                { width: 12 },
                { width: 12 },
                ...i.map(() => ({ width: 12 })),
              ];
              const o = await r.xlsx.writeBuffer(),
                a = new Blob([o], {
                  type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
                }),
                l = document.createElement("a");
              ((l.href = URL.createObjectURL(a)),
                (l.download =
                  "ig-sorter-" +
                  ((c = new Date()),
                  `${c.getFullYear()}${String(c.getMonth() + 1).padStart(2, "0")}${String(c.getDate()).padStart(2, "0")}-${String(c.getHours()).padStart(2, "0")}${String(c.getMinutes()).padStart(2, "0")}${String(c.getSeconds()).padStart(2, "0")}.xlsx`)),
                l.click());
              var c;
            })(oe)
          : n("empty list");
      }
      function teMetricCountEl() {
        return document.getElementById("top_metric_count");
      }
      function teSetTopMetricCountValue(value, title) {
        const el = teMetricCountEl();
        const n = Math.max(0, Math.floor(Number(value || 0) || 0));
        if (el) {
          el.dataset.count = String(n);
          el.textContent = String(n);
          el.title = title || "현재 조건에 맞는 실제 게시물 개수";
        }
        teLocalSet("filter/metric_count", String(n));
        teLocalSet("top_metric_count", String(n));
        return n;
      }
      function teGetTopMetricCountValue() {
        const el = teMetricCountEl();
        const raw = el ? (el.dataset.count || el.textContent || "0") : teLocalGetAny(["filter/metric_count", "top_metric_count"], "0");
        const n = Number.parseInt(raw, 10);
        return Number.isFinite(n) && n > 0 ? n : 0;
      }
      function teTopNCount() {
        // v179a12: Top sorted medias 개수는 사용자가 입력하는 input이 아니라 계산된 span(data-count) 값이다.
        return teGetTopMetricCountValue();
      }
      function teAutoPyTargetCount() {
        // v179a2: Auto PY 종료 목표. 0/빈값이면 조건에 맞는 전체(top_metric_count)를 사용한다.
        const el = document.getElementById("igblue_auto_py_target_count");
        const raw = el ? Number.parseInt(el.value, 10) : 0;
        const n = Number.isFinite(raw) && raw > 0 ? raw : 0;
        return n > 0 ? n : teTopNCount();
      }
      function teAutoPyTargetCountSave() {
        const el = document.getElementById("igblue_auto_py_target_count");
        if (el) teLocalSet("download/auto_py_target_count", el.value || "");
      }

      const IGBLUE_ACCOUNT_RESERVED_PATHS = new Set(["", "p", "reel", "reels", "explore", "explorer", "stories", "accounts", "direct", "graphql", "api", "oauth", "developer", "about", "privacy", "terms", "legal", "emails", "challenge", "search", "home", "instagram"]);
      const IGBLUE_ACCOUNT_DELETED_PREFIX = "🗑️";
      function teAccountQueueKey() { return "download/auto_py_account_urls"; }
      function teAccountQueueIndexKey() { return "download/auto_py_account_index"; }
      function teAccountQueueHeightKey() { return "download/auto_py_account_textarea_height_px"; }
      function teAccountQueueDisabledKey() { return "download/auto_py_account_queue_disabled"; }
      function teAutoPyAccountQueueDisabled() {
        return teChannelLocked();
      }
      function teAutoPySetAccountQueueDisabled(on) {
        const input = document.getElementById("channel_lock");
        if (input) input.checked = !!on;
        teLocalSet("run/current_channel_only", on ? "1" : "0");
        teLocalSet("target/channel_lock", on ? "1" : "0");
        if (on) teAutoPySetAccountQueueIndex(0);
        teAutoPyUpdateAccountQueueStatus();
      }
      function teAutoPyDeletedLine(raw) {
        return String(raw || "").trim().startsWith(IGBLUE_ACCOUNT_DELETED_PREFIX);
      }
      function teAutoPyStripDeletedPrefix(raw) {
        return String(raw || "").trim().replace(/^🗑️\s*/, "").trim();
      }
      function teAutoPyNormalizeAccountUsername(raw) {
        try {
          let value = teAutoPyStripDeletedPrefix(raw).replace(/^@+/, "").trim();
          if (!value) return "";
          if (/^(?:https?:\/\/|www\.|instagram\.com\/)/i.test(value)) {
            const url = /^https?:\/\//i.test(value) ? value : "https://" + value;
            const parsed = new URL(url);
            if (parsed.hostname.replace(/^www\./i, "").toLowerCase() !== "instagram.com") return "";
            const parts = String(parsed.pathname || "/").split("/").filter(Boolean);
            const first = String(parts[0] || "").trim();
            if (!first || IGBLUE_ACCOUNT_RESERVED_PATHS.has(first.toLowerCase())) return "";
            if (parts.length > 1 && String(parts[1] || "").toLowerCase() !== "reels") return "";
            value = first;
          } else if (value.includes("/")) return "";
          value = value.toLowerCase();
          if (!value || value.length > 30 || IGBLUE_ACCOUNT_RESERVED_PATHS.has(value)) return "";
          if (!/^[a-z0-9._]+$/.test(value)) return "";
          return value;
        } catch (_) { return ""; }
      }
      function teAutoPyAccountUrlFromUsername(raw) {
        if (teAutoPyDeletedLine(raw)) return "";
        const username = teAutoPyNormalizeAccountUsername(raw);
        return username ? `https://www.instagram.com/${username}/` : "";
      }
      function teAutoPyNormalizeAccountUrl(raw) {
        return teAutoPyAccountUrlFromUsername(raw);
      }
      function teAccountNameFromAccountUrl(value) {
        const username = teAutoPyNormalizeAccountUsername(value);
        return username ? "@" + username : "";
      }
      function teAutoPySameAccountUrl(a, b) {
        const ua = teAutoPyNormalizeAccountUsername(a), ub = teAutoPyNormalizeAccountUsername(b);
        return !!(ua && ub && ua === ub);
      }
      function teAutoPyNormalizeAccountQueueEntries(entries) {
        const byUsername = new Map();
        for (const rawEntry of Array.isArray(entries) ? entries : []) {
          const username = teAutoPyNormalizeAccountUsername(rawEntry && (rawEntry.username || rawEntry.account || rawEntry.account_url || rawEntry.url || rawEntry));
          if (!username) continue;
          const prev = byUsername.get(username);
          const deleted = !!(rawEntry && (rawEntry.deleted || rawEntry.is_deleted)) || !!(prev && prev.deleted);
          const enabled = deleted ? false : ((rawEntry && rawEntry.enabled !== false) && !(prev && prev.enabled === false));
          if (prev) Object.assign(prev, { deleted, enabled });
          else byUsername.set(username, { username, deleted, enabled });
        }
        return Array.from(byUsername.values());
      }
      function teAutoPySortAccountQueueEntries(entries) {
        return teAutoPyNormalizeAccountQueueEntries(entries).sort((a, b) => a.username.localeCompare(b.username, "en", { sensitivity: "base" }));
      }
      function teAutoPyAccountQueueEntries() {
        const el = document.getElementById("igblue_account_url_list");
        const raw = el ? String(el.value || "") : teLocalGet(teAccountQueueKey(), "");
        const entries = [];
        for (const line of String(raw || "").split(/\r?\n|\r/)) {
          const username = teAutoPyNormalizeAccountUsername(line);
          if (!username) continue;
          const deleted = teAutoPyDeletedLine(line);
          entries.push({ username, deleted, is_deleted: deleted, enabled: !deleted });
        }
        return teAutoPyNormalizeAccountQueueEntries(entries);
      }
      function teAutoPyAccountQueueUsernames(includeDisabled) {
        if (!includeDisabled && teAutoPyAccountQueueDisabled()) return [];
        return teAutoPyAccountQueueEntries().filter((entry) => !entry.deleted && entry.enabled !== false).map((entry) => entry.username);
      }
      function teAutoPyAccountQueueList(includeDisabled) {
        return teAutoPyAccountQueueUsernames(includeDisabled).map((username) => teAutoPyAccountUrlFromUsername(username)).filter(Boolean);
      }
      function teAutoPyStoredAccountQueueList(entries) {
        const list = teAutoPyNormalizeAccountQueueEntries(Array.isArray(entries) ? entries : teAutoPyAccountQueueEntries());
        return list.map((entry) => entry.deleted ? `${IGBLUE_ACCOUNT_DELETED_PREFIX} @${entry.username}` : `@${entry.username}`);
      }
      function teAutoPyAccountQueueIndex() {
        const n = Number.parseInt(teLocalGet(teAccountQueueIndexKey(), "0"), 10);
        return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
      }
      function teAutoPySetAccountQueueIndex(n) {
        const v = Math.max(0, Math.floor(Number(n || 0) || 0));
        teLocalSet(teAccountQueueIndexKey(), String(v));
        const input = document.getElementById("igblue_account_queue_index_input");
        if (input && String(input.value || "") !== String(v)) input.value = String(v);
        teAutoPyUpdateAccountQueueStatus();
      }
      function teAutoPyFindAccountQueueIndex(urls, currentUrl) {
        if (!Array.isArray(urls) || !urls.length) return -1;
        for (let i = 0; i < urls.length; i++) if (teAutoPySameAccountUrl(urls[i], currentUrl)) return i;
        return -1;
      }
      function teAutoPyQueueUrlIndexFromUiIndex(uiIndex, urls) {
        const n = Math.max(0, Math.floor(Number(uiIndex || 0) || 0));
        if (n <= 0) return -1;
        return Math.min(n - 1, Math.max(0, (Array.isArray(urls) ? urls.length : 0) - 1));
      }
      let teAccountQueueDirty = false;
      function teAutoPySetAccountQueueDirty(dirty) {
        teAccountQueueDirty = !!dirty;
        const saveBtn = document.getElementById("igblue_account_url_save");
        if (saveBtn) {
          saveBtn.classList.toggle("is-dirty", teAccountQueueDirty);
          saveBtn.textContent = teAccountQueueDirty ? "💾 저장*" : "💾 저장";
          saveBtn.title = teAccountQueueDirty ? "SQLite에 저장하지 않은 수집계정 변경사항이 있습니다." : "수집계정 목록을 Analyze8780 SQLite에 저장합니다.";
        }
      }
      let teAccountDatabaseInfo = { active_filename: "sort_local.sqlite3", files: [] };
      function teAutoPyNormalizeDatabaseFilename(raw) {
        let name = String(raw || "").trim().replace(/^.*[\\/]/, "");
        if (!name) return "";
        if (!/^[0-9A-Za-z가-힣._-]+$/.test(name) || name.startsWith(".")) return "";
        if (!/\.sqlite3$/i.test(name)) name += ".sqlite3";
        return name;
      }
      function teAutoPySetDatabaseUi(payload) {
        const info = payload && typeof payload === "object" ? payload : {};
        const active = teAutoPyNormalizeDatabaseFilename(info.db_filename || info.active_filename || teAccountDatabaseInfo.active_filename || "sort_local.sqlite3") || "sort_local.sqlite3";
        const files = Array.from(new Set((Array.isArray(info.files) ? info.files : teAccountDatabaseInfo.files || []).map(teAutoPyNormalizeDatabaseFilename).filter(Boolean)));
        if (!files.includes(active)) files.push(active);
        files.sort((a, b) => a.localeCompare(b, "ko", { sensitivity: "base" }));
        teAccountDatabaseInfo = { active_filename: active, active_path: String(info.db_path || info.active_path || ""), files };
        const input = document.getElementById("igblue_account_db_filename");
        const list = document.getElementById("igblue_account_db_files");
        const status = document.getElementById("igblue_account_db_status");
        if (input) input.value = active;
        if (list) list.innerHTML = files.map((name) => `<option value="${String(name).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;")}"></option>`).join("");
        if (status) status.textContent = `SQLite: ${active}${teAccountDatabaseInfo.active_path ? ` · ${teAccountDatabaseInfo.active_path}` : ""}`;
      }
      async function teAutoPyLoadDatabaseInfo() {
        try {
          const response = await fetch("http://127.0.0.1:8780/api/database-files", { cache: "no-store" });
          const payload = await response.json().catch(() => ({}));
          if (!response.ok || payload.ok !== true) throw new Error(String(payload.error || `HTTP ${response.status}`));
          teAutoPySetDatabaseUi(payload);
          return { ok: true, payload };
        } catch (error) {
          return { ok: false, error: String(error && error.message || error) };
        }
      }
      async function teAutoPyDatabaseAction(action) {
        const input = document.getElementById("igblue_account_db_filename");
        const filename = teAutoPyNormalizeDatabaseFilename(input && input.value);
        if (!filename) return { ok: false, error: "올바른 SQLite 파일명을 입력하세요." };
        const endpoint = action === "open" ? "open" : action === "new" ? "new" : "save-as";
        try {
          const response = await fetch(`http://127.0.0.1:8780/api/database/${endpoint}`, {
            method: "POST", headers: { "Content-Type": "application/json" }, cache: "no-store", body: JSON.stringify({ filename })
          });
          const payload = await response.json().catch(() => ({}));
          if (!response.ok || payload.ok !== true) throw new Error(String(payload.error || `HTTP ${response.status}`));
          teAutoPySetDatabaseUi(payload);
          const entries = teAutoPyNormalizeAccountQueueEntries((Array.isArray(payload.items) ? payload.items : []).map((item) => ({
            username: teAutoPyNormalizeAccountUsername(item && item.username),
            deleted: !!(item && (item.is_deleted || item.deleted)),
            is_deleted: !!(item && (item.is_deleted || item.deleted)),
            enabled: item && item.enabled !== false
          })).filter((item) => item.username));
          teAutoPyRenderAccountQueueEntries(entries, true);
          teAutoPySetAccountQueueDirty(false);
          teAutoPySetAccountQueueIndex(0);
          teAutoPyResetProgressState(`account_database_${action}`, { silent: true });
          return { ok: true, action, filename: String(payload.db_filename || payload.filename || filename), total: entries.length };
        } catch (error) {
          return { ok: false, error: String(error && error.message || error) };
        }
      }

      let teCurrentAccountAddTarget = { username: "", source: "", shortcode: "" };
      function teAutoPySetCurrentAccountAddTarget(username, source, shortcode) {
        const normalized = teAutoPyNormalizeAccountUsername(username);
        teCurrentAccountAddTarget = { username: normalized, source: normalized ? String(source || "") : "", shortcode: normalized ? String(shortcode || "") : "" };
        const button = document.getElementById("igblue_account_url_add_current");
        if (button) {
          button.textContent = normalized ? `${normalized} 계정추가` : "IG 계정 선택";
          button.disabled = !normalized;
          button.title = normalized ? `현재 추가 대상: @${normalized}` : "Side 카드의 IG 계정명을 클릭해 추가 대상을 선택하세요.";
          button.dataset.accountUsername = normalized;
        }
        try { teAnalyze8780SyncOpenLink(teAnalyze8780Online); } catch (_) {}
        return normalized;
      }
      function teAutoPyRenderAccountQueueEntries(entries, cache) {
        const el = document.getElementById("igblue_account_url_list");
        if (!el) return false;
        const lines = teAutoPyStoredAccountQueueList(entries);
        el.value = lines.join("\n");
        if (cache) teLocalSet(teAccountQueueKey(), lines.join("\n"));
        teAutoPyUpdateAccountQueueStatus();
        return true;
      }
      async function teAutoPySaveAccountQueue() {
        const el = document.getElementById("igblue_account_url_list");
        if (!el) return { ok: false, error: "account_queue_missing" };
        const entries = teAutoPyAccountQueueEntries();
        const items = entries.map((entry, index) => ({ username: entry.username, position: index + 1, enabled: !entry.deleted, is_deleted: !!entry.deleted }));
        try {
          const response = await fetch("http://127.0.0.1:8780/api/collection-accounts", {
            method: "PUT",
            headers: { "Content-Type": "application/json" },
            cache: "no-store",
            body: JSON.stringify({ items })
          });
          const payload = await response.json().catch(() => ({}));
          if (!response.ok || !payload || payload.ok !== true) throw new Error(String(payload && payload.error || `HTTP ${response.status}`));
          const savedEntries = teAutoPyNormalizeAccountQueueEntries((Array.isArray(payload.items) ? payload.items : items).map((item) => ({
            username: teAutoPyNormalizeAccountUsername(item && item.username),
            deleted: !!(item && (item.is_deleted || item.deleted)),
            is_deleted: !!(item && (item.is_deleted || item.deleted)),
            enabled: item && item.enabled !== false
          })).filter((item) => item.username));
          teAutoPyRenderAccountQueueEntries(savedEntries, true);
          teAutoPySetAccountQueueDirty(false);
          teAutoPySetAccountQueueIndex(Math.min(teAutoPyAccountQueueIndex(), teAutoPyAccountQueueList().length));
          return { ok: true, total: savedEntries.length };
        } catch (error) {
          teAutoPySetAccountQueueDirty(true);
          return { ok: false, error: String(error && error.message || error || "save_failed") };
        }
      }
      function teAutoPyAccountQueueStatus() {
        const disabled = teAutoPyAccountQueueDisabled();
        const usernames = teAutoPyAccountQueueUsernames();
        const urls = usernames.map((username) => teAutoPyAccountUrlFromUsername(username));
        const uiIndex = disabled ? 0 : teAutoPyAccountQueueIndex();
        if (!urls.length) return { has: false, disabled, index: uiIndex, ui_index: uiIndex, url_index: -1, total: 0, url: "", account: "" };
        if (uiIndex <= 0) {
          const curUrl = teCurrentTargetUrl ? String(teCurrentTargetUrl() || "") : "";
          const curAccount = teAccountNameFromAccountUrl(curUrl);
          return { has: true, index: 0, ui_index: 0, url_index: -1, total: urls.length, url: curUrl, account: curAccount, current_page: true };
        }
        const urlIdx = teAutoPyQueueUrlIndexFromUiIndex(uiIndex, urls);
        return { has: true, index: uiIndex, ui_index: uiIndex, url_index: urlIdx, total: urls.length, url: urls[urlIdx] || "", account: usernames[urlIdx] ? "@" + usernames[urlIdx] : "", current_page: false };
      }
      function teAutoPyAccountQueueLabel() {
        const st = teAutoPyAccountQueueStatus();
        if (!(st && st.has)) return "";
        return st.current_page ? `계정 0/${st.total} 현재페이지`.trim() : `계정 ${st.index}/${st.total} ${st.account || ""}`.trim();
      }
      function teAutoPyUpdateAccountQueueStatus() {
        const disabled = teAutoPyAccountQueueDisabled();
        const entries = teAutoPyAccountQueueEntries();
        const activeEntries = entries.filter((entry) => !entry.deleted && entry.enabled !== false);
        const deletedCount = entries.length - activeEntries.length;
        const urls = disabled ? [] : activeEntries.map((entry) => teAutoPyAccountUrlFromUsername(entry.username));
        let uiIndex = disabled ? 0 : teAutoPyAccountQueueIndex();
        if (uiIndex > urls.length) {
          uiIndex = urls.length;
          teLocalSet(teAccountQueueIndexKey(), String(uiIndex));
        }
        const input = document.getElementById("igblue_account_queue_index_input");
        const total = document.getElementById("igblue_account_queue_total");
        const detail = document.getElementById("igblue_account_queue_detail");
        const box = document.getElementById("igblue_account_queue_box");
        const dis = document.getElementById("igblue_account_queue_disabled");
        const startOne = document.getElementById("igblue_account_queue_start_one");
        if (box) box.classList.toggle("is-account-queue-disabled", !!disabled);
        if (dis && dis.checked !== !!disabled) dis.checked = !!disabled;
        if (input) {
          input.disabled = !!disabled;
          if (String(input.value || "") !== String(uiIndex)) input.value = String(uiIndex);
        }
        if (total) total.textContent = String(urls.length || 0);
        if (startOne) {
          const canStartOne = !disabled && urls.length > 0 && uiIndex === 0 && !teAutoPyRunning && !teAutoPyStopping;
          startOne.disabled = !canStartOne;
          startOne.setAttribute("aria-disabled", canStartOne ? "false" : "true");
          startOne.title = canStartOne ? "목록 첫 번째 계정부터 시작" : (uiIndex > 0 ? "이미 1 이상에서 시작하도록 설정됨" : (teAutoPyRunning || teAutoPyStopping ? "자동실행 중에는 변경할 수 없음" : "수집계정 목록이 없거나 목록 순회가 꺼짐"));
        }
        if (detail) {
          const deletedText = deletedCount ? ` · 삭제 ${deletedCount}개 제외` : "";
          if (disabled) detail.textContent = `목록 미사용 · SQLite 목록 ${entries.length}개 보존 · 이번 실행 표시는 0/0${deletedText}`;
          else if (!urls.length) detail.textContent = `수집계정 없음 · 0=현재 웹페이지${deletedText}`;
          else if (teAutoPyUiState && teAutoPyUiState.automatic_end) detail.textContent = `목록 ${urls.length}/${urls.length} · 자동종료${teAutoPyUiState.exhausted ? " · 더이상없음" : " · 목표완료"}${deletedText}`;
          else if (uiIndex <= 0) detail.textContent = `자동 시작 시 목록 1/${urls.length}부터 새 탭${deletedText}`;
          else {
            const idx = teAutoPyQueueUrlIndexFromUiIndex(uiIndex, urls);
            detail.textContent = `목록 ${uiIndex}/${urls.length} · @${activeEntries[idx] && activeEntries[idx].username || ""}${deletedText}`;
          }
        }
      }
      let teAnalyze8780DeletedPollTimer = null;
      async function teAutoPySyncDeletedAccountsFromAnalyze8780() {
        try {
          const response = await fetch("http://127.0.0.1:8780/api/deleted/accounts", { cache: "no-store" });
          if (!response.ok) return false;
          const payload = await response.json();
          const deleted = new Set((Array.isArray(payload && payload.items) ? payload.items : []).map((item) => teAutoPyNormalizeAccountUsername(item && item.username)).filter(Boolean));
          if (!deleted.size) return true;
          const el = document.getElementById("igblue_account_url_list");
          if (!el || teAccountQueueDirty) return false;
          const entries = teAutoPyAccountQueueEntries();
          let changed = false;
          entries.forEach((entry) => {
            if (deleted.has(entry.username) && !entry.deleted) { entry.deleted = true; entry.enabled = false; changed = true; }
          });
          if (changed) {
            teAutoPyRenderAccountQueueEntries(entries, true);
            teAutoPySetAccountQueueDirty(false);
            teAutoPySetAccountQueueIndex(Math.min(teAutoPyAccountQueueIndex(), teAutoPyAccountQueueList().length));
          }
          return true;
        } catch (_) { return false; }
      }
      function teAutoPyStartAnalyze8780DeletedPolling() {
        if (teAnalyze8780DeletedPollTimer) return;
        teAutoPySyncDeletedAccountsFromAnalyze8780();
        teAnalyze8780DeletedPollTimer = setInterval(teAutoPySyncDeletedAccountsFromAnalyze8780, 30000);
      }
      async function teAutoPyRestoreAccountQueue() {
        const el = document.getElementById("igblue_account_url_list");
        if (!el) return;
        const cached = teLocalGet(teAccountQueueKey(), "");
        if (cached && !String(el.value || "").trim()) el.value = cached;
        if (String(el.value || "").trim()) teAutoPyRenderAccountQueueEntries(teAutoPyAccountQueueEntries(), false);
        try {
          const h = Math.max(28, Math.min(520, parseInt(teLocalGet(teAccountQueueHeightKey(), ""), 10) || 0));
          if (h) el.style.height = h + "px";
          const saveHeight = () => { try { const hh = Math.max(28, Math.min(520, Math.round(el.getBoundingClientRect().height || el.offsetHeight || 0))); if (hh) teLocalSet(teAccountQueueHeightKey(), String(hh)); } catch (_) {} };
          el.addEventListener("mouseup", saveHeight);
          el.addEventListener("blur", saveHeight);
          if (window.ResizeObserver) { const ro = new ResizeObserver(saveHeight); ro.observe(el); }
        } catch (_) {}
        const idxInput = document.getElementById("igblue_account_queue_index_input");
        if (idxInput) {
          idxInput.value = String(teAutoPyAccountQueueIndex());
          const saveIdx = () => teAutoPySetAccountQueueIndex(Math.max(0, Math.floor(Number(idxInput.value || 0) || 0)));
          idxInput.addEventListener("input", saveIdx);
          idxInput.addEventListener("blur", saveIdx);
          idxInput.addEventListener("change", saveIdx);
        }
        teAutoPySetAccountQueueDirty(false);
        el.addEventListener("input", () => { teAutoPySetAccountQueueDirty(true); teAutoPyUpdateAccountQueueStatus(); });
        const startOneBtn = document.getElementById("igblue_account_queue_start_one");
        if (startOneBtn) startOneBtn.addEventListener("click", (ev) => {
          ev.preventDefault();
          ev.stopPropagation();
          if (startOneBtn.disabled || teAutoPyRunning || teAutoPyStopping || teAutoPyAccountQueueDisabled()) return;
          if (teAutoPyAccountQueueIndex() !== 0 || !teAutoPyAccountQueueList().length) return;
          teAutoPySetAccountQueueIndex(1);
          n("수집계정 목록 1번부터 시작하도록 설정했습니다.");
        }, true);
        const addCur = document.getElementById("igblue_account_url_add_current");
        const sortBtn = document.getElementById("igblue_account_url_sort");
        const saveBtn = document.getElementById("igblue_account_url_save");
        const dbOpenBtn = document.getElementById("igblue_account_db_open");
        const dbNewBtn = document.getElementById("igblue_account_db_new");
        const dbSaveAsBtn = document.getElementById("igblue_account_db_save_as");
        const runDbAction = async (action, label) => {
          const result = await teAutoPyDatabaseAction(action);
          if (!result.ok) return void n(`⚠ SQLite ${label} 실패: ${result.error}`);
          n(`SQLite ${label}: ${result.filename} · 수집계정 ${result.total}개 · 진행상태 0부터 시작`);
        };
        dbOpenBtn && dbOpenBtn.addEventListener("click", () => runDbAction("open", "파일열기"));
        dbNewBtn && dbNewBtn.addEventListener("click", () => runDbAction("new", "새 파일"));
        dbSaveAsBtn && dbSaveAsBtn.addEventListener("click", () => runDbAction("save_as", "다른이름 저장"));
        sortBtn && sortBtn.addEventListener("click", () => {
          const before = teAutoPyAccountQueueEntries();
          const sorted = teAutoPySortAccountQueueEntries(before);
          const beforeOrder = before.map((entry) => entry.username).join("\n");
          const afterOrder = sorted.map((entry) => entry.username).join("\n");
          if (beforeOrder === afterOrder) return void n("수집계정 목록이 이미 정렬되어 있습니다.");
          teAutoPyRenderAccountQueueEntries(sorted, false);
          teAutoPySetAccountQueueDirty(true);
          teAutoPyResetProgressState("account_queue_sorted", { silent: true });
          n("수집계정 목록을 정렬했습니다. 진행상태는 안전하게 초기화했으며, 💾 저장을 눌러 확정하세요.");
        });
        saveBtn && saveBtn.addEventListener("click", async () => {
          const result = await teAutoPySaveAccountQueue();
          if (!result.ok) return void n(`⚠ PC SQLite 저장 실패: ${result.error}`);
          n(`수집계정 ${result.total}개를 PC SQLite에 저장했습니다.`);
        });
        addCur && addCur.addEventListener("click", () => {
          const username = teCurrentAccountAddTarget.username;
          if (!username) return void n("Side 카드의 IG 계정명을 먼저 클릭하세요.");
          const entries = teAutoPyAccountQueueEntries();
          if (entries.some((entry) => entry.username === username)) return void n("이미 목록에 있는 계정입니다.");
          entries.push({ username, deleted: false, enabled: true });
          teAutoPyRenderAccountQueueEntries(entries, false);
          teAutoPySetAccountQueueDirty(true);
          n(`@${username} 추가됨. 현재 순서를 유지합니다. 💾 저장을 눌러 확정하세요.`);
        });
        teAutoPyUpdateAccountQueueStatus();
        await teAutoPyLoadDatabaseInfo();
        try {
          const response = await fetch("http://127.0.0.1:8780/api/collection-accounts", { cache: "no-store" });
          const payload = await response.json().catch(() => ({}));
          if (!response.ok || payload.ok !== true) throw new Error(String(payload.error || `HTTP ${response.status}`));
          teAutoPySetDatabaseUi(payload);
          if (teAccountQueueDirty) return;
          const sqliteEntries = teAutoPyNormalizeAccountQueueEntries((Array.isArray(payload.items) ? payload.items : []).map((item) => ({
            username: teAutoPyNormalizeAccountUsername(item && item.username),
            deleted: !!(item && (item.is_deleted || item.deleted)),
            is_deleted: !!(item && (item.is_deleted || item.deleted)),
            enabled: item && item.enabled !== false
          })).filter((item) => item.username));
          if (sqliteEntries.length) {
            teAutoPyRenderAccountQueueEntries(sqliteEntries, true);
            teAutoPySetAccountQueueDirty(false);
          } else if (teAutoPyAccountQueueEntries().length) {
            teAutoPySetAccountQueueDirty(true);
            n("기존 브라우저 목록을 불러왔습니다. 💾 저장을 눌러 PC SQLite로 이전하세요.");
          }
        } catch (error) {
          if (teAutoPyAccountQueueEntries().length) n(`8780 연결 실패 · 브라우저 캐시 사용 중: ${String(error && error.message || error)}`);
        }
        teAutoPyStartAnalyze8780DeletedPolling();
      }
      async function teAutoPyProbeAccountPageState(tabId, username) {
        const expected = teAutoPyNormalizeAccountUsername(username);
        if (!(Number(tabId || 0) > 0) || !expected) return { ok: false, state: "unknown", error: "account_page_probe_input_missing" };
        let last = null;
        for (let attempt = 1; attempt <= 4; attempt++) {
          const result = await teSendTabMessageOnce(Number(tabId), { type: "igblue_get_account_page_state", expected_username: expected }, true, 2200, "");
          last = result || null;
          if (result && result.ok && ["not_found", "private", "auth_required", "profile_loaded"].includes(String(result.state || ""))) return Object.assign({ attempt }, result);
          if (attempt < 4) await teSleep(900);
        }
        return Object.assign({ ok: false, state: "unknown", error: String(last && last.error || "account_page_state_unresolved") }, last || {});
      }
      async function teAutoPyMarkAccountDeleted(username, reason, pageState) {
        const normalized = teAutoPyNormalizeAccountUsername(username);
        if (!normalized) return { ok: false, error: "deleted_account_username_missing" };
        const entries = teAutoPyAccountQueueEntries();
        const target = entries.find((entry) => entry.username === normalized);
        if (!target) return { ok: false, error: "deleted_account_not_in_queue", username: normalized };
        target.deleted = true; target.is_deleted = true; target.enabled = false;
        teAutoPyRenderAccountQueueEntries(entries, true);
        teAutoPySetAccountQueueDirty(true);
        const saved = await teAutoPySaveAccountQueue();
        await teAutoPyCycle("S00_ACCOUNT_DELETED_TOMBSTONE", {
          cycle_id: teBuildCycleId("ACCOUNT_DELETED"), run_seq: Number(teAutoPyRunSeq || 0), decision: saved && saved.ok ? "deleted_account_saved" : "deleted_account_save_failed", action: "mark_tombstone_and_skip", reason: String(reason || "instagram_account_not_found"), account_username: normalized, account_state: pageState || null, sqlite_saved: !!(saved && saved.ok), sqlite_error: String(saved && saved.error || "")
        });
        if (!(saved && saved.ok)) return { ok: false, error: String(saved && saved.error || "deleted_account_sqlite_save_failed"), username: normalized };
        n(`🗑️ @${normalized} · Instagram에서 없어진 계정으로 확인되어 SQLite에 저장하고 다음 URL로 이동합니다.`);
        return { ok: true, username: normalized, total: saved.total };
      }
      async function teAutoPyValidateAccountAfterNavigation(tabId, username, reason) {
        const state = await teAutoPyProbeAccountPageState(tabId, username);
        if (state && state.explicit_not_found === true && String(state.state || "") === "not_found") {
          const marked = await teAutoPyMarkAccountDeleted(username, reason || "instagram_account_not_found", state);
          return { ok: false, deleted_account: true, error: marked && marked.ok ? "instagram_account_not_found" : String(marked && marked.error || "deleted_account_save_failed"), username: teAutoPyNormalizeAccountUsername(username), page_state: state, tombstone_saved: !!(marked && marked.ok) };
        }
        return { ok: true, page_state: state || null };
      }

      async function teAutoPyCommitTabHandoff(nav, tabId, mySeq, account, index, total) {
        if (!(nav && nav.handoff_required)) return { ok: true, skipped: true, tabId: Number(tabId || 0) || 0, previous_tab_closed: !!(nav && nav.previous_tab_closed) };
        const token = String(nav.handoff_token || "");
        if (!token) return { ok: false, error: "handoff_token_missing", tabId: Number(tabId || 0) || 0 };
        console.log("[YELLOW][TAB-HANDOFF][SIDE_COMMIT_REQUEST]", { handoff_token: token, tab_id: Number(tabId || 0) || 0, run_seq: String(mySeq || ""), account: String(account || ""), account_index: Number(index || 0) || 0, account_total: Number(total || 0) || 0 });
        const result = await teRuntimeMessageResponse({
          type: "IGBLUE_COMMIT_TAB_HANDOFF",
          handoff_token: token,
          tabId: Number(tabId || 0) || 0,
          run_seq: String(mySeq || ""),
          account: String(account || ""),
          account_index: Number(index || 0) || 0,
          account_total: Number(total || 0) || 0
        });
        console.log("[YELLOW][TAB-HANDOFF][SIDE_COMMIT_RESPONSE]", result || null);
        if (!(result && result.ok)) return { ok: false, error: String(result && result.error || "handoff_commit_failed"), result: result || null };
        teAutoPySessionTarget.generation = Number(result.generation || nav.generation || 0) || 0;
        return result;
      }
      async function teAutoPyClaimTabOwner(tabId, mySeq, reason, account, index, total) {
        const id = Number(tabId || 0) || 0;
        if (!(id > 0)) return { ok: false, error: "owner_tab_id_missing" };
        const result = await teRuntimeMessageResponse({
          type: "IGBLUE_CLAIM_TAB_OWNER",
          tabId: id,
          run_seq: String(mySeq || ""),
          reason: String(reason || "auto_start_claim"),
          account: String(account || ""),
          account_index: Number(index || 0) || 0,
          account_total: Number(total || 0) || 0
        });
        console.log("[YELLOW][TAB-CONTROL][SIDE_OWNER_CLAIM]", result || null);
        if (result && result.ok) teAutoPySessionTarget.generation = Number(result.generation || 0) || 0;
        return result || { ok: false, error: "owner_claim_empty_response" };
      }

      async function teAutoPyNavigateAccountUrl(url, mySeq, reason, index, total) {
        const targetUrl = teAutoPyNormalizeAccountUrl(url);
        if (!targetUrl) return { ok: false, error: "invalid_account_url" };
        if (mySeq && mySeq !== teAutoPyRunSeq) return { ok: false, error: "run_seq_changed" };
        const label = `${Math.max(1, Number(index || 0) || 1)}/${Number(total || 0) || "?"}`;
        const account = teAccountNameFromAccountUrl(targetUrl);
        const username = account.replace(/^@/, "");
        teProgressPreserveUntilNextDownload("account_navigation");
        teAutoPyAnnounce(`계정 ${label} 이동`, `${account} · ${targetUrl}`);
        await teAutoPyCycle("S00_ACCOUNT_NAVIGATE", { cycle_id: teBuildCycleId("ACCOUNT_NAV"), run_seq: mySeq || 0, decision: "navigate", action: "background_tab_replacement", reason: String(reason || "account_queue"), account_queue_index: Number(index || 0), account_queue_total: Number(total || 0), account_url: targetUrl, account_username: username });
        const preferredTabId = Number(teAutoPySessionTarget && teAutoPySessionTarget.tab_id || teCurrentTargetTabId || 0) || 0;
        console.log("[YELLOW][NAV][SIDE_REQUEST]", { target_url: targetUrl, tab_id: preferredTabId, reason: String(reason || "account_queue"), run_seq: String(mySeq || ""), account: username, account_index: Number(index || 0), account_total: Number(total || 0) });
        const nav = await teRuntimeMessageResponse({ type: "IGBLUE_NAVIGATE_ACTIVE_TAB", url: targetUrl, tabId: preferredTabId, reason: String(reason || "account_queue"), run_seq: String(mySeq || ""), account: username, account_index: Number(index || 0), account_total: Number(total || 0) });
        console.log("[YELLOW][NAV][SIDE_RESPONSE]", nav || null);
        if (!(nav && nav.ok && Number(nav.tabId || 0) > 0)) return { ok: false, error: String(nav && nav.error || "goto_failed"), navigate_result: nav || null };
        const tabId = Number(nav.tabId || 0) || 0;
        teAutoPySessionTarget.active = true;
        teAutoPySessionTarget.session_id = teAutoPySessionTarget.session_id || `auto_${Date.now()}_${tabId}`;
        teAutoPySessionTarget.tab_id = tabId;
        teAutoPySessionTarget.start_url = targetUrl;
        teAutoPySessionTarget.generation = Number(nav && nav.generation || 0) || 0;
        teAutoPySessionTarget.fatal_handled = false;
        teCurrentTargetTabId = tabId;
        teSetTargetLabel({ tabId, url: targetUrl });
        const handoff = await teAutoPyCommitTabHandoff(nav, tabId, mySeq, username, index, total);
        if (!(handoff && handoff.ok)) {
          await teAutoPyCycle("S00_ACCOUNT_HANDOFF_FAILED", { cycle_id: teBuildCycleId("ACCOUNT_HANDOFF_FAIL"), run_seq: mySeq || 0, decision: "handoff_failed", action: "keep_new_and_previous_tabs", reason: String(handoff && handoff.error || "handoff_commit_failed"), account_queue_index: Number(index || 0), account_queue_total: Number(total || 0), account_url: targetUrl, account_username: username, previous_tab_id: Number(nav && nav.previousTabId || 0) || 0, replacement_tab_id: tabId, handoff_result: handoff || null });
          return { ok: false, error: String(handoff && handoff.error || "handoff_commit_failed"), navigate_result: nav || null, handoff_result: handoff || null, tab_id: tabId };
        }
        teDownloadSessionAccount = teSafePath(username || teAccountFromUrl(targetUrl) || "instagram");
        teAutoPySetLastRank(0, "account_queue_navigation_reset");
        const navigationReady = nav && nav.navigation_ready || null;
        const unloadGuard = nav && nav.unload_guard || null;
        const validation = await teAutoPyValidateAccountAfterNavigation(tabId, username, reason || "account_queue");
        if (validation && validation.deleted_account) return Object.assign({ ok: false, url: targetUrl, account, tab_id: tabId }, validation);
        try { teRequestTargetState(true); } catch (_) {}
        try { teRefreshRanksAndMetricCount(); } catch (_) {}
        await teAutoPyCycle("S00_ACCOUNT_NAVIGATED", { cycle_id: teBuildCycleId("ACCOUNT_NAV_OK"), run_seq: mySeq || 0, decision: "navigated_ready", action: "resume_after_content_main_ready", reason: String(reason || "account_queue"), account_queue_index: Number(index || 0), account_queue_total: Number(total || 0), account_url: targetUrl, account_username: username, previous_tab_id: Number(nav && nav.previousTabId || 0) || 0, replacement_tab_id: Number(nav && nav.replacementTabId || tabId) || tabId, previous_tab_closed: !!(handoff && handoff.previous_tab_closed), unload_guard: handoff && handoff.previous_tab_close && handoff.previous_tab_close.unload_guard || unloadGuard, handoff_state: String(handoff && handoff.handoff_state || ""), generation: Number(handoff && handoff.generation || nav && nav.generation || 0) || 0, navigation_ready: navigationReady, account_page_state: validation && validation.page_state || null });
        return { ok: true, url: targetUrl, account, tab_id: tabId, generation: Number(handoff && handoff.generation || nav && nav.generation || 0) || 0, handoff: handoff || null, unload_guard: handoff && handoff.previous_tab_close && handoff.previous_tab_close.unload_guard || unloadGuard, navigation_ready: navigationReady, page_state: validation && validation.page_state || null };
      }
      function teAutoPyFreshStartPreview() {
        const status = teAutoPyAccountQueueStatus();
        if (!(status && status.has && Number(status.total || 0) > 0)) return status;
        if (!status.current_page && Number(status.index || 0) > 0 && status.url) return status;
        const urls = teAutoPyAccountQueueList();
        const firstUrl = String(urls[0] || "");
        return { has: true, disabled: false, index: 1, ui_index: 1, url_index: 0, total: urls.length, url: firstUrl, account: teAccountNameFromAccountUrl(firstUrl), current_page: false, forced_first_account: true };
      }

      async function teAutoPyPrepareAccountQueueForStart(mySeq) {
        let urls = teAutoPyAccountQueueList();
        let uiIndex = teAutoPyAccountQueueIndex();
        if (!urls.length) return { ok: true, skipped: true, has_queue: false, index: uiIndex, total: 0 };
        let desiredIndex = uiIndex <= 0 ? 1 : Math.min(Math.max(1, uiIndex), urls.length);
        if (uiIndex <= 0) teAutoPyAnnounce(`계정 1/${urls.length} 준비`, "목록 첫 계정을 새 자동 탭에서 시작");
        for (let guard = 0; guard < 100; guard++) {
          urls = teAutoPyAccountQueueList();
          if (!urls.length || desiredIndex > urls.length) return { ok: true, has_queue: true, queue_completed: true, deleted_only_remaining: true, index: Math.max(0, urls.length), total: urls.length };
          teAutoPySetAccountQueueIndex(desiredIndex);
          const urlIdx = teAutoPyQueueUrlIndexFromUiIndex(desiredIndex, urls);
          const wanted = urls[urlIdx];
          const account = teAccountNameFromAccountUrl(wanted);
          const username = account.replace(/^@/, "");
          teAutoPyAnnounce(`계정 ${desiredIndex}/${urls.length} 준비`, account || wanted);
          if (teAutoPySameAccountUrl(wanted, teCurrentTargetUrl()) && Number(teAutoPySessionTarget && teAutoPySessionTarget.tab_id || teCurrentTargetTabId || 0) > 0) {
            const tabId = Number(teAutoPySessionTarget && teAutoPySessionTarget.tab_id || teCurrentTargetTabId || 0) || 0;
            const validation = await teAutoPyValidateAccountAfterNavigation(tabId, username, "auto_start_prepare_account_same_url");
            if (validation && validation.deleted_account) {
              if (!validation.tombstone_saved) return Object.assign({ ok: false }, validation);
              continue;
            }
            teDownloadSessionAccount = teSafePath(username || teAccountFromUrl(wanted) || "instagram");
            return { ok: true, url: wanted, account, index: desiredIndex, url_index: urlIdx, total: urls.length, has_queue: true, page_state: validation && validation.page_state || null };
          }
          const nav = await teAutoPyNavigateAccountUrl(wanted, mySeq, "auto_start_prepare_account", desiredIndex, urls.length);
          if (nav && nav.deleted_account && nav.tombstone_saved) continue;
          return Object.assign({ has_queue: true, index: desiredIndex, total: urls.length }, nav || { ok: false, error: "goto_failed" });
        }
        return { ok: false, error: "deleted_account_skip_guard_exceeded", index: desiredIndex, total: urls.length };
      }
      function teAutoPyScheduleContinueAfterAccount(mySeq, reason) {
        teAutoPyContinuationPending = true;
        setTimeout(() => {
          try {
            if (mySeq && mySeq !== teAutoPyRunSeq) return;
            if (teAutoPyRunning || teAutoPyStopping) return;
            if (!teAutoPyEnabled()) return;
            teAutoPyStartOnceFromUi({ new_session: false, continuation: true, reason: String(reason || "account_queue_next") });
          } catch (err) { n("다음 계정 자동시작 실패 " + String(err && err.message || err)); }
        }, 1900);
      }
      function teAutoPyFinalizeAutomaticEnd(reason, meta) {
        const info = Object.assign({}, meta || {});
        const target = Math.max(1, Number(info.target_count || teAutoPyStats && teAutoPyStats.target_count || teAutoPyTargetCount && teAutoPyTargetCount() || 1) || 1);
        const completed = Math.max(0, Number(info.completed_count || teAutoPyCompletedPostCount() || 0) || 0);
        const acct = teAutoPyAccountQueueStatus();
        const queueComplete = !(acct && acct.has && Number(acct.total || 0) > 0) || !!info.queue_completed || Number(acct.index || 0) >= Number(acct.total || 0);
        if (!queueComplete) {
          teAutoPySetUiPhase("account_next", { automatic_end: false, target_count: target, completed_count: completed, message: "전체 URL 목록이 남아 자동종료를 보류합니다.", account_index: Number(acct.index || 0), account_total: Number(acct.total || 0) });
          n(`자동종료 보류: 실제 목록 ${Number(acct.index || 0)}/${Number(acct.total || 0)} 처리 중`);
          return false;
        }
        teAutoEtaPersist("automatic_end");
        teAutoPySetUiPhase("auto_end", {
          target_count: target,
          completed_count: completed,
          completed_shortcodes: Array.isArray(teAutoPyStats.completed_shortcodes) ? teAutoPyStats.completed_shortcodes.slice() : [],
          exhausted: !!info.exhausted,
          exhausted_reason: String(info.exhausted_reason || ""),
          automatic_end: true,
          message: String(reason || info.exhausted_reason || "자동 완료"),
          account_index: Math.max(0, Number(info.account_index || acct && acct.index || 0) || 0),
          account_total: Math.max(0, Number(info.account_total || acct && acct.total || 0) || 0)
        });
        try { teAutoPyUpdateAccountQueueStatus(); } catch (_) {}
        teAutoPyAnnounce("자동종료", String(reason || "자동 처리 완료"));
        teAutoPyClearProgressState();
        teAutoPyRenderRuntimeNotice("", "", "");
        const finalTabId = Number(teAutoPySessionTarget && teAutoPySessionTarget.tab_id || 0) || 0;
        if (finalTabId > 0) {
          teRuntimeMessageResponse({ type: "IGBLUE_CLOSE_MANAGED_AUTO_TAB", tabId: finalTabId, reason: "automatic_end" }).then((closeResult) => {
            console.log(`[CDP탭종료][TC-03][${closeResult && closeResult.ok ? "체크완료" : "체크실패"}] 마지막 자동 탭 종료`, { tab_id: finalTabId, result: closeResult || null });
            teAutoPyResetSessionTarget();
            try { teRequestTargetState(true); } catch (_) {}
          });
        }
        return true;
      }
      function teAutoPyFinishAccountQueue(reason, meta) {
        const urls = teAutoPyAccountQueueList();
        if (urls.length) teAutoPySetAccountQueueIndex(urls.length);
        teAutoPyFinalizeAutomaticEnd(reason || "마지막 계정까지 완료", Object.assign({}, meta || {}, { account_index: urls.length, account_total: urls.length, queue_completed: true }));
        if (urls.length) teAutoPySetAccountQueueIndex(0);
      }
      async function teAutoPySkipCurrentAccountAndAdvanceIfAny(mySeq, reason, detail, extra) {
        const urls = teAutoPyAccountQueueList();
        if (!urls.length) return { advanced: false, empty: true };
        const uiIndex = teAutoPyAccountQueueIndex();
        const payload = Object.assign({
          cycle_id: teBuildCycleId("ACCOUNT_SKIP"),
          run_seq: Number(mySeq || 0),
          decision: "skip_current_account",
          action: "next_account",
          reason: String(reason || "no_matching_media"),
          skip_reason: String(reason || "no_matching_media"),
          skip_reason_ko: String(detail || "조건에 맞는 대상 없음: 현재 계정 건너뛰고 다음 URL 이동"),
          account_queue_index: Number(uiIndex || 0),
          account_queue_total: urls.length,
          account_url: Number(uiIndex || 0) > 0 ? (urls[Math.max(0, uiIndex - 1)] || "") : (teCurrentTargetUrl ? String(teCurrentTargetUrl() || "") : "")
        }, extra || {});
        await teAutoPyCycle("S00_ACCOUNT_TARGET_EMPTY_SKIP", payload);
        n("조건에 맞는 대상 없음: 현재 계정 건너뛰고 다음 URL로 이동");
        const completionMeta = {
          target_count: Math.max(1, Number(extra && extra.target_count || teAutoPyTargetCount && teAutoPyTargetCount() || 1) || 1),
          completed_count: Math.max(0, Number(extra && extra.completed_count || teAutoPyCompletedPostCount() || 0) || 0),
          exhausted: extra && Object.prototype.hasOwnProperty.call(extra, "exhausted") ? !!extra.exhausted : true,
          exhausted_reason: String(extra && (extra.exhausted_reason || extra.reason) || reason || "no_matching_media")
        };
        return await teAutoPyAdvanceAccountQueueIfAny(mySeq, reason || "no_matching_media", completionMeta);
      }

      async function teAutoPyMediaFailAdvanceOrStop(mySeq, stage, cycleBase, code, detail, extra) {
        const payload = Object.assign({}, cycleBase || {}, extra || {}, {
          run_seq: Number(mySeq || 0),
          decision: "required_download_failed_stop",
          action: "pause_without_next",
          reason: String(code || "media_click_failed"),
          fail_policy: "stop_not_advance",
          next_account_blocked: true,
          required_download_failed: true
        });
        return await teAutoPyStopCycle(stage || "S_AUTOPLAY_STOPPED", payload, code || "media_click_failed", detail || "media_click_failed", { fail_policy: "stop_not_advance", next_account_blocked: true, required_download_failed: true });
      }

      async function teAutoPyAdvanceAccountQueueIfAny(mySeq, reason, completionMeta) {
        let urls = teAutoPyAccountQueueList();
        if (!urls.length) return { advanced: false, empty: true };
        const currentUiIndex = teAutoPyAccountQueueIndex();
        const nextUiIndex = currentUiIndex + 1;
        for (let guard = 0; guard < 100; guard++) {
          urls = teAutoPyAccountQueueList();
          if (nextUiIndex > urls.length) {
            teAutoPySetAccountQueueIndex(Math.min(currentUiIndex, urls.length));
            await teAutoPyCycle("S00_ACCOUNT_QUEUE_DONE", { cycle_id: teBuildCycleId("ACCOUNT_DONE"), run_seq: mySeq || 0, decision: "queue_done", action: "stop", reason: String(reason || "account_queue_completed"), account_queue_index: Math.min(currentUiIndex, urls.length), account_queue_total: urls.length, account_url: currentUiIndex > 0 ? urls[Math.max(0, Math.min(currentUiIndex - 1, urls.length - 1))] || "" : "current_page" });
            teAutoPyFinishAccountQueue(String(completionMeta && completionMeta.exhausted ? "더이상없음 · 마지막 계정까지 처리" : "마지막 계정까지 완료"), Object.assign({}, completionMeta || {}, { account_index: urls.length, account_total: urls.length }));
            return { advanced: false, completed_all: true, index: urls.length, total: urls.length };
          }
          const urlIdx = teAutoPyQueueUrlIndexFromUiIndex(nextUiIndex, urls);
          teAutoPySetAccountQueueIndex(nextUiIndex);
          const targetUrl = urls[urlIdx];
          const nav = await teAutoPyNavigateAccountUrl(targetUrl, mySeq, reason || "account_queue_next", nextUiIndex, urls.length);
          if (nav && nav.deleted_account && nav.tombstone_saved) continue;
          if (!(nav && nav.ok)) {
            await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", { cycle_id: teBuildCycleId("ACCOUNT_NAV_FAIL"), run_seq: mySeq || 0, account_queue_index: nextUiIndex, account_queue_total: urls.length, account_url: targetUrl, navigate_result: nav || null }, "account_navigation_failed", String(nav && nav.error || "goto_failed"));
            return { advanced: false, error: String(nav && nav.error || "goto_failed"), index: nextUiIndex, total: urls.length };
          }
          const refreshedTotal = teAutoPyAccountQueueList().length;
          teAutoPySetUiPhase("account_next", Object.assign({}, completionMeta || {}, { automatic_end: false, account_index: nextUiIndex, account_total: refreshedTotal }));
          teAutoPyAnnounce("다음 계정 자동시작 대기", `${nextUiIndex}/${refreshedTotal} ${teAccountNameFromAccountUrl(targetUrl)}`);
          teAutoPyScheduleContinueAfterAccount(mySeq, reason || "account_queue_next");
          return { advanced: true, index: nextUiIndex, total: refreshedTotal };
        }
        await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", { cycle_id: teBuildCycleId("ACCOUNT_DELETE_GUARD"), run_seq: mySeq || 0 }, "account_navigation_failed", "삭제 계정 연속 건너뛰기 안전 한도 초과");
        return { advanced: false, error: "deleted_account_skip_guard_exceeded", index: nextUiIndex, total: teAutoPyAccountQueueList().length };
      }

      function teSortedAllTopN(e) {
        if (!Array.isArray(se) || 0 === se.length) return [];
        const t = Math.min(e, se.length),
          r = G[Z] || "";
        let n = teSortCandidateList(se, r);
        return n.slice(0, t);
      }
      // IGblue local patch v246: distinguish missing metrics from numeric zero and rank private-view posts by active engagement inputs.
      function teMetricNumber(e) {
        if (null == e) return 0;
        if (typeof e === "number") return Number.isFinite(e) ? e : 0;
        const t = String(e).replace(/[,\s]/g, "").trim();
        const r = Number(t);
        return Number.isFinite(r) ? r : 0;
      }
      function teViewsAvailable(media) {
        if (!media || typeof media !== "object") return false;
        if (media.views_available !== null && media.views_available !== undefined && Number(media.views_available) === 0) return false;
        const raw = media.play_count ?? media.view_count;
        if (raw == null) return false;
        if (typeof raw === "string" && !raw.replace(/[,\s]/g, "").trim()) return false;
        const n = typeof raw === "number" ? raw : Number(String(raw).replace(/[,\s]/g, "").trim());
        return Number.isFinite(n) && n >= 0;
      }
      function teViewCountOrPrivate(media) {
        if (!teViewsAvailable(media)) return -1;
        const raw = media && (media.play_count ?? media.view_count);
        return Math.max(0, Math.floor(teMetricNumber(raw)));
      }
      function teMetricHasValue(media, field) {
        if (!media || !field) return false;
        if (field === "play_count" || field === "view_count" || field === "views") return teViewsAvailable(media);
        const raw = media[field];
        if (raw == null) return false;
        if (typeof raw === "string" && !raw.replace(/[,\s]/g, "").trim()) return false;
        const n = typeof raw === "number" ? raw : Number(String(raw).replace(/[,\s]/g, "").trim());
        return Number.isFinite(n);
      }
      function teTopMetricField() {
        const e = G[Z] || "";
        return ["like_count", "comment_count", "media_repost_count", "share_count", "play_count"].includes(e) ? e : "";
      }
      function teMetricLabel(e) {
        return "like_count" === e ? "Likes" : "comment_count" === e ? "Comments" : "media_repost_count" === e ? "Reposts" : "share_count" === e ? "Shares" : "play_count" === e ? "Views" : "Metric";
      }
      function teServerSortField(e) {
        return "like_count" === e ? "likes" : "comment_count" === e ? "comments" : "media_repost_count" === e ? "reposts" : "share_count" === e ? "shares" : "play_count" === e ? "play_count" : "created_at" === e ? "upload_at" : e || "";
      }
      function teSourceTypeFromUrl(url) {
        const s = String(url || teCurrentTargetUrl() || location.href || "");
        if (/instagram\.com\/explore/i.test(s)) return "explore";
        if (/instagram\.com\/(?:reel|reels)\//i.test(s)) return "reel";
        if (/instagram\.com\/p\//i.test(s)) return "post";
        if (/instagram\.com\/[^/?#]+\/reels/i.test(s)) return "profile_reels";
        if (/instagram\.com\/[^/?#]+/i.test(s)) return "profile";
        return "instagram";
      }
      function teThresholdLabelForMetric(rawField, threshold) {
        const n = Number(threshold || 0) || 0;
        return n > 0 ? `${teMetricLabel(rawField)} >= ${teShortNumber(n)}` : "";
      }
      function teMetricUnits() {
        const e = document.getElementById("top_n_count"),
          t = Number.parseFloat(e?.value);
        return Number.isFinite(t) && t > 0 ? t : 0;
      }
      function teMetricThresholdValue() {
        return Math.floor(1e4 * teMetricUnits());
      }
      function teMinCommentsValue() {
        const el = document.getElementById("igblue_min_comments");
        return Math.max(0, Math.floor(teMetricNumber(el ? el.value : teLocalGet("filter/min_comments", "0"))));
      }
      function teMinLikesValue() {
        const el = document.getElementById("igblue_min_likes");
        return Math.max(0, Math.floor(teMetricNumber(el ? el.value : teLocalGet("filter/min_likes", "0"))));
      }
      function teMinSharesValue() {
        return 0;
      }
      function teMinRepostsValue() {
        const el = document.getElementById("igblue_min_reposts");
        return Math.max(0, Math.floor(teMetricNumber(el ? el.value : teLocalGet("filter/min_reposts", "0"))));
      }
      function teEngagementFilterSpecs() {
        return [
          { field: "like_count", min: teMinLikesValue(), label: "Likes" },
          { field: "comment_count", min: teMinCommentsValue(), label: "Comments" },
          { field: "media_repost_count", min: teMinRepostsValue(), label: "Reposts" }
        ].filter((x) => x.min > 0);
      }
      function teSecondaryMetricFiltersActive() {
        return teEngagementFilterSpecs().length > 0;
      }
      function teMediaPassesSecondaryFilters(media) {
        return teEngagementFilterSpecs().every((spec) => teMetricHasValue(media, spec.field) && teMetricNumber(media && media[spec.field]) >= spec.min);
      }
      function teParticipationScore(media) {
        return teEngagementFilterSpecs().reduce((sum, spec) => {
          if (!teMetricHasValue(media, spec.field)) return sum;
          const value = teMetricNumber(media && media[spec.field]);
          return value > 0 ? sum + value : sum;
        }, 0);
      }
      function teParticipationMetricNames() {
        return teEngagementFilterSpecs().map((spec) => spec.field);
      }
      function tePrimaryMetricPasses(media, field, threshold) {
        const active = !!field && Number(threshold || 0) > 0;
        if (!active) return true;
        if (teMetricHasValue(media, field)) return teMetricNumber(media && media[field]) >= Number(threshold || 0);
        // 조회수 비공개·미제공만 예외: 활성화된 참여 항목 AND 조건으로 후보를 유지한다.
        return field === "play_count" && teSecondaryMetricFiltersActive() && teMediaPassesSecondaryFilters(media);
      }
      function teMediaPassesTopConditions(media, field, threshold) {
        return tePrimaryMetricPasses(media, field, threshold) && teMediaPassesSecondaryFilters(media);
      }
      function teCompareMediaForSort(a, b, field) {
        if (!field) return 0;
        const aHas = teMetricHasValue(a, field), bHas = teMetricHasValue(b, field);
        if (field === "play_count") {
          const scoreDiff = teParticipationScore(b) - teParticipationScore(a);
          // 조회수 기준 입력 0은 조회수 자체를 필터·순위에서 제외한다.
          if (teMetricThresholdValue() <= 0) return scoreDiff;
          if (aHas && bHas) return teMetricNumber(b && b[field]) - teMetricNumber(a && a[field]);
          if (aHas !== bHas) return aHas ? -1 : 1;
          return scoreDiff;
        }
        if (aHas && bHas) return teMetricNumber(b && b[field]) - teMetricNumber(a && a[field]);
        if (aHas !== bHas) return aHas ? -1 : 1;
        return 0;
      }
      function teSortCandidateList(list, field) {
        const arr = Array.isArray(list) ? list.slice() : [];
        return field ? arr.sort((a, b) => teCompareMediaForSort(a, b, field)) : arr;
      }
      function teShortNumber(e) {
        return e >= 1e6 ? `${Number(e / 1e6).toLocaleString()}M` : e >= 1e3 ? `${Number(e / 1e3).toLocaleString()}k` : String(e);
      }
      function teAutoPyDateFilterActive() {
        try { return Math.max(0, Number(K || 0) || 0) > 0; } catch (_) { return false; }
      }
      function teMetricSourceList() {
        // 날짜 필터가 활성화된 경우 oe=[] 자체가 정상적인 "필터 결과 0개"다.
        // 이때 전체 원본 se로 되돌아가면 오래된 게시물이 No001로 다시 살아난다.
        if (teAutoPyDateFilterActive()) return Array.isArray(oe) ? oe : [];
        return Array.isArray(oe) && oe.length ? oe : Array.isArray(se) ? se : [];
      }
      function teBuildRankContext(e) {
        const t = teTopMetricField(),
          r = teMetricThresholdValue(),
          n = new Map();
        const primaryActive = !!t && r > 0;
        const secondaryActive = teSecondaryMetricFiltersActive();
        if ((!primaryActive && !secondaryActive) || !Array.isArray(e)) return { active: !1, map: n };
        let i = 0;
        return (e.forEach((e) => {
          if (teMediaPassesTopConditions(e, primaryActive ? t : "", primaryActive ? r : 0) && e?.code) n.set(e.code, ++i);
        }), { active: !0, map: n });
      }
      function teRankAccountText(media) {
        const account = teDisplayAccountForMedia ? teDisplayAccountForMedia(media) : "";
        const clean = teCleanOwnerAccountName(account);
        return clean ? String(clean) : "";
      }
      function teApplySideAccountLabel(element, media) {
        if (!element) return;
        const span = element.querySelector("span");
        if (!span) return;
        const account = teRankAccountText(media);
        const shortcode = String(media && (media.code || media.shortcode) || "").trim();
        element.style.display = "flex";
        span.textContent = account ? `IG ${account}` : "IG";
        element.dataset.accountUsername = account;
        element.dataset.shortcode = shortcode;
        element.title = account ? `@${account} 계정추가 대상으로 선택` : "IG 계정 확인 불가";
        element.classList.toggle("is-account-selected", !!(account && teCurrentAccountAddTarget.source === "side_account_click" && teCurrentAccountAddTarget.username === account && (!teCurrentAccountAddTarget.shortcode || teCurrentAccountAddTarget.shortcode === shortcode)));
      }
      function teSelectSideAccountAddTarget(media, element) {
        const account = teRankAccountText(media);
        const shortcode = String(media && (media.code || media.shortcode) || "").trim();
        if (!account) return void n("이 게시물의 IG 계정을 확인하지 못했습니다.");
        teAutoPySetCurrentAccountAddTarget(account, "side_account_click", shortcode);
        document.querySelectorAll(".igblue-side-account.is-account-selected").forEach((node) => node.classList.remove("is-account-selected"));
        if (element) element.classList.add("is-account-selected");
        n(`@${account} 계정을 계정추가 대상으로 선택했습니다.`);
      }
      function teApplySideRankLabel(element, no, active) {
        if (!element) return;
        const span = element.querySelector("span");
        if (!span) return;
        const noText = teRankNoText(no);
        const settings = teRankBadgeSettings();
        const visible = !!(active && noText && settings.enabled && settings.side);
        element.style.display = visible ? "flex" : "none";
        span.textContent = visible ? noText : "";
        element.classList.toggle("igblue-rank-badge", visible);
        if (visible) teApplyRankBadgeVars(element, settings);
        else teClearRankBadgeVars(element);
      }
      function teRankNoText(no) {
        const n = Number(no) || 0;
        return n > 0 ? `No ${String(n).padStart(3, "0")}` : "";
      }
      function teMediaKindText(media) {
        if (media?.video_url) return "VID";
        if (media?.carousel_media) return "CAR";
        if (media?.img_origin || media?.img_small || media?.img_b64) return "IMG";
        return "UNK";
      }
      let teMainRankLabelsTimer = null;
      let teMainRankLabelsSignature = "";
      let teMainRankLabelsLastUrl = "";
      function teBuildMainRankLabels(list, ctx) {
        if (!Array.isArray(list)) return [];
        const active = !!(ctx && ctx.active);
        const map = ctx && ctx.map instanceof Map ? ctx.map : new Map();
        const labels = list.map((media, idx) => {
          const no = active ? Number(map.get(media?.code) || 0) : idx + 1;
          if (!media?.code || no <= 0) return null;
          return {
            code: media.code,
            username: teOwnerAccountForMedia(media),
            owner_username: teOwnerAccountForMedia(media),
            upload_stamp: teUploadStampFromMedia(media),
            is_single_media_page: teIsSingleMediaPage(),
            no,
            kind: teMediaKindText(media),
            has_image: !!(media?.img_origin || media?.img_small || media?.img_b64),
            has_video: !!media?.video_url,
            has_carousel: !!media?.carousel_media,
            likes: teMetricNumber(media?.like_count),
            comments: teMetricNumber(media?.comment_count),
            shares: 0,
            reposts: teMetricNumber(media?.media_repost_count),
            views: teViewCountOrPrivate(media),
            views_available: teViewsAvailable(media) ? 1 : 0,
            participation_score: teParticipationScore(media),
            metric_presence: {
              views: teViewsAvailable(media),
              likes: media?.like_count !== null && media?.like_count !== undefined && String(media.like_count).trim() !== "",
              comments: media?.comment_count !== null && media?.comment_count !== undefined && String(media.comment_count).trim() !== "",
              reposts: media?.media_repost_count !== null && media?.media_repost_count !== undefined && String(media.media_repost_count).trim() !== ""
            },
            download_prefix: teMakeDownloadPrefix(media, 0, 1, media?.video_url || media?.img_origin || media?.img_small || ""),
            download_prefix_image: teMakeDownloadPrefix(media, 0, 1, media?.img_origin || media?.img_small || media?.video_url || ""),
            download_prefix_video: teMakeDownloadPrefix(media, 0, 1, media?.video_url || media?.img_origin || media?.img_small || ""),
            instagram_original_filename: teOriginalInstagramFileInfo(media?.video_url || media?.img_origin || media?.img_small || "").original_filename,
            download_account: teActiveFolderAccountName(),
            download_root: "instagram",
          };
        }).filter(Boolean);
        const total = labels.length;
        labels.forEach((x) => {
          x.rank_total = total;
          x.rank_ready = true;
        });
        return labels;
      }
      function teMainRankLabelSignature(labels) {
        const url = teCurrentTargetUrl ? String(teCurrentTargetUrl() || "") : "";
        return url + "\n" + (Array.isArray(labels) ? labels.map(x => `${x.code}:${x.no}:${x.rank_total || 0}:${x.username}:${x.kind}:${x.download_prefix || ""}:${x.download_root || ""}:${x.download_account || ""}`).join("|") : "");
      }
      function teSendMainRankLabelsNow(labels, reason) {
        const payload = { type: "ndy_ig_main_rank_labels_changed", labels: Array.isArray(labels) ? labels : [], reason: String(reason || "rank_labels") };
        return teSendToTargetTab(payload, "").catch(() => {});
      }
      function teScheduleMainRankLabels(list, ctx, forceReason) {
        const labels = teBuildMainRankLabels(list, ctx);
        const url = teCurrentTargetUrl ? String(teCurrentTargetUrl() || "") : "";
        const signature = teMainRankLabelSignature(labels);
        const force = !!forceReason || url !== teMainRankLabelsLastUrl;
        teMainRankLabelsLastUrl = url;
        if (!force && signature === teMainRankLabelsSignature) return;
        teMainRankLabelsSignature = signature;
        if (teMainRankLabelsTimer) clearTimeout(teMainRankLabelsTimer);
        teMainRankLabelsTimer = setTimeout(() => {
          teMainRankLabelsTimer = null;
          teSendMainRankLabelsNow(labels, forceReason || "rank_labels_changed");
          setTimeout(() => teSendMainRankLabelsNow(labels, "rank_labels_rehydrate_1"), 650);
          setTimeout(() => teSendMainRankLabelsNow(labels, "rank_labels_rehydrate_2"), 1600);
        }, 80);
      }
      async function teAutoPyEnsureMainInjectionReady(labels, requestedNo, runSeq) {
        const arr = Array.isArray(labels) ? labels : [];
        const reqNo = Math.max(1, Number(requestedNo || 1) || 1);
        if (!arr.length) return { ok: false, error: "no_rank_labels" };
        let attempt = 0;
        while (!runSeq || runSeq === teAutoPyRunSeq) {
          if (teAutoPyStopping) return { ok: false, stopped: true, error: "user_stop", attempt };
          attempt += 1;
          await teSendMainRankLabelsNow(arr, `auto_py_main_rehydrate_${attempt}`);
          await teSleep(attempt === 1 ? 280 : 520);
          try { await teSendToTargetTabResponse({ type: "igblue_auto_py_go_no", no: reqNo, wait_ms: 320 }, "", 1600); } catch (_) {}
          const st = await teSendToTargetTabResponse({ type: "igblue_get_main_injection_state", requested_no: reqNo, wait_ms: 160 }, "", 1600).catch((err) => ({ ok: false, error: String(err && err.message || err) }));
          if (st && st.ok && st.internal_handler_found && st.labels_ready && (st.has_requested || Number(st.current_rank_no || 0) === reqNo || Number(st.loaded_max_no || 0) >= reqNo)) {
            teAutoPyLeaveWait("Main Injection 정상화");
            return Object.assign({ attempt }, st);
          }
          if (attempt >= 5) {
            teAutoPyEnterWait("main_injection_not_ready", "Main 내부 탐색 데이터와 shortcode 카드 연결을 기다리는 중입니다.");
            await teSleep(1200);
          }
        }
        return { ok: false, stopped: true, error: "run_seq_changed", attempt };
      }
      function teUpdateMetricThresholdCount() {
        const t = document.getElementById("top_metric_label"),
          r = document.getElementById("top_n_count"),
          n = teTopMetricField(),
          i = teMetricThresholdValue(),
          s = teMetricSourceList();
        r && (teLocalSet("filter/metric_threshold_10k", r.value || ""), teLocalSet("top_n_count", r.value || ""));
        t && (t.textContent = "(10k)");
        const primaryActive = !!n && i > 0;
        const secondaryActive = teSecondaryMetricFiltersActive();
        if (!primaryActive && !secondaryActive) {
          teSetTopMetricCountValue(0, "조건에 맞는 실제 게시물 개수");
          return;
        }
        const o = s.filter((e) => teMediaPassesTopConditions(e, primaryActive ? n : "", primaryActive ? i : 0)).length;
        const titleParts = [];
        if (primaryActive) titleParts.push(`${teMetricLabel(n)} >= ${i.toLocaleString()}${n === "play_count" && secondaryActive ? " · 조회수 미제공은 참여점수" : ""}`);
        teEngagementFilterSpecs().forEach((spec) => titleParts.push(`${spec.label} >= ${spec.min.toLocaleString()}`));
        teSetTopMetricCountValue(o, `${titleParts.join(" · ")} count`);
      }
      function teRefreshRanksAndMetricCount() {
        teUpdateMetricThresholdCount();
        Array.isArray(oe) && ee(oe);
      }
      function teRestoreMetricThreshold() {
        const e = document.getElementById("top_n_count");
        if (!e) return;
        const r = teLocalGetAny(["filter/metric_threshold_10k", "top_metric_units", "top_n_count"], e.value || "10.0");
        "" !== String(r) && (e.value = String(r));
        teSetTopMetricCountValue(teLocalGetAny(["filter/metric_count", "top_metric_count"], "0"), "조회수 기준에 맞는 실제 게시물 개수");
        const n = () => {
          teLocalSet("filter/metric_threshold_10k", e.value || "");
          teLocalSet("top_metric_units", e.value || "");
          teLocalSet("top_n_count", e.value || "");
          teApplyMetricFolderSuggestion("metric_threshold_change");
          teRefreshRanksAndMetricCount();
        };
        e.addEventListener("input", n);
        e.addEventListener("blur", n);
        e.addEventListener("change", n);
        const bindSecondaryFilter = (id, key) => {
          const input = document.getElementById(id);
          if (!input) return;
          input.value = teLocalGet(key, input.value || "0");
          const save = () => {
            const value = String(Math.max(0, Math.floor(teMetricNumber(input.value))));
            input.value = value;
            teLocalSet(key, value);
            teRefreshRanksAndMetricCount();
          };
          input.addEventListener("input", save);
          input.addEventListener("blur", save);
          input.addEventListener("change", save);
        };
        bindSecondaryFilter("igblue_min_comments", "filter/min_comments");
        bindSecondaryFilter("igblue_min_likes", "filter/min_likes");
        const shareInput = document.getElementById("igblue_min_shares");
        if (shareInput) shareInput.value = "0";
        teLocalSet("filter/min_shares", "0");
        bindSecondaryFilter("igblue_min_reposts", "filter/min_reposts");
        const autoTarget = document.getElementById("igblue_auto_py_target_count");
        if (autoTarget) {
          const savedTarget = teLocalGetAny(["download/auto_py_target_count", "auto_py_target_count"], autoTarget.value || "100");
          if (String(savedTarget || "") !== "") autoTarget.value = String(savedTarget);
          autoTarget.addEventListener("input", teAutoPyTargetCountSave);
          autoTarget.addEventListener("blur", teAutoPyTargetCountSave);
          autoTarget.addEventListener("change", teAutoPyTargetCountSave);
        }
        teUpdateMetricThresholdCount();
      }
      // IGblue local patch: popup window support. Chrome's native side-panel title bar cannot be edited,
      // so side.html exposes a one-time popout button inside the page.
      let teCurrentTargetTabId = 0;
      let teHasInstagramTarget = !1;
      let teAutoPySessionTarget = { active: false, session_id: "", tab_id: 0, generation: 0, start_url: "", waiting: false, wait_code: "", wait_message: "", wait_since: 0, previous_phase: "searching", fatal_handled: false };
      let teAutoPyContinuationPending = false;
      let teAutoPyProgressState = null;
      const TE_AUTO_PY_PROGRESS_KEY = "IGBLUE/auto_py_progress_state_v263";
      const TE_AUTO_PY_LEGACY_RESTART_KEY = "IGBLUE/auto_py_restart_snapshot_v261";
      let teAutoRuntimeNoticeMode = "";
      let teAutoRuntimeNoticeCode = "";
      let teAutoRuntimeNoticeResumeTimer = 0;
      let tePinnedTargetTabId = 0;
      let tePinnedTargetUrl = "";
      // Target tab ID is routing metadata only.
      let teDownloadSessionAccount = "";
      let teDownloadSessionAccountFolderMerge = null;
      let teDownloadStopRequested = !1;
      let teDownloadResumeState = null;
      const IGBLUE_TAB_SESSION_KEY = "target/tab_sessions_v179a12";
      const IGBLUE_TAB_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
      function teReadTabSessions() {
        try { const obj = JSON.parse(teLocalGet(IGBLUE_TAB_SESSION_KEY, "{}") || "{}"); return obj && typeof obj === "object" ? obj : {}; } catch (_) { return {}; }
      }
      function teWriteTabSessions(obj) { try { teLocalSet(IGBLUE_TAB_SESSION_KEY, JSON.stringify(obj || {})); } catch (_) {} }
      function tePruneTabSessions() {
        const now = Date.now();
        const obj = teReadTabSessions();
        let changed = false;
        Object.keys(obj).forEach((k) => {
          const last = Number(obj[k] && (obj[k].last_seen_at_ms || obj[k].updated_at_ms || obj[k].started_at_ms) || 0);
          if (!last || now - last > IGBLUE_TAB_SESSION_TTL_MS) { delete obj[k]; changed = true; }
        });
        if (changed) teWriteTabSessions(obj);
        return obj;
      }
      function teCurrentGuardTabId() {
        return Number(teCurrentTargetTabId || 0) || 0;
      }
      function teCurrentWsTargetPayload(reason, extra) {
        try {
          const tabId = teCurrentGuardTabId();
          const url = String((teChannelLocked && teChannelLocked() && tePinnedTargetUrl) ? tePinnedTargetUrl : (teCurrentTargetUrl ? teCurrentTargetUrl() : "") || "");
          return Object.assign({
            tab_id: tabId,
            request_tab_id: tabId,
            window_id: 0,
            url,
            href: url,
            account_url: url,
            account_username: teAccountFromUrl ? String(teAccountFromUrl(url) || "") : "",
            request_id: "side_" + Date.now() + "_" + Math.floor(Math.random() * 100000),
            request_reason: String(reason || "side_request"),
            build_version: IGBLUE_BUILD_VERSION,
            build_tag: IGBLUE_BUILD_TAG
          }, extra || {});
        } catch (_) {
          return Object.assign({ tab_id: 0, request_reason: String(reason || "side_request") }, extra || {});
        }
      }
      function teHandleNotActiveTargetResponse(res, reason) {
        return false;
      }
      function teAutoPyDisplayTabId() {
        const sessionId = teAutoPySessionTarget && teAutoPySessionTarget.active ? Number(teAutoPySessionTarget.tab_id || 0) || 0 : 0;
        if (sessionId > 0) return sessionId;
        return teCurrentGuardTabId();
      }
      function teUpdateTabIdBadge() {
        try {
          const id = teAutoPyDisplayTabId();
          const currentUrl = String((teChannelLocked && teChannelLocked() && tePinnedTargetUrl) ? tePinnedTargetUrl : (teCurrentTargetUrl ? teCurrentTargetUrl() : "") || "");
          const sessionUrl = teAutoPySessionTarget && teAutoPySessionTarget.active && Number(teAutoPySessionTarget.tab_id || 0) === id ? String(teAutoPySessionTarget.start_url || "") : "";
          const url = sessionUrl || currentUrl;
          const sessions = tePruneTabSessions();
          const session = id ? sessions[String(id)] || null : null;
          document.querySelectorAll(".igblue-tab-id-badge").forEach((el) => {
            el.textContent = `tab_id: ${id || "-"}`;
            el.title = `자동실행 tab_id: ${id || "-"}
state: ${(session && session.on) ? "on" : "off"}
url: ${url || "-"}`;
          });
        } catch (_) {}
      }
      function teTouchTabSession(on, reason) {
        try {
          const id = teCurrentGuardTabId();
          if (!id) { teUpdateTabIdBadge(); return; }
          const now = Date.now();
          const url = String((teChannelLocked && teChannelLocked() && tePinnedTargetUrl) ? tePinnedTargetUrl : (teCurrentTargetUrl ? teCurrentTargetUrl() : "") || "");
          const obj = tePruneTabSessions();
          const prev = obj[String(id)] || {};
          obj[String(id)] = Object.assign({}, prev, {
            tab_id: id,
            url,
            username: teAccountFromUrl ? String(teAccountFromUrl(url) || "") : "",
            on: !!on,
            reason: String(reason || (on ? "on" : "off")),
            started_at_ms: on ? (prev.started_at_ms || now) : (prev.started_at_ms || now),
            started_at: on ? (prev.started_at || new Date(now).toISOString()) : (prev.started_at || new Date(now).toISOString()),
            last_seen_at_ms: now,
            last_seen_at: new Date(now).toISOString()
          });
          if (!on) {
            obj[String(id)].ended_at_ms = now;
            obj[String(id)].ended_at = new Date(now).toISOString();
          }
          teWriteTabSessions(obj);
          teUpdateTabIdBadge();
        } catch (_) {}
      }
      function tePayloadAccountFromUrl(url) { try { return teSafePath(teAccountFromUrl(url || "") || ""); } catch (_) { return ""; } }
      function teShouldAcceptSideShopPayload(msg) {
        const tabId = Number(msg && msg.tabId || 0) || 0;
        const url = String(msg && (msg.url || msg.source_url || msg.href) || "");
        const targetTab = teCurrentGuardTabId();
        if (targetTab > 0 && tabId > 0 && tabId !== targetTab) return { ok: false, reason: "tab_id_mismatch", tab_id: tabId, target_tab_id: targetTab, url };
        return { ok: true, tab_id: tabId, target_tab_id: targetTab, url };
      }
      function teIsPopupMode() {
        try { return new URLSearchParams(location.search).get("popup") === "1"; } catch { return !1; }
      }
      function teIsInstagramUrl(e) {
        try {
          const t = new URL(e || "");
          return (t.protocol === "https:" || t.protocol === "http:") && t.hostname.replace(/^www\./i, "").toLowerCase() === "instagram.com";
        } catch {
          return !1;
        }
      }
      function teRuntimeMessageResponse(message) {
        return new Promise((resolve) => {
          try {
            chrome.runtime.sendMessage(message || {}, (res) => {
              if (chrome.runtime.lastError) return resolve({ ok: false, error: chrome.runtime.lastError.message || "runtime_message_failed" });
              resolve(res || { ok: false, error: "empty_response" });
            });
          } catch (err) { resolve({ ok: false, error: String(err && err.message || err) }); }
        });
      }
      function teChromeTabGet(tabId) {
        return new Promise((resolve) => {
          try {
            chrome.tabs.get(Number(tabId || 0), (tab) => {
              if (chrome.runtime.lastError || !tab) return resolve({ ok: false, error: chrome.runtime.lastError && chrome.runtime.lastError.message || "tab_missing" });
              resolve({ ok: true, tab });
            });
          } catch (err) { resolve({ ok: false, error: String(err && err.message || err) }); }
        });
      }
      function teChromeLastFocusedWindow() {
        return new Promise((resolve) => {
          try {
            chrome.windows.getLastFocused({}, (win) => {
              if (chrome.runtime.lastError || !win) return resolve({ ok: false, error: chrome.runtime.lastError && chrome.runtime.lastError.message || "window_missing" });
              resolve({ ok: true, window: win });
            });
          } catch (err) { resolve({ ok: false, error: String(err && err.message || err) }); }
        });
      }
      function teAutoPyAccountPositionText() {
        try {
          const st = teAutoPyAccountQueueStatus();
          if (st && st.has) return `계정 ${Math.max(0, Number(st.index || 0) || 0)}/${Math.max(0, Number(st.total || 0) || 0)}`;
        } catch (_) {}
        return "현재 계정";
      }
      function teAutoPyClonePlain(value, fallback) {
        try { return JSON.parse(JSON.stringify(value)); } catch (_) { return fallback; }
      }
      function teAutoPySaveProgressState(reason) {
        try {
          const state = {
            schema: "ig_auto_progress_v263",
            saved_at: new Date().toISOString(),
            saved_ts: Date.now(),
            reason: String(reason || "user_stop"),
            account_queue_index: teAutoPyAccountQueueIndex(),
            account_queue_total: teAutoPyAccountQueueList(true).length,
            account_username: String(teAutoPyAccountQueueStatus() && teAutoPyAccountQueueStatus().account || "").replace(/^@/, ""),
            last_rank: Math.max(0, Number(teAutoPyStats && teAutoPyStats.current_no || teAutoPyReadLastRankTextValue && teAutoPyReadLastRankTextValue() || 0) || 0),
            target_url: String(teCurrentTargetUrl() || ""),
            session_target: teAutoPyClonePlain(teAutoPySessionTarget, {}),
            stats: teAutoPyClonePlain(teAutoPyStats, {}),
            run_totals: teAutoPyClonePlain(teAutoPyRunTotals, {}),
            ui_state: teAutoPyClonePlain(teAutoPyUiState, {}),
            last_done_text: String(teAutoPyLastDoneText || "")
          };
          teAutoPyProgressState = state;
          localStorage.setItem(TE_AUTO_PY_PROGRESS_KEY, JSON.stringify(state));
          return state;
        } catch (_) { return null; }
      }
      function teAutoPyLoadProgressState() {
        try {
          if (teAutoPyProgressState && teAutoPyProgressState.schema === "ig_auto_progress_v263") return teAutoPyProgressState;
          const raw = localStorage.getItem(TE_AUTO_PY_PROGRESS_KEY);
          let state = raw ? JSON.parse(raw) : null;
          if (!(state && state.schema === "ig_auto_progress_v263")) {
            const legacyRaw = localStorage.getItem(TE_AUTO_PY_LEGACY_RESTART_KEY);
            const legacy = legacyRaw ? JSON.parse(legacyRaw) : null;
            if (legacy && legacy.schema === "ig_auto_restart_v261" && String(legacy.reason || "") === "user_stop") {
              state = Object.assign({}, legacy, { schema: "ig_auto_progress_v263", migrated_from: "ig_auto_restart_v261" });
              localStorage.setItem(TE_AUTO_PY_PROGRESS_KEY, JSON.stringify(state));
            }
            try { localStorage.removeItem(TE_AUTO_PY_LEGACY_RESTART_KEY); } catch (_) {}
          }
          if (!(state && state.schema === "ig_auto_progress_v263")) return null;
          teAutoPyProgressState = state;
          return state;
        } catch (_) { return null; }
      }
      function teAutoPyClearProgressState() {
        teAutoPyProgressState = null;
        try { localStorage.removeItem(TE_AUTO_PY_PROGRESS_KEY); } catch (_) {}
        try { localStorage.removeItem(TE_AUTO_PY_LEGACY_RESTART_KEY); } catch (_) {}
      }
      function teAutoPyRestoreProgressState(state, seq) {
        const saved = state && typeof state === "object" ? state : null;
        if (!saved) return false;
        if (Number(saved.account_queue_index || 0) >= 0) teAutoPySetAccountQueueIndex(Number(saved.account_queue_index || 0));
        teAutoPyRunTotals = Object.assign({ completed_post_count: 0, completed_shortcodes: [], mp4_count: 0, jpg_count: 0, expected_asset_keys: [], completed_asset_keys: [] }, teAutoPyClonePlain(saved.run_totals, {}));
        teAutoPyStats = Object.assign({ active: true, run_seq: Number(seq || 0) || 0, start_ts: Date.now(), current_no: 0, current_no_label: "No---", max_rank: 0, target_count: 1, completed_post_count: 0, completed_shortcodes: [], mp4_count: 0, jpg_count: 0, mp4_total_ms: 0, jpg_total_ms: 0, mp4_max_ms: 0, jpg_max_ms: 0, mp4_min_ms: 0, jpg_min_ms: 0, cycle_count: 0, cycle_total_ms: 0, cycle_max_ms: 0, cycle_min_ms: 0, last_reason: "progress_continue" }, teAutoPyClonePlain(saved.stats, {}), { active: true, run_seq: Number(seq || 0) || 0, last_reason: "progress_continue" });
        teAutoPyUiState = Object.assign({ phase: "prepare", target_count: 1, completed_count: 0, completed_shortcodes: [], current_no: 0, asset_kind: "", wait_remaining: 0, exhausted: false, exhausted_reason: "", automatic_end: false, message: "진행상태 이어하기", account_index: 0, account_total: 0 }, teAutoPyClonePlain(saved.ui_state, {}), { phase: "prepare", automatic_end: false, message: "보존된 진행상태에서 이어서 실행" });
        teAutoPyLastDoneText = String(saved.last_done_text || "");
        const lastRank = Math.max(0, Number(saved.last_rank || teAutoPyStats.current_no || 0) || 0);
        if (lastRank > 0) teAutoPySetLastRank(lastRank, "progress_continue_state");
        teAutoPyStatsTouch("progress_continue_restore", true);
        return true;
      }
      function teAutoPyRestoreProgressNotice() {
        try {
          const state = teAutoPyLoadProgressState();
          if (!state || teAutoPyRunning || teAutoPyStopping) return false;
          if (Number(state.account_queue_index || 0) >= 0) teAutoPySetAccountQueueIndex(Number(state.account_queue_index || 0));
          teAutoPyRunTotals = Object.assign({ completed_post_count: 0, completed_shortcodes: [], mp4_count: 0, jpg_count: 0, expected_asset_keys: [], completed_asset_keys: [] }, teAutoPyClonePlain(state.run_totals, {}));
          teAutoPyStats = Object.assign({}, teAutoPyStats || {}, teAutoPyClonePlain(state.stats, {}), { active: false, last_reason: "user_stop_saved" });
          teAutoPyUiState = Object.assign({}, teAutoPyUiState || {}, teAutoPyClonePlain(state.ui_state, {}), { phase: "user_stopped", automatic_end: false, message: "진행상태가 보존되어 있습니다." });
          teAutoPyLastDoneText = String(state.last_done_text || teAutoPyLastDoneText || "");
          teAutoPyStatsRender();
          teAutoPyRenderRuntimeNotice("stop", "user_stop", "진행상태가 보존되어 있습니다. 시작을 누르면 이어서 실행합니다.");
          return true;
        } catch (_) { return false; }
      }
      async function teAutoPyStartOrContinueFromUi() {
        if (teAutoPyRunning || teAutoPyStopping) return void n("자동시작: 이미 실행중");
        const state = teAutoPyLoadProgressState();
        return await teAutoPyStartOnceFromUi({
          new_session: !state,
          continuation: !!state,
          resume_progress: !!state,
          progress_state: state,
          reason: state ? "progress_continue" : "user_start"
        });
      }
      function teAutoPyResetProgressState(reason, options) {
        const opts = options && typeof options === "object" ? options : {};
        if (teAutoPyRunning || teAutoPyStopping) {
          if (!opts.silent) n("진행상태 리셋: 자동 실행을 먼저 중지하세요.");
          return false;
        }
        teAutoPyClearProgressState();
        teAutoPySetAccountQueueIndex(0);
        teAutoPySetLastRank(0, String(reason || "progress_reset"));
        teAutoPyRunTotals = { completed_post_count: 0, completed_shortcodes: [], mp4_count: 0, jpg_count: 0, expected_asset_keys: [], completed_asset_keys: [] };
        teAutoPyLastDoneText = "";
        teAutoPyResetSessionTarget();
        const target = Math.max(1, Number(teAutoPyTargetCount && teAutoPyTargetCount() || 1) || 1);
        teAutoPyUiReset(target, "진행상태 리셋", { reset_run_totals: true });
        teAutoPyStatsFinish("progress_reset");
        teAutoPyUpdateAccountQueueStatus();
        teAutoPyUpdateButtons(teAutoPyLastPyOk);
        if (!opts.silent) n("진행상태를 초기화했습니다. 다음 자동시작은 0부터 시작합니다.");
        return true;
      }
      function teAutoPyRenderRuntimeNotice(mode, code, message) {
        const el = document.getElementById("igblue_aiwk_setup_notice");
        const def = document.getElementById("igblue_aiwk_setup_default");
        const runtime = document.getElementById("igblue_auto_runtime_notice");
        const title = document.getElementById("igblue_auto_runtime_notice_title");
        const text = document.getElementById("igblue_auto_runtime_notice_text");
        if (!el || !runtime || !title || !text) return;
        if (teAutoRuntimeNoticeResumeTimer) { clearTimeout(teAutoRuntimeNoticeResumeTimer); teAutoRuntimeNoticeResumeTimer = 0; }
        teAutoRuntimeNoticeMode = String(mode || "");
        teAutoRuntimeNoticeCode = String(code || "");
        el.classList.remove("is-auto-wait", "is-auto-resume", "is-auto-stop");
        if (!mode) {
          teAutoRuntimeNoticeCode = "";
          runtime.hidden = true;
          if (def) def.hidden = false;
          try { teAiwkRefreshSetupNotice(true); } catch (_) { el.classList.remove("is-visible"); }
          return;
        }
        if (def) def.hidden = true;
        runtime.hidden = false;
        el.classList.add("is-visible", mode === "wait" ? "is-auto-wait" : mode === "resume" ? "is-auto-resume" : "is-auto-stop");
        el.setAttribute("aria-hidden", "false");
        title.textContent = mode === "wait" ? "⚠ 자동 대기중" : mode === "resume" ? "✅ 자동 재개됨" : "⛔ 자동 중단";
        const position = teAutoPyAccountPositionText();
        text.textContent = mode === "wait"
          ? `사유: ${String(message || code || "정상화 대기")}\n위치: ${position} · 현재 단계와 전체 실행 집계를 유지합니다.\n조건이 정상화되면 같은 단계에서 자동으로 이어서 실행합니다.`
          : mode === "resume"
            ? `해결: ${String(message || code || "대기 조건 정상화")}\n위치: ${position} · 중단했던 단계부터 계속합니다.`
            : `사유: ${String(message || code || "복구할 수 없는 중단")}\n위치: ${position}\n시작을 누르면 보존된 진행상태에서 이어서 실행합니다.`;
      }
      function teAutoPyEnterWait(code, message) {
        const c = String(code || "auto_wait");
        const m = String(message || "정상화 대기");
        const same = teAutoPySessionTarget.waiting && teAutoPySessionTarget.wait_code === c && teAutoPySessionTarget.wait_message === m;
        if (!teAutoPySessionTarget.waiting) teAutoPySessionTarget.previous_phase = String(teAutoPyUiState && teAutoPyUiState.phase || "searching");
        teAutoPySessionTarget.waiting = true;
        teAutoPySessionTarget.wait_code = c;
        teAutoPySessionTarget.wait_message = m;
        if (!teAutoPySessionTarget.wait_since) teAutoPySessionTarget.wait_since = Date.now();
        teAutoPySetUiPhase("paused", { automatic_end: false, message: m, completed_count: teAutoPyCompletedPostCount() });
        if (!same) {
          teAutoPyRenderRuntimeNotice("wait", c, m);
          try { n(`자동 대기중: ${m}`); } catch (_) {}
        }
      }
      function teAutoPyWaitCodeIsSession(code) {
        return /^(start_instagram_tab_required|user_window_in_use|auto_start_tab_non_instagram)$/i.test(String(code || ""));
      }
      function teAutoPyWaitCodeIsTransport(code) {
        return /^(main_injection_loading)$/i.test(String(code || ""));
      }
      function teAutoPyLeaveWait(message) {
        if (!teAutoPySessionTarget.waiting) return;
        const previous = String(teAutoPySessionTarget.previous_phase || "searching");
        teAutoPySessionTarget.waiting = false;
        teAutoPySessionTarget.wait_code = "";
        teAutoPySessionTarget.wait_message = "";
        teAutoPySessionTarget.wait_since = 0;
        teAutoPySetUiPhase(previous === "paused" || previous === "failed" || previous === "auto_end" ? "searching" : previous, { automatic_end: false, message: String(message || "자동 재개"), completed_count: teAutoPyCompletedPostCount() });
        teAutoPyRenderRuntimeNotice("resume", "auto_resumed", String(message || "대기 조건 정상화"));
        try { n(`자동 재개: ${String(message || "대기 조건 정상화")}`); } catch (_) {}
        teAutoRuntimeNoticeResumeTimer = setTimeout(() => {
          if (!teAutoPySessionTarget.waiting && teAutoRuntimeNoticeMode === "resume") teAutoPyRenderRuntimeNotice("", "", "");
        }, 2600);
      }
      function teAutoPyResetSessionTarget() {
        teAutoPySessionTarget = { active: false, session_id: "", tab_id: 0, generation: 0, start_url: "", waiting: false, wait_code: "", wait_message: "", wait_since: 0, previous_phase: "searching", fatal_handled: false };
        teAutoPyRenderRuntimeNotice("", "", "");
      }
      async function teAutoPyAcquireStartTab(mySeq) {
        while (!mySeq || mySeq === teAutoPyRunSeq) {
          if (teAutoPyStopping) return { ok: false, stopped: true, error: "user_stop" };
          const state = await teRuntimeMessageResponse({ type: "IGBLUE_GET_TARGET_STATE" });
          const tabId = Number(state && state.tabId || 0) || 0;
          const url = String(state && state.url || "");
          if (tabId > 0 && teIsInstagramUrl(url)) {
            teAutoPySessionTarget.active = true;
            teAutoPySessionTarget.session_id = `auto_${Date.now()}_${tabId}`;
            teAutoPySessionTarget.tab_id = tabId;
            teAutoPySessionTarget.start_url = url;
            teAutoPySessionTarget.fatal_handled = false;
            teCurrentTargetTabId = tabId;
            teSetTargetLabel({ tabId, url });
            const account = teAccountFromUrl(url) || "";
            const queueState = teAutoPyAccountQueueStatus();
            const claimed = await teAutoPyClaimTabOwner(tabId, mySeq, "auto_start_existing_tab", account, Number(queueState && queueState.index || 0) || 0, Number(queueState && queueState.total || 0) || 0);
            if (!(claimed && claimed.ok)) return { ok: false, error: String(claimed && claimed.error || "owner_claim_failed"), tab_id: tabId, url };
            if (teAutoPyWaitCodeIsSession(teAutoPySessionTarget.wait_code)) teAutoPyLeaveWait("자동실행 Instagram 탭 확인");
            return { ok: true, tab_id: tabId, generation: Number(claimed.generation || 0) || 0, url };
          }
          teAutoPyEnterWait("start_instagram_tab_required", "자동실행을 시작할 Instagram 탭을 활성화하세요.");
          await teSleep(1000);
        }
        return { ok: false, stopped: true, error: "run_seq_changed" };
      }
      async function teAutoPyProbeSessionTarget(mySeq) {
        if (!teAutoPySessionTarget.active || !(Number(teAutoPySessionTarget.tab_id || 0) > 0)) return { ok: false, acquire_required: true, error: "start_tab_not_acquired" };
        if (mySeq && mySeq !== teAutoPyRunSeq) return { ok: false, stopped: true, error: "run_seq_changed" };
        if (teAutoPyStopping) return { ok: false, stopped: true, error: "user_stop" };
        const got = await teChromeTabGet(teAutoPySessionTarget.tab_id);
        if (!(got && got.ok && got.tab)) return { ok: false, fatal: true, error: "auto_start_tab_closed" };
        const tab = got.tab;
        const url = String(tab.url || "");
        if (!teIsInstagramUrl(url)) return { ok: false, wait: true, error: "auto_start_tab_non_instagram", message: "자동실행 탭이 Instagram 외 주소입니다. 같은 탭을 Instagram으로 되돌리세요.", tab };
        const focused = await teChromeLastFocusedWindow();
        const activeHere = !!tab.active;
        const sameWindow = !(focused && focused.ok && focused.window && Number(focused.window.id || 0) > 0) || Number(focused.window.id || 0) === Number(tab.windowId || 0);
        if (!activeHere || !sameWindow) return { ok: false, wait: true, error: "user_window_in_use", message: "사용자가 다른 탭 또는 Chrome 창을 사용 중입니다.", tab };
        teCurrentTargetTabId = Number(tab.id || teAutoPySessionTarget.tab_id || 0) || 0;
        teSetTargetLabel({ tabId: teCurrentTargetTabId, url });
        return { ok: true, tab_id: teCurrentTargetTabId, url, tab };
      }
      async function teAutoPyHandleFatalSession(errorCode, detail) {
        if (teAutoPySessionTarget.fatal_handled) return;
        teAutoPySessionTarget.fatal_handled = true;
        const msg = String(detail || "자동실행 시작 탭이 닫혔습니다.");
        teAutoPyRenderRuntimeNotice("stop", errorCode || "auto_start_tab_closed", msg);
        teAutoPySetUiPhase("failed", { automatic_end: false, message: msg, completed_count: teAutoPyCompletedPostCount() });
        try { await teAutoPyCycle("S00_AUTOPLAY_START_TAB_CLOSED", { cycle_id: teBuildCycleId("START_TAB_CLOSED"), run_seq: Number(teAutoPyRunSeq || 0), decision: "auto_stopped", action: "stop", reason: String(errorCode || "auto_start_tab_closed"), stop_reason_ko: msg, start_tab_id: Number(teAutoPySessionTarget.tab_id || 0) || 0 }); } catch (_) {}
        teAutoPyContinuationPending = false;
        teAutoPyStopping = true;
        teAutoPyRunning = false;
        teAutoPyRunSeq += 1;
        teAutoPySessionTarget.active = false;
        teAutoPySessionTarget.tab_id = 0;
        teCurrentTargetTabId = 0;
        teUpdateTabIdBadge();
        try { n("자동 중단: " + msg); } catch (_) {}
      }
      async function teAutoPyWaitForSessionTarget(mySeq, waitRequired) {
        if (!teAutoPyRunning) return { ok: false, error: "auto_not_running" };
        if (!teAutoPySessionTarget.active) {
          if (!waitRequired) return { ok: false, error: "start_tab_not_acquired" };
          const acquired = await teAutoPyAcquireStartTab(mySeq || teAutoPyRunSeq);
          if (!(acquired && acquired.ok)) return acquired;
        }
        while (!mySeq || mySeq === teAutoPyRunSeq) {
          const state = await teAutoPyProbeSessionTarget(mySeq || teAutoPyRunSeq);
          if (state && state.ok) {
            if (teAutoPyWaitCodeIsSession(teAutoPySessionTarget.wait_code)) teAutoPyLeaveWait("원래 자동실행 탭 정상화");
            return state;
          }
          if (state && state.fatal) {
            await teAutoPyHandleFatalSession(state.error, "자동실행 시작 탭이 닫혔습니다.");
            return state;
          }
          if (!waitRequired) return state || { ok: false, error: "auto_waiting" };
          if (state && state.stopped) return state;
          teAutoPyEnterWait(state && state.error || "auto_wait", state && state.message || "자동실행 탭 정상화 대기중입니다.");
          await teSleep(1000);
        }
        return { ok: false, stopped: true, error: "run_seq_changed" };
      }
      function teAutoPyMessageWaitRequired(msg) {
        const type = String(msg && msg.type || "");
        if (type === "igblue_auto_status_update") return false;
        if (type === "igblue_py_state" && String(msg && msg.value && msg.value.state || "") === "auto_stopped") return false;
        return true;
      }
      function teShortIgUrl(e) {
        try {
          const t = new URL(e || "");
          if (!/instagram\.com$/i.test(t.hostname.replace(/^www\./i, ""))) return "No active Instagram tab";
          const r = t.pathname || "/";
          const n = r.split("/").filter(Boolean)[0] || "home";
          return `Linked: ${n}  ·  ${t.origin}${r}`;
        } catch {
          return "No active Instagram tab";
        }
      }
      // IGblue local patch v17: channel lock freezes target tab/account and bulk download folder.
      function teChannelLocked() {
        const e = document.getElementById("channel_lock");
        return !!(e && e.checked);
      }
      function tePinnedAccountName() {
        return teSafePath(teAccountFromUrl(tePinnedTargetUrl || teCurrentTargetUrl()) || "instagram");
      }
      function tePinTarget(e) {
        const t = e && e.url ? String(e.url) : teCurrentTargetUrl();
        if (teIsInstagramUrl(t)) {
          tePinnedTargetUrl = t;
          tePinnedTargetTabId = Number(e?.tabId || teCurrentTargetTabId || 0);
          teLocalSet("target/pinned_url", tePinnedTargetUrl);
          teLocalSet("target/pinned_tab_id", tePinnedTargetTabId || "");
          teUpdateDownloadFolderPath(tePinnedAccountName());
        }
      }
      const teChannelLockTabMissingKo = "채널고정 탭이 없어 채널고정을 취소합니다.";
      let teChannelLockLastRemovedLogAt = 0;
      function teChannelLockWsLog(reason, detail) {
        try {
          const payload = Object.assign({
            schema: IGBLUE_CYCLE_SCHEMA,
            build_version: IGBLUE_BUILD_VERSION,
            build_tag: IGBLUE_BUILD_TAG,
            cycle_id: teBuildCycleId("CHANNEL_LOCK_TAB_REMOVED"),
            cycle_stage: "S00_CHANNEL_LOCK_TAB_REMOVED",
            decision: "channel_lock_disabled",
            action: "channel_lock_auto_uncheck",
            reason: String(reason || "pinned_instagram_tab_removed"),
            message: teChannelLockTabMissingKo,
            message_ko: teChannelLockTabMissingKo,
            pinned_tab_id: Number(detail && detail.tabId || 0) || 0,
            pinned_url: String(detail && detail.url || ""),
            href: teCurrentTargetUrl(),
            host: teDownTxtHost()
          }, detail || {});
          return teAiwkWsRequest("ig_auto_cycle_state", payload, 1200).catch(() => null);
        } catch (_) {
          return Promise.resolve(null);
        }
      }
      function teDisableChannelLockForMissingTab(reason, detail) {
        const now = Date.now();
        const oldId = Number(detail && detail.tabId || tePinnedTargetTabId || 0) || 0;
        const oldUrl = String(detail && detail.url || tePinnedTargetUrl || "");
        const cb = document.getElementById("channel_lock");
        if (cb) cb.checked = !1;
        teLocalSet("target/channel_lock", "0");
        tePinnedTargetTabId = 0;
        tePinnedTargetUrl = "";
        teLocalSet("target/pinned_url", "");
        teLocalSet("target/pinned_tab_id", "");
        teSetTargetLabel(null);
        teUpdateDownloadFolderPath();
        if (!teDownloadSessionAccount) teLoadDownTxtLocal(teActiveFolderAccountName());
        P();
        n(teChannelLockTabMissingKo);
        if (now - teChannelLockLastRemovedLogAt > 1500) {
          teChannelLockLastRemovedLogAt = now;
          teChannelLockWsLog(reason || "pinned_instagram_tab_removed", Object.assign({}, detail || {}, { tabId: oldId, url: oldUrl }));
        }
      }
      function teValidatePinnedTargetAlive(reason, cb) {
        const pinnedId = Number(tePinnedTargetTabId || 0) || 0;
        if (!(teChannelLocked() && pinnedId > 0)) {
          cb && cb(true);
          return;
        }
        try {
          chrome.tabs.get(pinnedId, (tab) => {
            if (chrome.runtime.lastError || !tab || !tab.id) {
              teDisableChannelLockForMissingTab(reason || "pinned_instagram_tab_removed", { tabId: pinnedId, url: tePinnedTargetUrl, error: chrome.runtime.lastError && chrome.runtime.lastError.message || "tab_missing" });
              cb && cb(false);
              return;
            }
            const url = String(tab.url || tePinnedTargetUrl || "");
            if (!teIsInstagramUrl(url)) {
              teDisableChannelLockForMissingTab("pinned_instagram_tab_not_instagram", { tabId: pinnedId, url });
              cb && cb(false);
              return;
            }
            tePinnedTargetUrl = url;
            teLocalSet("target/pinned_url", tePinnedTargetUrl);
            cb && cb(true);
          });
        } catch (err) {
          teDisableChannelLockForMissingTab(reason || "pinned_instagram_tab_removed", { tabId: pinnedId, url: tePinnedTargetUrl, error: String(err && err.message || err) });
          cb && cb(false);
        }
      }
      function teInitChannelLock() {
        const e = document.getElementById("channel_lock");
        if (!e) return;
        const migrated = teLocalGet("run/current_channel_only", teLocalGet("target/channel_lock", "0"));
        e.checked = migrated === "1";
        tePinnedTargetUrl = "";
        tePinnedTargetTabId = 0;
        teLocalSet("target/pinned_url", "");
        teLocalSet("target/pinned_tab_id", "");
        const sync = () => {
          teLocalSet("run/current_channel_only", e.checked ? "1" : "0");
          teLocalSet("target/channel_lock", e.checked ? "1" : "0");
          if (e.checked) teAutoPySetAccountQueueIndex(0);
          const badge = document.getElementById("igblue_account_scope_badge");
          if (badge) badge.textContent = e.checked ? "현재 채널만 · 목록 0/0" : "계정 목록 순회";
          teAutoPyUpdateAccountQueueStatus();
          P();
        };
        e.addEventListener("change", sync);
        sync();
      }
      function teActiveFolderAccountName() {
        return teSafePath(teDownloadSessionAccount || (teChannelLocked() && tePinnedTargetUrl ? tePinnedAccountName() : teAccountFromUrl(teCurrentTargetUrl())) || "instagram");
      }
      function teSetTargetLabel(e) {
        try {
          const t = document.getElementById("igblue_target_url");
          const tabId = Number(e && e.tabId || 0) || 0;
          const r = e && e.url ? String(e.url) : "";
          const isIg = teIsInstagramUrl(r);
          teCurrentTargetTabId = isIg && tabId > 0 ? tabId : 0;
          teHasInstagramTarget = !!isIg;
          if (!t) { P(); return; }
          if (isIg) {
            t.textContent = teShortIgUrl(r);
            t.title = r;
            t.classList.remove("not-linked");
          } else {
            t.textContent = "No active Instagram tab";
            t.title = "";
            t.classList.add("not-linked");
          }
          teUpdateTabIdBadge();
          P();
        } catch (_) { P(); }
      }
      function teApplyTargetInfo(e) {
        if (!e || !Array.isArray(e.info)) return !1;
        const guard = teShouldAcceptSideShopPayload(e);
        if (!(guard && guard.ok)) {
          try { localStorage.setItem("IG_dbg/side.js/ndy_side_shop_ignored", JSON.stringify(Object.assign({ts:new Date().toISOString()}, guard || {}))); } catch (_) {}
          return !1;
        }
        u || ((u = !0), P());
        c && clearTimeout(c);
        c = setTimeout(F, 2e3);
        se = e.info;
        if (Number(e.tabId || 0)) teCurrentTargetTabId = Number(e.tabId || 0);
        if (e.url) teSetTargetLabel({ tabId: Number(e.tabId || 0) || teCurrentTargetTabId, url: String(e.url || "") });
        teTouchTabSession(!!(teAutoPyRunning && !teAutoPyStopping), "side_payload_accept");
        ae(e.info, K, Z, !1, !1);
        return !0;
      }
      function teRequestTargetState(applyInfo = !1, done = null) {
        try {
          chrome.runtime.sendMessage({ type: "IGBLUE_GET_TARGET_STATE" }, (state) => {
            if (chrome.runtime.lastError || !state) { teSetTargetLabel(null); if (typeof done === "function") done(null); return; }
            teCurrentTargetTabId = Number(state.tabId || 0) || 0;
            teSetTargetLabel(state);
            if (applyInfo) teApplyTargetInfo(state);
            if (typeof done === "function") done(state);
          });
        } catch { teSetTargetLabel(null); if (typeof done === "function") done(null); }
      }
      function tePopupHandoffRequestId() {
        try { return String(new URLSearchParams(location.search).get("handoff") || ""); } catch { return ""; }
      }
      function teConfirmPopupHandoff(state) {
        const requestId = tePopupHandoffRequestId();
        if (!requestId || !state || !teIsInstagramUrl(state.url) || !(Number(state.tabId || 0) > 0)) return;
        try {
          chrome.runtime.sendMessage({ type:"IGBLUE_POPUP_READY", request_id:requestId, tabId:Number(state.tabId || 0), url:String(state.url || "") }, () => { chrome.runtime.lastError; });
        } catch (_) {}
      }
      function teInitPopupUi() {
        try {
          const isPopup = teIsPopupMode();
          isPopup && document.body.classList.add("igblue-popup-mode");
          const bindPopout = (btn) => {
            if (!btn) return;
            if (isPopup) {
              btn.disabled = !0;
              btn.style.display = "none";
              btn.setAttribute("aria-disabled", "true");
            } else {
              btn.addEventListener("click", () => {
                if (teAutoPyRunning || teAutoPyStopping) {
                  n("자동실행 중에는 새 창으로 전환할 수 없습니다. 중지 후 다시 시도하세요.");
                  return;
                }
                const requestId = `side_handoff_${Date.now()}_${Math.floor(Math.random()*100000)}`;
                btn.disabled = true;
                chrome.runtime.sendMessage({ type: "IGBLUE_OPEN_POPUP", request_id:requestId, tabId:Number(teCurrentTargetTabId || 0) }, (res) => {
                  btn.disabled = false;
                  if (chrome.runtime.lastError || !(res && res.status === "ok")) n("새 창 연결 실패: 기존 Side를 유지합니다.");
                  else n("새 창 연결 확인중");
                });
              });
            }
          };
          bindPopout(document.getElementById("open_popup"));
          bindPopout(document.getElementById("open_popup_inline"));
          teInitChannelLock();
          teRequestTargetState(isPopup, (state) => { if (isPopup) teConfirmPopupHandoff(state); });
        } catch {}
      }
      function teGetTargetTabId(cb) {
        try {
          chrome.runtime.sendMessage({ type: "IGBLUE_GET_TARGET_STATE" }, (res) => {
            if (!chrome.runtime.lastError && res) {
              teCurrentTargetTabId = Number(res && res.tabId || 0) || 0;
              teSetTargetLabel(res);
              if (teCurrentTargetTabId > 0) return void cb(teCurrentTargetTabId);
            }
            cb(0);
          });
        } catch { cb(0); }
      }
      function teTabMessageJsonSize(value) {
        try { return JSON.stringify(value || {}).length; } catch (_) { return 999999999; }
      }
      function teSlimValueForTab(value, depth = 0) {
        try {
          if (value == null) return value;
          if (typeof value === "string") return value.length > 16000 ? value.slice(0, 16000) + `...<${value.length}>` : value;
          if (typeof value !== "object") return value;
          if (depth >= 4) return Array.isArray(value) ? `[array len=${value.length}]` : "[object truncated]";
          if (Array.isArray(value)) return value.slice(-24).map((v) => teSlimValueForTab(v, depth + 1));
          const out = {};
          const skipHuge = new Set(["calibration_log", "calibration_log_text", "runtime_memory_events", "cycle_log", "cycle_log_text", "cycle_log_lines", "latest_click", "click_result", "startup_check"]);
          Object.keys(value).slice(0, 90).forEach((k) => {
            const v = value[k];
            if (skipHuge.has(k)) {
              if (typeof v === "string") out[k] = v.slice(-12000);
              else if (Array.isArray(v)) out[k] = v.slice(-20).map((x) => teSlimValueForTab(x, depth + 1));
              else if (v && typeof v === "object") out[k] = { truncated: true, keys: Object.keys(v).slice(0, 30) };
              else out[k] = v;
              return;
            }
            out[k] = teSlimValueForTab(v, depth + 1);
          });
          return out;
        } catch (_) { return "[slim_failed]"; }
      }
      function teSafeTabMessage(msg) {
        try {
          if (teTabMessageJsonSize(msg) <= 512 * 1024) return msg;
          const slim = teSlimValueForTab(msg, 0);
          if (teTabMessageJsonSize(slim) <= 1024 * 1024) return slim;
          return {
            type: msg && msg.type || "igblue_py_state",
            value: {
              state: msg && msg.value && msg.value.state || msg && msg.state || "message_too_large_truncated",
              message: "side.js tabs.sendMessage payload truncated before Chrome 64MiB limit",
              error: msg && msg.value && (msg.value.error || msg.value.error_code) || msg && (msg.error || msg.error_code) || "payload_too_large",
              build_tag: IGBLUE_BUILD_TAG
            }
          };
        } catch (_) {
          return { type: msg && msg.type || "igblue_py_state", value: { state: "message_too_large_truncated", error: "payload_slim_exception", build_tag: IGBLUE_BUILD_TAG } };
        }
      }
      async function teResolveTargetTabForMessage(msg) {
        if (teAutoPyRunning && teAutoPySessionTarget.active) {
          const waitRequired = teAutoPyMessageWaitRequired(msg);
          const ready = await teAutoPyWaitForSessionTarget(teAutoPyRunSeq, waitRequired);
          return ready && ready.ok ? { ok: true, tab_id: Number(ready.tab_id || teAutoPySessionTarget.tab_id || 0) || 0 } : Object.assign({ ok: false, tab_id: 0 }, ready || {});
        }
        return await new Promise((resolve) => teGetTargetTabId((tabId) => resolve(tabId ? { ok: true, tab_id: tabId } : { ok: false, tab_id: 0, error: "no_instagram_tab" })));
      }
      async function teSendTabMessageOnce(tabId, tabMsg, expectResponse, timeoutMs, failMsg) {
        return await new Promise((resolve) => {
          let done = false;
          const finish = (res) => { if (done) return; done = true; resolve(res); };
          const send = () => {
            const timer = expectResponse ? setTimeout(() => finish({ ok: false, error: "content_timeout" }), Math.max(500, Number(timeoutMs) || 3000)) : 0;
            chrome.tabs.sendMessage(tabId, tabMsg, (res) => {
              if (timer) clearTimeout(timer);
              if (!chrome.runtime.lastError) return finish(expectResponse ? (res || { ok: true }) : { ok: true });
              try {
                chrome.runtime.sendMessage({ type: "IGBLUE_ENSURE_CONTENT", tabId }, (ens) => {
                  if (chrome.runtime.lastError || !ens || ens.status !== "ok") {
                    failMsg && n(failMsg + " / script not ready");
                    return finish({ ok: false, error: "script_not_ready" });
                  }
                  const timer2 = expectResponse ? setTimeout(() => finish({ ok: false, error: "content_retry_timeout" }), Math.max(500, Number(timeoutMs) || 3000)) : 0;
                  chrome.tabs.sendMessage(tabId, tabMsg, (res2) => {
                    if (timer2) clearTimeout(timer2);
                    chrome.runtime.lastError ? (failMsg && n(failMsg + " / retry failed"), finish({ ok: false, error: chrome.runtime.lastError.message || "retry_failed" })) : finish(expectResponse ? (res2 || { ok: true }) : { ok: true });
                  });
                });
              } catch (err) {
                failMsg && n(failMsg);
                finish({ ok: false, error: String(err && err.message || err) });
              }
            });
          };
          send();
        });
      }
      function teAutoPyTransportRecoverable(error) {
        return /^(content_timeout|content_retry_timeout|script_not_ready|retry_failed|receiving end does not exist|message port closed)/i.test(String(error || ""));
      }
      async function teSendToTargetTab(msg, failMsg) {
        const tabMsg = teSafeTabMessage(msg);
        const waitRequired = teAutoPyMessageWaitRequired(msg);
        for (;;) {
          const target = await teResolveTargetTabForMessage(msg);
          if (!(target && target.ok && target.tab_id)) return false;
          const res = await teSendTabMessageOnce(target.tab_id, tabMsg, false, 3000, failMsg);
          if (res && res.ok) { if (teAutoPyRunning && teAutoPyWaitCodeIsTransport(teAutoPySessionTarget.wait_code)) teAutoPyLeaveWait("Instagram 명령 연결 정상화"); return true; }
          if (!(teAutoPyRunning && waitRequired && teAutoPyTransportRecoverable(res && res.error))) return false;
          teAutoPyEnterWait("main_injection_loading", "Instagram 페이지 또는 Main Injection 로딩을 기다리는 중입니다.");
          await teSleep(1200);
        }
      }
      async function teSendToTargetTabResponse(msg, failMsg, timeoutMs = 3000) {
        const tabMsg = teSafeTabMessage(msg);
        const waitRequired = teAutoPyMessageWaitRequired(msg);
        for (;;) {
          const target = await teResolveTargetTabForMessage(msg);
          if (!(target && target.ok && target.tab_id)) return { ok: false, error: target && target.error || "no_instagram_tab" };
          const res = await teSendTabMessageOnce(target.tab_id, tabMsg, true, timeoutMs, failMsg);
          if (res && (res.ok || !teAutoPyTransportRecoverable(res.error))) { if (teAutoPyRunning && res.ok && teAutoPyWaitCodeIsTransport(teAutoPySessionTarget.wait_code)) teAutoPyLeaveWait("Instagram 응답 정상화"); return res || { ok: false, error: "empty_response" }; }
          if (!(teAutoPyRunning && waitRequired && teAutoPyTransportRecoverable(res && res.error))) return res || { ok: false, error: "empty_response" };
          teAutoPyEnterWait("main_injection_loading", "Instagram 페이지 또는 Main Injection 응답을 기다리는 중입니다.");
          await teSleep(1200);
        }
      }
      // IGblue local patch v10: prevent accidental duplicate download commands from rapid double click,
      // touch/click duplication, or multiple UI instances calling the same URL immediately.
      const teClickLocks = new Map();
      const teDownloadLocks = new Map();
      function teLockOnce(key, ms = 1200) {
        const now = Date.now();
        const until = Number(teClickLocks.get(key) || 0);
        if (until > now) return !1;
        teClickLocks.set(key, now + ms);
        setTimeout(() => {
          if (Number(teClickLocks.get(key) || 0) <= Date.now()) teClickLocks.delete(key);
        }, ms + 80);
        return !0;
      }
      function teRunButtonOnce(buttonId, key, ms, fn) {
        const btn = document.getElementById(buttonId);
        if (!teLockOnce(key, ms)) return void n("Ignored duplicate click");
        if (btn) btn.disabled = !0;
        try {
          return fn();
        } finally {
          setTimeout(() => {
            if (btn) btn.disabled = !1;
            P();
          }, ms);
        }
      }
      function teDownloadOnce(url, prefix, sender) {
        dbg("download_once", "entry", 58530, {prefix, url:String(url||"").slice(0,220)});
        const key = `${String(url || "")}@@${String(prefix || "")}`;
        const now = Date.now();
        const until = Number(teDownloadLocks.get(key) || 0);
        if (until > now) return dbg("download_once", "duplicate_lock", 58534, {prefix, url:String(url||"").slice(0,220), until, now}), n(`Ignored duplicate download: ${prefix || "media"}`), Promise.resolve(!1);
        teDownloadLocks.set(key, now + 1800);
        setTimeout(() => {
          if (Number(teDownloadLocks.get(key) || 0) <= Date.now()) teDownloadLocks.delete(key);
        }, 1900);
        return sender();
      }
      // IGblue local patch v16/v20: Chrome down API target folder helpers.
      function tePathPartsFromUrl(e) {
        try {
          const t = new URL(String(e || ""));
          if (!teIsInstagramUrl(t.href)) return [];
          return (t.pathname || "/").split("/").filter(Boolean);
        } catch { return []; }
      }
      function teUrlMode(e) {
        const t = tePathPartsFromUrl(e), r = String(t[0] || "home").toLowerCase();
        return r === "explore" ? "explore" : r === "p" ? "p" : r === "reel" ? "reel" : r === "reels" ? "reels" : r === "stories" ? "stories" : "profile";
      }
      function teAccountFromUrl(e) {
        const t = tePathPartsFromUrl(e), r = String(t[0] || "home").toLowerCase();
        if (!t.length) return "home";
        // /p/SHORTCODE and /reel/SHORTCODE do not have a channel in the URL, so keep a stable path folder.
        if ((r === "p" || r === "reel") && t[1]) return `${r}/${t[1]}`;
        if (r === "stories" && t[1]) return `stories/${t[1]}`;
        if (r === "explore") return "explore";
        return t[0] || "home";
      }
      function teSafePath(e) {
        const t = String(e || "instagram").replace(/\\+/g, "/").split("/").map((e) => teSafeName(e)).filter(Boolean);
        return t.length ? t.join("/") : "instagram";
      }
      function teIsPlaceholderAccountName(value) {
        const v = String(value || "").replace(/^@/, "").trim().toLowerCase();
        return !v || ["instagram", "explore", "explorer", "reel", "reels", "p", "home", "ig", "unknown", "undefined", "null", "none"].includes(v);
      }
      function teCleanOwnerAccountName(value) {
        const raw = String(value || "").replace(/^@/, "").trim();
        const safe = teSafeName(raw);
        return teIsPlaceholderAccountName(safe) ? "" : safe;
      }
      function teMediaOwnerName(e) {
        const m = e || {};
        const choices = [
          m?.owner_username,
          m?.username,
          m?.user?.username,
          m?.owner?.username,
          m?.caption?.user?.username,
          m?.media?.owner_username,
          m?.media?.username,
          m?.media?.user?.username,
          m?.media?.owner?.username,
          m?.media?.caption?.user?.username
        ];
        for (const c of choices) {
          const owner = teCleanOwnerAccountName(c);
          if (owner) return owner;
        }
        return "";
      }
      function teCurrentTargetUrl() {
        const e = document.getElementById("igblue_target_url");
        return e && e.title && teIsInstagramUrl(e.title) ? e.title : "";
      }
      function teAutoPyHashStartNoFromUrl(url) {
        try {
          const u = new URL(String(url || teCurrentTargetUrl() || ""));
          const raw = String(u.hash || "").replace(/^#/, "").trim();
          const m = raw.match(/^No\s*0*(\d{1,6})$/i);
          const n = m ? Number(m[1]) : 0;
          return Number.isFinite(n) && n > 0 ? n : 0;
        } catch (_) {
          return 0;
        }
      }
      function teAutoPyActiveHashNo(state) {
        try {
          const direct = Number(state && (state.active_hash_no || state.hash_rank_no) || 0) || 0;
          if (direct > 0) return direct;
          return teAutoPyHashStartNoFromUrl(state && state.href || teCurrentTargetUrl() || "");
        } catch (_) { return 0; }
      }
      function teAutoPyLastRankKey() { return "download/auto_py_last_rank_no"; }
      function teAutoPySetLastRank(no, reason) {
        const nNo = Math.max(0, Number(no || 0) || 0);
        try { teLocalSet(teAutoPyLastRankKey(), String(nNo)); } catch (_) {}
        try { teLocalSet("auto_py_last_rank_no", String(nNo)); } catch (_) {}
        const el = document.getElementById("igblue_auto_py_last_rank");
        if (el && String(el.value || "") !== String(nNo)) el.value = String(nNo);
        try { chrome.storage && chrome.storage.local && chrome.storage.local.set({ "IGblue/download/auto_py_last_rank_no": nNo, "IGblue/auto_py_last_rank_no": nNo, "IGblue/auto_py_last_rank_reason": String(reason || "") }); } catch (_) {}
        return nNo;
      }
      function teAutoPyGetLastRank() {
        const el = document.getElementById("igblue_auto_py_last_rank");
        const raw = el && String(el.value || "").trim() ? el.value : teLocalGetAny([teAutoPyLastRankKey(), "auto_py_last_rank_no"], "0");
        const nNo = Math.max(0, Number(raw || 0) || 0);
        return Number.isFinite(nNo) ? nNo : 0;
      }
      function teAutoPyReadLastRankTextValue() {
        const el = document.getElementById("igblue_auto_py_last_rank");
        const raw = el ? String(el.value || "").trim() : "";
        if (!raw) return 0;
        const nNo = Math.max(0, Number(raw) || 0);
        return Number.isFinite(nNo) ? nNo : 0;
      }
      function teAutoPyLoadedUiCount() {
        const nNo = Math.max(0, Number(teGetTopMetricCountValue && teGetTopMetricCountValue() || 0) || 0);
        return Number.isFinite(nNo) ? nNo : 0;
      }
      function teAutoPyAnnounce(message, detail) {
        const msg = String(message || "").trim();
        if (!msg) return;
        try { n(msg); } catch (_) {}
        try {
          const el = document.getElementById("down_progress");
          if (el) el.textContent = msg + (detail ? "\n" + String(detail) : "");
        } catch (_) {}
      }
      function teAutoPyCountsSnapshot(requestedNo, state) {
        try { teRefreshRanksAndMetricCount(); } catch (_) {}
        const counts = O();
        const metricCount = Math.max(0, Number(teAutoPyLoadedUiCount() || 0) || 0);
        const allCount = Number(counts && counts.all || 0) || 0;
        const pageFilteredCount = Math.max(0, Number(counts && counts.filtered || 0) || 0);
        const metricSourceLen = teAutoPyFilteredSourceList().length;
        const topSortedCount = teAutoPyTopSortedReadyCount();
        const domLoadedMax = Number(state && state.loaded_max_no || 0) || 0;
        return {
          requested_no: Number(requestedNo || 0) || 0,
          all_count: allCount,
          filtered_count: pageFilteredCount,
          metric_count: metricCount,
          metric_source_count: metricSourceLen,
          top_sorted_count: topSortedCount,
          ready_count: topSortedCount,
          loaded_max_no: topSortedCount,
          dom_loaded_max_no: domLoadedMax,
          loaded_count: Number(state && state.loaded_count || 0) || 0,
          current_rank_no: Number(state && state.current_rank_no || 0) || 0,
          active_rank_no: Number(state && (state.active_rank_no || state.current_rank_no) || 0) || 0,
          active_hash_no: teAutoPyActiveHashNo(state),
          loaded_ready_no: topSortedCount,
          has_requested: topSortedCount >= (Number(requestedNo || 0) || 0) && Number(requestedNo || 0) > 0,
          dom_has_requested: !!(state && state.has_requested),
          at_bottom: !!(state && state.at_bottom),
          scroll_y: Number(state && state.scroll_y || 0) || 0,
          scroll_height: Number(state && state.scroll_height || 0) || 0,
          inner_height: Number(state && state.inner_height || 0) || 0,
          href: String(state && state.href || teCurrentTargetUrl() || "")
        };
      }
      function teAutoPyMetricFilterActive() {
        try { return !!(teTopMetricField && teTopMetricField()) && Number(teMetricThresholdValue && teMetricThresholdValue() || 0) > 0; } catch (_) { return false; }
      }
      function teAutoPyAnyFilterActive() {
        return !!(teAutoPyDateFilterActive() || teAutoPyMetricFilterActive() || teSecondaryMetricFiltersActive());
      }
      function teAutoPyFilteredSourceList() {
        try { teRefreshRanksAndMetricCount(); } catch (_) {}
        const base = Array.isArray(teMetricSourceList && teMetricSourceList()) ? teMetricSourceList().filter(Boolean) : [];
        const primaryActive = teAutoPyMetricFilterActive();
        const field = teTopMetricField();
        const threshold = Number(teMetricThresholdValue && teMetricThresholdValue() || 0) || 0;
        return base.filter((media) => teMediaPassesTopConditions(media, primaryActive ? field : "", primaryActive ? threshold : 0));
      }
      function teAutoPyTopSortedReadyCount() {
        try { teRefreshRanksAndMetricCount(); } catch (_) {}
        // v170 inherited v169 절대 원칙:
        // Auto PY 시작 가능 여부는 Top sorted medias 구역의 top_metric_count 값만 사용한다.
        // page_info_filtered, page_info_all, DOM max, URL hash, source length는 다른 구역/참고값이므로
        // 최종 다운로드 지점 확보 판단에 섞지 않는다.
        const metricUi = Math.max(0, Number(teAutoPyLoadedUiCount && teAutoPyLoadedUiCount() || 0) || 0);
        return Number.isFinite(metricUi) ? metricUi : 0;
      }
      function teAutoPyFilteredSortedTopN(limitNo) {
        try { teRefreshRanksAndMetricCount(); } catch (_) {}
        const raw = teAutoPyFilteredSourceList();
        const t = Math.max(0, Number(limitNo || raw.length || 0) || 0);
        const field = G[Z] || "";
        let arr = teSortCandidateList(raw, field);
        return t > 0 ? arr.slice(0, Math.min(t, arr.length)) : arr;
      }
      function teAutoPyBuildDynamicRankPlan(requestedNo) {
        const reqNo = Math.max(1, Number(requestedNo || 0) || 1);
        try { teRefreshRanksAndMetricCount(); } catch (_) {}
        const topSortedCount = teAutoPyTopSortedReadyCount();
        const list = teAutoPyFilteredSortedTopN(Math.max(reqNo, topSortedCount));
        const ctx = teBuildRankContext(list);
        const labels = teBuildMainRankLabels(list, ctx);
        try { teScheduleMainRankLabels(list, ctx); } catch (_) {}
        const counts = O();
        const dateFilterActive = teAutoPyDateFilterActive();
        const metricFilterActive = teAutoPyMetricFilterActive();
        const engagementFilterActive = teSecondaryMetricFiltersActive();
        const filterActive = !!(dateFilterActive || metricFilterActive || engagementFilterActive);
        return {
          requested_no: reqNo,
          list,
          ctx,
          labels,
          max_rank_no: Math.max(labels.length, topSortedCount),
          all_count: Number(counts && counts.all || 0) || 0,
          filtered_count: Number(counts && counts.filtered || 0) || 0,
          metric_count: Number(teAutoPyLoadedUiCount() || 0) || 0,
          top_sorted_count: topSortedCount,
          ready_count: topSortedCount,
          date_filter_active: dateFilterActive,
          metric_filter_active: metricFilterActive,
          engagement_filter_active: engagementFilterActive,
          filter_active: filterActive,
          filter_result_count: list.length,
          filter_result_empty: !!(filterActive && list.length === 0)
        };
      }
      function teAutoPyDateRangeLoadSnapshot() {
        const threshold = Math.max(0, Number(K || 0) || 0);
        const active = threshold > 0;
        const map = new Map();
        const list = Array.isArray(se) ? se.filter(Boolean) : [];
        list.forEach((media, index) => {
          const code = String(media && (media.code || media.shortcode || media.id || media.pk) || "").trim();
          const stamp = Number(media && (media.created_at || media.taken_at || media.caption && media.caption.created_at) || 0) || 0;
          const key = code || `${stamp}:${String(media && (media.url || media.display_url || media.thumbnail_src) || "").slice(0, 160)}:${index}`;
          map.set(key, stamp);
        });
        return { active, threshold, map, loaded_count: list.length };
      }
      function teAutoPyDateRangeDelta(before) {
        const prev = before && before.map instanceof Map ? before.map : new Map();
        const now = teAutoPyDateRangeLoadSnapshot();
        let newKnown = 0, newValid = 0, newOutOfRange = 0, newUnknown = 0;
        now.map.forEach((stamp, key) => {
          if (prev.has(key)) return;
          if (!(stamp > 0)) newUnknown += 1;
          else if (stamp > now.threshold) { newKnown += 1; newValid += 1; }
          else { newKnown += 1; newOutOfRange += 1; }
        });
        return Object.assign({}, now, {
          new_count: Math.max(0, now.map.size - prev.size),
          new_known_count: newKnown,
          new_valid_count: newValid,
          new_out_of_range_count: newOutOfRange,
          new_unknown_count: newUnknown,
          exhausted_batch: !!(now.active && newKnown > 0 && newValid === 0 && newOutOfRange === newKnown)
        });
      }
      function teAutoPyEstimateSwipeNeed(requestedNo, snap) {
        const reqNo = Math.max(1, Number(requestedNo || 1) || 1);
        const allCount = Math.max(1, Number(snap && snap.all_count || 0) || 0);
        const topSortedCount = Math.max(0, Number(snap && (snap.top_sorted_count || snap.ready_count || snap.metric_count) || 0) || 0);
        if (topSortedCount >= reqNo) return 0;
        if (topSortedCount <= 0) return Math.min(10, Math.max(3, Math.ceil(reqNo / 2)));
        const rate = Math.max(0.01, Math.min(1, topSortedCount / allCount));
        const needAll = Math.ceil(reqNo / rate);
        const gapAll = Math.max(0, needAll - allCount);
        return Math.max(1, Math.ceil(gapAll / 20));
      }
      async function teAutoPyWaitLoadingStable(requestedNo, mySeq, base, timeoutMs) {
        const reqNo = Math.max(1, Number(requestedNo || 1) || 1);
        const endAt = Date.now() + Math.max(2600, Number(timeoutMs || 7000) || 7000);
        let lastSig = "";
        let stableTicks = 0;
        let lastSnap = null;
        while (Date.now() < endAt) {
          if (mySeq && mySeq !== teAutoPyRunSeq) return { ok: false, error: "run_seq_changed", snapshot: lastSnap };
          await teSleep(650);
          const plan = teAutoPyBuildDynamicRankPlan(reqNo);
          const state = await teAutoPyReadLoadedRankState(180, reqNo);
          const snap = teAutoPyCountsSnapshot(reqNo, state);
          lastSnap = Object.assign({}, snap, { max_rank_no: plan.max_rank_no, top_sorted_count: snap.top_sorted_count, dom_loaded_max_no: snap.dom_loaded_max_no });
          const ready = Number(snap.top_sorted_count || snap.ready_count || 0) >= reqNo;
          if (ready) return { ok: true, ready: true, snapshot: lastSnap, plan };
          const sig = [snap.all_count, snap.filtered_count, snap.metric_count, snap.top_sorted_count, snap.dom_loaded_max_no, snap.scroll_y, snap.scroll_height].join("/");
          if (sig === lastSig) stableTicks += 1;
          else stableTicks = 0;
          lastSig = sig;
          if (stableTicks >= 3) return { ok: true, stable: true, snapshot: lastSnap, plan };
        }
        return { ok: true, timeout: true, snapshot: lastSnap, plan: teAutoPyBuildDynamicRankPlan(reqNo) };
      }
      async function teAutoPyEnsureDownloadPointLoaded(requestedNo, mySeq, reason) {
        const reqNo = Math.max(0, Number(requestedNo || 0) || 0);
        const uiTargetCount = Math.max(1, Number(teAutoPyStats && teAutoPyStats.target_count || teAutoPyTargetCount && teAutoPyTargetCount() || reqNo || 1) || 1);
        const plan0 = teAutoPyBuildDynamicRankPlan(Math.max(1, reqNo || teTopNCount() || 1));
        if (!(reqNo > 0)) return Object.assign({ ok: true, skipped: true, reason: "no_download_point" }, plan0);
        const base = { cycle_id: teBuildCycleId("LOAD_POINT"), run_seq: Number(mySeq || 0), rank_no: reqNo, rank_label: teAutoPyNoLabel(reqNo), requested_rank_no: reqNo, start_source: String(reason || "auto_start_load_first"), build_version: IGBLUE_BUILD_VERSION, build_tag: IGBLUE_BUILD_TAG };
        await teAutoPyCycle("S00_LOAD_FIRST_START", Object.assign({}, base, { decision: "load_for_direct_download", action: "ensure_download_point_loaded", reason: "direct_download_requires_media_data" }));
        teAutoPyAnnounce(`${teAutoPyNoLabel(reqNo)} 이상 직접 다운로드 데이터를 확인합니다.`);
        let totalSwipe = 0;
        let firstSwipeDone = false;
        let noGrowth = 0;
        let previousSig = "";
        let estimatedMax = 12;
        let consecutiveDateExhausted = 0;
        for (let attempt = 1; attempt <= 80; attempt++) {
          if (mySeq && mySeq !== teAutoPyRunSeq) return { ok: false, error: "run_seq_changed" };
          teAutoPySetUiPhase("searching", { target_count: uiTargetCount, completed_count: teAutoPyCompletedPostCount(), message: `목표 ${reqNo}개 탐색` });
          const plan = teAutoPyBuildDynamicRankPlan(reqNo);
          const state = await teAutoPyReadLoadedRankState(240, reqNo);
          const snap = teAutoPyCountsSnapshot(reqNo, state);
          const effectiveLoaded = Number(snap.top_sorted_count || snap.ready_count || 0) || 0;
          const ready = effectiveLoaded >= reqNo;
          const estimate = teAutoPyEstimateSwipeNeed(reqNo, snap);
          if (attempt === 1) estimatedMax = Math.min(20, Math.max(8, estimate + 4));
          await teAutoPyCycle("S00_LOAD_FIRST_CHECK", Object.assign({}, base, snap, { decision: ready ? "download_point_ready" : "download_point_not_ready", action: "check_top_sorted_after_origin", reason: ready ? "top_sorted_target_loaded" : "top_sorted_target_shortage", attempt, active_hash_no: snap.active_hash_no, loaded_ready_no: effectiveLoaded, effective_loaded_max_no: effectiveLoaded, top_sorted_count: effectiveLoaded, dom_loaded_max_no: snap.dom_loaded_max_no, estimated_swipe_need: estimate, total_swipe_count: totalSwipe, max_swipe_count: estimatedMax }));
          if (ready) {
            teAutoPyAnnounce(`최종 다운로드 지점 ${teAutoPyNoLabel(reqNo)} 이상으로 로딩 완료하였습니다.`, `All medias ${snap.all_count} / Top sorted ${effectiveLoaded}`);
            await teAutoPyCycle("S00_LOAD_FIRST_READY", Object.assign({}, base, snap, { decision: "ready", action: "start_download_next", reason: "top_sorted_loaded_after_origin", active_hash_no: snap.active_hash_no, loaded_ready_no: effectiveLoaded, effective_loaded_max_no: effectiveLoaded, top_sorted_count: effectiveLoaded, dom_loaded_max_no: snap.dom_loaded_max_no }));
            return Object.assign({ ok: true, snapshot: snap }, plan, { max_rank_no: Math.max(plan.max_rank_no, effectiveLoaded) });
          }
          const sig = [snap.top_sorted_count, snap.all_count, snap.scroll_y, snap.scroll_height, snap.at_bottom ? 1 : 0].join("/");
          noGrowth = sig === previousSig ? noGrowth + 1 : 0;
          previousSig = sig;
          // v170 inherited v169: top_metric_count가 목표보다 작으면 고정 slow-swipe 횟수 때문에 중단하지 않는다.
          // 오직 top_metric_count가 더 이상 증가하지 않고, 페이지 하단으로 판단될 때만 현재 주소 끝 도달로 본다.
          const endReached = !!(snap.at_bottom && noGrowth >= 4 && attempt > 4);
          if (endReached) {
            const endReason = "current_url_end_reached";
            const partialPlan = teAutoPyBuildDynamicRankPlan(reqNo);
            const partialCount = Math.max(0, Number(partialPlan.filter_result_count || partialPlan.list && partialPlan.list.length || 0) || 0);
            const hasPartial = partialCount > 0;
            const msg = hasPartial ? "게시물 처음까지 확인했습니다. 있는 대상만 다운로드합니다." : "게시물 처음까지 확인했지만 대상이 없습니다.";
            teAutoPySetUiPhase("searching", { target_count: uiTargetCount, exhausted: true, exhausted_reason: endReason, message: "더이상없음" });
            teAutoPyAnnounce(msg, `현재 All medias ${snap.all_count} / Top sorted ${effectiveLoaded} / 목표 ${teAutoPyNoLabel(reqNo)}`);
            await teAutoPyCycle("S00_LOAD_FIRST_END_REACHED", Object.assign({}, base, snap, partialPlan, { decision: hasPartial ? "ready_partial" : "filter_result_empty", action: hasPartial ? "process_loaded_media" : "skip_current_account_or_finish", reason: endReason, attempt, active_hash_no: snap.active_hash_no, loaded_ready_no: effectiveLoaded, top_sorted_count: effectiveLoaded, dom_loaded_max_no: snap.dom_loaded_max_no, total_swipe_count: totalSwipe, max_swipe_count: estimatedMax, no_growth_count: noGrowth }));
            return Object.assign({ ok: hasPartial, partial: hasPartial, exhausted: true, reason: endReason, error: hasPartial ? "" : endReason, snapshot: snap }, partialPlan);
          }
          const dateBeforeSwipe = teAutoPyDateRangeLoadSnapshot();
          const swipeCount = firstSwipeDone ? 1 : Math.max(1, Math.min(10, estimate || 10));
          firstSwipeDone = true;
          totalSwipe += swipeCount;
          const action = swipeCount >= 10 ? "swipe_10" : "swipe_down";
          teAutoPySetUiPhase("swipe", { target_count: uiTargetCount, completed_count: teAutoPyCompletedPostCount(), message: swipeCount >= 10 ? "Swipe 10회" : "Swipe 1회" });
          teAutoPyAnnounce(swipeCount >= 10 ? "Swipe 10회를 천천히 실행합니다." : "Swipe down 1회를 실행합니다.", `로딩 대기 중입니다. All medias ${snap.all_count} / Top sorted ${effectiveLoaded}`);
          await teAutoPyCycle("S00_LOAD_FIRST_SWIPE", Object.assign({}, base, snap, { decision: "load_more", action, reason: "top_sorted_target_shortage", attempt, active_hash_no: snap.active_hash_no, loaded_ready_no: effectiveLoaded, top_sorted_count: effectiveLoaded, dom_loaded_max_no: snap.dom_loaded_max_no, swipe_count: swipeCount, total_swipe_count: totalSwipe, estimated_swipe_need: estimate }));
          await teAutoPySwipeLoad(swipeCount, "download_point_load_first");
          teAutoPySetUiPhase("loading", { target_count: uiTargetCount, completed_count: teAutoPyCompletedPostCount(), message: "Swipe 후 로딩 대기" });
          teAutoPyAnnounce("로딩 대기중입니다...");
          const waitRes = await teAutoPyWaitLoadingStable(reqNo, mySeq, base, swipeCount >= 10 ? 9500 : 6200);
          await teAutoPyCycle("S00_LOAD_FIRST_WAIT_DONE", Object.assign({}, base, { decision: waitRes && waitRes.ready ? "loaded" : "stable_or_wait_done", action: "wait_loading_stable", reason: waitRes && waitRes.ready ? "loading_completed_target_ready" : waitRes && waitRes.stable ? "loading_stable" : waitRes && waitRes.timeout ? "loading_wait_timeout" : String(waitRes && waitRes.error || "wait_done"), attempt, active_hash_no: waitRes && waitRes.snapshot && waitRes.snapshot.active_hash_no, loaded_ready_no: waitRes && waitRes.snapshot && waitRes.snapshot.loaded_ready_no, top_sorted_count: waitRes && waitRes.snapshot && waitRes.snapshot.top_sorted_count, dom_loaded_max_no: waitRes && waitRes.snapshot && waitRes.snapshot.dom_loaded_max_no, wait_result: waitRes || null }));
          const dateDelta = teAutoPyDateRangeDelta(dateBeforeSwipe);
          consecutiveDateExhausted = dateDelta.exhausted_batch ? consecutiveDateExhausted + 1 : (dateDelta.new_valid_count > 0 ? 0 : consecutiveDateExhausted);
          await teAutoPyCycle("S00_LOAD_FIRST_DATE_RANGE_CHECK", Object.assign({}, base, {
            decision: dateDelta.exhausted_batch ? "date_range_out_of_range" : "date_range_continue",
            action: "check_new_media_date_range",
            reason: dateDelta.exhausted_batch ? "new_media_all_older_than_filter" : "valid_or_unknown_date_media_present",
            attempt,
            date_filter_active: dateDelta.active,
            date_filter_threshold: dateDelta.threshold,
            new_media_count: dateDelta.new_count,
            new_known_date_count: dateDelta.new_known_count,
            new_valid_date_count: dateDelta.new_valid_count,
            new_out_of_range_count: dateDelta.new_out_of_range_count,
            new_unknown_date_count: dateDelta.new_unknown_count,
            consecutive_out_of_range_swipes: consecutiveDateExhausted
          }));
          if (dateDelta.active && consecutiveDateExhausted >= 1) {
            const partialPlan = teAutoPyBuildDynamicRankPlan(reqNo);
            const emptyFiltered = !!partialPlan.filter_result_empty;
            teAutoPyAnnounce(
              "날짜 필터 범위의 끝에 도달했습니다.",
              emptyFiltered ? "필터 결과 0개: 현재 URL을 종료하고 다음 URL을 확인합니다." : `필터 결과 ${partialPlan.filter_result_count || partialPlan.filtered_count || 0}개만 처리합니다.`
            );
            await teAutoPyCycle("S00_LOAD_FIRST_DATE_RANGE_END", Object.assign({}, base, partialPlan, {
              decision: emptyFiltered ? "filter_result_empty" : "ready_partial",
              action: emptyFiltered ? "skip_current_account_or_finish" : "process_loaded_filtered_media",
              reason: emptyFiltered ? "date_filter_range_exhausted_empty" : "date_filter_range_exhausted",
              requested_rank_no: reqNo,
              loaded_ready_no: partialPlan.top_sorted_count || 0,
              consecutive_out_of_range_swipes: consecutiveDateExhausted,
              date_range_result: dateDelta
            }));
            teAutoPySetUiPhase("searching", { target_count: uiTargetCount, exhausted: true, exhausted_reason: emptyFiltered ? "date_filter_range_exhausted_empty" : "date_filter_range_exhausted", message: "기간 내 더이상없음" });
            return Object.assign({
              ok: true,
              partial: true,
              exhausted: true,
              reason: emptyFiltered ? "date_filter_range_exhausted_empty" : "date_filter_range_exhausted",
              date_range_result: dateDelta
            }, partialPlan);
          }
        }
        const finalPlan = teAutoPyBuildDynamicRankPlan(reqNo);
        teAutoPyAnnounce("목표 No까지 아직 부족하지만 반복 안전 횟수에 도달했습니다.", `Top sorted ${finalPlan.top_sorted_count || 0} / 목표 ${teAutoPyNoLabel(reqNo)}`);
        await teAutoPyCycle("S00_LOAD_FIRST_ATTEMPTS_EXCEEDED", Object.assign({}, base, finalPlan, { decision: "not_ready", action: "stop_or_next_account", reason: "download_point_load_attempts_exceeded", requested_rank_no: reqNo, loaded_ready_no: finalPlan.top_sorted_count || 0, top_sorted_count: finalPlan.top_sorted_count || 0, total_swipe_count: totalSwipe }));
        return Object.assign({ ok: false, error: "download_point_load_attempts_exceeded" }, finalPlan);
      }
      function teAutoPyResolveStartNo() {
        const textNo = teAutoPyReadLastRankTextValue();
        if (textNo > 0) return { no: textNo, source: "last_rank_textbox" };
        const hashNo = teAutoPyHashStartNoFromUrl(teCurrentTargetUrl());
        if (hashNo > 0) return { no: hashNo, source: "url_hash" };
        const savedNo = teAutoPyGetLastRank();
        if (savedNo > 0) return { no: savedNo, source: "saved_last_rank" };
        return { no: 0, source: "default_current" };
      }
      function teInitAutoPyLastRankUi() {
        const el = document.getElementById("igblue_auto_py_last_rank");
        if (!el) return;
        const markEdited = () => { el.__igblueUserEdited = true; };
        const apply = () => { markEdited(); teAutoPySetLastRank(el.value, "manual_edit"); };
        el.value = String(teAutoPyGetLastRank());
        try { chrome.storage && chrome.storage.local && chrome.storage.local.get(["IGblue/download/auto_py_last_rank_no", "IGblue/auto_py_last_rank_no"], (got) => {
          const saved = got && (got["IGblue/download/auto_py_last_rank_no"] ?? got["IGblue/auto_py_last_rank_no"]);
          const nNo = Math.max(0, Number(saved || 0) || 0);
          const curNo = teAutoPyReadLastRankTextValue();
          if (nNo && !el.__igblueUserEdited && document.getElementById("igblue_auto_py_last_rank") === el && !curNo) el.value = String(nNo);
        }); } catch (_) {}
        el.addEventListener("input", () => { markEdited(); clearTimeout(el.__igblueRankTimer); el.__igblueRankTimer = setTimeout(apply, 250); });
        el.addEventListener("change", apply);
        el.addEventListener("blur", apply);
        el.addEventListener("keydown", (ev) => { if (ev.key === "Enter") { ev.preventDefault(); apply(); } });
      }
      async function teAutoPyReadLoadedRankState(waitMs = 120, requestedNo = 0) {
        try {
          return await teSendToTargetTabResponse({ type: "igblue_auto_py_loaded_rank_state", wait_ms: waitMs, requested_no: Number(requestedNo || 0) || 0 }, "", 1600);
        } catch (err) {
          return { ok: false, error: String(err && err.message || err) };
        }
      }
      async function teAutoPySwipeLoad(count, reason) {
        const c = Math.max(1, Math.min(10, Number(count || 1) || 1));
        try {
          await teSendToTargetTab({ type: "ndy_swipe", count: c }, "Instagram tab not found");
        } catch (_) {}
        n(c >= 10 ? "No 로딩 대기: Swipe 10" : "No 로딩 대기: Swipe down");
        await teSleep(900 + Math.min(2200, c * 120));
        return { ok: true, count: c, reason: String(reason || "preload_scroll") };
      }
      function teAutoPyRankNoOfMediaInList(media, list, ctx) {
        const m = media && typeof media === "object" ? media : null;
        if (!m) return 0;
        const code = String(m.code || m.shortcode || "").trim();
        const c = ctx && typeof ctx === "object" ? ctx : null;
        try {
          if (code && c && c.active && c.map instanceof Map && c.map.has(code)) return Number(c.map.get(code)) || 0;
        } catch (_) {}
        const explicit = Number(m.rank_no || m.rank || m.no || m.file_no || 0) || 0;
        if (explicit > 0) return explicit;
        const label = String(m.rank_label || m.no_text || m.file_no_text || "").trim();
        const mm = label.match(/No\s*0*(\d+)/i);
        if (mm) return Number(mm[1]) || 0;
        const arr = Array.isArray(list) ? list : [];
        if (arr.length) {
          const idx = arr.findIndex((x) => x === m || (!!code && String(x && (x.code || x.shortcode) || "").trim() === code));
          if (idx >= 0) return idx + 1;
        }
        return 0;
      }
      function teAutoPyFindMediaByRankNo(rankNo, list, ctx) {
        const no = Number(rankNo || 0) || 0;
        if (!(no > 0)) return null;
        const arr = Array.isArray(list) && list.length ? list.filter(Boolean) : teMetricSourceList().filter(Boolean);
        const c = ctx && typeof ctx === "object" ? ctx : teBuildRankContext(arr);
        try {
          if (c && c.active && c.map instanceof Map) {
            for (const m of arr) {
              const code = String(m && (m.code || m.shortcode) || "").trim();
              if (code && Number(c.map.get(code) || 0) === no) return m;
            }
          }
        } catch (_) {}
        for (let i = 0; i < arr.length; i++) {
          const gotNo = teAutoPyRankNoOfMediaInList(arr[i], arr, c);
          if (gotNo === no) return arr[i];
        }
        return arr[Math.max(0, Math.min(arr.length - 1, no - 1))] || null;
      }
      function teAutoPyCloneSnapshotMedia(media, no, snapshotId) {
        const m = media && typeof media === "object" ? media : {};
        const code = String(m.code || m.shortcode || "").trim();
        const rank = Number(no || 0) || 0;
        const label = rank > 0 ? teAutoPyNoLabel(rank) : "";
        return Object.assign({}, m, {
          rank_no: rank,
          rank: rank,
          rank_label: label,
          no_text: label,
          __igblue_auto_snapshot_no: rank,
          __igblue_auto_snapshot_label: label,
          __igblue_auto_snapshot_shortcode: code,
          __igblue_auto_snapshot_id: String(snapshotId || ""),
          __igblue_auto_snapshot_key: rank > 0 && code ? (label + "|" + code) : ""
        });
      }
      function teAutoPyBuildNoSnapshotIndex(list, ctx, reason) {
        let arr = Array.isArray(list) ? list.filter(Boolean) : [];
        const c = ctx && typeof ctx === "object" ? ctx : teBuildRankContext(arr);
        const byNo = new Map(), byNoCode = new Map(), byCode = new Map(), entries = [];
        const snapshotId = [IGBLUE_BUILD_TAG, Date.now(), String(reason || "auto"), String(G[Z] || ""), String(teMetricThresholdValue())].join("|");
        for (let i = 0; i < arr.length; i++) {
          const raw = arr[i];
          const code = String(raw && (raw.code || raw.shortcode) || "").trim();
          let no = 0;
          try {
            no = c && c.active && c.map instanceof Map && code ? Number(c.map.get(code) || 0) || 0 : 0;
          } catch (_) { no = 0; }
          if (c && c.active && !(no > 0)) continue;
          if (!(no > 0)) no = i + 1;
          if (!(no > 0) || !code) continue;
          const media = teAutoPyCloneSnapshotMedia(raw, no, snapshotId);
          const entry = {
            no,
            rank_no: no,
            label: teAutoPyNoLabel(no),
            shortcode: code,
            code,
            key: teAutoPyNoLabel(no) + "|" + code,
            media,
            list_index: i,
            sort_field: String(G[Z] || ""),
            threshold: teMetricThresholdValue(),
            snapshot_id: snapshotId
          };
          entries.push(entry);
          if (!byNo.has(no)) byNo.set(no, entry);
          byNoCode.set(String(no) + "|" + code, entry);
          if (!byCode.has(code)) byCode.set(code, entry);
        }
        return {
          schema: "ig_auto_py_no_snapshot_index_v177",
          snapshot_id: snapshotId,
          created_at: Date.now(),
          reason: String(reason || "auto_start"),
          sort_field: String(G[Z] || ""),
          threshold: teMetricThresholdValue(),
          count: entries.length,
          max_rank_no: entries.reduce((max, e) => Math.max(max, Number(e.no || 0) || 0), 0),
          entries, byNo, byNoCode, byCode
        };
      }
      function teAutoPySnapshotEntryByNo(snapshot, rankNo) {
        const s = snapshot && typeof snapshot === "object" ? snapshot : null;
        const no = Number(rankNo || 0) || 0;
        if (!s || !(no > 0)) return null;
        try {
          if (s.byNo instanceof Map && s.byNo.has(no)) return s.byNo.get(no) || null;
        } catch (_) {}
        const arr = Array.isArray(s.entries) ? s.entries : [];
        return arr.find((e) => Number(e && e.no || 0) === no) || null;
      }
      function teAutoPySnapshotMediaByNo(snapshot, rankNo) {
        const entry = teAutoPySnapshotEntryByNo(snapshot, rankNo);
        return entry && entry.media || null;
      }
      function teAutoPySnapshotMediaSummary(snapshot, rankNo) {
        const entry = teAutoPySnapshotEntryByNo(snapshot, rankNo);
        if (!entry) return null;
        return {
          snapshot_id: String(entry.snapshot_id || snapshot && snapshot.snapshot_id || ""),
          rank_no: Number(entry.no || 0) || 0,
          rank_label: entry.label || teAutoPyNoLabel(entry.no),
          shortcode: String(entry.shortcode || ""),
          key: String(entry.key || ""),
          list_index: Number(entry.list_index || 0) || 0,
          sort_field: String(entry.sort_field || snapshot && snapshot.sort_field || ""),
          threshold: Number(entry.threshold || snapshot && snapshot.threshold || 0) || 0
        };
      }
      function teAutoPyDownloadableKindsFromCache(media) {
        const m = media && typeof media === "object" ? media : {};
        const out = [];
        if (String(m.video_url || "").trim()) out.push("mp4");
        if (String(m.img_origin || m.img_large || m.img_small || m.img_b64 || "").trim()) out.push("jpg");
        return out;
      }
      function teAutoPyCacheTargetForMedia(media, rankNo) {
        const m = media && typeof media === "object" ? media : {};
        const rank = Number(rankNo || 0) || 0;
        return {
          rank,
          code: String(m.code || m.shortcode || "").trim(),
          shortcode: String(m.code || m.shortcode || "").trim(),
          asset_kind: m.video_url ? "video" : "image",
          source: "side_cache_rank_fallback"
        };
      }
      function teAutoPySnapshotTargetForMedia(media, rankNo) {
        const m = media && typeof media === "object" ? media : {};
        const rank = Number(rankNo || m.__igblue_auto_snapshot_no || 0) || 0;
        const code = String(m.__igblue_auto_snapshot_shortcode || m.code || m.shortcode || "").trim();
        const username = String(teMediaOwnerName(m) || "").trim();
        return {
          rank,
          internal_no: rank,
          code,
          shortcode: code,
          username,
          owner_username: username,
          asset_kind: m.video_url ? "video" : (m.img_origin || m.img_large || m.img_small || m.img_b64 ? "image" : ""),
          source: "auto_snapshot_rank_target",
          ts: Date.now()
        };
      }
      function teAutoPySnapshotDirectReady(media, rankNo) {
        const m = media && typeof media === "object" ? media : {};
        const target = teAutoPySnapshotTargetForMedia(m, rankNo);
        const kinds = teAutoPyDownloadableKindsFromCache(m);
        return {
          ok: !!(target.rank > 0 && target.code && target.owner_username && kinds.length > 0),
          rank_no: target.rank,
          shortcode: target.code,
          owner_username: target.owner_username,
          downloadable_kinds: kinds,
          target
        };
      }
      function teAutoPySnapshotRecoveryCheck(target, media, rankNo) {
        const t = target && typeof target === "object" ? target : {};
        const m = media && typeof media === "object" ? media : {};
        const requestedRank = Number(rankNo || m.__igblue_auto_snapshot_no || 0) || 0;
        const snapshotRank = Number(m.__igblue_auto_snapshot_no || requestedRank || 0) || 0;
        const expectedShortcode = String(m.__igblue_auto_snapshot_shortcode || m.code || m.shortcode || "").trim();
        const activeRank = Number(t.rank || 0) || 0;
        const activeShortcode = String(t.code || t.shortcode || "").trim();
        const ownerUsername = String(teMediaOwnerName(m) || "").trim();
        const downloadableKinds = teAutoPyDownloadableKindsFromCache(m);
        const exactSnapshot = !!(requestedRank > 0 && snapshotRank === requestedRank && expectedShortcode);
        const activeExact = !!(activeRank === requestedRank && activeShortcode === expectedShortcode);
        const shortcodeConflict = !!(activeShortcode && expectedShortcode && activeShortcode !== expectedShortcode);
        const rankConflict = !!(activeRank && requestedRank && activeRank !== requestedRank && activeShortcode && activeShortcode !== expectedShortcode);
        const snapshotReady = !!(exactSnapshot && ownerUsername && downloadableKinds.length > 0);
        const ok = !!(snapshotReady && !shortcodeConflict && !rankConflict);
        return {
          ok,
          reason: ok ? (activeExact ? "main_target_exact" : "snapshot_exact_recovery_ready") : shortcodeConflict ? "active_shortcode_conflict" : rankConflict ? "active_rank_shortcode_conflict" : !exactSnapshot ? "snapshot_rank_shortcode_incomplete" : !ownerUsername ? "snapshot_owner_missing" : "snapshot_download_source_missing",
          requested_rank_no: requestedRank,
          snapshot_rank_no: snapshotRank,
          active_rank_no: activeRank,
          expected_shortcode: expectedShortcode,
          active_shortcode: activeShortcode,
          owner_username: ownerUsername,
          downloadable_kinds: downloadableKinds,
          active_exact: activeExact,
          actual_conflict: !!(shortcodeConflict || rankConflict)
        };
      }
      function teAutoPyResolveTargetForRank(target, media, rankNo) {
        const t = target && typeof target === "object" ? target : {};
        const m = media && typeof media === "object" ? media : {};
        const recovery = teAutoPySnapshotRecoveryCheck(t, m, rankNo);
        if (recovery.active_exact) {
          return { target: t, source: "main_active_target", recovered: false, recovery_check: recovery };
        }
        if (recovery.ok) {
          const snapshotTarget = teAutoPySnapshotTargetForMedia(m, recovery.requested_rank_no);
          return { target: snapshotTarget, source: "auto_snapshot_rank_target", recovered: true, previous_target: t, recovery_check: recovery };
        }
        return { target: t, source: recovery.actual_conflict ? "main_active_conflict" : "unresolved_target", recovered: false, conflict: recovery.actual_conflict, recovery_check: recovery };
      }
      async function teAutoPyDownloadMediaPairFromCache(media, rankNo, cycleBase, reason) {
        const m = media && typeof media === "object" ? media : null;
        const no = Number(rankNo || 0) || 0;
        const base = Object.assign({}, cycleBase || {}, { rank_no: no, rank_label: teAutoPyNoLabel(no), requested_rank_no: no });
        const target = teAutoPyCacheTargetForMedia(m, no);
        const shortcode = String(target.code || "").trim();
        const kinds = teAutoPyDownloadableKindsFromCache(m);
        const mediaSummary = m ? { shortcode, rank_no: no, has_video_url: !!m.video_url, has_image_url: !!(m.img_origin || m.img_large || m.img_small || m.img_b64) } : null;
        await teAutoPyCycle("S00_CACHE_LOOKUP_REQUEST", Object.assign({}, base, {
          decision: "cache_lookup",
          action: base.auto_snapshot_lookup ? "find_requested_No_in_auto_snapshot_index" : "find_requested_No_in_side_array",
          reason: String(reason || "active_no_mismatch_or_preload_failed"),
          shortcode,
          cache_candidate_found: !!m,
          auto_snapshot_lookup: !!base.auto_snapshot_lookup,
          auto_snapshot_match: !!base.auto_snapshot_match,
          auto_snapshot: base.auto_snapshot || null
        }));
        if (!m || !kinds.length || !shortcode) {
          await teAutoPyCycle("S00_CACHE_LOOKUP_MISS", Object.assign({}, base, {
            decision: "cache_miss",
            action: "fallback_to_strict_stop",
            reason: !m ? "requested_no_not_found_in_side_array" : !shortcode ? "cache_media_shortcode_missing" : "cache_media_url_missing",
            shortcode,
            cache_downloadable_kinds: kinds.join(",")
          }));
          return { ok: false, error: !m ? "cache_media_not_found" : !shortcode ? "cache_shortcode_missing" : "cache_url_missing", media_summary: mediaSummary, kinds };
        }
        const collectMeta = teBuildAutoPyCollectMeta(m, target);
        await teAutoPyCycle("S00_CACHE_LOOKUP_OK", Object.assign({}, base, {
          decision: "cache_target_found",
          action: base.auto_snapshot_lookup ? "use_auto_snapshot_download_url" : "use_side_array_download_url",
          reason: base.auto_snapshot_match ? "requested_no_found_in_auto_snapshot_cache" : "requested_no_found_in_ext_cache",
          shortcode,
          cache_downloadable_kinds: kinds.join(","),
          has_video_url: !!m.video_url,
          has_image_url: !!(m.img_origin || m.img_large || m.img_small || m.img_b64),
          filename_stem: collectMeta.filename_stem || ""
        }));
        const results = [];
        for (const kind of ["mp4", "jpg"]) {
          if (!kinds.includes(kind)) continue;
          teAutoPySetUiPhase("downloading", { current_no: no, asset_kind: kind, target_count: Math.max(1, Number(teAutoPyStats.target_count || teAutoPyTargetCount && teAutoPyTargetCount() || 1) || 1), completed_count: teAutoPyCompletedPostCount() });
          const meta = teAutoPyCollectMetaForMedia(collectMeta, m, kind);
          const url = teAutoPySourceUrlForMedia(m, collectMeta, kind);
          const prefix = teAutoPyPrefixForMedia(collectMeta, kind);
          if (!url) continue;
          if (!prefix || collectMeta.owner_missing || meta.owner_missing || !teCleanOwnerAccountName(meta.owner_username)) {
            await teAutoPyCycle("S00_CACHE_DOWNLOAD_OWNER_MISSING", Object.assign({}, base, {
              decision: "download_blocked",
              action: "owner_required_before_download",
              reason: "media_owner_username_not_found",
              media_type: kind,
              asset_kind: meta.asset_kind,
              shortcode
            }));
            return { ok: false, error: "owner_missing", media_summary: mediaSummary, kinds, results };
          }
          await teAutoPyCycle(kind === "jpg" ? "S00_CACHE_DOWNLOAD_JPG_START" : "S00_CACHE_DOWNLOAD_MP4_START", Object.assign({}, base, {
            decision: "download_from_cache",
            action: "ws8771_direct_download_url",
            reason: "cached_media_url_available",
            media_type: kind,
            asset_kind: meta.asset_kind,
            shortcode,
            file_name: prefix,
            source_url: String(url).slice(0, 260),
          }));
          const direct = await teAutoPyDirectDownloadAsset(kind, m, collectMeta, base, {
            cache_fallback: true,
            fallback_reason: String(reason || "active_no_mismatch_or_preload_failed"),
            active_dom_required: false,
            requested_rank_no: no
          });
          const ok = !!(direct && direct.ok);
          if (ok) teAutoPyStatsRecordAsset(kind, Number(direct.elapsed_ms || 0) || 0);
          results.push(Object.assign({ media_type: kind, asset_kind: meta.asset_kind, ok, file_name: prefix }, direct || {}));
          await teAutoPyCycle(kind === "jpg" ? "S00_CACHE_DOWNLOAD_JPG_OK" : "S00_CACHE_DOWNLOAD_MP4_OK", Object.assign({}, base, {
            decision: ok ? "download_ok" : "download_failed",
            action: "ws8771_direct_download_url_result",
            reason: ok ? "cache_url_download_or_already_done" : "cache_url_download_failed",
            media_type: kind,
            asset_kind: meta.asset_kind,
            shortcode,
            file_name: prefix,
            result_ok: !!ok
          }));
          if (!ok) return { ok: false, error: "cache_" + kind + "_download_failed", media_summary: mediaSummary, kinds, results };
          await teSleep(900);
        }
        const anyOk = results.some((x) => x.ok);
        const pairDelaySec = anyOk ? Math.max(2, Number(teDownloadDelaySec && teDownloadDelaySec() || 0) || 0) : 0;
        if (pairDelaySec > 0) {
          await teAutoPyCycle("S00_CACHE_DOWNLOAD_PAIR_DELAY", Object.assign({}, base, {
            decision: "wait_after_media_pair",
            action: "delay_before_next_rank",
            reason: "cache_mp4_jpg_pair_done",
            wait_sec: pairDelaySec,
            shortcode,
            results
          }));
          await teAutoPyWaitCountdown(pairDelaySec, { current_no: no, target_count: Math.max(1, Number(teAutoPyStats.target_count || teAutoPyTargetCount && teAutoPyTargetCount() || 1) || 1), completed_count: teAutoPyCompletedPostCount() });
        }
        await teAutoPyCycle("S00_CACHE_DOWNLOAD_PAIR_DONE", Object.assign({}, base, {
          decision: anyOk ? "cache_download_done" : "cache_download_empty",
          action: "complete_cached_url_downloads",
          reason: anyOk ? "requested_no_cached_url_download_done" : "no_cached_url_download_executed",
          shortcode,
          results
        }));
        if (anyOk) teAutoPySetLastRank(no, "cache_download_done");
        return { ok: anyOk, cache_download_done: anyOk, media_summary: mediaSummary, kinds, results, shortcode };
      }
      async function teAutoPyTryCacheDownloadForRank(rankNo, list, ctx, cycleBase, reason) {
        const no = Number(rankNo || 0) || 0;
        const arr = Array.isArray(list) && list.length ? list : teMetricSourceList();
        const c = ctx && typeof ctx === "object" ? ctx : teBuildRankContext(arr);
        const snap = c && (c.auto_snapshot_index || c.snapshot_index || c.snapshotIndex) || null;
        const snapEntry = teAutoPySnapshotEntryByNo(snap, no);
        const media = snapEntry && snapEntry.media ? snapEntry.media : teAutoPyFindMediaByRankNo(no, arr, c);
        const base = Object.assign({}, cycleBase || {}, {
          auto_snapshot_lookup: !!snap,
          auto_snapshot_match: !!snapEntry,
          auto_snapshot: snapEntry ? teAutoPySnapshotMediaSummary(snap, no) : null
        });
        const expectedCode = String(base.expected_shortcode || snapEntry && snapEntry.shortcode || "").trim();
        const mediaCode = String(media && (media.__igblue_auto_snapshot_shortcode || media.code || media.shortcode) || "").trim();
        if (expectedCode && mediaCode !== expectedCode) {
          await teAutoPyCycle("S00_CACHE_SHORTCODE_MISMATCH", Object.assign({}, base, {
            decision: "cache_blocked",
            action: "require_exact_No_shortcode_before_direct_download",
            reason: "cache_shortcode_mismatch",
            expected_shortcode: expectedCode,
            cache_shortcode: mediaCode
          }));
          return { ok: false, error: "cache_shortcode_mismatch", requested_rank_no: no, expected_shortcode: expectedCode, cache_shortcode: mediaCode };
        }
        return teAutoPyDownloadMediaPairFromCache(media, no, base, reason);
      }
      async function teAutoPyPreloadRequestedNo(requestedNo, maxRank, base, mySeq, sourceList, rankCtx, opts = {}) {
        const reqNo = Number(requestedNo || 0) || 0;
        if (!(reqNo > 0)) return { ok: true, skipped: true, reason: "no_requested_no" };
        const allowCacheFallback = !(opts && opts.allow_cache_fallback === false);
        const startSource = String(base && base.start_source || base && base.hash_start_source || "requested_no");
        const info = Object.assign({}, base || {}, { rank_no: reqNo, rank_label: teAutoPyNoLabel(reqNo), requested_rank_no: reqNo, max_rank_no: Math.max(Number(maxRank || 0) || 0, reqNo), loaded_ui_count: teAutoPyLoadedUiCount(), start_source: startSource });
        await teAutoPyCycle("S00_HASH_START_REQUEST", Object.assign({}, info, { decision: "start_rank_request", action: "read_start_No", reason: startSource + "_preload" }));
        for (let attempt = 1; attempt <= 8; attempt++) {
          if (mySeq && mySeq !== teAutoPyRunSeq) return { ok: false, error: "run_seq_changed" };
          try { teAutoPyBuildDynamicRankPlan(reqNo); } catch (_) {}
          const uiLoadedCountNow = teAutoPyLoadedUiCount();
          const state = await teAutoPyReadLoadedRankState(180, reqNo);
          const snap = teAutoPyCountsSnapshot(reqNo, state);
          const loadedMax = Number(state && state.loaded_max_no || 0) || 0;
          const effectiveLoadedMax = Number(snap.top_sorted_count || snap.ready_count || 0) || 0;
          const hasRequested = effectiveLoadedMax >= reqNo;
          const uiSaysLoaded = uiLoadedCountNow >= reqNo;
          await teAutoPyCycle("S00_LOAD_CHECK", Object.assign({}, info, snap, { decision: hasRequested ? "requested_loaded" : "requested_not_loaded", action: "check_top_sorted_rank", reason: hasRequested ? "top_sorted_count_enough" : "top_sorted_requested_missing", attempt, active_hash_no: teAutoPyActiveHashNo(state), loaded_ready_no: effectiveLoadedMax, loaded_max_no: effectiveLoadedMax, dom_loaded_max_no: loadedMax, effective_loaded_max_no: effectiveLoadedMax, loaded_ui_count: uiLoadedCountNow, top_sorted_count: effectiveLoadedMax, requested_rank_no: reqNo, loaded_count: Number(state && state.loaded_count || 0) || 0, current_rank_no: Number(state && state.current_rank_no || 0) || 0, state_response: state || null }));
          if (hasRequested) {
            const goRes = await teAutoPyGoNoWithConfirm(reqNo, Object.assign({}, info, { next_reason: "hash_start_preload_ready" }), 1800, "S00_PRELOAD_GO_NO");
            const read = await teAutoPyReadCurrentNoNav(700);
            const afterRank = Number(read && read.rank_no || goRes && goRes.after_rank || 0) || 0;
            const ok = afterRank === reqNo || !!(goRes && goRes.target_found && Number(goRes.after_rank || 0) === reqNo);
            await teAutoPyCycle(ok ? "S00_PRELOAD_OK" : "S00_PRELOAD_GO_RETRY", Object.assign({}, info, { decision: ok ? "preload_ok" : "preload_go_unconfirmed", action: "go_no_after_loaded", reason: ok ? "requested_no_current" : "requested_no_go_unconfirmed", attempt, loaded_max_no: loadedMax, effective_loaded_max_no: effectiveLoadedMax, loaded_ui_count: uiLoadedCountNow, active_rank_no: afterRank, go_response: goRes || null, current_read: read || null }));
            if (ok) return { ok: true, active_rank_no: afterRank, loaded_max_no: effectiveLoadedMax, go_response: goRes || null };
            if (hasRequested || uiSaysLoaded) {
              await teSleep(500);
              continue;
            }
          }
          const gap = reqNo - Math.max(effectiveLoadedMax, 0);
          const swipeCount = gap > 3 ? 10 : 1;
          await teAutoPyCycle("S00_PRELOAD_SCROLL", Object.assign({}, info, { decision: "scroll_for_requested_no", action: swipeCount >= 10 ? "swipe_10" : "swipe_down", reason: "requested_no_not_loaded", attempt, active_hash_no: teAutoPyActiveHashNo(state), loaded_ready_no: effectiveLoadedMax, loaded_max_no: loadedMax, effective_loaded_max_no: effectiveLoadedMax, loaded_ui_count: uiLoadedCountNow, requested_rank_no: reqNo, swipe_count: swipeCount }));
          await teAutoPySwipeLoad(swipeCount, "start_no_preload");
          await teAutoPyCycle("S00_PRELOAD_WAIT", Object.assign({}, info, { decision: "wait_after_scroll", action: "wait_rank_loading", reason: "scroll_requested", attempt, requested_rank_no: reqNo }));
          await teSleep(700);
        }
        const cacheRes = allowCacheFallback ? await teAutoPyTryCacheDownloadForRank(reqNo, sourceList, rankCtx, info, "requested_no_not_loaded_after_scroll") : { ok: false, skipped: true, reason: "cache_fallback_disabled" };
        if (allowCacheFallback && cacheRes && cacheRes.ok) {
          await teAutoPyCycle("S00_PRELOAD_CACHE_FALLBACK_OK", Object.assign({}, info, {
            decision: "preload_cache_download_ok",
            action: "continue_after_cache_download",
            reason: "requested_no_downloaded_from_ext_cache_after_preload_failed",
            requested_rank_no: reqNo,
            cache_result: cacheRes
          }));
          return Object.assign({ ok: true, reason: "cache_download_after_preload_failed", requested_no: reqNo }, cacheRes, { cache_download_done: true });
        }
        await teAutoPyCycle("S00_PRELOAD_FAILED", Object.assign({}, info, { decision: "preload_failed", action: "stop_before_auto_click", reason: "requested_no_not_loaded_after_scroll", requested_rank_no: reqNo, cache_result: cacheRes || null }));
        return { ok: false, error: "requested_no_not_loaded_after_scroll", requested_no: reqNo, cache_result: cacheRes || null };
      }
      function teMetricThresholdFolderName(unitsRaw) {
        const units = Number(unitsRaw);
        if (!Number.isFinite(units) || units <= 0) return "";
        const views = units * 10000;
        const trim = (v) => {
          const n = Math.round(Number(v) * 10) / 10;
          return String(Number.isInteger(n) ? Math.trunc(n) : n).replace(/\.0$/, "");
        };
        if (views >= 10000) return trim(views / 10000) + "만";
        if (views >= 1000) return trim(views / 1000) + "천";
        return String(Math.max(1, Math.round(views)));
      }
      function teCurrentMetricFolderSuggestion() { return ""; }
      function teApplyMetricFolderSuggestion() { return false; }
      function teDownloadRoot() { return "instagram"; }
      function tePersistDownloadRootNow() {
        try {
          teLocalSet("download/root_folder", "");
          teLocalSet("download_root_folder", "");
          teLocalSet("download/account_folder_merge", "0");
          chrome.storage && chrome.storage.local && chrome.storage.local.remove(["IGblue/download/root_folder", "IGblue/download_root_folder", "IGblue/download/account_folder_merge"]);
        } catch (_) {}
        return "instagram";
      }
      function teDownloadAccountFolderMerge() { return false; }
      function tePersistDownloadAccountFolderMergeNow() {
        try { teLocalSet("download/account_folder_merge", "0"); } catch (_) {}
        return false;
      }
      function teAccountFromPrefix(e) {
        const t = String(e || "").match(/^(.*)_\d{8}_\d{6}(?:_\d+)?$/);
        return t && t[1] ? teSafeName(t[1]) : "";
      }
      function teCurrentAccountName(e) {
        return teMediaOwnerName(e || {}) || teSafeName(teAccountFromUrl(teCurrentTargetUrl()) || "instagram");
      }
      function teUpdateDownloadFolderPath() {
        return `..\\_StDown\\instagram\\${teSafePath(teActiveFolderAccountName() || "instagram")}\\`;
      }
      function teInitDownloadFolderUi() { tePersistDownloadRootNow(); }

      // IGblue local patch v96: WS 통신 본체는 side_ws.js로 분리한다.
      const teAiwkSideWs = window.IGBLUE_SIDE_WS || {};
      const teAiwkSetupZipUrl = teAiwkSideWs.setupZipUrl || "http://aiwk.yjm.kr/down/_St.zip";
      function teAiwkIsWsFailure(res) {
        if (teAiwkSideWs && typeof teAiwkSideWs.isWsFailure === "function") return teAiwkSideWs.isWsFailure(res);
        if (!res || res.ok) return false;
        const code = String(res.error_code || res.error || "").toUpperCase();
        return /AIWK|WEBSOCKET|WS_|TIMEOUT|CONNECTION|REFUSED|EMPTY_RESPONSE|BAD_JSON|UNKNOWN_ACT|BAD_TOKEN/.test(code);
      }
      function teAiwkSetupNotice(show, detail) {
        const el = document.getElementById("igblue_aiwk_setup_notice");
        if (!el) return;
        if (teAutoRuntimeNoticeMode && !show) {
          el.classList.add("is-visible");
          el.setAttribute("aria-hidden", "false");
          return;
        }
        if (show && teAutoRuntimeNoticeMode) {
          // 로컬 엔진 미연결 안내는 자동 대기 상태보다 우선한다.
          teAutoRuntimeNoticeMode = "";
          teAutoRuntimeNoticeCode = "";
        }
        const def = document.getElementById("igblue_aiwk_setup_default");
        const runtime = document.getElementById("igblue_auto_runtime_notice");
        if (def) def.hidden = false;
        if (runtime) runtime.hidden = true;
        el.classList.remove("is-auto-wait", "is-auto-resume", "is-auto-stop");
        el.classList.toggle("is-visible", !!show);
        el.setAttribute("aria-hidden", show ? "false" : "true");
        if (show && detail) el.title = String(detail);
        else if (!show) el.removeAttribute("title");
      }
      function teAiwkTrackWsResult(res) {
        if (res && (res.ok || res.connected)) teAiwkSetupNotice(false);
        else if (teAiwkIsWsFailure(res) || (res && res.connected === false)) teAiwkSetupNotice(true, res && (res.error_code || res.error || res.message) || "WS 대기중 / 로컬 host 미접속");
        return res;
      }
      function teAiwkRefreshSetupNotice(force) {
        try {
          chrome.runtime.sendMessage({
            type: "IGBLUE_RTC_STATUS",
            force: !!force,
            force_probe: !!force,
            href: String(location && location.href || ""),
            host: String(location && location.host || "")
          }, (res) => {
            if (chrome.runtime.lastError) {
              teAiwkSetupNotice(true, chrome.runtime.lastError.message || "WS 상태 확인 실패");
              return;
            }
            teAiwkTrackWsResult(res || {ok:false, connected:false, error_code:"AIWK_EMPTY_STATUS"});
          });
        } catch (err) {
          teAiwkSetupNotice(true, String(err && err.message || err || "WS 상태 확인 예외"));
        }
      }
      function teAiwkBindSetupNotice() {
        const el = document.getElementById("igblue_aiwk_setup_notice");
        if (!el || el.dataset.igblueBound) return;
        el.dataset.igblueBound = "1";
        teAiwkRefreshSetupNotice(true);
        setInterval(() => teAiwkRefreshSetupNotice(false), 10000);
      }
      if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", teAiwkBindSetupNotice, {once:true});
      else teAiwkBindSetupNotice();
      setTimeout(teAiwkBindSetupNotice, 500);

      let teAnalyze8780Timer = null;
      let teAnalyze8780Online = false;
      function teAnalyze8780Account() {
        const candidate = teCurrentAccountAddTarget.username || teAccountFromUrl(teCurrentTargetUrl()) || teDownloadSessionAccount || "";
        const account = teAutoPyNormalizeAccountUsername(candidate) || teCleanOwnerAccountName(candidate);
        return account ? teSafePath(account) : "";
      }
      function teAnalyze8780OpenUrl() {
        const account = teAnalyze8780Account();
        return account ? `http://127.0.0.1:8780/?account=${encodeURIComponent(account)}` : "http://127.0.0.1:8780/";
      }
      function teAnalyze8780SyncOpenLink(online) {
        const link = document.getElementById("igblue_analyze8780_open");
        if (!link) return;
        if (online) {
          const url = teAnalyze8780OpenUrl();
          link.href = url;
          link.title = url;
          link.setAttribute("aria-disabled", "false");
          link.tabIndex = 0;
        } else {
          link.removeAttribute("href");
          link.title = "http://127.0.0.1:8780 연결 후 활성화됩니다.";
          link.setAttribute("aria-disabled", "true");
          link.tabIndex = -1;
        }
      }
      function teAnalyze8780Render(online, detail) {
        teAnalyze8780Online = !!online;
        const row = document.getElementById("igblue_analyze8780_row");
        const status = document.getElementById("igblue_analyze8780_status");
        if (row) { row.classList.toggle("is-online", !!online); row.classList.toggle("is-offline", !online); }
        if (status) status.textContent = online ? `Analyze8780 연결됨${teAnalyze8780Account() ? " · " + teAnalyze8780Account() : ""}` : `Analyze8780 미연결${detail ? " · " + detail : ""}`;
        teAnalyze8780SyncOpenLink(!!online);
      }
      async function teAnalyze8780Check() {
        const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
        const timer = setTimeout(() => { try { controller && controller.abort(); } catch (_) {} }, 1800);
        try {
          const res = await fetch("http://127.0.0.1:8780/api/health", { cache: "no-store", signal: controller && controller.signal });
          const payload = await res.json().catch(() => ({}));
          const ok = !!(res.ok && (payload.ok === true || payload.status === "ok" || payload.service));
          teAnalyze8780Render(ok, ok ? "" : `HTTP ${res.status}`);
          return ok;
        } catch (err) {
          teAnalyze8780Render(false, "실행 필요");
          return false;
        } finally { clearTimeout(timer); }
      }
      function teInitAnalyze8780Ui() {
        teAnalyze8780SyncOpenLink(false);
        teAnalyze8780Check();
        if (!teAnalyze8780Timer) teAnalyze8780Timer = setInterval(teAnalyze8780Check, 10000);
        window.addEventListener("focus", teAnalyze8780Check);
      }
      function teDownTxtStatus(msg) {
        const el = document.getElementById("igblue_down_txt_status");
        if (el) el.textContent = "목록: " + String(msg || "");
      }
      function teDownTxtAccount() {
        return teSafePath(teActiveFolderAccountName() || "instagram");
      }
      function teDownTxtHost() {
        try {
          const u = new URL(teCurrentTargetUrl() || "https://www.instagram.com/");
          return u.hostname || "www.instagram.com";
        } catch { return "www.instagram.com"; }
      }
      function teDownTxtStorageKey(account) {
        return "download/folder_json_list/instagram/" + teSafePath(account || teDownTxtAccount());
      }
      function teDownItemKey(item) {
        const it = item && typeof item === "object" ? item : {};
        const sc = String(it.shortcode || "").trim();
        const kind = String(it.asset_kind || it.kind || "video").toLowerCase();
        const idx = Number(it.media_index || 0) || 0;
        return [sc, kind, idx].join("|").toLowerCase();
      }
      function teRankLabelFromItem(item) {
        const it = item && typeof item === "object" ? item : {};
        const label = String(it.rank_label || "").trim();
        if (/^No\s*\d+/i.test(label)) return label.replace(/\s+/g, "");
        const no = Number(it.rank_no || 0) || 0;
        return no > 0 ? teAutoPyNoLabel(no) : "No---";
      }
      function teNormalizeDownItems(items) {
        const out = [];
        const seen = new Set();
        if (Array.isArray(items)) {
          for (const raw of items) {
            if (!raw || typeof raw !== "object") continue;
            const kind = String(raw.asset_kind || raw.kind || "").toLowerCase();
            if (kind && !/video|mp4|image|jpg|jpeg/.test(kind)) continue;
            const it = Object.assign({}, raw, { asset_kind: /image|jpg|jpeg/.test(kind) ? "image" : "video" });
            const k = teDownItemKey(it) || JSON.stringify([it.rank_label, it.rank_no, it.asset_kind, it.file_name || ""]);
            if (seen.has(k)) continue;
            seen.add(k);
            out.push(it);
          }
        } else {
          const text = String(items || "");
          text.split(/\r?\n/).map(x => x.trim()).filter(Boolean).forEach((name, i) => {
            const file = name.split(/[\\/]/).pop();
            if (!/\.mp4$/i.test(file)) return;
            const m = file.match(/_No(\d{3})_/i);
            const it = { rank_no: m ? Number(m[1]) : 0, rank_label: m ? ("No" + m[1]) : "", asset_kind: "video", file_name: file, shortcode: "" };
            const k = teDownItemKey(it) || file.toLowerCase();
            if (seen.has(k)) return;
            seen.add(k);
            out.push(it);
          });
        }
        out.sort((a, b) => (Number(a.rank_no || 999999) - Number(b.rank_no || 999999)) || String(a.shortcode || "").localeCompare(String(b.shortcode || "")));
        return out;
      }
      let teDownItems = [];
      let teDownIndex = Object.create(null);
      function teRebuildDownIndex() {
        teDownIndex = Object.create(null);
        for (const it of teDownItems) {
          const k = teDownItemKey(it);
          if (k && !/^\|/.test(k)) teDownIndex[k] = true;
        }
      }
      function teDownItemExists(meta, kind) {
        const m = meta && typeof meta === "object" ? meta : {};
        const k = [String(m.shortcode || "").trim(), String(kind || m.asset_kind || "video").toLowerCase(), Number(m.media_index || 0) || 0].join("|").toLowerCase();
        return !!(k && teDownIndex[k]);
      }
      function teRenderDownList(items, account) {
        teDownItems = teNormalizeDownItems(items);
        teRebuildDownIndex();
        const el = document.getElementById("igblue_down_file_list");
        if (el) {
          el.innerHTML = "";
          if (!teDownItems.length) {
            const empty = document.createElement("div");
            empty.className = "igblue-downlog-empty";
            empty.textContent = "목록 없음";
            el.appendChild(empty);
          } else {
            for (const item of teDownItems) {
              const row = document.createElement("div");
              row.className = "igblue-downlog-item";
              row.dataset.shortcode = String(item.shortcode || "");
              row.dataset.kind = String(item.asset_kind || "");
              row.dataset.rank = teRankLabelFromItem(item);
              const rank = document.createElement("span"); rank.className = "rank"; rank.textContent = teRankLabelFromItem(item);
              const kind = document.createElement("span"); kind.className = "kind"; kind.textContent = /image|jpg/i.test(String(item.asset_kind || "")) ? "JPG" : "MP4";
              const meta = document.createElement("span"); meta.className = "meta";
              const parts = [];
              if (item.views) parts.push("조회 " + item.views);
              if (item.likes) parts.push("하트 " + item.likes);
              if (item.comments) parts.push("댓글 " + item.comments);
              if (item.shortcode) parts.push(String(item.shortcode));
              meta.textContent = parts.join(" · ") || "완료";
              row.append(rank, kind, meta);
              el.appendChild(row);
            }
            el.scrollTop = el.scrollHeight;
          }
        }
        try { teLocalSet(teDownTxtStorageKey(account), JSON.stringify(teDownItems)); } catch (_) {}
        return teDownItems;
      }
      function teNormalizeDownTxt(text) {
        return teNormalizeDownItems(text).map(x => x.file_name || x.shortcode || teRankLabelFromItem(x)).filter(Boolean).join("\n");
      }
      function teGetDownTxtText() {
        return teDownItems.map(x => x.file_name || x.shortcode || teRankLabelFromItem(x)).filter(Boolean).join("\n");
      }
      function teSetDownTxtText(text, account) {
        const items = teNormalizeDownItems(text);
        teRenderDownList(items, account);
        return items.map(x => x.file_name || x.shortcode || teRankLabelFromItem(x)).filter(Boolean).join("\n");
      }
      function teLoadDownTxtLocal(account) {
        let raw = teLocalGet(teDownTxtStorageKey(account), "");
        let items = [];
        try { items = raw ? JSON.parse(raw) : []; } catch (_) { items = teNormalizeDownItems(raw); }
        teRenderDownList(items, account);
        teDownTxtStatus("local instagram/" + teSafePath(account || teDownTxtAccount()) + ".json");
      }
      async function teAiwkPollThreadQueueResult(accepted, originalAct, timeoutMs = 4500, opts = {}) {
        const jobId = String(accepted && accepted.job_id || "").trim();
        if (!jobId || !teAiwkSideWs || typeof teAiwkSideWs.request !== "function") return accepted;
        const started = Date.now();
        const deadline = started + Math.max(1500, Number(timeoutMs || 4500) || 4500);
        const pollMs = Math.max(120, Math.min(1500, Number(accepted.poll_interval_ms || 250) || 250));
        let last = null;
        while (Date.now() < deadline) {
          const payload = Object.assign({}, teCurrentWsTargetPayload("ig_thread_queue_result", { host: teDownTxtHost() }), { job_id: jobId, original_act: originalAct });
          try {
            const poll = await teAiwkSideWs.request("ig_thread_queue_result", payload, {
              timeoutMs: Math.min(2200, Math.max(800, deadline - Date.now())),
              forceProbe: true,
              from: { endpoint: "thread_queue_poll", href: teCurrentTargetUrl(), host: teDownTxtHost(), tab_id: payload.tab_id || 0 }
            });
            last = poll;
            if (poll && poll.done) {
              const result = poll.result && typeof poll.result === "object" ? poll.result : {};
              result.thread_queue_mode = true;
              result.thread_queue_job_id = jobId;
              result.thread_queue_status = poll.status || "done";
              return result;
            }
            if (poll && poll.ok === false && poll.error === "job_not_found") {
              return Object.assign({ ok: false, error: "thread_queue_job_not_found", job_id: jobId, original_act: originalAct }, poll || {});
            }
          } catch (err) {
            last = { ok: false, error: String(err && err.message || err) };
          }
          await teSleep(pollMs);
        }
        return { ok: false, error: "thread_queue_result_timeout", job_id: jobId, original_act: originalAct, accepted, last_poll: last };
      }

      function teAiwkWsRequest(act, data, timeoutMs = 4500, opts = {}) {
        if (!teAiwkSideWs || typeof teAiwkSideWs.request !== "function") {
          return Promise.resolve({ ok: 0, error_code: "SIDE_WS_MODULE_NOT_LOADED", state: "ws_waiting", expected_offline: true });
        }
        const payload = Object.assign({}, teCurrentWsTargetPayload(act, { host: teDownTxtHost() }), data || {});
        return teAiwkSideWs.request(String(act || ""), payload, {
          timeoutMs,
          forceProbe: opts && opts.forceProbe === true,
          from: { endpoint: "domain_down_txt", href: teCurrentTargetUrl(), host: teDownTxtHost(), tab_id: payload.tab_id || 0 }
        }).then(async (res) => {
          if (res && res.thread_queue_mode && res.queued && res.job_id) {
            res = await teAiwkPollThreadQueueResult(res, act, timeoutMs, opts || {});
          }
          return teAiwkTrackWsResult(res);
        });
      }

      const TE_STATS_MODE_KEY = "stats/mode";
      const TE_STATS_STANDALONE = "standalone";
      const TE_STATS_LINKED = "linked";
      const TE_STATS_BATCH_SIZE = 100;
      const TE_STATS_SIGNATURE_LIMIT = 5000;
      let teStatsObservationQueue = Promise.resolve();
      let teStatsPendingSnapshot = null;
      let teStatsDrainRunning = false;
      const teStatsLocalSignatureMap = new Map();
      let teStatsCounters = { local: 0, sent: 0, received: 0, failed: 0, skippedHour: 0 };

      function teStatsMode() {
        const value = String(teLocalGet(TE_STATS_MODE_KEY, TE_STATS_STANDALONE) || "").toLowerCase();
        return value === TE_STATS_LINKED ? TE_STATS_LINKED : TE_STATS_STANDALONE;
      }
      function teSaveStatsMode(mode) {
        const value = mode === TE_STATS_LINKED ? TE_STATS_LINKED : TE_STATS_STANDALONE;
        teLocalSet(TE_STATS_MODE_KEY, value);
        teUpdateStatsStatus();
        return value;
      }
      function teCreateStatsModeSettings() {
        const wrap = document.createElement("div");
        wrap.style.display = "flex"; wrap.style.flexDirection = "column"; wrap.style.gap = "12px";
        const title = document.createElement("div"); title.textContent = "통계 모드"; title.style.fontSize = "15px"; title.style.fontWeight = "800";
        const description = document.createElement("div"); description.style.fontSize = "12px"; description.style.color = "#475569";
        description.textContent = "통계는 10분 구간마다 확인하고 실제 수치가 변경된 게시물만 로컬에 저장합니다. 통계 연동은 계정별 1시간에 한 번 중앙 서버와 송수신합니다.";
        const current = teStatsMode();
        const radios = {};
        const make = (value, label, note) => {
          const row = document.createElement("label"); row.style.display = "grid"; row.style.gridTemplateColumns = "20px 110px 1fr"; row.style.gap = "8px"; row.style.alignItems = "start"; row.style.padding = "10px"; row.style.border = "1px solid #dbe3ec"; row.style.borderRadius = "8px";
          const input = document.createElement("input"); input.type = "radio"; input.name = "igblue_stats_mode"; input.value = value; input.checked = current === value;
          const strong = document.createElement("strong"); strong.textContent = label;
          const small = document.createElement("span"); small.textContent = note; small.style.fontSize = "12px"; small.style.color = "#52677a";
          row.appendChild(input); row.appendChild(strong); row.appendChild(small); radios[value] = input; return row;
        };
        wrap.appendChild(title); wrap.appendChild(description);
        wrap.appendChild(make(TE_STATS_STANDALONE, "통계 단독", "내가 수집한 통계만 로컬에 저장하고 이용합니다."));
        wrap.appendChild(make(TE_STATS_LINKED, "통계 연동", "로컬 저장 후 중앙 서버로 보내고, 연동된 전체 통계를 함께 받습니다."));
        return { element: wrap, getValue: () => radios[TE_STATS_LINKED].checked ? TE_STATS_LINKED : TE_STATS_STANDALONE };
      }
      function teUpdateStatsStatus(extra) {
        const mode = teStatsMode();
        const box = document.getElementById("igblue_stats_sync_status");
        const modeEl = document.getElementById("igblue_stats_sync_mode");
        const detailEl = document.getElementById("igblue_stats_sync_detail");
        if (box) box.classList.toggle("is-linked", mode === TE_STATS_LINKED);
        if (modeEl) modeEl.textContent = mode === TE_STATS_LINKED ? "📊 통계 연동" : "📊 통계 단독";
        if (detailEl) {
          const base = mode === TE_STATS_LINKED
            ? `로컬 ${teStatsCounters.local} · 전송 ${teStatsCounters.sent} · 수신 ${teStatsCounters.received} · 실패 ${teStatsCounters.failed}`
            : `로컬 ${teStatsCounters.local} · 10분 변경분 저장`;
          detailEl.textContent = extra ? `${base} · ${String(extra)}` : base;
        }
      }
      function teStatsHourBucket(date = new Date()) {
        const d = new Date(date);
        d.setUTCMinutes(0, 0, 0);
        return d.toISOString().replace(/:00:00\.000Z$/, ":00:00Z");
      }
      function teStatsHourStorageKey(account) {
        return `stats/linked_hour/${String(account || "").toLowerCase()}`;
      }
      function teStatsObservationSignature(item) {
        const row = item && typeof item === "object" ? item : {};
        return [row.shortcode, row.views_available, row.views, row.likes, row.comments, row.reposts, row.sort_by, row.sort_value].map((v) => String(v ?? "")).join("|");
      }
      function teStatsHourState(account, hourBucket) {
        const raw = teLocalGet(teStatsHourStorageKey(account), "");
        let state = null;
        try { state = raw && typeof raw === "object" ? raw : JSON.parse(String(raw || "")); } catch (_) {}
        if (!state || typeof state !== "object" || String(state.hour_bucket || "") !== String(hourBucket || "")) {
          return { hour_bucket: String(hourBucket || ""), sent: {} };
        }
        return { hour_bucket: String(hourBucket || ""), sent: state.sent && typeof state.sent === "object" ? Object.assign({}, state.sent) : {} };
      }
      function teStatsSaveHourState(account, state) {
        const sent = state && state.sent && typeof state.sent === "object" ? state.sent : {};
        const keys = Object.keys(sent);
        if (keys.length > TE_STATS_SIGNATURE_LIMIT) keys.slice(0, keys.length - TE_STATS_SIGNATURE_LIMIT).forEach((key) => delete sent[key]);
        teLocalSet(teStatsHourStorageKey(account), JSON.stringify({ hour_bucket: String(state && state.hour_bucket || ""), sent }));
      }
      function teStatsChunk(items, size = TE_STATS_BATCH_SIZE) {
        const rows = Array.isArray(items) ? items : [];
        const chunks = [];
        const limit = Math.max(1, Number(size || TE_STATS_BATCH_SIZE) || TE_STATS_BATCH_SIZE);
        for (let i = 0; i < rows.length; i += limit) chunks.push(rows.slice(i, i + limit));
        return chunks;
      }
      function teStatsTenMinuteBucket(observedAt) {
        const date = new Date(String(observedAt || ""));
        if (!Number.isFinite(date.getTime())) return "";
        date.setUTCMinutes(Math.floor(date.getUTCMinutes() / 10) * 10, 0, 0);
        return date.toISOString().slice(0, 16);
      }
      function teStatsChangedLocalItems(items, sourceUrl) {
        const changed = [];
        const scope = String(sourceUrl || "");
        (Array.isArray(items) ? items : []).forEach((item) => {
          const shortcode = String(item && item.shortcode || "");
          if (!shortcode) return;
          const key = `${scope}|${shortcode}`;
          const bucket = teStatsTenMinuteBucket(item && item.observed_at);
          const signature = teStatsObservationSignature(item);
          const previous = teStatsLocalSignatureMap.get(key);
          if (previous && previous.bucket === bucket) return;
          teStatsLocalSignatureMap.delete(key);
          teStatsLocalSignatureMap.set(key, { bucket, signature });
          if (!previous || previous.signature !== signature) changed.push(item);
        });
        while (teStatsLocalSignatureMap.size > TE_STATS_SIGNATURE_LIMIT) teStatsLocalSignatureMap.delete(teStatsLocalSignatureMap.keys().next().value);
        return changed;
      }
      function teStatsSortField() {
        return teServerSortField(teTopMetricField() || String(G[Z] || ""));
      }
      function teStatsSortValue(media, sortField) {
        const fieldMap = { likes: "like_count", comments: "comment_count", reposts: "media_repost_count", play_count: "play_count", views: "play_count", upload_at: "created_at", engagement_rate: "engagement_rate" };
        const rawField = fieldMap[sortField] || String(G[Z] || "");
        if (rawField === "play_count" && !teViewsAvailable(media)) return teParticipationScore(media);
        return Math.max(0, Math.floor(Number(media && media[rawField]) || 0));
      }
      function teStatsPublishedAt(media) {
        const seconds = Number(media?.created_at || media?.taken_at || media?.caption?.created_at || 0);
        return Number.isFinite(seconds) && seconds > 0 ? new Date(seconds * 1000).toISOString() : "";
      }
      function teStatsObservationFromMedia(media, observedAt, sourceKind = "LOCAL", sourceScope = "") {
        const username = teOwnerAccountForMedia(media || {});
        const shortcode = String(media?.code || "").trim();
        if (!username || !shortcode) return null;
        const sortBy = teStatsSortField();
        const videoUrl = String(media?.video_url || "");
        const viewsAvailable = teViewsAvailable(media);
        return {
          username, shortcode,
          profile_url: `https://www.instagram.com/${encodeURIComponent(username)}/`,
          post_url: teMediaPageUrl(media, videoUrl),
          caption: String(media?.caption?.text || media?.caption_text || ""),
          published_at: teStatsPublishedAt(media),
          thumbnail_url: String(media?.img_origin || media?.thumbnail_url || media?.display_url || ""),
          video_url: videoUrl,
          views: teViewCountOrPrivate(media),
          views_available: viewsAvailable ? 1 : 0,
          likes: Math.max(0, Math.floor(Number(media?.like_count) || 0)),
          comments: Math.max(0, Math.floor(Number(media?.comment_count) || 0)),
          reposts: Math.max(0, Math.floor(Number(media?.media_repost_count) || 0)),
          shares: 0, share_count: 0,
          participation_score: teParticipationScore(media),
          sort_by: sortBy,
          sort_value: teStatsSortValue(media, sortBy),
          source_kind: sourceKind,
          observed_at: observedAt,
          source_scope: String(sourceScope || teCurrentTargetUrl() || ""),
        };
      }
      function teStatsScopeFilter(info, sourceUrl) {
        const rows = Array.isArray(info) ? info : [];
        const unique = new Map();
        let profileAccount = "";
        let postShortcode = "";
        try {
          const url = new URL(String(sourceUrl || teCurrentTargetUrl() || ""));
          const parts = String(url.pathname || "").split("/").filter(Boolean);
          const reserved = new Set(["explore", "reels", "reel", "p", "tv", "accounts", "direct", "stories"]);
          if (parts.length === 1 && !reserved.has(String(parts[0] || "").toLowerCase())) profileAccount = String(parts[0] || "").replace(/^@/, "").toLowerCase();
          if (parts.length >= 2 && ["p", "reel", "tv"].includes(String(parts[0] || "").toLowerCase())) postShortcode = String(parts[1] || "");
        } catch (_) {}
        rows.forEach((media) => {
          const shortcode = String(media?.code || "").trim();
          const owner = String(teOwnerAccountForMedia(media || {}) || "").replace(/^@/, "").toLowerCase();
          if (!shortcode || !owner) return;
          if (profileAccount && owner !== profileAccount) return;
          if (postShortcode && shortcode !== postShortcode) return;
          unique.set(shortcode, media);
        });
        return Array.from(unique.values());
      }
      async function teStatsSaveLocalBatch(items) {
        const rows = Array.isArray(items) ? items : [];
        if (!rows.length) return { ok: true, saved_count: 0, batch_count: 0 };
        let savedCount = 0;
        let batchCount = 0;
        for (const chunk of teStatsChunk(rows)) {
          const response = await fetch("http://127.0.0.1:8780/api/observations/batch", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ items: chunk }),
          });
          const body = await response.json().catch(() => ({}));
          if (!response.ok || body.ok === false) throw new Error(String(body.error || `Analyze8780 HTTP ${response.status}`));
          const chunkSaved = Number(body.saved_count);
          savedCount += Number.isFinite(chunkSaved) ? chunkSaved : chunk.length;
          batchCount += 1;
        }
        return { ok: true, saved_count: savedCount, batch_count: batchCount };
      }
      function teStatsRemoteItems(account, items, observedAt, sortBy) {
        return (Array.isArray(items) ? items : []).map((item) => ({
          username: String(item?.ig_account_id || item?.account_name || account || "").replace(/^@/, ""),
          shortcode: String(item?.shortcode || ""),
          post_url: String(item?.media_url || item?.post_url || ""),
          published_at: String(item?.upload_at || ""),
          thumbnail_url: "",
          video_url: "",
          views: Number(item?.views_available || 0) ? Math.max(0, Math.floor(Number(item?.views) || 0)) : -1,
          views_available: Number(item?.views_available || 0) ? 1 : 0,
          likes: Math.max(0, Math.floor(Number(item?.likes) || 0)),
          comments: Math.max(0, Math.floor(Number(item?.comments) || 0)),
          reposts: Math.max(0, Math.floor(Number(item?.reposts) || 0)),
          share_count: 0, shares: 0,
          sort_by: String(item?.sort_by || sortBy || ""),
          sort_value: Math.max(0, Math.floor(Number(item?.sort_value) || 0)),
          source_kind: "SERVER",
          observed_at: String(item?.observed_at_utc || item?.observed_at || observedAt || ""),
          source_scope: String(item?.source_scope || "central_hour"),
        })).filter((item) => item.username && item.shortcode);
      }
      async function teStatsSyncAccountHour(account, items, observedAt) {
        if (teStatsMode() !== TE_STATS_LINKED) return { ok: true, skipped: "standalone" };
        const hourBucket = teStatsHourBucket(observedAt);
        const state = teStatsHourState(account, hourBucket);
        const pending = (Array.isArray(items) ? items : []).filter((item) => {
          const shortcode = String(item && item.shortcode || "");
          if (!shortcode) return false;
          return String(state.sent[shortcode] || "") !== teStatsObservationSignature(item);
        });
        if (!pending.length) {
          teStatsCounters.skippedHour += 1;
          teUpdateStatsStatus("1시간 구간 유지");
          return { ok: true, skipped: "same_hour_no_change", sent_count: 0 };
        }
        let sentCount = 0;
        let receivedCount = 0;
        let batchCount = 0;
        for (const chunk of teStatsChunk(pending)) {
          const result = await teAiwkWsRequest("ig_observation_sync_hour", { account, hour_bucket: hourBucket, items: chunk, sort_by: teStatsSortField(), share_count: 0 }, 120000, { forceProbe: true });
          if (!(result && result.ok)) {
            teStatsCounters.failed += Number(result && result.failed_count || 1) || 1;
            teUpdateStatsStatus("연동 재시도 대기");
            throw new Error(String(result && (result.error || result.error_code) || "통계 연동 실패"));
          }
          const remote = teStatsRemoteItems(account, result && result.items, observedAt, teStatsSortField());
          if (remote.length) {
            const merged = await teStatsSaveLocalBatch(remote);
            const mergedSaved = Number(merged.saved_count);
            const received = Number.isFinite(mergedSaved) ? mergedSaved : remote.length;
            teStatsCounters.received += received;
            receivedCount += received;
          }
          chunk.forEach((item) => {
            const shortcode = String(item && item.shortcode || "");
            if (shortcode) state.sent[shortcode] = teStatsObservationSignature(item);
          });
          teStatsSaveHourState(account, state);
          const sent = Number(result.sent_count || chunk.length) || 0;
          teStatsCounters.sent += sent;
          sentCount += sent;
          batchCount += 1;
          teUpdateStatsStatus(`신규 ${sentCount}개 연동`);
        }
        teUpdateStatsStatus(`신규 ${sentCount}개 연동 · 1시간 구간 유지`);
        return { ok: true, sent_count: sentCount, received_count: receivedCount, batch_count: batchCount };
      }
      async function teHandleStatsPayload(snapshot) {
        const observedAt = new Date().toISOString();
        const sourceUrl = String(snapshot && snapshot.source_url || teCurrentTargetUrl() || "");
        const scopedMedia = teStatsScopeFilter(snapshot && snapshot.items, sourceUrl);
        const localItems = scopedMedia.map((media) => teStatsObservationFromMedia(media, observedAt, "LOCAL", sourceUrl)).filter(Boolean);
        if (!localItems.length) return;
        const changedLocalItems = teStatsChangedLocalItems(localItems, sourceUrl);
        if (changedLocalItems.length) {
          try {
            const saved = await teStatsSaveLocalBatch(changedLocalItems);
            const savedCount = Number(saved.saved_count);
            teStatsCounters.local += Number.isFinite(savedCount) ? savedCount : changedLocalItems.length;
            teUpdateStatsStatus();
          } catch (error) {
            changedLocalItems.forEach((item) => teStatsLocalSignatureMap.delete(`${sourceUrl}|${String(item && item.shortcode || "")}`));
            teStatsCounters.failed += 1;
            teUpdateStatsStatus("로컬 저장 실패");
            dbg("stats", "local_batch_failed", 4429, { error: String(error && error.message || error), count: changedLocalItems.length });
          }
        }
        if (teStatsMode() !== TE_STATS_LINKED) return;
        const grouped = new Map();
        localItems.forEach((item) => { const list = grouped.get(item.username) || []; list.push(item); grouped.set(item.username, list); });
        for (const [account, items] of grouped.entries()) {
          try { await teStatsSyncAccountHour(account, items, observedAt); }
          catch (error) { dbg("stats", "hour_sync_failed", 4429, { account, error: String(error && error.message || error) }); }
        }
      }
      function teStatsCompactMedia(media) {
        const m = media && typeof media === "object" ? media : {};
        return {
          code: String(m.code || ""), username: String(m.username || ""), owner_username: String(m.owner_username || ""), userid: String(m.userid || ""),
          created_at: Number(m.created_at || 0) || 0, taken_at: Number(m.taken_at || 0) || 0,
          caption_text: String(m.caption_text || ""), caption: m.caption && typeof m.caption === "object" ? { text: String(m.caption.text || ""), created_at: Number(m.caption.created_at || 0) || 0, user: m.caption.user && typeof m.caption.user === "object" ? { username: String(m.caption.user.username || "") } : null } : null,
          img_origin: String(m.img_origin || ""), thumbnail_url: String(m.thumbnail_url || ""), display_url: String(m.display_url || ""), video_url: String(m.video_url || ""),
          play_count: m.play_count, view_count: m.view_count, like_count: m.like_count, comment_count: m.comment_count, media_repost_count: m.media_repost_count, engagement_rate: m.engagement_rate
        };
      }
      async function teDrainStatsPayloadQueue() {
        if (teStatsDrainRunning) return;
        teStatsDrainRunning = true;
        try {
          while (teStatsPendingSnapshot) {
            const snapshot = teStatsPendingSnapshot;
            teStatsPendingSnapshot = null;
            await teHandleStatsPayload(snapshot);
          }
        } finally {
          teStatsDrainRunning = false;
        }
      }
      function teQueueStatsPayload(info, sourceUrl) {
        teStatsPendingSnapshot = {
          items: Array.isArray(info) ? info.map(teStatsCompactMedia) : [],
          source_url: String(sourceUrl || teCurrentTargetUrl() || "")
        };
        if (!teStatsDrainRunning) teStatsObservationQueue = teDrainStatsPayloadQueue().catch(() => {});
      }


      function teArmWsAutoEnter(reason, detail = {}) {
        try {
          return teAiwkWsRequest("ig_rtc_enter_auto_arm", Object.assign({
            reason: reason || "manual_main_click",
            source: "main_manual_download_button",
            href: teCurrentTargetUrl(),
            host: teDownTxtHost()
          }, detail || {}), 2500, { forceProbe: true }).then(teAiwkTrackWsResult);
        } catch (err) {
          return Promise.resolve({ ok: false, error: String(err && err.message || err) });
        }
      }
      let tePyAutoStatusTimer = null;
      let tePyStatusProbeBusy = false;
      const tePyStatusProbeMs = 10000;
      let tePyLastOkAt = 0;
      let tePyStatusFailSince = 0;
      function tePyStartStatusProbe(reason) {
        if (tePyAutoStatusTimer) clearInterval(tePyAutoStatusTimer);
        setTimeout(() => { tePyProbeStatusOnce(reason || "init"); }, 350);
        tePyAutoStatusTimer = setInterval(() => { tePyProbeStatusOnce("interval"); }, tePyStatusProbeMs);
      }
      async function tePyProbeStatusOnce(reason) {
        if (tePyStatusProbeBusy) return teAutoPyLastPyOk;
        tePyStatusProbeBusy = true;
        try {
          return await tePyPingOnce(true);
        } finally {
          tePyStatusProbeBusy = false;
        }
      }
      const teAutoPyFeatureCheckSeen = new Set();
      function teAutoPyFeatureCheckReset(runSeq) {
        teAutoPyFeatureCheckSeen.clear();
        teAutoPyFeatureCheckSeen.add(`RUN|${Number(runSeq || 0)}`);
      }
      function teAutoPyFeatureCheck(checkId, status, messageKo, detail = {}) {
        try {
          const check = String(checkId || "").trim();
          const state = String(status || "").trim();
          if (!check || !state) return Promise.resolve(null);
          const d = detail && typeof detail === "object" ? detail : {};
          const cycleId = String(d.cycle_id || d.feature_cycle_id || d.client_batch_key || `run_${Number(teAutoPyRunSeq || 0)}`);
          const asset = String(d.asset_kind || d.media_type || "");
          const key = [Number(teAutoPyRunSeq || 0), cycleId, check, state, asset].join("|");
          if (teAutoPyFeatureCheckSeen.has(key)) return Promise.resolve(null);
          teAutoPyFeatureCheckSeen.add(key);
          const featureNameKo = check.startsWith("AS-") ? "자동실행" : "자동다운로드";
          const line = `[${featureNameKo}][${check}][${state}] ${String(messageKo || "").trim()}`;
          try { n(line); } catch (_) {}
          const payload = Object.assign({
            schema: IGBLUE_CYCLE_SCHEMA,
            build_version: IGBLUE_BUILD_VERSION,
            build_tag: IGBLUE_BUILD_TAG,
            cycle_stage: "FEATURE_CHECK",
            feature_check_mode: true,
            feature_name_ko: featureNameKo,
            check_id: check,
            check_status: state,
            check_message_ko: String(messageKo || "").trim(),
            cycle_id: cycleId,
            run_seq: Number(teAutoPyRunSeq || 0),
            href: teCurrentTargetUrl(),
            host: teDownTxtHost()
          }, d);
          return teAiwkWsRequest("ig_auto_cycle_state", payload, 1800, { forceProbe: true }).catch(() => null);
        } catch (_) {
          return Promise.resolve(null);
        }
      }
      function teAutoPyCheckedRaw() {
        const cb = document.getElementById("igblue_auto_py_down");
        return !!(cb && cb.checked);
      }
      function teAutoPyEnabled() {
        // v107: 체크 상태와 WS 연결 상태를 분리한다.
        return teAutoPyCheckedRaw();
      }
      let teAutoPyRunSeq = 0;
      let teAutoPyRunning = false;
      let teAutoPyStopping = false;
      let teAutoPyLastPyOk = false;
      let teAutoPyCdpReady = false;
      const IGBLUE_CDP_REQUIRED_TEXT = "CDP는 자동예약에만 필요합니다. 일반 자동실행과 수동 다운로드의 기본 조건은 Auto8700/WS8771입니다.";
      function teAutoPyApplyCdpNotice() {
        const notice = document.getElementById("igblue_auto_cdp_notice");
        if (notice) {
          notice.textContent = IGBLUE_CDP_REQUIRED_TEXT;
          // 엔진 미실행 시에는 _St.zip/BAT 안내가 먼저이며 CDP 문구로 덮지 않는다.
          notice.classList.toggle("is-visible", !!teAutoPyLastPyOk && !teAutoPyCdpReady);
        }
      }
      function teAutoPySetCdpReady(on, detail = "") {
        teAutoPyCdpReady = !!on;
        teAutoPyApplyCdpNotice();
        teAutoPyUpdateButtons(teAutoPyLastPyOk);
        return teAutoPyCdpReady;
      }
      window.__IGBLUE_SET_CDP_READY_V277__ = teAutoPySetCdpReady;
      function teAutoPyUpdateButtons(ok = teAutoPyLastPyOk) {
        teAutoPyLastPyOk = !!ok;
        const cb = document.getElementById("igblue_auto_py_down"), startBtn = document.getElementById("igblue_auto_py_start"), stopBtn = document.getElementById("igblue_auto_py_stop"), resetBtn = document.getElementById("igblue_auto_progress_reset");
        if (cb) cb.disabled = !!(teAutoPyRunning || teAutoPyStopping);
        if (startBtn) {
          startBtn.textContent = "시작";
          startBtn.disabled = !teAutoPyLastPyOk || !teAutoPyCheckedRaw() || teAutoPyRunning || teAutoPyStopping;
        }
        if (stopBtn) {
          stopBtn.textContent = teAutoPyStopping ? "중지중..." : "중지";
          stopBtn.disabled = !(teAutoPyRunning || teAutoPyStopping);
          stopBtn.title = "자동 실행을 즉시 중지합니다.\n단축키: Ctrl + Shift + F12";
        }
        if (resetBtn) {
          resetBtn.disabled = !!(teAutoPyRunning || teAutoPyStopping);
          resetBtn.title = teAutoPyRunning || teAutoPyStopping
            ? "자동 실행을 중지한 뒤 진행상태를 리셋할 수 있습니다."
            : "보존된 계정 위치·최근 No·현재/전체 집계·완료 shortcode를 초기화합니다.";
        }
        teAutoPyUpdateAccountQueueStatus();
      }
      function tePySetStatus(ok, detail) {
        const st = document.getElementById("igblue_py_status");
        const good = !!ok;
        const now = Date.now();
        if (good) {
          tePyLastOkAt = now;
          tePyStatusFailSince = 0;
        } else if (!tePyStatusFailSince) {
          tePyStatusFailSince = now;
        }
        const recentOk = !!(tePyLastOkAt && now - tePyLastOkAt < 20000);
        const transientWait = !good && !!teAutoPyRunning && (recentOk || now - tePyStatusFailSince < 60000);
        if (st) {
          st.textContent = good ? (teAutoPyRunning ? "Py RUN" : "Py OK") : (transientWait ? "WS 재확인" : "WS 대기");
          st.classList.toggle("is-ok", !!(good || transientWait));
          st.title = detail ? (transientWait ? ("WS 재확인 중: " + String(detail)) : String(detail)) : (good ? "WS 연결됨" : (transientWait ? "WS 재확인 중" : "WS 연결 대기"));
        }
        teAutoPyUpdateButtons(!!(good || transientWait));
        teAutoPyApplyCdpNotice();
      }
      async function tePyPingOnce(forceProbe = false) {
        try {
          const res = await teAiwkWsRequest("ping", { host: teDownTxtHost(), href: teCurrentTargetUrl() }, 1500, { forceProbe: !!forceProbe });
          const ok = !!(res && res.ok);
          tePySetStatus(ok, ok ? "ping ok" : ((res && (res.error_code || res.error)) || "ping failed"));
          return ok;
        } catch (err) {
          tePySetStatus(false, String(err && err.message || err));
          return false;
        }
      }
      async function tePyPingStable(reason = "auto", attempts = 4, intervalMs = 900, forceProbe = false) {
        const total = Math.max(1, Number(attempts || 1) || 1);
        let i = 0;
        for (;;) {
          i += 1;
          if (!teAutoPyCheckedRaw()) return { ok: false, user_off: true, reason: "auto_py_checkbox_off", attempts: i - 1 };
          if (teAutoPyStopping) return { ok: false, stopped: true, reason: "user_stop", attempts: i - 1 };
          const ok = await tePyPingOnce(!!forceProbe);
          if (ok) { if (teAutoPyRunning) teAutoPyLeaveWait("WS8771 연결 정상화"); return { ok: true, attempts: i, reason: "ping_ok" }; }
          if (!teAutoPyRunning && i >= total) return { ok: false, user_off: false, reason: "ws_ping_failed_after_retry", attempts: i };
          const st = document.getElementById("igblue_py_status");
          if (st) { st.textContent = "WS 재확인"; st.title = `WS ping 재확인 중: ${String(reason || "auto")} (${i})`; }
          if (teAutoPyRunning) teAutoPyEnterWait("ws8771_disconnected", "WS8771 연결이 끊어져 정상화를 기다리는 중입니다.");
          await teSleep(intervalMs);
        }
      }
      function teInitPyAutoUi() {
        const cb = document.getElementById("igblue_auto_py_down");
        if (!cb) return;
        cb.checked = "1" === teLocalGet("download/auto_py", "1");
        teInitAutoPyLastRankUi();
        cb.addEventListener("change", () => {
          teLocalSet("download/auto_py", cb.checked ? "1" : "0");
          n(cb.checked ? "자동실행 준비" : "자동실행 사용 꺼짐");
          if (cb.checked) setTimeout(() => { tePyProbeStatusOnce("checkbox_on"); }, 50);
        });
        tePySetStatus(false, "WS 확인중...");
        // v147: auto_service_manager가 WS8771을 띄운 뒤에도 Side가 Py OFF에 고정되지 않도록
        // Side 표시용 ping만 느린 주기로 수행한다. ping은 정상 WebSocket handshake라서 빈 TCP health check와 다르다.
        tePyStartStatusProbe("side_init");
      }
      function teSleep(ms) { return new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0))); }
      function teAutoPySafeTarget(target) {
        const t = target && typeof target === "object" ? target : {};
        return !!(Number(t.rank || 0) && String(t.code || t.shortcode || "").trim());
      }
      function teAutoPyRequestWsStop(reason, detail) {
        try {
          return teAiwkWsRequest("ig_auto_cycle_state", {
            schema: IGBLUE_CYCLE_SCHEMA,
            build_version: IGBLUE_BUILD_VERSION,
            build_tag: IGBLUE_BUILD_TAG,
            cycle_stage: "S00_EXT_STOP_COMMAND",
            decision: "auto_stopped",
            action: "stop",
            reason: String(reason || "ext_stop"),
            stop_reason: String(reason || "ext_stop"),
            stop_reason_ko: String(detail || reason || "EXT stop"),
            autoplay_stop_reason: String(reason || "ext_stop"),
            autoplay_stop_reason_ko: String(detail || reason || "EXT stop"),
            href: teCurrentTargetUrl(),
            host: teDownTxtHost()
          }, 1200).catch(() => null);
        } catch (_) {
          return Promise.resolve(null);
        }
      }
      
      
      async function teAutoPyStop(reason) {
        if (!teAutoPyRunning && !teAutoPyStopping && !teAutoPyContinuationPending) return void n("자동실행: 실행중 아님");
        if (teAutoPyStopping) return;
        teAutoPySaveProgressState(reason || "user_stop");
        teAutoPyContinuationPending = false;
        teAutoPyStopping = true;
        teAutoPyRunning = false;
        const stopSeq = ++teAutoPyRunSeq;
        teAutoPySetUiPhase("user_stopped", { automatic_end: false, message: "사용자가 자동 실행을 중단했습니다.", completed_count: teAutoPyCompletedPostCount() });
        teAutoPyRenderRuntimeNotice("stop", "user_stop", "사용자가 자동 실행을 중단했습니다.");
        teAutoPyUpdateButtons(teAutoPyLastPyOk);
        teAutoEtaPersist("user_stop_requested");
        try {
          await Promise.race([
            teAutoPyRequestWsStop("user_stop", reason || "user"),
            teSleep(900)
          ]);
        } catch (_) {}
        if (Number(teAutoPyRunSeq || 0) !== Number(stopSeq || 0) || teAutoPyRunning) return;
        teAutoPyStopping = false;
        teAutoPyStatsFinish("user_stop_complete");
        teTouchTabSession(false, "user_stop_complete");
        teAutoPyUpdateButtons(teAutoPyLastPyOk);
        n("자동중지" + (reason ? ": " + reason : ""));
      }
      function teAutoPyStopReasonKo(code, detail) {
        const c = String(code || "unknown_stop");
        const map = {
          py_off: "WS8771 연결 대기 중 사용자가 자동을 중단함",
          auto_start_tab_closed: "자동실행 시작 탭이 닫혀 자동 중단",
          auto_start_tab_non_instagram: "자동실행 탭의 Instagram 주소 복귀 대기",
          user_window_in_use: "사용자가 다른 탭 또는 Chrome 창을 사용 중",
          user_stop: "사용자가 AutoPlay 중지를 요청함",
          run_seq_changed: "새 AutoPlay 실행/중지 신호로 현재 사이클 중단",
          direct_media_url_missing: "현재 No의 직접 다운로드 URL을 못 읽어 자동 중단",
          direct_download_failed: "Chrome 직접 다운로드 또는 저장 완료 처리가 실패해 자동 중단",
          direct_finalize_failed: "파일 저장 후 JSON·보고서 완료 처리가 실패해 자동 중단",
          target_rank_mismatch: "요청 No와 Main active No가 달라 자동 중단",
          target_shortcode_mismatch: "요청 No의 snapshot shortcode와 Main active shortcode가 달라 자동 중단",
          target_incomplete: "현재 No의 필수 정보가 부족해 자동 중단",
          stopped_before_target_read: "target 읽기 직전 AutoPlay가 중단됨",
          stopped_before_next_rank: "다음 No 처리 직전 AutoPlay가 중단됨",
          download_point_not_loaded: "최종 다운로드 지점까지 로딩하지 못해 자동 중단",
          current_url_end_reached: "현재 주소의 끝까지 도달해 자동 중단",
        };
        let msg = map[c] || ("AutoPlay 중단: " + c);
        const d = String(detail || "").trim();
        if (d && !msg.includes(d)) msg += " / " + d;
        return msg;
      }
      function teAutoPyTargetReadCheck(target, media, rankNo) {
        const t = target && typeof target === "object" ? target : {};
        const m = media && typeof media === "object" ? media : {};
        const activeRank = Number(t.rank || 0) || 0;
        const requestedRank = Number(rankNo || m.__igblue_auto_snapshot_no || 0) || 0;
        const activeShortcode = String(t.code || t.shortcode || "").trim();
        const expectedShortcode = String(m.__igblue_auto_snapshot_shortcode || m.code || m.shortcode || "").trim();
        const shortcode = String(activeShortcode || expectedShortcode || "").trim();
        const videoUrl = teAutoPySourceUrlForMedia(m, {}, "mp4");
        const imageUrl = teAutoPySourceUrlForMedia(m, {}, "jpg");
        const assetKind = videoUrl ? "video" : imageUrl ? "image" : "";
        const hasDirectSource = !!(videoUrl || imageUrl);
        const ownerUsername = String(teMediaOwnerName(m) || t.owner_username || t.username || "").trim();
        const strictSnapshot = !!(m.__igblue_auto_snapshot_no || m.__igblue_auto_snapshot_key);
        const hasRank = !!(activeRank && requestedRank);
        const rawRankMatches = !!(activeRank && requestedRank && activeRank === requestedRank);
        const shortcodeConfirmsRequested = !!(activeShortcode && expectedShortcode && activeShortcode === expectedShortcode);
        const rankStaleConfirmed = !!(!strictSnapshot && !rawRankMatches && shortcodeConfirmsRequested);
        const rankMatches = strictSnapshot ? rawRankMatches : (rawRankMatches || rankStaleConfirmed);
        const shortcodeMatches = strictSnapshot ? shortcodeConfirmsRequested : (!activeShortcode || !expectedShortcode || shortcodeConfirmsRequested);
        const A = !!(hasRank && rankMatches);
        const B = !!shortcode;
        const C = !!assetKind;
        const D = !!hasDirectSource;
        const missing = [];
        if (!hasRank) missing.push("missing_rank");
        if (hasRank && !rankMatches) missing.push("rank_mismatch");
        if (!B) missing.push("missing_shortcode");
        if (strictSnapshot && !activeShortcode) missing.push("missing_active_shortcode");
        if (strictSnapshot && activeShortcode && expectedShortcode && !shortcodeMatches) missing.push("shortcode_mismatch");
        if (!C) missing.push("missing_asset_kind");
        if (!D) missing.push("missing_direct_media_url");
        if (!ownerUsername) missing.push("missing_owner_username");
        const ok = A && B && C && D && shortcodeMatches && !!ownerUsername;
        const targetTs = Number(t.ts || t.updated_ts || 0) || 0;
        const ageMs = targetTs ? Math.max(0, Date.now() - targetTs) : 0;
        const mismatchReason = missing.includes("shortcode_mismatch") || missing.includes("missing_active_shortcode") ? "target_shortcode_mismatch" : missing.includes("rank_mismatch") ? "target_rank_mismatch" : missing.includes("missing_direct_media_url") ? "direct_media_url_missing" : "target_incomplete";
        return {
          ok,
          reason: ok ? (rankStaleConfirmed ? "target_complete_rank_stale_shortcode_confirmed" : strictSnapshot ? "target_complete_snapshot_rank_shortcode_confirmed" : "target_complete") : missing.join(","),
          stop_reason: ok ? "" : mismatchReason,
          stop_reason_ko: ok ? "" : teAutoPyStopReasonKo(mismatchReason, missing.join(",")),
          active_rank_no: activeRank,
          requested_rank_no: requestedRank,
          active_shortcode: activeShortcode,
          expected_shortcode: expectedShortcode,
          shortcode,
          asset_kind: assetKind,
          owner_username: ownerUsername,
          target_source: String(t.source || "main_active_target"),
          target_A_rank: A,
          target_B_shortcode: B,
          target_C_asset_kind: C,
          target_D_direct_media_url: D,
          target_D_button_rect: false,
          target_missing_fields: missing.join(","),
          rank_raw_match: rawRankMatches,
          rank_stale_confirmed: rankStaleConfirmed,
          shortcode_confirms_requested: shortcodeConfirmsRequested,
          shortcode_match: shortcodeMatches,
          strict_snapshot_check: strictSnapshot,
          auto_snapshot_key: String(m.__igblue_auto_snapshot_key || ""),
          active_target_age_ms: ageMs,
          target_has_raw: !!target,
          target_has_code_raw: !!String(t.code || ""),
          target_resolved_from_media: !String(t.code || "") && !!String(m.code || ""),
          direct_video_url_present: !!videoUrl,
          direct_image_url_present: !!imageUrl
        };
      }
      function teAutoPyMediaNorm(mediaType) {
        const m = String(mediaType || "").toLowerCase();
        if (m === "jpg" || m === "jpeg" || m === "image" || m === "photo") return "jpg";
        return "mp4";
      }
      function teAutoPyAssetKindForMedia(mediaType) {
        return teAutoPyMediaNorm(mediaType) === "jpg" ? "image" : "video";
      }
      function teAutoPyButtonMediaOf(btn, total) {
        const b = btn && typeof btn === "object" ? btn : {};
        const direct = String(b.media_type || b.kind || "").toLowerCase().trim();
        if (/^(mp4|video)$/.test(direct)) return "mp4";
        if (/^(jpg|jpeg|image|photo)$/.test(direct)) return "jpg";
        const text = String(b.text || "").toLowerCase();
        if (/mp4|video|동영상/.test(text)) return "mp4";
        if (/jpg|jpeg|image|photo|사진|이미지/.test(text)) return "jpg";
        const idx = Number(b.index);
        if (Number.isFinite(idx) && Number(total || 0) > 1 && idx === Number(total || 0) - 1) return "mp4";
        return "jpg";
      }
      function teAutoPyRectFromButtonItem(btn) {
        const b = btn && typeof btn === "object" ? btn : {};
        const r = b.rect && typeof b.rect === "object" ? b.rect : b.button_rect && typeof b.button_rect === "object" ? b.button_rect : b;
        const cx = Number(r.cx || 0) || (Number(r.x || r.left || 0) + Number(r.w || r.width || 0) / 2);
        const cy = Number(r.cy || 0) || (Number(r.y || r.top || 0) + Number(r.h || r.height || 0) / 2);
        const w = Number(r.w || r.width || 0) || 0;
        const h = Number(r.h || r.height || 0) || 0;
        if (!Number.isFinite(cx) || !Number.isFinite(cy) || !cx || !cy) return null;
        return Object.assign({}, r, { cx: Math.round(cx), cy: Math.round(cy), w: Math.round(w), h: Math.round(h) });
      }
      function teAutoPyButtonRectForMedia(target, mediaType) {
        const want = teAutoPyMediaNorm(mediaType);
        const t = target && typeof target === "object" ? target : {};
        const automationList = Array.isArray(t.automation_button_rects) ? t.automation_button_rects : [];
        const automationRequired = t.automation_target_required === true || t.visual_overlay_enabled === true;
        const list = automationList.length ? automationList : (automationRequired ? [] : (Array.isArray(t.download_button_rects) ? t.download_button_rects : []));
        for (const item of list) {
          if (!item || typeof item !== "object") continue;
          if (item.visible === false) continue;
          const got = teAutoPyButtonMediaOf(item, list.length);
          if (got !== want) continue;
          const rect = teAutoPyRectFromButtonItem(item);
          if (rect) return { rect, source: automationList.length ? "automation_button_rects" : "download_button_rects", item };
        }
        if (want === "mp4") {
          const rect = teAutoPyRectFromButtonItem(t.button_rect || {});
          if (rect) return { rect, source: "active_button_rect", item: null };
        }
        return null;
      }
      function teAutoPyTargetForMedia(target, mediaType) {
        const want = teAutoPyMediaNorm(mediaType);
        const found = teAutoPyButtonRectForMedia(target, want);
        if (!found || !found.rect) return null;
        const t = Object.assign({}, target || {});
        t.button_rect = found.rect;
        t.target_media_type = want;
        t.asset_kind = teAutoPyAssetKindForMedia(want);
        t.button_rect_source = found.source;
        t.button_rect_item = found.item || null;
        return t;
      }
      function teAutoPyViewportOfTarget(target) {
        const t = target && typeof target === "object" ? target : {};
        const vp = t.viewport && typeof t.viewport === "object" ? t.viewport : {};
        const w = Number(vp.innerWidth || vp.width || vp.clientWidth || 0) || 0;
        const h = Number(vp.innerHeight || vp.height || vp.clientHeight || 0) || 0;
        return { w, h, raw: vp };
      }
      function teAutoPyMediaButtonState(target, mediaType) {
        const want = teAutoPyMediaNorm(mediaType);
        const found = teAutoPyButtonRectForMedia(target, want);
        if (!found || !found.rect) return { ok: false, media_type: want, reason: "missing_" + want + "_button_rect", rect: null, found: null };
        const r = found.rect || {};
        const vp = teAutoPyViewportOfTarget(target);
        const cx = Number(r.cx || 0), cy = Number(r.cy || 0);
        const x = Number(r.x || r.left || 0), y = Number(r.y || r.top || 0);
        const w = Number(r.w || r.width || 0), h = Number(r.h || r.height || 0);
        const hasPoint = Number.isFinite(cx) && Number.isFinite(cy) && cx > 0 && cy > 0;
        if (!hasPoint) return { ok: false, media_type: want, reason: "invalid_" + want + "_button_center", rect: r, found };
        if (vp.w > 0 && vp.h > 0) {
          const centerOutside = cx < 1 || cy < 1 || cx > vp.w - 1 || cy > vp.h - 1;
          const rectClipped = Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0 && (x < 0 || y < 0 || x + w > vp.w || y + h > vp.h);
          if (centerOutside) return { ok: false, media_type: want, reason: "unsafe_" + want + "_button_center_outside_viewport", rect: r, viewport: vp, found };
          if (rectClipped) return { ok: false, media_type: want, reason: "clipped_" + want + "_button_rect", rect: r, viewport: vp, found };
        }
        return { ok: true, media_type: want, reason: "button_rect_ok", rect: r, viewport: vp, found };
      }
      async function teAutoPyReadActiveTargetOnce(waitMs = 260) {
        await teSleep(Math.max(0, Number(waitMs || 0) || 0));
        const got = await teSendToTargetTabResponse({ type: "igblue_get_active_download_target" }, "", 1800).catch((err) => ({ ok: false, error: String(err && err.message || err) }));
        return { got, target: got && got.target };
      }
      async function teAutoPyRefocusCurrentForMediaButton(mediaType, target, rankNo, currentMedia, cycleBase, reason) {
        const want = teAutoPyMediaNorm(mediaType);
        const before = teAutoPyMediaButtonState(target, want);
        await teAutoPyCycle(want === "jpg" ? "S21_JPG_BUTTON_REFOCUS" : "S10_MP4_BUTTON_REFOCUS", Object.assign({}, cycleBase || {}, {
          decision: "refocus_current_no",
          action: "dom_click_NoNav_Current_then_reread_target",
          media_type: want,
          reason: String(reason || before.reason || "button_rect_not_ready"),
          before_button_state: before,
          requested_rank_no: Number(rankNo || 0) || 0,
          expected_shortcode: String(currentMedia && (currentMedia.code || currentMedia.shortcode) || "")
        }));
        const clickRes = await teAutoPyClickCurrentNoNav(Object.assign({}, cycleBase || {}, { current_refocus: true, current_reason: "media_button_rect_retry_" + want, media_type: want, refocus_reason: String(reason || before.reason || "button_rect_not_ready") }), 220, want === "jpg" ? "S21_JPG_CURRENT_RECLICK" : "S10_MP4_CURRENT_RECLICK");
        const rr = await teAutoPyReadActiveTargetOnce(220);
        const afterTarget = rr.target || null;
        const after = teAutoPyMediaButtonState(afterTarget, want);
        await teAutoPyCycle(want === "jpg" ? "S21_JPG_BUTTON_RECHECK" : "S10_MP4_BUTTON_RECHECK", Object.assign({}, cycleBase || {}, {
          decision: after.ok ? "button_rect_ready" : "button_rect_still_bad",
          action: "reread_active_target_after_current_click",
          media_type: want,
          reason: after.reason,
          after_button_state: after,
          current_click_response: clickRes || null,
          reread_response: rr.got || null,
          active_rank_no: Number(afterTarget && afterTarget.rank || 0) || 0,
          active_shortcode: String(afterTarget && (afterTarget.code || afterTarget.shortcode) || "")
        }));
        return { ok: !!after.ok, target: afterTarget, state: after, before, current_click_response: clickRes || null, reread_response: rr.got || null, error: after.reason };
      }
      function teAutoPySourceUrlForMedia(media, collectMeta, mediaType) {
        const m = media && typeof media === "object" ? media : {};
        const c = collectMeta && typeof collectMeta === "object" ? collectMeta : {};
        if (teAutoPyMediaNorm(mediaType) === "jpg") {
          return String(m.img_origin || m.img_large || m.img_small || c.image_url || c.img_origin || c.source_image_url || "");
        }
        return String(m.video_url || c.video_url || c.source_url || "");
      }
      function teAutoPyPrefixForMedia(collectMeta, mediaType) {
        const c = collectMeta && typeof collectMeta === "object" ? collectMeta : {};
        const exp = c.expected_files && typeof c.expected_files === "object" ? c.expected_files : {};
        return teAutoPyMediaNorm(mediaType) === "jpg" ? String(exp.jpg || `${c.filename_stem || "media"}.jpg`) : String(exp.mp4 || `${c.filename_stem || "media"}.mp4`);
      }
      function teAutoPyCollectMetaForMedia(baseMeta, media, mediaType) {
        const want = teAutoPyMediaNorm(mediaType);
        const url = teAutoPySourceUrlForMedia(media, baseMeta, want);
        const prefix = teAutoPyPrefixForMedia(baseMeta, want);
        const ext = want === "jpg" ? "jpg" : "mp4";
        const originalInfo = teOriginalInstagramFileInfo(url || "");
        return Object.assign({}, baseMeta || {}, {
          asset_kind: teAutoPyAssetKindForMedia(want),
          media_kind: teAutoPyAssetKindForMedia(want),
          target_media_type: want,
          source_url: url,
          media_url: url,
          file_name: prefix,
          download_prefix: prefix,
          instagram_original_ext: originalInfo.original_ext || ext,
          instagram_original_filename: originalInfo.original_filename,
          instagram_original_filename_with_ext: originalInfo.original_filename_with_ext
        });
      }
      function teAutoPyWsTemporaryFailure(value) {
        const v = value && typeof value === "object" ? value : {};
        const nested = v.exists && typeof v.exists === "object" ? v.exists : {};
        const parts = [
          v.error_code, v.error, v.message, v.state,
          nested.error_code, nested.error, nested.message, nested.state
        ].map((x) => String(x || "").trim()).filter(Boolean);
        const code = parts.join(" ").toUpperCase();
        if (v.expected_offline || nested.expected_offline || String(v.state || nested.state || "").toLowerCase() === "ws_waiting") return true;
        return [
          "SIDE_WS_MODULE_NOT_LOADED",
          "AUTO_DIRECT_WS_NOT_READY",
          "AUTO_DIRECT_PRECHECK_EXCEPTION",
          "IGBLUE_WS_NOT_READY",
          "IGBLUE_WS_PRECHECK_EXCEPTION",
          "IGBLUE_BG_WS_EXCEPTION",
          "RUNTIME_SEND_FAILED",
          "EMPTY_DOWNLOAD_RESPONSE",
          "THREAD_QUEUE_RESULT_TIMEOUT",
          "THREAD_QUEUE_JOB_NOT_FOUND",
          "WEBSOCKET",
          "CONNECTION REFUSED",
          "ECONNREFUSED",
          "WS_WAITING"
        ].some((token) => code.includes(token));
      }
      async function teAutoPyWaitForWsRetry(seq, message) {
        if (!teAutoPyRunning || teAutoPyStopping || Number(seq || 0) !== Number(teAutoPyRunSeq || 0)) return false;
        teAutoPyEnterWait("ws8771_disconnected", String(message || "WS8771 연결이 끊어져 정상화를 기다리는 중입니다."));
        await teSleep(900);
        const stable = await tePyPingStable("auto_ws_operation_retry", 4, 900, true);
        return !!(stable && stable.ok && teAutoPyRunning && !teAutoPyStopping && Number(seq || 0) === Number(teAutoPyRunSeq || 0));
      }
      async function teAutoPyPrecheckMedia(mediaType, currentMedia, target, collectMeta, cycleBase) {
        const want = teAutoPyMediaNorm(mediaType);
        const meta = teAutoPyCollectMetaForMedia(collectMeta, currentMedia, want);
        const url = teAutoPySourceUrlForMedia(currentMedia, collectMeta, want);
        const prefix = teAutoPyPrefixForMedia(collectMeta, want);
        const assetKind = teAutoPyAssetKindForMedia(want);
        if (!prefix || meta.owner_missing || !teCleanOwnerAccountName(meta.owner_username)) {
          await teAutoPyCycle(want === "jpg" ? "S20_JPG_OWNER_MISSING" : "S07_MP4_OWNER_MISSING", Object.assign({}, cycleBase, {
            decision: "download_blocked",
            action: "owner_required_before_precheck",
            reason: "media_owner_username_not_found",
            media_type: want,
            asset_kind: assetKind,
            shortcode: String(meta.shortcode || target?.code || currentMedia?.code || "")
          }));
          return { media_type: want, asset_kind: assetKind, url, prefix, meta, precheck: { ok: false, error: "owner_missing" }, duplicate: { duplicate_confirmed: false, reason: "owner_missing" }, duplicate_confirmed: false, owner_missing: true, error: "owner_missing" };
        }
        const sideMemoryDuplicate = assetKind === "video" && teDownItemExists(meta, assetKind);
        // v221: Side 메모리 중복만으로 완료 처리하지 않는다.
        // 기존 파일의 JSON/ECharts 보고서 재생성 결과를 받아야 하므로 WS ig_media_exists를 항상 호출한다.
        const retrySeq = Number(teAutoPyRunSeq || 0);
        let pre = null;
        for (;;) {
          pre = await teMediaPrecheckAiwk({
            url,
            prefix,
            account: teDownTxtAccount(),
            cycle: Object.assign({}, cycleBase, { cycle_stage: want === "jpg" ? "S20_JPG_DUPLICATE_PRECHECK" : "S07_DUPLICATE_PRECHECK" }),
            meta: Object.assign({}, meta, { asset_kind: assetKind, file_name: prefix, download_prefix: prefix, source_url: url })
          });
          if (!teAutoPyWsTemporaryFailure(pre)) break;
          const recovered = await teAutoPyWaitForWsRetry(retrySeq, "WS8771 중복 확인 연결이 끊어져 같은 게시물에서 정상화를 기다리는 중입니다.");
          if (!recovered) return { media_type: want, asset_kind: assetKind, url, prefix, meta, precheck: pre, duplicate: { duplicate_confirmed: false, reason: "stopped_while_waiting_ws" }, duplicate_confirmed: false, stopped: true, error: "stopped_while_waiting_ws" };
        }
        const dup = teAutoPyDuplicateDecision(pre, assetKind, String(collectMeta.shortcode || target.code || (currentMedia && currentMedia.code) || ""));
        if (pre && pre.local && pre.local.item) teRecordDownloadedItem(pre.local.item);
        else if (pre && pre.item) teRecordDownloadedItem(pre.item);
        return { media_type: want, asset_kind: assetKind, url, prefix, meta, precheck: pre, duplicate: dup, duplicate_confirmed: !!(dup && dup.duplicate_confirmed), side_memory_duplicate: !!sideMemoryDuplicate };
      }
      function teAutoPyDirectReportResultFromResponse(res) {
        const r = res && typeof res === "object" ? res : {};
        const direct = r.result && typeof r.result === "object" ? r.result : {};
        const directFinalize = direct.finalize && typeof direct.finalize === "object" ? direct.finalize : {};
        const notify = r.notify_aiwk && typeof r.notify_aiwk === "object"
          ? r.notify_aiwk
          : (Object.keys(directFinalize).length ? directFinalize : {});
        const exists = r.exists && typeof r.exists === "object" ? r.exists : {};
        const refresh = exists.metadata_refresh && typeof exists.metadata_refresh === "object" ? exists.metadata_refresh : {};
        const duplicateReport = refresh.report && typeof refresh.report === "object" ? refresh.report : {};
        const alreadyDone = r.status === "already_done";
        const jsonPath = String(notify.json_path || r.json_path || directFinalize.json_path || refresh.json_path || "");
        const reportOk = alreadyDone ? !!duplicateReport.ok : !!(notify.report_ok || r.report_ok || directFinalize.report_ok);
        const reportPath = String((alreadyDone ? duplicateReport.report_path : (notify.report_path || r.report_path || directFinalize.report_path)) || "");
        const reportError = String((alreadyDone ? duplicateReport.error : (notify.report_error || r.report_error || directFinalize.report_error)) || "");
        const notifyOk = alreadyDone
          ? !!(r.ok && jsonPath)
          : !!(notify.ok || r.storage_ok || direct.finalize_ok || directFinalize.storage_ok);
        const fileSaved = alreadyDone ? !!r.file_path : !!(r.file_saved || r.file_path || direct.file_path);
        return {
          ok: !!(fileSaved && notifyOk && jsonPath && reportOk && reportPath),
          already_done: alreadyDone,
          file_saved: fileSaved,
          json_saved: !!jsonPath,
          json_path: jsonPath,
          report_saved: !!(reportOk && reportPath),
          report_ok: reportOk,
          report_path: reportPath,
          report_error: reportError,
          notify_ok: notifyOk,
          notify: notify,
          response: r
        };
      }
      async function teAutoPyDirectDownloadAsset(mediaType, currentMedia, collectMeta, cycleBase, extraMeta = {}) {
        const want = teAutoPyMediaNorm(mediaType);
        const meta = Object.assign({}, teAutoPyCollectMetaForMedia(collectMeta, currentMedia, want), extraMeta || {}, {
          auto_direct_download: true,
          direct_finalize_required: true
        });
        const url = teAutoPySourceUrlForMedia(currentMedia, collectMeta, want);
        const prefix = teAutoPyPrefixForMedia(collectMeta, want);
        const accountFolderMerge = false;
        const account = accountFolderMerge ? "" : teSafePath(meta.owner_username || teDownTxtAccount() || teActiveFolderAccountName() || "instagram");
        const stagePrefix = want === "jpg" ? "S2" : "S1";
        const base = Object.assign({}, cycleBase || {}, {
          media_type: want,
          asset_kind: meta.asset_kind,
          shortcode: String(meta.shortcode || ""),
          account,
          account_folder_merge: accountFolderMerge,
          file_name: prefix
        });
        if (!url) {
          await teAutoPyCycle(stagePrefix + "0_DIRECT_URL_MISSING", Object.assign({}, base, { decision: "download_blocked", action: "require_direct_media_url", reason: "direct_media_url_missing" }));
          return { ok: false, error: "direct_media_url_missing", error_code: "direct_media_url_missing", file_saved: false, json_saved: false, report_saved: false };
        }
        if (!prefix || meta.owner_missing || !teCleanOwnerAccountName(meta.owner_username)) {
          await teAutoPyCycle(stagePrefix + "0_DIRECT_OWNER_MISSING", Object.assign({}, base, { decision: "download_blocked", action: "require_owner_before_direct_download", reason: "media_owner_username_not_found" }));
          return { ok: false, error: "owner_missing", error_code: "owner_missing", file_saved: false, json_saved: false, report_saved: false };
        }
        teUpdateDownloadFolderPath(account);
        await teAutoPyCycle(stagePrefix + "0_DIRECT_DOWNLOAD_BEGIN", Object.assign({}, base, {
          decision: "direct_download_begin",
          action: "ws8771_direct_download",
          reason: "auto_download_via_ws8771_direct_url",
          source_url_present: true,
          source_url: String(url).slice(0, 260)
        }));
        const startedAt = Date.now();
        const retrySeq = Number(teAutoPyRunSeq || 0);
        const sendDirectOnce = () => teDownloadOnce(url, prefix, () => new Promise((resolve) => {
          try {
            chrome.runtime.sendMessage({
              type: "IGBLUE_DOWNLOAD_URL",
              url,
              prefix,
              root: tePersistDownloadRootNow(),
              account,
              account_folder_merge: accountFolderMerge,
              notify_aiwk: true,
              auto_direct_download: true,
              href: teCurrentTargetUrl(),
              meta: tePrepareDownloadFlowMeta(Object.assign({ href: teCurrentTargetUrl(), page_url: teCurrentTargetUrl(), account_folder_merge: accountFolderMerge }, meta), { source_mode: "TOP_SORTED", client_batch_key: String(base.cycle_id || teDownloadClientId("top-batch")), batch_requested_count: 1 })
            }, (res) => {
              if (chrome.runtime.lastError) return resolve({ status: "error", message: chrome.runtime.lastError.message, error_code: "runtime_send_failed" });
              resolve(res || { status: "error", message: "empty_download_response", error_code: "empty_download_response" });
            });
          } catch (err) {
            resolve({ status: "error", message: String(err && err.message || err), error_code: "direct_download_exception" });
          }
        }));
        let response = null;
        for (;;) {
          response = await sendDirectOnce();
          if (response === false) {
            await teSleep(1900);
            continue;
          }
          if (!teAutoPyWsTemporaryFailure(response)) break;
          const recovered = await teAutoPyWaitForWsRetry(retrySeq, "WS8771 다운로드 연결이 끊어져 같은 파일에서 정상화를 기다리는 중입니다.");
          if (!recovered) return { ok: false, stopped: true, error: "stopped_while_waiting_ws", error_code: "stopped_while_waiting_ws", file_saved: false, json_saved: false, report_saved: false };
        }
        const result = teAutoPyDirectReportResultFromResponse(response);
        result.media_type = want;
        result.asset_kind = meta.asset_kind;
        result.file_name = prefix;
        result.file_path = String(response && (response.filename || response.file_path) || "");
        result.elapsed_ms = Date.now() - startedAt;
        result.error = result.ok ? "" : String(result.report_error || response && (response.error_code || response.message) || (result.notify_ok ? "direct_finalize_incomplete" : "direct_finalize_failed"));
        result.error_code = result.ok ? "" : (result.file_saved ? "direct_finalize_failed" : "direct_download_failed");
        if (result.file_path) teRecordDownloadedFile(result.file_path, result.notify && result.notify.item);
        await teAutoPyCycle(stagePrefix + "4_DIRECT_FINALIZE_RESULT", Object.assign({}, base, {
          decision: result.ok ? "direct_asset_complete" : "direct_asset_failed",
          action: "file_json_report_finalize_result",
          reason: result.ok ? "file_json_report_all_saved" : result.error_code,
          file_path: result.file_path,
          file_saved: result.file_saved,
          json_saved: result.json_saved,
          json_path: result.json_path,
          report_saved: result.report_saved,
          report_path: result.report_path,
          report_error: result.report_error,
          notify_ok: result.notify_ok,
          elapsed_ms: result.elapsed_ms
        }));
        return result;
      }
      async function teAutoPyDownloadMediaPairDirect(currentMedia, target, rankNo, cycleBase) {
        const collectMeta = teBuildAutoPyCollectMeta(currentMedia, target || { rank: rankNo, code: currentMedia && (currentMedia.code || currentMedia.shortcode) || "" });
        const results = [];
        for (const kind of ["mp4", "jpg"]) {
          teAutoPySetUiPhase("downloading", { current_no: Number(rankNo || 0) || 0, asset_kind: kind, target_count: Math.max(1, Number(teAutoPyStats.target_count || teAutoPyTargetCount && teAutoPyTargetCount() || 1) || 1), completed_count: teAutoPyCompletedPostCount() });
          const pre = await teAutoPyPrecheckMedia(kind, currentMedia, target || {}, collectMeta, cycleBase || {});
          if (pre && pre.stopped) return { ok: false, stopped: true, error: String(pre.error || "stopped_while_waiting_ws"), error_code: String(pre.error || "stopped_while_waiting_ws"), results };
          if (pre && pre.owner_missing) return { ok: false, error: "owner_missing", error_code: "owner_missing", results };
          const progressShortcode = String(collectMeta.shortcode || currentMedia && (currentMedia.code || currentMedia.shortcode) || "").trim();
          if (pre && pre.duplicate_confirmed) {
            teAutoPyRegisterExpectedAsset(progressShortcode, kind);
            const dupRaw = pre.precheck && typeof pre.precheck === "object" ? pre.precheck : {};
            const dupReportOk = !!(dupRaw.report_ok && dupRaw.report_path);
            const duplicateResult = { media_type: kind, ok: dupReportOk, skipped: true, duplicate: true, precheck: pre, json_saved: !!dupRaw.json_path, json_path: String(dupRaw.json_path || ""), report_saved: dupReportOk, report_path: String(dupRaw.report_path || ""), report_error: String(dupRaw.report_error || "") };
            results.push(duplicateResult);
            if (dupReportOk) teAutoPyRegisterCompletedAsset(progressShortcode, kind);
            await teAutoPyCycle(kind === "jpg" ? "S24_DIRECT_DUPLICATE_SKIP" : "S14_DIRECT_DUPLICATE_SKIP", Object.assign({}, cycleBase || {}, { decision: dupReportOk ? "duplicate_report_ready" : "duplicate_report_failed", action: "skip_direct_download_after_metadata_report_refresh", reason: dupReportOk ? (pre.duplicate && pre.duplicate.reason || "duplicate_confirmed") : "duplicate_report_finalize_failed", media_type: kind, duplicate: pre.duplicate || null, json_path: duplicateResult.json_path, report_path: duplicateResult.report_path, report_saved: duplicateResult.report_saved, report_error: duplicateResult.report_error }));
            if (!dupReportOk) return { ok: false, error: "duplicate_report_finalize_failed", error_code: "direct_finalize_failed", results, collect_meta: collectMeta };
            continue;
          }
          if (!pre || !pre.url) {
            results.push({ media_type: kind, ok: true, skipped: true, source_missing: true });
            await teAutoPyCycle(kind === "jpg" ? "S24_DIRECT_SOURCE_SKIP" : "S14_DIRECT_SOURCE_SKIP", Object.assign({}, cycleBase || {}, { decision: "source_missing_skip", action: "skip_unavailable_asset", reason: "direct_media_url_missing", media_type: kind }));
            continue;
          }
          teAutoPyRegisterExpectedAsset(progressShortcode, kind);
          const asset = await teAutoPyDirectDownloadAsset(kind, currentMedia, collectMeta, cycleBase || {}, { requested_rank_no: Number(rankNo || 0) || 0 });
          results.push(asset);
          if (!(asset && asset.ok)) return { ok: false, error: asset && asset.error || "direct_download_failed", error_code: asset && asset.error_code || "direct_download_failed", results, collect_meta: collectMeta };
          teAutoPyRegisterCompletedAsset(progressShortcode, kind);
          teAutoPyStatsRecordAsset(kind, asset.elapsed_ms || 0);
        }
        const completed = results.filter((x) => x && x.ok && !x.skipped);
        const alreadySaved = results.filter((x) => x && x.ok && x.duplicate === true);
        const acceptable = results.length > 0 && results.every((x) => x && x.ok) && (completed.length > 0 || alreadySaved.length > 0);
        return { ok: acceptable, direct_download_done: completed.length > 0, already_saved_done: alreadySaved.length > 0, results, collect_meta: collectMeta, shortcode: String(collectMeta.shortcode || "") };
      }
      async function teAutoPyStopCycle(stage, base, code, detail, extra) {
        if (teAutoPySessionTarget && teAutoPySessionTarget.fatal_handled && String(code || "") !== "auto_start_tab_closed") return Object.assign({}, base || {}, extra || {}, { decision: "fatal_session_already_stopped", reason: String(code || "") });
        const msg = teAutoPyStopReasonKo(code, detail);
        const payload = Object.assign({}, base || {}, extra || {}, {
          decision: "auto_stopped",
          action: "stop",
          reason: code || "unknown_stop",
          stop_reason: code || "unknown_stop",
          stop_reason_ko: msg,
          autoplay_stop_reason: code || "unknown_stop",
          autoplay_stop_reason_ko: msg
        });
        await teAutoPyCycle(stage || "S99_AUTOPLAY_STOPPED", payload);
        teAutoPySetUiPhase(String(code || "") === "user_stop" ? "user_stopped" : "failed", { automatic_end: false, message: msg, completed_count: teAutoPyCompletedPostCount() });
        try { await teSendToTargetTabResponse({ type: "igblue_py_state", value: { state: "auto_stopped", message: msg, stop_reason: code || "unknown_stop", stop_reason_ko: msg, cycle: payload } }, "", 900); } catch {}
        const stoppedTabId = Number(teAutoPySessionTarget && teAutoPySessionTarget.tab_id || 0) || 0;
        if (stoppedTabId > 0) {
          const closeResult = await teRuntimeMessageResponse({ type: "IGBLUE_CLOSE_MANAGED_AUTO_TAB", tabId: stoppedTabId, reason: `auto_stop_${String(code || "unknown")}` });
          await teAutoPyCycle("S00_AUTO_TAB_FORCE_CLOSE", { cycle_id: teBuildCycleId("AUTO_TAB_FORCE_CLOSE"), run_seq: Number(teAutoPyRunSeq || 0), decision: closeResult && closeResult.ok ? "managed_tab_closed" : "managed_tab_close_failed", action: "cdp_target_close", reason: String(code || "unknown_stop"), tab_id: stoppedTabId, close_result: closeResult || null });
          teAutoPyResetSessionTarget();
        }
        n("자동 중단: " + msg);
        return payload;
      }
      function teAutoPyCurrentGeometrySignature() {
        try { return String(teWindowMoveState && teWindowMoveState.geometry_signature || ""); } catch (_) { return ""; }
      }
      async function teAutoPyStartOnceFromUi(options) {
        if (teAutoPyRunning) return void n("자동시작: 이미 실행중");
        // v279aa: Auto8700/WS8771이 기본 엔진이고 CDP는 예약 옵션이다.
        const baseReady = !!teAutoPyLastPyOk || !!(await tePyPingOnce(true));
        if (!baseReady) {
          teAutoRuntimeNoticeMode = "";
          teAutoRuntimeNoticeCode = "";
          teAiwkSetupNotice(true, "Auto8700/WS8771 미실행");
          n("Auto8700/WS8771이 실행되지 않았습니다. _St.zip 설치 후 _St\\Auto8700\\run_auto_service8700_manager.bat를 실행하세요.");
          return;
        }
        const opts = options && typeof options === "object" ? options : {};
        const newSession = opts.new_session !== false;
        const resumeProgress = !!opts.resume_progress;
        const progressState = resumeProgress ? (opts.progress_state || teAutoPyLoadProgressState()) : null;
        teAutoPyContinuationPending = false;
        if (newSession) {
          teProgressPreserveUntilNextDownload("auto_new_session_prepare");
          teAutoPyClearProgressState();
          teAutoPyResetSessionTarget();
          teAutoPyRunTotals = { completed_post_count: 0, completed_shortcodes: [], mp4_count: 0, jpg_count: 0, expected_asset_keys: [], completed_asset_keys: [] };
        } else if (resumeProgress && progressState) {
          teAutoPyResetSessionTarget();
          teAutoPySetAccountQueueIndex(Math.max(0, Number(progressState.account_queue_index || 0) || 0));
          teAutoPyRunTotals = Object.assign({ completed_post_count: 0, completed_shortcodes: [], mp4_count: 0, jpg_count: 0, expected_asset_keys: [], completed_asset_keys: [] }, teAutoPyClonePlain(progressState.run_totals, {}));
        }
        const seq = ++teAutoPyRunSeq;
        const runMode = resumeProgress && progressState ? "이어하기" : (newSession ? "처음부터" : "계정계속");
        teAutoPyFeatureCheckReset(seq);
        await teAutoPyFeatureCheck("AS-01", "[체크시작]", "자동시작 요청", { cycle_id: `run_${seq}`, run_mode: runMode });
        await teAutoPyFeatureCheck("AD-01", "[체크시작]", "자동 시작 상태 확인", { cycle_id: `run_${seq}` });
        if (!teAutoPyEnabled()) {
          await teAutoPyFeatureCheck("AS-01", "[체크실패]", "자동실행 사용 체크가 꺼져 있음 / 다음점검=Side 자동실행 사용 체크", { cycle_id: `run_${seq}`, run_mode: runMode, error_code: "auto_py_checkbox_off" });
          await teAutoPyFeatureCheck("AD-01", "[체크실패]", "자동 시작 체크가 꺼져 있음 / 다음점검=Side 자동 시작 체크", { cycle_id: `run_${seq}`, error_code: "auto_py_checkbox_off" });
          return void n("자동시작: 체크 필요");
        }
        const indexedStartPreview = (newSession || resumeProgress) ? teAutoPyFreshStartPreview() : null;
        let freshStartTarget = null;
        if (newSession || resumeProgress) {
          if (indexedStartPreview && indexedStartPreview.has && indexedStartPreview.url) {
            freshStartTarget = {
              url: String(indexedStartPreview.url || ""),
              account: String(indexedStartPreview.account || teAccountFromUrl(indexedStartPreview.url) || ""),
              index: Number(indexedStartPreview.index || 0) || 0,
              total: Number(indexedStartPreview.total || 0) || 0,
              reason: "auto_start_first_account"
            };
          } else {
            const startState = await teRuntimeMessageResponse({ type: "IGBLUE_GET_TARGET_STATE" });
            const startTabId = Number(startState && startState.tabId || 0) || 0;
            const startUrl = String(startState && startState.url || "");
            if (!(startTabId > 0 && teIsInstagramUrl(startUrl))) {
              const startMessage = "현재 페이지가 Instagram 페이지가 아닙니다. Instagram 페이지에서 시작을 눌러주세요.";
              teAutoPyRunning = false;
              teAutoPyStopping = false;
              teAutoPyResetSessionTarget();
              teSetTargetLabel(null);
              teAutoPySetUiPhase("failed", { automatic_end: false, message: startMessage, completed_count: teAutoPyCompletedPostCount() });
              teAutoPyRenderRuntimeNotice("stop", "start_instagram_tab_required", startMessage);
              await teAutoPyFeatureCheck("AS-02", "[체크실패]", startMessage, { cycle_id: `run_${seq}`, run_mode: runMode, error_code: "start_instagram_tab_required", account_queue_index: 0 });
              teAutoPyUpdateButtons(teAutoPyLastPyOk);
              n(startMessage);
              return { ok: false, error: "start_instagram_tab_required" };
            }
            freshStartTarget = { url: startUrl, account: teAccountFromUrl(startUrl) || "", index: 0, total: 0, reason: "auto_start_current_page_fresh_tab" };
          }
        }
        teAutoPyRunning = true;
        teAutoPyStopping = false;
        teAutoPyUpdateButtons(teAutoPyLastPyOk || true);
        await teAutoPyFeatureCheck("AD-01", "[체크완료]", "자동 시작 활성화", { cycle_id: `run_${seq}` });
        await teAutoPyFeatureCheck("AS-02", "[체크완료]", `실행방식=${runMode}`, { cycle_id: `run_${seq}`, run_mode: runMode, account_queue_index: teAutoPyAccountQueueIndex(), last_rank: Math.max(0, Number(progressState && progressState.last_rank || 0) || 0) });
        let sessionReady = null;
        if (freshStartTarget && freshStartTarget.url) {
          console.log("[YELLOW][NAV][SIDE_REQUEST]", { target_url: freshStartTarget.url, tab_id: 0, reason: freshStartTarget.reason, run_seq: String(seq || ""), account: freshStartTarget.account, account_index: freshStartTarget.index, account_total: freshStartTarget.total });
          const bootstrap = await teRuntimeMessageResponse({ type: "IGBLUE_NAVIGATE_ACTIVE_TAB", url: freshStartTarget.url, tabId: 0, reason: freshStartTarget.reason, run_seq: String(seq || ""), account: freshStartTarget.account, account_index: freshStartTarget.index, account_total: freshStartTarget.total });
          console.log("[YELLOW][NAV][SIDE_RESPONSE]", bootstrap || null);
          if (bootstrap && bootstrap.ok && Number(bootstrap.tabId || 0) > 0) {
            const tabId = Number(bootstrap.tabId || 0) || 0;
            teAutoPySessionTarget.active = true;
            teAutoPySessionTarget.session_id = `auto_${Date.now()}_${tabId}`;
            teAutoPySessionTarget.tab_id = tabId;
            teAutoPySessionTarget.start_url = freshStartTarget.url;
            teAutoPySessionTarget.generation = Number(bootstrap && bootstrap.generation || 0) || 0;
            teAutoPySessionTarget.fatal_handled = false;
            teCurrentTargetTabId = tabId;
            teSetTargetLabel({ tabId, url: freshStartTarget.url });
            const bootstrapAccount = String(freshStartTarget.account || teAccountFromUrl(freshStartTarget.url) || "").replace(/^@/, "");
            const bootstrapHandoff = await teAutoPyCommitTabHandoff(bootstrap, tabId, seq, bootstrapAccount, freshStartTarget.index, freshStartTarget.total);
            if (!(bootstrapHandoff && bootstrapHandoff.ok)) {
              sessionReady = { ok: false, error: String(bootstrapHandoff && bootstrapHandoff.error || "bootstrap_handoff_failed"), handoff_result: bootstrapHandoff || null };
            } else {
              teAutoPyAnnounce(freshStartTarget.index > 0 ? `계정 ${freshStartTarget.index}/${freshStartTarget.total} 새 탭 로드` : "현재 Instagram 페이지 새 자동 탭 로드", freshStartTarget.account || freshStartTarget.url);
              sessionReady = await teAutoPyWaitForSessionTarget(seq, true);
            }
          } else {
            sessionReady = { ok: false, error: String(bootstrap && bootstrap.error || "active_web_tab_not_found") };
          }
        } else {
          sessionReady = await teAutoPyWaitForSessionTarget(seq, true);
        }
        if (!(sessionReady && sessionReady.ok) || seq !== teAutoPyRunSeq) {
          if (seq === teAutoPyRunSeq && !teAutoPyStopping) await teAutoPyFeatureCheck("AS-02", "[체크실패]", "Instagram 실행 탭 확보 실패", { cycle_id: `run_${seq}`, run_mode: runMode, error_code: String(sessionReady && sessionReady.error || "start_tab_not_ready") });
          teAutoPyRunning = false;
          teAutoPyUpdateButtons(teAutoPyLastPyOk);
          return;
        }
        teTouchTabSession(true, resumeProgress ? "progress_continue" : (newSession ? "auto_start" : "auto_account_continue"));
        try {
          const accountPrep = await teAutoPyPrepareAccountQueueForStart(seq);
          if (!(accountPrep && accountPrep.ok) || seq !== teAutoPyRunSeq) {
            if (seq !== teAutoPyRunSeq || teAutoPyStopping || teAutoPySessionTarget.fatal_handled) return;
            await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", { cycle_id: teBuildCycleId("ACCOUNT_PREP_FAIL"), run_seq: seq, account_queue_result: accountPrep || null }, "account_prepare_failed", String(accountPrep && accountPrep.error || "account_prepare_failed"));
            return;
          }
          if (accountPrep.queue_completed) {
            teAutoPyFinishAccountQueue("없어진 계정 휴지통 처리 후 목록 완료", { account_index: Number(accountPrep.index || 0), account_total: Number(accountPrep.total || 0), queue_completed: true, exhausted: true, exhausted_reason: "deleted_accounts_only" });
            return { ok: true, account_queue_completed: true, deleted_accounts_only: true };
          }
          await teAutoPyFeatureCheck("AS-03", "[체크완료]", `진행상태=${Math.max(0, Number(accountPrep.index || teAutoPyAccountQueueIndex() || 0) || 0)}/${Math.max(0, Number(accountPrep.total || teAutoPyAccountQueueList().length || 0) || 0)}`, { cycle_id: `run_${seq}`, run_mode: runMode, account_queue_index: Math.max(0, Number(accountPrep.index || teAutoPyAccountQueueIndex() || 0) || 0), account_queue_total: Math.max(0, Number(accountPrep.total || teAutoPyAccountQueueList().length || 0) || 0), account: String(accountPrep.account || "") });
          if (!resumeProgress) teProgressPreserveUntilNextDownload(newSession ? "auto_start_new_run" : "auto_account_continue");
          const targetLimit = Math.max(1, Number(teAutoPyTargetCount && teAutoPyTargetCount() || 0) || 1);
          if (resumeProgress && progressState) teAutoPyRestoreProgressState(progressState, seq);
          else teAutoPyUiReset(targetLimit, newSession ? "자동 시작" : "다음 계정 계속", { reset_run_totals: newSession });
          try { await teLoadDownTxtAiwk(); } catch (_) { try { teLoadDownTxtLocal(teDownTxtAccount()); } catch (__) {} }
          const startPlan = resumeProgress && progressState
            ? { no: Math.max(0, Number(progressState.last_rank || teAutoPyStats.current_no || 0) || 0), source: "progress_continue_state" }
            : teAutoPyResolveStartNo();
          const targetNo = Number(startPlan && startPlan.no || 0) || 0;
          let requiredLoadCount = Math.max(1, targetNo || 1);
          teAutoPyStatsStart(seq, targetNo || 0, targetLimit, resumeProgress ? "progress_continue" : "auto_start_clicked");
          teAutoPyAnnounce("자동다운로드 준비: 후보를 발견하는 즉시 저장하고 목표가 남으면 다음 후보를 계속 탐색합니다.");
          let searchPass = 0;
          while (seq === teAutoPyRunSeq && !teAutoPyStopping && teAutoPyCompletedPostCount() < targetLimit && searchPass++ < 200) {
            const loadPlan = await teAutoPyEnsureDownloadPointLoaded(requiredLoadCount, seq, searchPass === 1 ? (startPlan && startPlan.source || "auto_start_target_scan") : "continue_after_immediate_download");
            if (!(loadPlan && loadPlan.ok) || seq !== teAutoPyRunSeq) {
              if (seq !== teAutoPyRunSeq || teAutoPyStopping || teAutoPySessionTarget.fatal_handled) return;
              const loadErr = String(loadPlan && (loadPlan.error || loadPlan.reason) || "download_point_not_loaded");
              if (loadErr === "current_url_end_reached" || loadErr === "date_filter_range_exhausted_empty") {
                const exhaustedMeta = { target_count: targetLimit, completed_count: teAutoPyCompletedPostCount(), exhausted: true, exhausted_reason: loadErr, filter_active: !!(loadPlan && loadPlan.filter_active), filter_result_empty: true, load_result: loadPlan || null };
                return await teAutoPyClickFirstTarget([], seq, Object.assign({}, startPlan || {}, exhaustedMeta));
              }
              await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", { cycle_id: teBuildCycleId("LOAD_POINT_FAIL"), run_seq: seq, rank_no: requiredLoadCount, rank_label: teAutoPyNoLabel(requiredLoadCount), load_result: loadPlan || null }, "download_point_not_loaded", String(loadPlan && (loadPlan.error || loadPlan.reason) || "download_point_load_failed"));
              return;
            }
            const exhaustedEmpty = !!(loadPlan && loadPlan.reason === "date_filter_range_exhausted_empty" && loadPlan.filter_result_empty);
            if (exhaustedEmpty) {
              teAutoPyAnnounce("필터 기간 안의 게시물이 더 이상 없습니다.", "현재 완료 수를 유지하고 다음 계정으로 이동합니다.");
              return await teAutoPyClickFirstTarget([], seq, Object.assign({}, startPlan || {}, { filter_result_empty: true, filter_empty_reason: "date_filter_range_exhausted_empty", exhausted: true, exhausted_reason: "date_filter_range_exhausted_empty", target_count: targetLimit, completed_count: teAutoPyCompletedPostCount() }));
            }
            const requested = Math.max(1, requiredLoadCount);
            const finalPlan = teAutoPyBuildDynamicRankPlan(requested);
            const filterActive = !!(finalPlan && finalPlan.filter_active || loadPlan && loadPlan.filter_active);
            const rawList = filterActive
              ? (Array.isArray(finalPlan && finalPlan.list) ? finalPlan.list : Array.isArray(loadPlan && loadPlan.list) ? loadPlan.list : [])
              : (finalPlan && finalPlan.list && finalPlan.list.length ? finalPlan.list : (loadPlan && loadPlan.list && loadPlan.list.length ? loadPlan.list : teSortedAllTopN(requested)));
            const listLimit = Math.max(targetLimit, requested, rawList.length);
            const list = rawList.slice(0, listLimit);
            const pendingNo = teAutoPyFirstPendingRankNo(list);
            if (!list.length) {
              await teAutoPyFeatureCheck("AD-02", "[체크실패]", "조건에 맞는 대상 게시물이 없음 / 날짜 범위 안이면 swipe 계속", { cycle_id: `run_${seq}_search_${searchPass}`, candidate_count: 0, target_count: targetLimit, requested_rank_no: requested, error_code: "no_matching_media_yet" });
            } else if (pendingNo > 0) {
              const first = list[pendingNo - 1] || {};
              const missingViewCount = list.filter((media) => !teMetricHasValue(media, "play_count")).length;
              const participationMetrics = teParticipationMetricNames();
              const rankMode = missingViewCount > 0 ? "private_views_active_metrics_sum" : "selected_metric";
              await teAutoPyFeatureCheck("AD-02", "[체크완료]", `신규 대상 즉시 선정 / 후보=${list.length} 다음=${teAutoPyNoLabel(pendingNo)} 조회수미제공=${missingViewCount} 첫대상=${String(first.code || first.shortcode || "?")}`, { cycle_id: `run_${seq}_search_${searchPass}`, candidate_count: list.length, pending_rank_no: pendingNo, target_count: targetLimit, shortcode: String(first.code || first.shortcode || ""), missing_view_count: missingViewCount, rank_mode: rankMode, participation_metrics: participationMetrics, first_views: teViewCountOrPrivate(first), first_views_available: teViewsAvailable(first) ? 1 : 0, first_participation_score: teParticipationScore(first) });
            }
            if (!(pendingNo > 0)) {
              if (loadPlan && (loadPlan.exhausted || loadPlan.partial)) {
                return await teAutoPyClickFirstTarget([], seq, Object.assign({}, startPlan || {}, { exhausted: true, exhausted_reason: String(loadPlan.reason || "current_url_end_reached"), target_count: targetLimit, completed_count: teAutoPyCompletedPostCount() }));
              }
              requiredLoadCount = Math.max(requested + 1, list.length + 1);
              teAutoPySetUiPhase("searching", { target_count: targetLimit, completed_count: teAutoPyCompletedPostCount(), message: `완료 ${teAutoPyCompletedPostCount()}/${targetLimit} · 다음 후보 탐색` });
              continue;
            }
            teAutoPySetUiPhase("searching", { target_count: targetLimit, completed_count: teAutoPyCompletedPostCount(), current_no: pendingNo, message: `${teAutoPyNoLabel(pendingNo)} 즉시 다운로드 준비` });
            teAutoPyAnnounce(`${teAutoPyNoLabel(pendingNo)} 후보를 즉시 다운로드합니다.`, `완료 ${teAutoPyCompletedPostCount()}/${targetLimit}`);
            const result = await teAutoPyClickFirstTarget(list, seq, Object.assign({}, startPlan || {}, { no: pendingNo, source: "first_pending_snapshot", filter_active: filterActive, filter_result_empty: !!(filterActive && list.length === 0), exhausted: !!(loadPlan && (loadPlan.exhausted || loadPlan.partial)), exhausted_reason: String(loadPlan && loadPlan.reason || ""), date_range_result: loadPlan && loadPlan.date_range_result || null, target_count: targetLimit }));
            if (!(result && result.continue_search)) return result;
            requiredLoadCount = Math.max(requested + 1, Number(result.loaded_candidate_count || list.length || 0) + 1);
          }
          if (teAutoPyCompletedPostCount() >= targetLimit) {
            const doneMeta = { target_count: targetLimit, completed_count: teAutoPyCompletedPostCount(), exhausted: false, exhausted_reason: "" };
            const accountAdvance = await teAutoPyAdvanceAccountQueueIfAny(seq, "actual_completed_posts_reached_target", doneMeta);
            if (accountAdvance && (accountAdvance.advanced || accountAdvance.completed_all)) return accountAdvance;
            teAutoPyFinalizeAutomaticEnd(`목표 ${doneMeta.completed_count}/${doneMeta.target_count} 완료`, doneMeta);
            return { ok: true, reason: "target_reached", completion: doneMeta };
          }
          await teAutoPyStopCycle("S00_SEARCH_GUARD_STOP", { cycle_id: teBuildCycleId("SEARCH_GUARD"), run_seq: seq, target_count: targetLimit, completed_count: teAutoPyCompletedPostCount(), requested_rank_no: requiredLoadCount }, "download_point_not_loaded", "즉시 다운로드 탐색 반복 안전 한도 초과");
          return;
        } finally {
          if (seq === teAutoPyRunSeq || teAutoPyStopping) {
            const statsStopReason = teAutoPyStopping ? "auto_stopped" : "auto_finished";
            teAutoPyRunning = false;
            teAutoPyStopping = false;
              teAutoPyStatsFinish(statsStopReason);
            teTouchTabSession(false, statsStopReason);
            if (!teAutoPyContinuationPending && !teAutoPySessionTarget.waiting && teAutoRuntimeNoticeMode === "resume") teAutoPyRenderRuntimeNotice("", "", "");
            teAutoPyUpdateButtons(teAutoPyLastPyOk);
          }
        }
      }
      async function teAutoPyClickFirstTarget(list, runSeq, startPlan) {
        const mySeq = Number(runSeq || teAutoPyRunSeq || 0);
        const arr = Array.isArray(list) ? list.filter(Boolean) : [];
        const targetCount = Math.max(1, Number(startPlan && startPlan.target_count || teAutoPyTargetCount && teAutoPyTargetCount() || 1) || 1);
        const exhausted = !!(startPlan && startPlan.exhausted);
        const exhaustedReason = String(startPlan && startPlan.exhausted_reason || "");
        if (!arr.length) {
          const filterEmpty = !!(startPlan && startPlan.filter_result_empty || teAutoPyAnyFilterActive());
          const emptyReason = filterEmpty ? "filter_result_empty" : "no_matching_media";
          const emptyDetail = filterEmpty
            ? "필터 결과 0개: 현재 URL 정상 종료 후 다음 URL 이동"
            : "조건에 맞는 Top media 0개 / empty_list";
          const adv = await teAutoPySkipCurrentAccountAndAdvanceIfAny(mySeq, emptyReason, emptyDetail, {
            top_sorted_count: 0,
            filter_active: filterEmpty,
            filter_result_empty: filterEmpty,
            filter_empty_reason: String(startPlan && startPlan.filter_empty_reason || ""),
            target_count: Number(teAutoPyTargetCount && teAutoPyTargetCount() || 0) || 0
          });
          if (adv && (adv.advanced || adv.completed_all)) return;
          const emptyMeta = { target_count: Math.max(1, Number(startPlan && startPlan.target_count || teAutoPyTargetCount && teAutoPyTargetCount() || 1) || 1), completed_count: teAutoPyCompletedPostCount(), exhausted: true, exhausted_reason: String(startPlan && (startPlan.exhausted_reason || startPlan.filter_empty_reason) || emptyReason) };
          teAutoPyFinalizeAutomaticEnd("더이상없음 · 대상 게시물 없음", emptyMeta);
          return;
        }
        const pyStable = await tePyPingStable("before_auto_snapshot", 4, 900, false);
        const pyOk = !!(pyStable && pyStable.ok);
        if (!pyOk || (mySeq && mySeq !== teAutoPyRunSeq)) {
          if ((mySeq && mySeq !== teAutoPyRunSeq) || teAutoPyStopping || teAutoPySessionTarget.fatal_handled) return;
          await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", { cycle_id: teBuildCycleId("PY_OFF"), run_seq: mySeq, ping_result: pyStable || null }, "py_off", String(pyStable && pyStable.reason || "py_ping_failed"));
          return;
        }
        let ctx = null;
        let autoSnapshotIndex = null;
        let labels = [];
        while ((!mySeq || mySeq === teAutoPyRunSeq) && !teAutoPyStopping) {
          ctx = teBuildRankContext(arr);
          autoSnapshotIndex = teAutoPyBuildNoSnapshotIndex(arr, ctx, "auto_start");
          try { ctx.auto_snapshot_index = autoSnapshotIndex; } catch (_) {}
          labels = teBuildMainRankLabels(arr, ctx);
          if (labels.length) break;
          teAutoPyEnterWait("no_rank_labels", "정렬 대상의 rank label이 준비되기를 기다리는 중입니다.");
          await teSleep(1200);
          const refreshedPlan = teAutoPyBuildDynamicRankPlan(targetCount);
          const refreshedList = Array.isArray(refreshedPlan && refreshedPlan.list) ? refreshedPlan.list.filter(Boolean).slice(0, targetCount) : [];
          if (refreshedList.length) arr = refreshedList;
        }
        if (!labels.length) return;
        teAutoPyLeaveWait("rank label 정상화");
        teScheduleMainRankLabels(arr, ctx, "auto_py_snapshot_ready");
        const requestedStartForMain = Math.max(1, Number(startPlan && startPlan.no || 0) || Number(teAutoPyResolveStartNo && teAutoPyResolveStartNo().no || 1) || 1);
        const requestedSnapshotMedia = teAutoPySnapshotMediaByNo(autoSnapshotIndex, requestedStartForMain);
        const snapshotDirect = teAutoPySnapshotDirectReady(requestedSnapshotMedia, requestedStartForMain);
        const mainReady = snapshotDirect.ok
          ? { ok: true, snapshot_direct: true, requested_no: requestedStartForMain, shortcode: snapshotDirect.shortcode, owner_username: snapshotDirect.owner_username, downloadable_kinds: snapshotDirect.downloadable_kinds }
          : await teAutoPyEnsureMainInjectionReady(labels, requestedStartForMain, mySeq);
        if (snapshotDirect.ok) teAutoPyLeaveWait("자동 snapshot 직접 다운로드 준비");
        await teAutoPyCycle("S00_MAIN_INJECTION_READY", { cycle_id: teBuildCycleId("MAIN_INJECT"), run_seq: mySeq, requested_rank_no: requestedStartForMain, decision: mainReady && mainReady.ok ? (snapshotDirect.ok ? "snapshot_direct_ready" : "main_ready") : "main_not_ready", action: snapshotDirect.ok ? "use_snapshot_without_main_visual_current" : "verify_main_right_panel_and_No_labels", reason: mainReady && mainReady.ok ? (snapshotDirect.ok ? "snapshot_shortcode_owner_media_ready" : "main_injection_ready") : String(mainReady && mainReady.error || "main_injection_not_ready"), main_injection_state: mainReady || null });
        if (!(mainReady && mainReady.ok)) {
          if (mainReady && mainReady.stopped || (mySeq && mySeq !== teAutoPyRunSeq) || teAutoPyStopping) return;
          await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", { cycle_id: teBuildCycleId("MAIN_INJECT_FAIL"), run_seq: mySeq, requested_rank_no: requestedStartForMain, main_injection_state: mainReady || null }, "target_incomplete", String(mainReady && mainReady.error || "main_injection_not_ready"));
          return;
        }
        const snapshotDirectMode = !!snapshotDirect.ok;
        const maxRank = Math.min(labels.length, arr.length, Number(autoSnapshotIndex && autoSnapshotIndex.max_rank_no || labels.length) || labels.length);
        try { teAutoPyStatsStart(mySeq, Number(startPlan && startPlan.no || 0) || 0, targetCount, "auto_snapshot_ready"); } catch (_) {}
        teAutoPySetUiPhase("searching", { target_count: targetCount, completed_count: teAutoPyCompletedPostCount(), exhausted, exhausted_reason: exhaustedReason, message: `후보 ${maxRank}개 처리 준비` });
        await teAutoPyCycle("S00_AUTO_SNAPSHOT_READY", {
          cycle_id: teBuildCycleId("AUTO_SNAPSHOT"),
          run_seq: mySeq,
          rank_no: 0,
          rank_label: "SNAPSHOT",
          decision: autoSnapshotIndex && autoSnapshotIndex.count ? "snapshot_ready" : "snapshot_empty",
          action: "build_internal_shortcode_snapshot_index",
          reason: "auto_py_download_order_lock",
          snapshot_id: autoSnapshotIndex && autoSnapshotIndex.snapshot_id || "",
          snapshot_count: autoSnapshotIndex && autoSnapshotIndex.count || 0,
          snapshot_max_rank_no: autoSnapshotIndex && autoSnapshotIndex.max_rank_no || 0,
          sort_field: autoSnapshotIndex && autoSnapshotIndex.sort_field || "",
          threshold: autoSnapshotIndex && autoSnapshotIndex.threshold || 0
        });
        n(`자동시작 연속 준비: 현재 No부터 / 후보 ${labels.length}`);
        await teSleep(1200);

        const resolvedStart = startPlan && Number(startPlan.no || 0) > 0 ? startPlan : teAutoPyResolveStartNo();
        const startNo = Number(resolvedStart && resolvedStart.no || 0) || 0;
        const startSource = String(resolvedStart && resolvedStart.source || "default_current");
        let startNoCacheDownloaded = false;
        if (startNo > 0 && !snapshotDirectMode) {
          const hashBase = { cycle_id: teBuildCycleId("HASH_START"), run_seq: mySeq, rank_no: startNo, rank_label: teAutoPyNoLabel(startNo), max_rank_no: Math.max(maxRank, startNo), requested_rank_no: startNo, hash_start_rank: startNo, hash_start_source: startSource, start_source: startSource, loaded_ui_count: teAutoPyLoadedUiCount(), hash_start_url: teCurrentTargetUrl() || "" };
          teAutoPySetLastRank(startNo, startSource + "_request");
          n(`${teAutoPyNoLabel(startNo)} 로딩 확보 후 현재 No 이동`);
          const preload = await teAutoPyPreloadRequestedNo(startNo, Math.max(maxRank, startNo), hashBase, mySeq, arr, ctx);
          if (!(preload && preload.ok)) {
            await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", Object.assign({}, hashBase, { preload_result: preload || null }), "target_incomplete", String(preload && (preload.error || preload.reason) || "start_no_preload_failed"));
            return;
          }
          startNoCacheDownloaded = !!(preload && preload.cache_download_done);
          if (startNoCacheDownloaded) {
            const recoveredCode = String(preload && preload.shortcode || teAutoPySnapshotEntryByNo(autoSnapshotIndex, startNo) && teAutoPySnapshotEntryByNo(autoSnapshotIndex, startNo).shortcode || "");
            await teAutoPyFeatureCheck("AD-03", "[복구완료]", `Main 다운로드 지점 없이 snapshot 검증으로 다운로드 계속 / ${teAutoPyNoLabel(startNo)} 계정=${String(teMediaOwnerName(requestedSnapshotMedia) || "?")} shortcode=${recoveredCode || "?"}`, Object.assign({}, hashBase, { shortcode: recoveredCode, owner_username: String(teMediaOwnerName(requestedSnapshotMedia) || ""), recovery_source: "preload_snapshot_cache", cache_result_ok: true }));
            teAutoPyRecordCompletedPost(recoveredCode, "cache_preload_complete");
          }
          await teSleep(350);
        } else if (startNo > 0 && snapshotDirectMode) {
          teAutoPySetLastRank(startNo, startSource + "_snapshot_direct");
          await teAutoPyCycle("S00_SNAPSHOT_DIRECT_START", { cycle_id: teBuildCycleId("SNAPSHOT_DIRECT_START"), run_seq: mySeq, rank_no: startNo, rank_label: teAutoPyNoLabel(startNo), requested_rank_no: startNo, max_rank_no: maxRank, decision: "snapshot_direct_ready", action: "skip_main_preload", reason: "snapshot_shortcode_owner_media_ready" });
        }

        const findMediaForTarget = (target, rankNo) => {
          const snapMedia = teAutoPySnapshotMediaByNo(autoSnapshotIndex, rankNo);
          if (snapMedia) return snapMedia;
          const code = String(target && target.code || "");
          if (code) {
            const entry = autoSnapshotIndex && autoSnapshotIndex.byCode instanceof Map ? autoSnapshotIndex.byCode.get(code) : null;
            if (entry && entry.media) return entry.media;
            const byCode = arr.find((m) => String(m && m.code || "") === code);
            if (byCode) return byCode;
          }
          return teAutoPyFindMediaByRankNo(rankNo, arr, ctx) || arr[Math.max(0, Math.min(arr.length - 1, Number(rankNo || 1) - 1))] || arr[0] || {};
        };

        let startRead = await teAutoPyReadCurrentNoNav(700);
        // Main의 시각 No UI가 없어도 자동 snapshot의 시작 No를 내부 Current로 사용한다.
        let rankNo = Number(startRead && startRead.rank_no || 0) || startNo || 0;
        if (startNo > 0 && startNo <= maxRank && rankNo && rankNo !== startNo) {
          const hashRetryBase = { cycle_id: teBuildCycleId("HASH_START_RETRY"), run_seq: mySeq, rank_no: startNo, rank_label: teAutoPyNoLabel(startNo), max_rank_no: maxRank, hash_start_rank: startNo, hash_start_source: startSource, start_source: startSource, active_rank_no: rankNo, requested_rank_no: startNo, loaded_ui_count: teAutoPyLoadedUiCount(), hash_start_url: teCurrentTargetUrl() || "" };
          if (startNoCacheDownloaded && rankNo === startNo + 1 && rankNo <= maxRank) {
            await teAutoPyCycle("S00_CACHE_START_ADVANCE", Object.assign({}, hashRetryBase, {
              decision: "continue_from_next_after_cache_download",
              action: "skip_downloaded_start_rank_without_retry",
              reason: "start_rank_downloaded_from_cache_active_is_exact_next",
              active_rank_no: rankNo,
              requested_rank_no: startNo,
              next_to_rank_no: rankNo,
              current_read: startRead || null
            }));
          } else if (startNoCacheDownloaded && rankNo > startNo + 1 && rankNo <= maxRank) {
            const nextNo = Math.min(maxRank, startNo + 1);
            await teAutoPyCycle("S00_CACHE_START_ACTIVE_AHEAD", Object.assign({}, hashRetryBase, {
              decision: "active_hash_ahead_but_keep_sequential",
              action: "go_to_exact_next_after_cached_start",
              reason: "screen_active_ahead_during_loading_not_download_order",
              active_rank_no: rankNo,
              requested_rank_no: startNo,
              next_to_rank_no: nextNo,
              current_read: startRead || null
            }));
            await teAutoPyGoNoWithConfirm(nextNo, Object.assign({}, hashRetryBase, { next_reason: "cached_start_rank_done_active_ahead_keep_sequential" }), 1400, "S00_CACHE_START_GO_EXACT_NEXT");
            await teSleep(400);
            startRead = await teAutoPyReadCurrentNoNav(700);
            rankNo = Number(startRead && startRead.rank_no || 0) || nextNo;
          } else {
            await teAutoPyCycle("S00_HASH_READ_MISMATCH", Object.assign({}, hashRetryBase, { decision: "start_rank_not_current", action: "retry_dom_go_no", reason: "current_rank_different_from_start_no" }));
            await teAutoPyGoNoWithConfirm(startNo, Object.assign({}, hashRetryBase, { next_reason: "start_No_retry" }), 1700, "S00_HASH_GO_NO_RETRY");
            await teSleep(500);
            startRead = await teAutoPyReadCurrentNoNav(700);
            rankNo = Number(startRead && startRead.rank_no || 0) || 0;
            if (rankNo && rankNo !== startNo) {
              const previousRankNo = rankNo;
              rankNo = startNo;
              await teAutoPyCycle("S00_SNAPSHOT_START_CURRENT", Object.assign({}, hashRetryBase, {
                decision: "use_auto_snapshot_current",
                action: "set_internal_current_no_without_download",
                reason: "visual_or_dom_current_no_unconfirmed_use_snapshot_before_AD03",
                previous_active_rank_no: previousRankNo,
                requested_rank_no: startNo,
                current_read: startRead || null,
                ad03_gate_required: true
              }));
            }
          }
        }
        if (startNoCacheDownloaded && startNo > 0 && startNo < maxRank && rankNo === startNo) {
          const nextNo = startNo + 1;
          const nextBase = { cycle_id: teBuildCycleId("CACHE_START_NEXT"), run_seq: mySeq, rank_no: nextNo, rank_label: teAutoPyNoLabel(nextNo), requested_rank_no: startNo, max_rank_no: maxRank, hash_start_source: startSource, start_source: startSource };
          await teAutoPyCycle("S00_CACHE_START_NEXT", Object.assign({}, nextBase, { decision: "continue_to_next_after_cache_download", action: "go_no_after_cached_start_rank", reason: "start_rank_already_downloaded_from_cache" }));
          const goNext = await teAutoPyGoNoWithConfirm(nextNo, Object.assign({}, nextBase, { next_reason: "cached_start_rank_done" }), 1200, "S00_CACHE_START_GO_NEXT");
          await teSleep(300);
          startRead = await teAutoPyReadCurrentNoNav(500);
          rankNo = Number(startRead && startRead.rank_no || goNext && goNext.after_rank || nextNo) || nextNo;
        }
        if (!rankNo) {
          rankNo = startNo > 0 ? startNo : Number(autoSnapshotIndex && autoSnapshotIndex.min_rank_no || 1) || 1;
          await teAutoPyCycle("S00_SNAPSHOT_CURRENT_RECOVERY", {
            cycle_id: teBuildCycleId("SNAPSHOT_CURRENT"),
            run_seq: mySeq,
            rank_no: rankNo,
            rank_label: teAutoPyNoLabel(rankNo),
            requested_rank_no: startNo,
            max_rank_no: maxRank,
            decision: "recover_internal_current_from_snapshot",
            action: "use_snapshot_no_without_visual_current",
            reason: "NoNav_Current_missing",
            ad03_gate_required: true
          });
          teAutoPyLeaveWait("자동 snapshot Current 사용");
        }
        teAutoPySetLastRank(rankNo, "auto_start_current");
        n(`자동시작: ${teAutoPyNoLabel(rankNo)} 현재 항목부터`);
        let loopGuard = 0;
        while (rankNo > 0 && rankNo <= maxRank && teAutoPyCompletedPostCount() < targetCount && loopGuard++ < Math.max(1, maxRank + 8)) {
          if (mySeq && mySeq !== teAutoPyRunSeq) {
            await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", { run_seq: mySeq, rank_no: rankNo, rank_label: teAutoPyNoLabel(rankNo), max_rank_no: maxRank }, "stopped_before_next_rank", "run_seq_changed_before_next_rank");
            return;
          }
          if (teAutoPyStopping) {
            await teAutoPyStopCycle("S00_AUTOPLAY_STOPPED", { run_seq: mySeq, rank_no: rankNo, rank_label: teAutoPyNoLabel(rankNo), max_rank_no: maxRank }, "user_stop", "stop_requested_before_next_rank");
            return;
          }

          const autoCycleStartedAt = Date.now();
          teAutoPyStatsSetRank(rankNo, "cycle_attempt");
          teAutoPySetLastRank(rankNo, "cycle_attempt");
          const cycleId = teBuildCycleId(teAutoPyNoLabel(rankNo));
          const cycleBase = { cycle_id: cycleId, run_seq: mySeq, rank_no: rankNo, rank_label: teAutoPyNoLabel(rankNo), max_rank_no: maxRank, auto_snapshot: teAutoPySnapshotMediaSummary(autoSnapshotIndex, rankNo) };
          const snapshotMediaForRank = findMediaForTarget(null, rankNo) || {};
          const snapshotCodeForRank = String(snapshotMediaForRank.__igblue_auto_snapshot_shortcode || snapshotMediaForRank.code || snapshotMediaForRank.shortcode || "").trim();
          if (snapshotCodeForRank && teAutoPyCompletedShortcodeSet().has(snapshotCodeForRank)) {
            await teAutoPyCycle("S01_COMPLETED_SHORTCODE_SKIP", Object.assign({}, cycleBase, { decision: "already_completed", action: "skip_duplicate_shortcode", reason: "completed_shortcode_not_downloaded_twice", shortcode: snapshotCodeForRank, next_to_rank_no: rankNo < maxRank ? rankNo + 1 : 0 }));
            rankNo += 1;
            continue;
          }
          const cycleSnapshotDirect = teAutoPySnapshotDirectReady(snapshotMediaForRank, rankNo);
          let cycleSnap = { top_sorted_count: maxRank, ready_count: maxRank, snapshot_direct: cycleSnapshotDirect.ok };
          if (cycleSnapshotDirect.ok) {
            await teAutoPyCycle("S01_DOWNLOAD_POINT_CHECK", Object.assign({}, cycleBase, { decision: "snapshot_direct_ready", action: "skip_main_download_point_wait", reason: "snapshot_shortcode_owner_media_ready", shortcode: cycleSnapshotDirect.shortcode, owner_username: cycleSnapshotDirect.owner_username, downloadable_kinds: cycleSnapshotDirect.downloadable_kinds }));
          } else {
            await teAutoPyCycle("S01_DOWNLOAD_POINT_CHECK", Object.assign({}, cycleBase, { decision: "check", action: "ensure_rank_loaded_before_cycle", reason: "every_cycle_start_requires_download_point" }));
            const cycleState = await teAutoPyReadLoadedRankState(160, rankNo);
            cycleSnap = teAutoPyCountsSnapshot(rankNo, cycleState);
            if (!((Number(cycleSnap.top_sorted_count || cycleSnap.ready_count || 0) || 0) >= rankNo)) {
              teAutoPyAnnounce(`${teAutoPyNoLabel(rankNo)} 다운로드 지점 확보 중입니다.`);
              const cyclePreload = await teAutoPyPreloadRequestedNo(rankNo, Math.max(maxRank, rankNo), Object.assign({}, cycleBase, cycleSnap, { start_source: "cycle_download_point_check" }), mySeq, arr, ctx);
              if (cyclePreload && cyclePreload.cache_download_done) {
                const recoveredCode = String(cyclePreload.shortcode || teAutoPySnapshotEntryByNo(autoSnapshotIndex, rankNo) && teAutoPySnapshotEntryByNo(autoSnapshotIndex, rankNo).shortcode || "");
                await teAutoPyFeatureCheck("AD-03", "[복구완료]", `Main 다운로드 지점 없이 snapshot 검증으로 다운로드 계속 / ${teAutoPyNoLabel(rankNo)} 계정=${String(teMediaOwnerName(snapshotMediaForRank) || "?")} shortcode=${recoveredCode || "?"}`, Object.assign({}, cycleBase, { shortcode: recoveredCode, owner_username: String(teMediaOwnerName(snapshotMediaForRank) || ""), recovery_source: "cycle_preload_snapshot_cache", cache_result_ok: true }));
                teAutoPyRecordCompletedPost(recoveredCode, "cache_cycle_complete");
                const targetReachedByCache = teAutoPyCompletedPostCount() >= targetCount;
                const hasNextRank = !targetReachedByCache && rankNo < maxRank;
                const nextRank = hasNextRank ? rankNo + 1 : 0;
                await teAutoPyCycle("S01_CACHE_FALLBACK_ADVANCE", Object.assign({}, cycleBase, cycleSnap, { decision: hasNextRank ? "cache_download_then_advance" : "cache_download_last_done", action: "skip_dom_click_for_requested_rank_after_download_point_fail", reason: "requested_rank_downloaded_from_side_cache_after_download_point_not_loaded", requested_rank_no: rankNo, next_to_rank_no: nextRank, next_to_rank_label: nextRank > 0 ? teAutoPyNoLabel(nextRank) : "END", cache_result: cyclePreload }));
                await teAutoPyCycle("S99_CYCLE_END", Object.assign({}, cycleBase, { decision: "cycle_done", action: hasNextRank ? "cache_fallback_next_prepared" : "cache_fallback_last_rank_done", reason: hasNextRank ? "next_rank_after_cache_fallback" : "last_rank_downloaded_from_side_cache", next_to_rank_no: nextRank, next_to_rank_label: nextRank > 0 ? teAutoPyNoLabel(nextRank) : "END", next_clicked: false, cache_result: cyclePreload }));
                teAutoPyStatsRecordCycle(autoCycleStartedAt, "cache_fallback_cycle");
                if (!hasNextRank) break;
                rankNo = nextRank;
                continue;
              }
              if (!(cyclePreload && cyclePreload.ok)) {
                await teAutoPyStopCycle("S01_DOWNLOAD_POINT_FAILED", Object.assign({}, cycleBase, { preload_result: cyclePreload || null }), "download_point_not_loaded", String(cyclePreload && (cyclePreload.error || cyclePreload.reason) || "cycle_download_point_not_loaded"));
                return;
              }
            }
          }
          await teAutoPyCycle("S01_CYCLE_START", Object.assign({}, cycleBase, { decision: "start", action: "prepare_active", cycle_log: "new_cycle" }));
          let goRes = { ok: true, action: "current_NoNav", requested_no: rankNo, after_rank: rankNo, changed: false, target_found: true };
          await teAutoPyCycle("S02_CURRENT_NO", Object.assign({}, cycleBase, { decision: "use_current", action: "read_NoNav_Current", next_button_found: true, next_clicked: false, first_current_click: loopGuard === 1, go_response: goRes }));
          if (loopGuard === 1 && !cycleSnapshotDirect.ok) {
            const currentClickRes = await teAutoPyClickCurrentNoNav(Object.assign({}, cycleBase, { current_reason: "auto_start_first_cycle_activate_current_NoNav", first_current_click: true }), 1300, "S02_CURRENT_NO_CLICK");
            if (currentClickRes && currentClickRes.ok && Number(currentClickRes.after_rank || 0) > 0) {
              rankNo = Number(currentClickRes.after_rank || rankNo) || rankNo;
            }
          }
          await teSleep(cycleSnapshotDirect.ok ? 80 : (loopGuard === 1 ? 1100 : 900));
          if (mySeq && mySeq !== teAutoPyRunSeq) {
            await teAutoPyStopCycle("S03_AUTOPLAY_STOPPED", cycleBase, "stopped_before_target_read", "run_seq_changed_before_target_read");
            return;
          }

          let got = cycleSnapshotDirect.ok ? { ok: true, snapshot_direct: true, target: cycleSnapshotDirect.target } : await teSendToTargetTabResponse({ type: "igblue_get_active_download_target" }, "Instagram tab not found", 1800);
          let target = got && got.target;
          let currentMedia = findMediaForTarget(target, rankNo) || {};
          const expectedCodeForRank = String(currentMedia && (currentMedia.__igblue_auto_snapshot_shortcode || currentMedia.code || currentMedia.shortcode) || "");
          const targetCodeNow = String(target && (target.code || target.shortcode) || "");
          const rankOkNow = Number(target && target.rank || 0) === rankNo && !!targetCodeNow && !!expectedCodeForRank && targetCodeNow === expectedCodeForRank;
          if (!cycleSnapshotDirect.ok && (!teAutoPySafeTarget(target) || !rankOkNow)) {
            await teSleep(2000);
            got = await teSendToTargetTabResponse({ type: "igblue_get_active_download_target" }, "", 1800);
            target = got && got.target;
            currentMedia = findMediaForTarget(target, rankNo) || {};
          }
          const rawTarget = target && typeof target === "object" ? target : {};
          const snapshotRecovery = teAutoPySnapshotRecoveryCheck(rawTarget, currentMedia, rankNo);
          const resolvedTarget = teAutoPyResolveTargetForRank(rawTarget, currentMedia, rankNo);
          target = resolvedTarget.target;
          let targetCheck = teAutoPyTargetReadCheck(target, currentMedia, rankNo);
          await teAutoPyCycle("S04_TARGET_READ", Object.assign({}, cycleBase, targetCheck, {
            decision: targetCheck.ok ? "target_ok" : "target_incomplete",
            action: targetCheck.ok ? "read_active_target" : "retry_read",
            reason: targetCheck.reason,
            active_rank_no: targetCheck.active_rank_no,
            requested_rank_no: targetCheck.requested_rank_no,
            active_shortcode: targetCheck.active_shortcode,
            expected_shortcode: targetCheck.expected_shortcode,
            active_target_age_ms: targetCheck.active_target_age_ms,
            shortcode: targetCheck.shortcode,
            asset_kind: targetCheck.asset_kind,
            direct_media_url_present: targetCheck.target_D_direct_media_url,
            go_response: got || null,
            target_resolved_source: resolvedTarget.source,
            target_recovered_from_snapshot: !!resolvedTarget.recovered
          }));
          if (!targetCheck.ok) {
            let cacheRes = null;
            if (snapshotRecovery.ok) {
              await teAutoPyFeatureCheck("AD-03-E01", "[예외추적]", `Main active 정보가 불완전하여 snapshot 직접 다운로드 확인 / ${teAutoPyNoLabel(rankNo)} shortcode=${String(snapshotRecovery.expected_shortcode || "?")}`, Object.assign({}, cycleBase, snapshotRecovery, { next_check: "snapshot rank·shortcode·owner·직접 URL 검증" }));
              cacheRes = await teAutoPyTryCacheDownloadForRank(rankNo, arr, ctx, Object.assign({}, cycleBase, targetCheck, snapshotRecovery, { expected_shortcode: snapshotRecovery.expected_shortcode, active_rank_no: snapshotRecovery.active_rank_no, active_shortcode: snapshotRecovery.active_shortcode }), targetCheck.target_missing_fields || targetCheck.reason || "main_active_incomplete");
              if (cacheRes && cacheRes.ok) {
                await teAutoPyFeatureCheck("AD-03", "[복구완료]", `Main active 정보 없이 snapshot 검증으로 다운로드 계속 / ${teAutoPyNoLabel(rankNo)} 계정=${String(snapshotRecovery.owner_username || "?")} shortcode=${String(cacheRes.shortcode || snapshotRecovery.expected_shortcode || "?")}`, Object.assign({}, cycleBase, snapshotRecovery, { shortcode: String(cacheRes.shortcode || snapshotRecovery.expected_shortcode || ""), recovery_source: "auto_snapshot_cache", cache_result_ok: true }));
                teAutoPyRecordCompletedPost(cacheRes.shortcode || snapshotRecovery.expected_shortcode || "", "snapshot_cache_recovery_complete");
                const targetReachedByCache = teAutoPyCompletedPostCount() >= targetCount;
                const hasNextRank = !targetReachedByCache && rankNo < maxRank;
                const nextRank = hasNextRank ? rankNo + 1 : 0;
                await teAutoPyCycle("S04_CACHE_RECOVERY_ADVANCE", Object.assign({}, cycleBase, targetCheck, snapshotRecovery, {
                  decision: hasNextRank ? "cache_download_then_advance" : "cache_download_last_done",
                  action: hasNextRank ? "continue_sequential_next_rank" : "finish_current_account_after_cache_download",
                  reason: "main_active_incomplete_snapshot_exact_download_ok",
                  requested_rank_no: rankNo,
                  next_to_rank_no: nextRank,
                  next_to_rank_label: nextRank > 0 ? teAutoPyNoLabel(nextRank) : "END",
                  cache_result: cacheRes
                }));
                await teAutoPyCycle("S99_CYCLE_END", Object.assign({}, cycleBase, { decision: "cycle_done", action: hasNextRank ? "snapshot_cache_next_prepared" : "snapshot_cache_last_rank_done", reason: hasNextRank ? "next_rank_after_snapshot_cache_recovery" : "last_rank_downloaded_from_snapshot_cache", next_to_rank_no: nextRank, next_to_rank_label: nextRank > 0 ? teAutoPyNoLabel(nextRank) : "END", next_clicked: false, cache_result: cacheRes }));
                teAutoPyStatsRecordCycle(autoCycleStartedAt, "snapshot_cache_recovery_cycle");
                if (!hasNextRank) break;
                rankNo = nextRank;
                continue;
              }
            }
            const conflictReason = snapshotRecovery.actual_conflict ? String(snapshotRecovery.reason || "active_target_conflict") : String(targetCheck.target_missing_fields || targetCheck.reason || "target_incomplete");
            await teAutoPyFeatureCheck("AD-03", "[체크실패]", `게시물 계정·shortcode·원본 URL 확인 실패 / 다음점검=${conflictReason}`, Object.assign({}, cycleBase, snapshotRecovery, { shortcode: String(targetCheck.shortcode || targetCheck.expected_shortcode || snapshotRecovery.expected_shortcode || ""), owner_username: String(targetCheck.owner_username || snapshotRecovery.owner_username || ""), active_shortcode: String(targetCheck.active_shortcode || snapshotRecovery.active_shortcode || ""), expected_shortcode: String(targetCheck.expected_shortcode || snapshotRecovery.expected_shortcode || ""), error_code: snapshotRecovery.actual_conflict ? "active_target_conflict" : String(targetCheck.stop_reason || "target_incomplete"), next_check: snapshotRecovery.actual_conflict ? "Main active 대상과 snapshot 실제 불일치" : "snapshot rank·shortcode·owner·직접 URL 연결", cache_result: cacheRes || null }));
            await teAutoPyStopCycle("S04_AUTOPLAY_STOPPED", Object.assign({}, cycleBase, targetCheck, snapshotRecovery, { shortcode: targetCheck.shortcode, asset_kind: targetCheck.asset_kind, cache_result: cacheRes || null }), snapshotRecovery.actual_conflict ? "target_shortcode_mismatch" : targetCheck.stop_reason, conflictReason, { active_rank_no: targetCheck.active_rank_no || snapshotRecovery.active_rank_no, requested_rank_no: targetCheck.requested_rank_no || snapshotRecovery.requested_rank_no, active_shortcode: targetCheck.active_shortcode || snapshotRecovery.active_shortcode, expected_shortcode: targetCheck.expected_shortcode || snapshotRecovery.expected_shortcode, active_target_age_ms: targetCheck.active_target_age_ms });
            return;
          }
          if (targetCheck.shortcode && !String(target.code || "").trim()) target.code = targetCheck.shortcode;
          if (targetCheck.rank_stale_confirmed && Number(target.rank || 0) !== rankNo) target.rank = rankNo;

          const ad03Status = resolvedTarget.recovered ? "[복구완료]" : "[체크완료]";
          const ad03Message = resolvedTarget.recovered
            ? `Main active 정보 없이 snapshot 검증 완료 / ${teAutoPyNoLabel(rankNo)} 계정=${String(targetCheck.owner_username || "?")} shortcode=${String(targetCheck.shortcode || "?")}`
            : `게시물 정보 확인 / ${teAutoPyNoLabel(rankNo)} 계정=${String(targetCheck.owner_username || "?")} shortcode=${String(targetCheck.shortcode || "?")} 경로=${String(targetCheck.target_source || resolvedTarget.source || "?")}`;
          await teAutoPyFeatureCheck("AD-03", ad03Status, ad03Message, Object.assign({}, cycleBase, { shortcode: String(targetCheck.shortcode || ""), owner_username: String(targetCheck.owner_username || ""), target_source: String(targetCheck.target_source || resolvedTarget.source || ""), recovery_source: resolvedTarget.recovered ? "auto_snapshot_rank_target" : "", views: teViewCountOrPrivate(currentMedia), views_available: teViewsAvailable(currentMedia) ? 1 : 0, participation_score: teParticipationScore(currentMedia), direct_video_url_present: !!targetCheck.direct_video_url_present, direct_image_url_present: !!targetCheck.direct_image_url_present }));
          await teAutoPyCycle("S05_DIRECT_DOWNLOAD_READY", Object.assign({}, cycleBase, {
            decision: "direct_download_ready",
            action: "direct_media_url_ready",
            reason: "media_url_and_rank_shortcode_confirmed",
              direct_video_url_present: !!targetCheck.direct_video_url_present,
            direct_image_url_present: !!targetCheck.direct_image_url_present
          }));

          const directPair = await teAutoPyDownloadMediaPairDirect(currentMedia, target, rankNo, cycleBase);
          if (!(directPair && directPair.ok)) {
            const failReason = String(directPair && (directPair.error_code || directPair.error) || "direct_download_failed");
            await teAutoPyFeatureCheck("AD-06", "[체크실패]", `파일 저장·후처리 실패 / 원인=${failReason}`, Object.assign({}, cycleBase, { error_code: failReason, next_check: "WS8771 직접 다운로드 결과" }));
            n("직접 다운로드 실패: " + failReason);
            await teAutoPyMediaFailAdvanceOrStop(mySeq, "S24_DIRECT_AUTOPLAY_STOPPED", Object.assign({}, cycleBase, {
              result_ok: false,
              direct_download_result: directPair || null,
              file_saved: !!(directPair && directPair.results && directPair.results.some((x) => x && x.file_saved)),
              json_saved: !!(directPair && directPair.results && directPair.results.every((x) => !x || x.skipped || x.json_saved)),
              report_saved: !!(directPair && directPair.results && directPair.results.every((x) => !x || x.skipped || x.report_saved))
            }), failReason === "direct_finalize_failed" ? "direct_finalize_failed" : "direct_download_failed", failReason);
            return;
          }
          const directSummary = directPair.results || [];
          teAutoEtaRecordCompletedPost(directSummary, "direct_pair_complete", Date.now() - autoCycleStartedAt);
          const completedAfterPair = teAutoPyRecordCompletedPost(directPair.shortcode || targetCheck.shortcode || target.code || currentMedia.code || "", "direct_pair_complete");
          await teAutoPyFeatureCheck("AD-06", "[체크완료]", `파일 저장·JSON·보고서 완료 / 자산=${directSummary.length}`, Object.assign({}, cycleBase, { direct_results_count: directSummary.length, shortcode: String(directPair.shortcode || "") }));
          await teAutoPyFeatureCheck("AD-07", "[체크완료]", "현재 게시물 완료 / 다음 게시물 진행 가능", Object.assign({}, cycleBase, { next_rank_no: rankNo < maxRank ? rankNo + 1 : 0 }));
          await teAutoPyCycle("S30_MEDIA_PAIR_COMPLETE", Object.assign({}, cycleBase, {
            decision: "media_pair_direct_complete",
            action: "mp4_then_jpg_file_json_report_complete",
            result_ok: true,
              direct_results: directSummary
          }));
          if (completedAfterPair >= targetCount) {
            await teAutoPyCycle("S31_TARGET_REACHED", Object.assign({}, cycleBase, { decision: "target_reached", action: "finish_current_account", target_count: targetCount, completed_count: completedAfterPair, reason: "actual_completed_posts_reached_target" }));
            teAutoPyStatsRecordCycle(autoCycleStartedAt, "target_reached_cycle");
            break;
          }
          if (cycleSnapshotDirect.ok) {
            const nextRank = rankNo < maxRank ? rankNo + 1 : 0;
            await teAutoPyCycle("S31_SNAPSHOT_DIRECT_NEXT", Object.assign({}, cycleBase, { decision: nextRank > 0 ? "next_loaded_candidate" : "continue_search_required", action: nextRank > 0 ? "advance_snapshot_rank" : "return_to_swipe_search", reason: nextRank > 0 ? "snapshot_direct_pair_complete" : "loaded_snapshot_candidates_complete", next_to_rank_no: nextRank, completed_count: completedAfterPair, target_count: targetCount }));
            teAutoPyStatsRecordCycle(autoCycleStartedAt, "snapshot_direct_cycle_done");
            if (!(nextRank > 0)) break;
            rankNo = nextRank;
            teAutoPySetLastRank(rankNo, "snapshot_direct_next");
            continue;
          }
          const delaySec = Math.max(2, Number(teDownloadDelaySec && teDownloadDelaySec() || 0) || 0);
          await teAutoPyCycle("S31_NEXT_REQUIRED", Object.assign({}, cycleBase, { decision: "next_required", action: "wait_before_next_after_finalize", next_required: true, wait_sec: delaySec, reason: "file_json_report_complete_then_next" }));
          n(`자동시작: JSON·보고서 완료 후 Next 전 ${delaySec}s 대기`);
          await teAutoPyWaitCountdown(delaySec, { current_no: rankNo, target_count: targetCount, completed_count: completedAfterPair });
          const nextRes = await teAutoPyClickNextNoNav(Object.assign({}, cycleBase, { next_reason: "after_direct_media_pair_finalize", direct_download_result: directPair }), 1300, "S31_NEXT_DOM_CLICK");
          await teAutoPyCycle("S99_CYCLE_END", Object.assign({}, cycleBase, { decision: "cycle_done", action: "next_prepared", next_required: true, next_to_rank_no: Number(nextRes && nextRes.after_rank || 0) || 0, next_clicked: !!(nextRes && nextRes.ok && nextRes.changed), next_response: nextRes || null }));
          teAutoPyStatsRecordCycle(autoCycleStartedAt, "normal_cycle_done");
          if (!(nextRes && nextRes.ok && nextRes.changed && Number(nextRes.after_rank || 0) > 0)) {
            if (rankNo < maxRank) {
              const retryNo = rankNo + 1;
              await teAutoPyCycle("S13_NEXT_RETRY_GO_NO", Object.assign({}, cycleBase, { decision: "retry_go_exact_next", action: "go_no_after_next_no_change", reason: "next_click_no_change_but_target_remaining", next_to_rank_no: retryNo, next_response: nextRes || null, target_count: maxRank, completed_count: rankNo }));
              const goRes = await teAutoPyGoNoWithConfirm(retryNo, Object.assign({}, cycleBase, { next_reason: "next_click_no_change_go_exact_next" }), 1700, "S13_NEXT_GO_EXACT_NEXT");
              if (goRes && goRes.ok && Number(goRes.after_rank || retryNo) === retryNo) { rankNo = retryNo; teAutoPySetLastRank(rankNo, "next_exact_retry"); await teSleep(600); continue; }
              await teAutoPyStopCycle("S13_NEXT_UNREACHABLE", Object.assign({}, cycleBase, { next_response: nextRes || null, go_no_response: goRes || null, target_count: maxRank, completed_count: rankNo, next_to_rank_no: retryNo }), "next_rank_unreachable", "No변화 없음: 다음 목표가 남아 있어 종료하지 않고 중단");
              return;
            }
            break;
          }
          rankNo = Number(nextRes.after_rank || 0) || 0;
          teAutoPySetLastRank(rankNo, "next_prepared");
          await teSleep(600);
        }
        const completedCount = teAutoPyCompletedPostCount();
        const targetReached = completedCount >= targetCount;
        const noMoreMedia = !!exhausted;
        if (!targetReached && !noMoreMedia) {
          teAutoPySetUiPhase("searching", { target_count: targetCount, completed_count: completedCount, message: `완료 ${completedCount}/${targetCount} · 다음 후보 탐색` });
          await teAutoPyCycle("S32_CONTINUE_SEARCH", { cycle_id: teBuildCycleId("CONTINUE_SEARCH"), run_seq: mySeq, target_count: targetCount, completed_count: completedCount, max_rank_no: maxRank, decision: "continue_search", action: "swipe_for_next_pending_candidate", reason: "loaded_candidates_completed_target_remaining" });
          return { ok: true, continue_search: true, completed_count: completedCount, target_count: targetCount, loaded_candidate_count: maxRank };
        }
        const completionMeta = { target_count: targetCount, completed_count: completedCount, exhausted: !!(!targetReached && noMoreMedia), exhausted_reason: targetReached ? "" : (exhaustedReason || "current_url_end_reached") };
        const completionReason = targetReached ? "actual_completed_posts_reached_target" : "no_more_media_before_target";
        const accountAdvance = await teAutoPyAdvanceAccountQueueIfAny(mySeq, completionReason, completionMeta);
        if (accountAdvance && accountAdvance.advanced) return { ok: true, next_account_scheduled: true, completion: completionMeta };
        if (accountAdvance && accountAdvance.completed_all) return { ok: true, account_queue_completed: true, completion: completionMeta };
        teAutoPyFinalizeAutomaticEnd(targetReached ? `목표 ${completedCount}/${targetCount} 완료` : `${teAutoPyRoundedDecilePct(completedCount, targetCount)}% 진행 · 더이상없음`, completionMeta);
        n(`자동시작 연속 완료: 실제 완료 ${completedCount}/${targetCount}`);
        return { ok: true, reason: targetReached ? "target_reached" : "no_more_media", completion: completionMeta };
      }
      function teDownTxtPayload(account) {
        const acc = teSafePath(account || teDownTxtAccount());
        const href = teCurrentTargetUrl() || "https://www.instagram.com/";
        return { host: teDownTxtHost(), href, root: "instagram", account: acc, account_folder_merge: false, filename: (acc.split("/").pop() + ".json") };
      }
      async function teSaveDownTxtAiwk(account) {
        const payload = Object.assign(teDownTxtPayload(account), { items: teDownItems.slice(0) });
        const res = teAiwkTrackWsResult(await teAiwkWsRequest("domain_folder_json_save_hint", payload, 2500));
        teDownTxtStatus(res && res.ok ? "저장 기준 확인" : ("저장 실패 " + ((res && (res.error_code || res.error)) || "")));
        return !!(res && res.ok);
      }
      async function teLoadDownTxtAiwk() {
        const account = teDownTxtAccount();
        teDownTxtStatus("loading from yjm_win2rtc...");
        const payload = teDownTxtPayload(account);
        let res = teAiwkTrackWsResult(await teAiwkWsRequest("domain_folder_json_read", payload));
        if (res && res.ok) {
          teRenderDownList(res.items || [], account);
          teDownTxtStatus("loaded " + (Number(res.count || (res.items && res.items.length) || 0)) + " items");
        } else {
          res = teAiwkTrackWsResult(await teAiwkWsRequest("domain_down_txt_read", payload));
          if (res && res.ok) {
            teSetDownTxtText(res.text || "", account);
            teDownTxtStatus("legacy loaded");
          } else {
            teLoadDownTxtLocal(account);
            teDownTxtStatus("load failed; local shown " + ((res && (res.error_code || res.error)) || ""));
          }
        }
        try {
          const startRes = teAiwkTrackWsResult(await teAiwkWsRequest("domain_auto_start", payload, 2500));
          if (startRes && startRes.ok) teDownTxtStatus("loaded + auto_start");
        } catch {}
      }
      function teFilenameOnly(path) {
        return String(path || "").split(/[\\/]/).pop().trim();
      }
      function teNum(v) {
        const n = Number(v);
        return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
      }
      function teMysqlDateFromEpoch(v) {
        const n = Number(v);
        if (!Number.isFinite(n) || n <= 0) return "";
        const d = new Date(n * 1000);
        const p = (x) => String(x).padStart(2, "0");
        return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
      }
      function teMediaPageUrl(m, mediaUrl) {
        const code = String(m?.code || "").trim();
        if (!code) return String(mediaUrl || teCurrentTargetUrl() || "");
        const kind = teIsVideoUrl(mediaUrl) || Number(m?.media_type) === 2 ? "reel" : "p";
        return `https://www.instagram.com/${kind}/${encodeURIComponent(code)}/`;
      }
      function teBuildMediaDownloadMeta(m, mediaUrl, prefix, mediaIndex, totalMedia) {
        const viewsAvailable = teViewsAvailable(m);
        const views = teViewCountOrPrivate(m);
        const likes = teNum(m?.like_count);
        const comments = teNum(m?.comment_count);
        const shares = 0;
        const reposts = teNum(m?.media_repost_count);
        const shortcode = String(m?.code || "").trim();
        const ownerName = teOwnerAccountForMedia(m || {});
        const accountName = teSafePath(teActiveFolderAccountName() || teDownloadChannelAccountName() || "instagram");
        const originalInfo = teOriginalInstagramFileInfo(mediaUrl || "");
        const firstLoadStamp = teFirstLoadStamp(ownerName || accountName, shortcode || "no_shortcode", shortcode || "media");
        const firstLoadedAt = teFirstLoadIsoFromStamp(firstLoadStamp);
        const rankNo = teRankNoFromPrefix(prefix) || teRankNoForMedia(m, 0);
        const rankLabel = rankNo > 0 ? `No${String(rankNo).padStart(3, "0")}` : "";
        const assetKind = teIsVideoUrl(mediaUrl) ? "video" : "image";
        const rawSortField = teTopMetricField() || String(G[Z] || "");
        const sortField = teServerSortField(rawSortField);
        const sortValue = rawSortField === "play_count" && !viewsAvailable ? teParticipationScore(m) : (rawSortField ? teMetricNumber(m?.[rawSortField]) : 0);
        const thresholdValue = teMetricThresholdValue();
        const nowIso = new Date().toISOString();
        const filenameStem = teFilenameOnly(prefix || "").replace(/\.[^.]+$/, "");
        const rankTotal = Array.isArray(teMetricSourceList()) ? teMetricSourceList().length : 0;
        const topTarget = Math.max(0, Number(teAutoPyTargetCount && teAutoPyTargetCount() || 0) || 0);
        const href = teCurrentTargetUrl() || "";
        const accountQueueStatus = (typeof teAutoPyAccountQueueStatus === "function") ? teAutoPyAccountQueueStatus() : null;
        return {
          shortcode,
          asset_kind: assetKind,
          rank_no: rankNo,
          rank_label: rankLabel,
          rank_meaning: "sort_rank_at_collection_time",
          rank_total: rankTotal,
          top_n: topTarget,
          collected_at: nowIso,
          source_type: teSourceTypeFromUrl(href),
          target_url: href,
          account_queue_index: accountQueueStatus && accountQueueStatus.has ? Number(accountQueueStatus.index || 0) : 0,
          account_queue_total: accountQueueStatus && accountQueueStatus.has ? Number(accountQueueStatus.total || 0) : 0,
          account_queue_url: accountQueueStatus && accountQueueStatus.has ? String(accountQueueStatus.url || "") : "",
          account_queue_username: accountQueueStatus && accountQueueStatus.has ? String(accountQueueStatus.account || "").replace(/^@/, "") : "",
          pass_id: `${IGBLUE_BUILD_TAG}_${teCollectDateStamp()}_${teSourceTypeFromUrl(href)}`,
          threshold_value: thresholdValue,
          threshold_label: teThresholdLabelForMetric(rawSortField, thresholdValue),
          filter_min_comments: teMinCommentsValue(),
          filter_min_likes: teMinLikesValue(),
          filter_min_shares: teMinSharesValue(),
          filter_min_reposts: teMinRepostsValue(),
          participation_score: teParticipationScore(m),
          media_url: teMediaPageUrl(m, mediaUrl),
          thumbnail_url: String(m?.img_origin || m?.thumbnail_url || m?.display_url || ""),
          video_url: assetKind === "video" ? String(mediaUrl || m?.video_url || "") : String(m?.video_url || ""),
          instagram_original_filename: originalInfo.original_filename,
          instagram_original_filename_with_ext: originalInfo.original_filename_with_ext,
          instagram_original_ext: originalInfo.original_ext,
          first_loaded_stamp: firstLoadStamp,
          first_loaded_at: firstLoadedAt,
          account_name: accountName,
          owner_username: ownerName,
          profile_url: ownerName ? `https://www.instagram.com/${encodeURIComponent(ownerName)}/` : "",
          post_url: teMediaPageUrl(m, mediaUrl),
          caption: String(m?.caption?.text || m?.caption_text || ""),
          owner_missing: ownerName ? false : true,
          upload_at: teMysqlDateFromEpoch(m?.created_at || m?.taken_at || m?.caption?.created_at),
          file_name: teFilenameOnly(prefix || ""),
          filename_stem: filenameStem,
          folder_name: ownerName,
          collection_folder_name: "instagram",
          views, views_available: viewsAvailable ? 1 : 0, likes, comments, shares, reposts,
          sort: {
            field: sortField,
            value: sortValue,
            views,
            likes,
            comments,
            shares,
            reposts,
            participation_score: teParticipationScore(m),
            created_at: teNum(m?.created_at || m?.taken_at || m?.caption?.created_at),
            engagement_rate: Number(m?.engagement_rate || 0) || 0,
            captured_at: nowIso
          },
          sort_field: sortField,
          sort_value: sortValue,
          source_url: String(mediaUrl || ""),
          current_url: href,
          download_prefix: String(prefix || ""),
          media_index: Number(mediaIndex) || 0,
          total_media: Number(totalMedia) || 1,
          media_type: m?.media_type || "",
          stable_identity: {
            priority: ["shortcode", "asset_kind", "media_index", "instagram_original_filename", "sha256", "md5", "media_id", "media_url", "filename"],
            shortcode,
            asset_kind: assetKind,
            media_index: Number(mediaIndex) || 0,
            media_id: String(m?.id || m?.pk || ""),
            pk: String(m?.pk || ""),
            instagram_original_filename: originalInfo.original_filename,
            media_url: teMediaPageUrl(m, mediaUrl),
            source_url: String(mediaUrl || "")
          }
        };
      }
      function teCollectDateStamp() {
        // 파일명에는 사용하지 않는다. 수집/조회 메타용 현재 시각만 필요할 때 사용한다.
        const d = new Date(), p = (x) => String(x).padStart(2, "0");
        return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}_${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
      }
      function teUploadStampFromMedia(media) {
        const sec = Number(media?.created_at || media?.taken_at || media?.caption?.created_at || 0);
        if (!Number.isFinite(sec) || sec <= 0) return "upload_unknown";
        const d = new Date(sec * 1000), p = (x) => String(x).padStart(2, "0");
        return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}_${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
      }
      function teOwnerAccountForMedia(media) {
        return teMediaOwnerName(media || {});
      }
      function teIsSingleMediaPage() {
        try {
          const parts = String(teCurrentTargetUrl() || location.href || "").split("instagram.com/").pop().split(/[?#]/)[0].split("/").filter(Boolean);
          return !!(parts.length >= 2 && ["reel", "reels", "p"].includes(parts[0]));
        } catch (_) { return false; }
      }

      function teAutoPyAckMatches(ack, rankNo, shortcode, sinceMs) {
        try {
          if (!ack || typeof ack !== "object") return false;
          const ts = Number(ack.ts || ack.clicked_at_ms || 0) || 0;
          if (sinceMs && ts && ts < Number(sinceMs || 0)) return false;
          const ar = Number(ack.rank || ack.rank_no || 0) || 0;
          if (rankNo && ar && ar !== Number(rankNo)) return false;
          const ac = String(ack.code || ack.shortcode || "").trim();
          if (shortcode && ac && ac !== String(shortcode)) return false;
          const mt = String(ack.media_type || ack.asset_kind || "").toLowerCase();
          const bt = String(ack.button_text || "").toLowerCase();
          if (mt && !/mp4|video|download|jpg|image/.test(mt) && !bt.includes("download") && !bt.includes("down")) return false;
          return true;
        } catch (_) { return false; }
      }
      function teUseNoInFilename(totalMedia) {
        // v99: No is controlled by page type + actual rank, not by rank_total timing.
        // rank_total can be missing/0 in late main payloads, which caused _No001_ to disappear.
        return !teIsSingleMediaPage();
      }
      function teBuildLocalArchiveStem(media, target) {
        const account = teOwnerAccountForMedia(media || {});
        if (!account) return "";
        const code = teSafeName((target && target.code) || media?.code || media?.shortcode || "media");
        const upload = teUploadStampFromMedia(media || {});
        const snapshotNo = Number(media && media.__igblue_auto_snapshot_no || 0) || 0;
        const rank = Number(target && target.rank || snapshotNo || teRankNoForMedia(media || {}, 0)) || 0;
        const noPart = rank > 0 ? `_No${String(rank).padStart(3, "0")}` : `_No없음`;
        return `${account}_${upload}${noPart}_${code}`;
      }
      function teBuildAutoPyCoordinateProbe(target, collectMeta, opts) {
        const t = target && typeof target === "object" ? target : {};
        const cm = collectMeta && typeof collectMeta === "object" ? collectMeta : {};
        const vp = t.viewport && typeof t.viewport === "object" ? t.viewport : {};
        const br = t.button_rect && typeof t.button_rect === "object" ? t.button_rect : {};
        const tr = t.thumbnail_rect && typeof t.thumbnail_rect === "object" ? t.thumbnail_rect : (t.card_rect || {});
        const cr = t.card_rect && typeof t.card_rect === "object" ? t.card_rect : {};
        return {
          schema: "ig_coordinate_probe_v59",
          purpose: "dom_rect_to_rtc_screen_coordinate_calibration",
          captured_at: new Date().toISOString(),
          rank: Number(t.rank || 0) || 0,
          rank_label: cm.rank_label || (t.rank ? `No${String(Number(t.rank)).padStart(3, "0")}` : ""),
          shortcode: String(t.code || cm.shortcode || ""),
          after_nav_sleep_ms: Number(opts && opts.after_nav_sleep_ms || 0) || 0,
          run_seq: Number(opts && opts.run_seq || 0) || 0,
          href: teCurrentTargetUrl() || "",
          viewport: vp,
          rects: {
            button: br,
            thumbnail: tr,
            card: cr,
            overlay: t.overlay_rect || {},
            automation_download_buttons: Array.isArray(t.automation_button_rects) ? t.automation_button_rects : [],
            visual_download_buttons: Array.isArray(t.visual_button_rects) ? t.visual_button_rects : [],
            all_download_buttons: Array.isArray(t.automation_button_rects) && t.automation_button_rects.length ? t.automation_button_rects : (Array.isArray(t.download_button_rects) ? t.download_button_rects : [])
          },
          expected_click: { x: Number(br.cx || 0) || 0, y: Number(br.cy || 0) || 0 },
          notes: [
            "Next/Prev 뒤 좌표 재사용 금지",
            "DOM rect는 힌트이며 RTC가 window/client/monitor 기준 보정 로그를 남긴다",
            "패턴이 일정하면 DOM+offset, 불안정하면 화면인식으로 전환"
          ]
        };
      }
      function teBuildAutoPyCollectMeta(media, target) {
        const m = media || {};
        const rank = Number(target && target.rank || 0) || 0;
        const sortField = teTopMetricField();
        const sortValue = sortField ? teMetricNumber(m?.[sortField]) : 0;
        const stem = teBuildLocalArchiveStem(m, target || {});
        if (!stem) {
          return {
            schema: "ig_local_collect_v2",
            purpose: "local_duplicate_prevention_and_sort_history",
            owner_missing: true,
            owner_missing_reason: "media_owner_username_not_found",
            shortcode: String(m?.code || ""),
            rank_no: rank,
            rank_label: rank > 0 ? `No${String(rank).padStart(3, "0")}` : "",
            filename_stem: ""
          };
        }
        const base = teBuildMediaDownloadMeta(m, String(m?.video_url || m?.img_origin || m?.img_small || ""), stem, 0, 1);
        return Object.assign(base, {
          schema: "ig_local_collect_v2",
          purpose: "local_duplicate_prevention_and_sort_history",
          collected_at: new Date().toISOString(),
          rank_no: rank,
          rank_label: rank > 0 ? `No${String(rank).padStart(3, "0")}` : "",
          rank_meaning: "sort_rank_at_collection_time",
          filename_stem: stem,
          expected_files: { jpg: `${stem}.jpg`, mp4: `${stem}.mp4`, md5: `${stem}.md5` },
          sort_snapshot: {
            field: teServerSortField(sortField) || "",
            value: sortValue,
            views: teViewCountOrPrivate(m),
            views_available: teViewsAvailable(m) ? 1 : 0,
            likes: teNum(m?.like_count),
            comments: teNum(m?.comment_count),
            shares: 0,
            reposts: teNum(m?.media_repost_count),
            participation_score: teParticipationScore(m),
            engagement_rate: Number(m?.engagement_rate || 0) || 0,
            created_at: teNum(m?.created_at || m?.taken_at || m?.caption?.created_at)
          },
          archive_layout: {
            index_json_location: "parent_folder/<final_folder_name>.json",
            sidecar_location: "final_folder/<filename_stem>.md5",
            set_files: ["<filename_stem>.jpg", "<filename_stem>.mp4", "<filename_stem>.md5"]
          }
        });
      }
      function teAutoPyNoLabel(rankNo) {
        return `No${String(Number(rankNo || 0) || 0).padStart(3, "0")}`;
      }
      function teAutoPyPathExt(pathOrName) {
        const m = String(pathOrName || "").toLowerCase().match(/\.([a-z0-9]+)(?:[?#].*)?$/);
        return m ? m[1] : "";
      }
      function teAutoPyKindFromExt(pathOrName) {
        const ext = teAutoPyPathExt(pathOrName);
        if (["mp4", "mov", "webm", "m4v"].includes(ext)) return "video";
        if (["jpg", "jpeg", "png", "webp", "avif"].includes(ext)) return "image";
        return "";
      }
      function teAutoPyFirstPathFromPrecheck(pre) {
        const local = pre && pre.local && typeof pre.local === "object" ? pre.local : {};
        if (pre && pre.file_path) return String(pre.file_path);
        if (local && local.file_path) return String(local.file_path);
        const paths = [];
        if (Array.isArray(pre && pre.file_paths)) paths.push(...pre.file_paths);
        if (Array.isArray(local && local.existing_file_paths)) paths.push(...local.existing_file_paths);
        if (Array.isArray(local && local.file_paths)) paths.push(...local.file_paths);
        return paths.find(Boolean) ? String(paths.find(Boolean)) : "";
      }
      function teAutoPyDuplicateDecision(pre, expectedKind, shortcode) {
        const local = pre && pre.local && typeof pre.local === "object" ? pre.local : {};
        const filePath = teAutoPyFirstPathFromPrecheck(pre);
        const pathKind = teAutoPyKindFromExt(filePath);
        const expected = String(expectedKind || "").toLowerCase();
        const sc = String(shortcode || "").trim();
        const localExists = !!(local && (local.exists === 1 || local.exists === true || (Array.isArray(local.existing_file_paths) && local.existing_file_paths.length)));
        const exists = !!(pre && (pre.exists === 1 || pre.exists === true || localExists));
        const dupFlag = !(pre && pre.duplicate_confirmed === false) && !(local && local.duplicate_confirmed === false);
        const ext_match = !expected || (!!pathKind && pathKind === expected);
        const shortcode_match = !sc || !filePath || String(filePath).includes(sc);
        const confirmed = !!(pre && pre.ok && exists && localExists && dupFlag && ext_match && shortcode_match);
        return {
          duplicate_confirmed: confirmed,
          exists,
          local_exists: localExists,
          server_exists: !!(pre && (pre.server_exists === 1 || pre.server_exists === true)),
          dup_flag: dupFlag,
          expected_kind: expected,
          path_kind: pathKind,
          ext_match,
          shortcode_match,
          file_path: filePath,
          match_by: String((pre && pre.match_by) || (local && local.match_by) || ""),
          reason: confirmed ? "local_asset_ext_shortcode_match" : (!localExists ? "local_file_not_confirmed" : !ext_match ? "asset_ext_mismatch" : !shortcode_match ? "shortcode_path_mismatch" : !dupFlag ? "duplicate_flag_false" : "not_confirmed")
        };
      }
      let teAutoPyCycleLogLines = [];
      let teAutoPyCycleLogId = "";
      function teAutoPyCycleLogReset(cycleId) {
        teAutoPyCycleLogId = String(cycleId || "");
        teAutoPyCycleLogLines = [];
      }
      function teAutoPyCycleLogPush(stage, info) {
        try {
          const d = info || {};
          if (stage === "S01_CYCLE_START") teAutoPyCycleLogReset(d.cycle_id || "");
          const line = [
            stage || "cycle",
            d.rank_label || (d.rank_no ? teAutoPyNoLabel(d.rank_no) : ""),
            d.decision ? "decision=" + d.decision : "",
            d.action ? "action=" + d.action : "",
            d.reason ? "reason=" + d.reason : "",
            d.active_rank_no != null ? "active=" + d.active_rank_no : "",
            d.active_hash_no != null ? "hash=" + d.active_hash_no : "",
            d.requested_rank_no != null ? "req=" + d.requested_rank_no : "",
            d.loaded_ready_no != null ? "ready=" + d.loaded_ready_no : "",
            d.next_to_rank_no != null ? "next_to=" + d.next_to_rank_no : "",
            d.next_clicked != null ? "next_clicked=" + (!!d.next_clicked) : "",
            d.next_after_rank != null ? "next_after=" + d.next_after_rank : "",
            d.tab_id != null ? "tab_id=" + d.tab_id : "",
            d.active_tab_id != null ? "active_tab_id=" + d.active_tab_id : "",
            d.request_tab_id != null ? "request_tab_id=" + d.request_tab_id : "",
            d.run_seq != null ? "run_seq=" + d.run_seq : "",
            d.account_queue_index != null ? "account=" + d.account_queue_index + "/" + (d.account_queue_total || 0) : ""
          ].filter(Boolean).join("\n");
          if (line) teAutoPyCycleLogLines.push(line);
          if (teAutoPyCycleLogLines.length > 40) teAutoPyCycleLogLines = teAutoPyCycleLogLines.slice(-40);
        } catch (_) {}
      }
      async function teAutoPyReportCycleState(info) {
        try {
          const payload = Object.assign({ schema: IGBLUE_CYCLE_SCHEMA, build_version: IGBLUE_BUILD_VERSION, build_tag: IGBLUE_BUILD_TAG, href: teCurrentTargetUrl(), source: "side_auto_py" }, info || {});
          payload.cycle_log_lines = teAutoPyCycleLogLines.slice();
          payload.cycle_log_text = payload.cycle_log_lines.join("\n");
          payload.cycle_log_id = teAutoPyCycleLogId || String(payload.cycle_id || "");
          return await teAiwkWsRequest("ig_auto_cycle_state", payload, 1600);
        } catch (err) {
          dbg("auto_py", "cycle_state_report_failed", 59301, { error: String(err && err.message || err), info });
          return null;
        }
      }
      const teAutoPyLogOnceState = { map: {} };
      function teAutoPyLogOnce(message, minGapMs) {
        try {
          const msg = String(message || "");
          const now = Date.now();
          const gap = Math.max(8000, Number(minGapMs || 12000) || 12000);
          const key = msg.replace(/\d{1,2}시\s*\d{1,2}분\s*\d{1,2}초\s*/g, "").replace(/\s+/g, " ").trim();
          teAutoPyLogOnceState.map = teAutoPyLogOnceState.map || {};
          const prev = teAutoPyLogOnceState.map[key] || 0;
          if (prev && now - prev < gap) return false;
          teAutoPyLogOnceState.map[key] = now;
          n(msg);
          return true;
        } catch (_) { try { n(String(message || "")); } catch (_) {} return true; }
      }
      function teAutoPyCycleLine(stage, info) {
        const d = info || {};
        if (String(d.action || "").toLowerCase() === "stop" || String(d.decision || "").toLowerCase() === "auto_stopped") {
          const stopKo = String(d.stop_reason_ko || d.autoplay_stop_reason_ko || d.reason || stage || "자동 중단");
          teAutoPyLogOnce(`[자동다운로드][중단][체크실패] ${stopKo}`, 2500);
        }
        dbg("auto_py", stage, 59302, d);
      }
      async function teAutoPyCycle(stage, info) {
        const ctx = teCurrentWsTargetPayload(stage || "cycle", {});
        const acct = teAutoPyAccountQueueStatus ? teAutoPyAccountQueueStatus() : null;
        const payload = Object.assign({
          cycle_stage: stage,
          feature_check_mode: true,
          updated_at: new Date().toISOString(),
          tab_id: Number(ctx && ctx.tab_id || 0) || 0,
          request_tab_id: Number(ctx && ctx.tab_id || 0) || 0,
          window_id: Number(ctx && ctx.window_id || 0) || 0,
          bg_version: IGBLUE_BUILD_TAG,
          bg_build_tag: IGBLUE_BUILD_TAG,
          account_queue_index: acct && acct.has ? Number(acct.index || 0) : 0,
          account_queue_total: acct && acct.has ? Number(acct.total || 0) : 0,
          account_queue_url: acct && acct.has ? String(acct.url || "") : "",
          account_queue_username: acct && acct.has ? String(acct.account || "").replace(/^@/, "") : ""
        }, info || {});
        teAutoPyCycleLogPush(stage, payload);
        teAutoPyCycleLine(stage, payload);
        await teAutoPyReportCycleState(payload);
        return payload;
      }

      // v179a39 window movement pause state.
      let teWindowMoveState = { moving:false, stable:false, last_move_at:0, screen_x:null, screen_y:null, geometry_signature:"", recalibration_promise:null, last_reported_state:"", pending_recalibration:false };
      async function teAutoPyHandleWindowMoveState(detail) {
        const d = detail && detail.window_move ? detail.window_move : detail || {};
        const state = String(d.state || '');
        if (!state) return;
        teWindowMoveState.screen_x = Number(d.screen_x);
        teWindowMoveState.screen_y = Number(d.screen_y);
        teWindowMoveState.geometry_signature = String(d.geometry_signature || teWindowMoveState.geometry_signature || "");
        teWindowMoveState.last_move_at = Number(d.last_move_at || Date.now());
        if (state === 'WINDOW_BASELINE') {
          teWindowMoveState.last_reported_state = state;
          teWindowMoveState.moving = false; teWindowMoveState.stable = true;
          return;
        }
        if (state === 'WINDOW_MOVING' || state === 'WINDOW_STABILIZING') {
          const firstForState = teWindowMoveState.last_reported_state !== state;
          teWindowMoveState.last_reported_state = state;
          teWindowMoveState.moving = false; teWindowMoveState.stable = false;
          teWindowMoveState.pending_recalibration = false;
          if (firstForState) {
            try { await teAutoPyCycle('S_WINDOW_MOVE_DIRECT_CONTINUE', {decision:'continue', action:'direct_download_no_mouse', reason:state, window_move:d}); } catch (_) {}
          }
          return;
        }
        if (state === 'WINDOW_STABLE') {
          teWindowMoveState.last_reported_state = state;
          teWindowMoveState.moving = false; teWindowMoveState.stable = true;
          teWindowMoveState.pending_recalibration = false;
          teWindowMoveState.recalibration_promise = null;
          if (teAutoPyRunning && !teAutoPyStopping) {
            try { await teAutoPyCycle('S_WINDOW_STABLE_DIRECT_CONTINUE', {decision:'continue', action:'direct_download_no_mouse', reason:'window_stable', window_move:d}); } catch (_) {}
          }
        }
      }
      async function teAutoPyWaitWindowMoveGate() {
        if (!teWindowMoveState.moving && !teWindowMoveState.recalibration_promise) return {ok:true};
        const start=Date.now();
        while ((teWindowMoveState.moving || teWindowMoveState.recalibration_promise) && Date.now()-start<180000) {
          if (teAutoPyStopping) return {ok:false,error:'user_stop'};
          await teSleep(250);
        }
        return {ok:!teAutoPyStopping, waited_ms:Date.now()-start};
      }

      async function teAutoPyWaitOrPause(reason, limitSec, checkFn, info) {
        const code = String(reason || "wait_or_pause");
        const maxMs = Math.max(1000, Number(limitSec || 30) * 1000);
        const start = Date.now();
        const base = Object.assign({}, info || {}, { decision: "wait", action: "wait_or_pause", reason: code, limit_sec: Math.round(maxMs / 1000) });
        await teAutoPyCycle("S_WAIT_OR_PAUSE", base);
        while (Date.now() - start < maxMs) {
          if (teAutoPyStopping) return { ok: false, stopped: true, error: "user_stop" };
          try {
            const res = typeof checkFn === "function" ? await checkFn() : { ok: true };
            if (res && res.ok) {
              await teAutoPyCycle("S_WAIT_OR_PAUSE_RESUME", Object.assign({}, base, { decision: "resume", waited_ms: Date.now() - start, result: res || null }));
              return Object.assign({ ok: true, waited_ms: Date.now() - start }, res || {});
            }
          } catch (err) {
            await teAutoPyCycle("S_WAIT_OR_PAUSE_RETRY", Object.assign({}, base, { decision: "retry", waited_ms: Date.now() - start, error: String(err && err.message || err) }));
          }
          await teSleep(1000);
        }
        await teAutoPyCycle("S_WAIT_OR_PAUSE_TIMEOUT", Object.assign({}, base, { decision: "timeout", waited_ms: Date.now() - start }));
        return { ok: false, timeout: true, error: code + "_timeout", waited_ms: Date.now() - start };
      }
      async function teAutoPyReadCurrentNoNav(waitMs = 500) {
        return teSendToTargetTabResponse({ type: "igblue_auto_py_read_current_no", wait_ms: Math.max(120, Number(waitMs || 500) || 500) }, "", Math.max(1000, Number(waitMs || 500) + 700)).catch((err) => ({ ok: false, error: String(err && err.message || err) }));
      }
      async function teAutoPyClickCurrentNoNav(info, waitMs = 1300, stage = "S02_CURRENT_NO_CLICK") {
        const reqWait = Math.max(180, Number(waitMs || 1300) || 1300);
        const res = await teSendToTargetTabResponse({ type: "igblue_auto_py_current", wait_ms: reqWait }, "", Math.max(900, reqWait + 900)).catch((err) => ({ ok: false, error: String(err && err.message || err) }));
        const currentOk = !!(res && res.ok && (res.confirmed_current || res.target_found || Number(res.after_rank || 0) > 0));
        await teAutoPyCycle(stage || "S02_CURRENT_NO_CLICK", Object.assign({}, info || {}, {
          decision: currentOk ? "current_clicked" : "current_click_unconfirmed",
          action: "dom_click_NoNav_Current",
          first_current_click: !!((info || {}).first_current_click),
          current_refocus: !!((info || {}).current_refocus),
          next_requested: false,
          next_clicked: false,
          current_button_found: !!(res && res.current_button_found),
          active_rank_no: Number(res && res.after_rank || 0) || Number(res && res.before_rank || 0) || 0,
          requested_rank_no: Number(res && res.before_rank || 0) || 0,
          active_shortcode: res && res.after_shortcode || res && res.before_shortcode || "",
          current_response: res || null,
          reason: currentOk ? "NoNav_Current_clicked_and_target_refreshed" : "NoNav_Current_click_unconfirmed"
        }));
        return res;
      }
      async function teAutoPyClickNextNoNav(info, waitMs = 1300, stage = "S13_NEXT_DOM_CLICK") {
        const res = await teSendToTargetTabResponse({ type: "igblue_auto_py_next", wait_ms: Math.max(500, Number(waitMs || 1300) || 1300) }, "", Math.max(1600, Number(waitMs || 1300) + 900)).catch((err) => ({ ok: false, error: String(err && err.message || err) }));
        const nextClicked = !!(res && res.ok && res.changed && Number(res.after_rank || 0) > 0);
        await teAutoPyCycle(stage || "S13_NEXT_DOM_CLICK", Object.assign({}, info || {}, {
          decision: nextClicked ? "next_clicked" : "next_click_unconfirmed",
          action: "dom_click_NoNav_Next",
          next_requested: true,
          next_button_found: !!(res && res.next_button_found),
          next_clicked: nextClicked,
          next_to_rank_no: Number(res && res.after_rank || 0) || 0,
          next_to_rank_label: res && res.after_label || (Number(res && res.after_rank || 0) ? teAutoPyNoLabel(Number(res.after_rank || 0)) : ""),
          next_before_rank: res && res.before_rank,
          next_after_rank: res && res.after_rank,
          next_before_shortcode: res && res.before_shortcode,
          next_after_shortcode: res && res.after_shortcode,
          next_response: res || null,
          reason: nextClicked ? "NoNav_Next_changed_current" : "NoNav_Next_no_change_observed"
        }));
        return res;
      }
      async function teAutoPyGoNoWithConfirm(no, info, waitMs, stage) {
        const requestedNo = Number(no || 1) || 1;
        const res = await teSendToTargetTabResponse({ type: "igblue_auto_py_go_no", no: requestedNo, wait_ms: Math.max(300, Number(waitMs || 900) || 900) }, "", Math.max(1200, Number(waitMs || 900) + 900)).catch((err) => ({ ok: false, error: String(err && err.message || err) }));
        const nextClicked = !!(res && res.ok && (res.changed || Number(res.after_rank || 0) === requestedNo));
        await teAutoPyCycle(stage || "S09_DUPLICATE_NEXT", Object.assign({}, info || {}, {
          decision: nextClicked ? "next_clicked" : "next_requested_but_not_confirmed",
          action: "dom_go_no_next",
          next_requested: true,
          next_button_found: !!(res && res.target_found),
          next_clicked: nextClicked,
          next_to_rank_no: requestedNo,
          next_to_rank_label: teAutoPyNoLabel(requestedNo),
          next_before_rank: res && res.before_rank,
          next_after_rank: res && res.after_rank,
          next_before_shortcode: res && res.before_shortcode,
          next_after_shortcode: res && res.after_shortcode,
          next_response: res || null,
          reason: nextClicked ? "target_changed_or_requested_rank_active" : "content_go_no_no_change_observed"
        }));
        return res;
      }

      async function teMediaPrecheckAiwk(entry) {
        if (!entry || !entry.meta) return null;
        const accountFolderMerge = false;
        return teAiwkTrackWsResult(await teAiwkWsRequest("ig_media_exists", {
          root: tePersistDownloadRootNow(),
          account: accountFolderMerge ? "" : (entry.account || teDownTxtAccount()),
          account_folder_merge: accountFolderMerge,
          prefix: entry.prefix || "",
          url: entry.url || "",
          href: teCurrentTargetUrl(),
          cycle: entry.cycle || null,
          meta: entry.meta
        }, 4500));
      }
      async function teNotifyMediaDownloadedAiwk(entry, downRes) {
        if (!entry) return null;
        const accountFolderMerge = false;
        const meta = Object.assign({ account_folder_merge: accountFolderMerge }, entry.meta || {});
        if (downRes && downRes.filename) meta.file_name = teFilenameOnly(downRes.filename);
        const notify = teAiwkTrackWsResult(await teAiwkWsRequest("ig_media_downloaded", {
          root: tePersistDownloadRootNow(),
          account: accountFolderMerge ? "" : (entry.account || teDownTxtAccount()),
          account_folder_merge: accountFolderMerge,
          prefix: entry.prefix || "",
          url: entry.url || "",
          file_path: downRes && downRes.filename ? String(downRes.filename) : "",
          download_id: downRes && downRes.id ? downRes.id : "",
          meta
        }, 20000));
        if (notify && notify.ok && notify.item) teRecordDownloadedItem(notify.item);
        return notify;
      }
      function teRecordDownloadedItem(item) {
        if (!item || typeof item !== "object") return;
        const merged = teNormalizeDownItems([item])[0];
        if (!merged) return;
        const k = teDownItemKey(merged);
        const old = teDownItems.filter(x => teDownItemKey(x) !== k);
        old.push(merged);
        teRenderDownList(old, teDownTxtAccount());
      }
      function teRecordDownloadedFile(pathOrName, item) {
        if (item && typeof item === "object") return teRecordDownloadedItem(item);
        const name = teFilenameOnly(pathOrName);
        if (!/\.mp4$/i.test(name)) return;
        const m = name.match(/_No(\d{3})_/i);
        teRecordDownloadedItem({ rank_no: m ? Number(m[1]) : 0, rank_label: m ? ("No" + m[1]) : "", asset_kind: "video", file_name: name, shortcode: "" });
      }
      async function teRebuildAllAccountReports() {
        const btn = document.getElementById("igblue_report_rebuild_all");
        if (btn) btn.disabled = true;
        teDownTxtStatus("기존 계정 보고서 재생성 중...");
        try {
          const res = teAiwkTrackWsResult(await teAiwkWsRequest("ig_report_rebuild_all", { root: "instagram" }, 120000, { forceProbe: true }));
          if (res && res.ok) {
            teDownTxtStatus(`보고서 재생성 완료 ${Number(res.success_count || 0)}/${Number(res.requested_count || 0)}`);
            return true;
          }
          teDownTxtStatus("보고서 재생성 실패 " + String(res && (res.error_code || res.error || res.message) || "WS 응답 없음"));
          return false;
        } catch (error) {
          teDownTxtStatus("보고서 재생성 실패 " + String(error && error.message || error));
          return false;
        } finally {
          if (btn) btn.disabled = false;
        }
      }
      function teInitDownTxtUi() {
        const ta = document.getElementById("igblue_down_file_list"), load = document.getElementById("igblue_down_txt_load_aiwk"), save = document.getElementById("igblue_down_txt_save_aiwk"), clear = document.getElementById("igblue_down_txt_clear"), rebuild = document.getElementById("igblue_report_rebuild_all");
        if (!ta && !load && !save && !clear && !rebuild) return;
        teLoadDownTxtLocal(teDownTxtAccount());
        const setupLink = document.getElementById("igblue_aiwk_setup_zip_link");
        setupLink && setupLink.addEventListener("click", (ev) => {
          ev.preventDefault();
          try { window.open(teAiwkSetupZipUrl, "_blank", "noopener,noreferrer"); } catch { location.href = teAiwkSetupZipUrl; }
        });
        load && load.addEventListener("click", () => teLoadDownTxtAiwk());
        save && save.addEventListener("click", () => teSaveDownTxtAiwk(teDownTxtAccount()));
        clear && clear.addEventListener("click", () => { teRenderDownList([], teDownTxtAccount()); teSaveDownTxtAiwk(teDownTxtAccount()); });
        rebuild && rebuild.addEventListener("click", () => teRebuildAllAccountReports());
      }

      function teRestoreLastInfoForPopup() {
        if (!teIsPopupMode()) return;
        teRequestTargetState(!0);
      }
      function H() {
        var e,
          t = IGBLUE_PACKAGE_VERSION;
        ((document.querySelector(".version").textContent = t),
          teInitPopupUi(),
          (() => {
            const e = document.getElementById("upgrade");
            e && e.addEventListener("click", z);
          })(),
          document.getElementById("export_xlsx").addEventListener("click", () => teRunButtonOnce("export_xlsx", "export_xlsx", 1200, U)),
          teInitDownloadOptions(),
          teInitDownloadFolderUi(),
          teInitDownTxtUi(),
          teInitAnalyze8780Ui(),
          teInitPyAutoUi(),
          teAutoPyRestoreAccountQueue(),
          teAutoPyRestoreProgressNotice(),
          (function(){
            const startBtn = document.getElementById("igblue_auto_py_start"), stopBtn = document.getElementById("igblue_auto_py_stop"), resetBtn = document.getElementById("igblue_auto_progress_reset");
            if (startBtn) startBtn.addEventListener("click", () => teAutoPyStartOrContinueFromUi());
            if (stopBtn) stopBtn.addEventListener("click", () => teAutoPyStop("user"));
            if (resetBtn) resetBtn.addEventListener("click", () => teAutoPyResetProgressState("user_reset"));
          })(),
          (function () {
            (document
              .getElementById("auto_swipe")
              .addEventListener("click", () => Y(1)),
              document
                .getElementById("auto_swipe_10")
                .addEventListener("click", () => V(10)),
              document
                .getElementById("auto_swipe_30")
                .addEventListener("click", () => V(30)),
              document
                .getElementById("auto_top")
                .addEventListener("click", () =>
                  (function () {
                    const e = `top-${Date.now()}`;
                    B(e);
                    teSendToTargetTab({ type: "ndy_top" }, "Instagram tab not found").finally(() => D(e));
                  })(),
                ),
              document
                .getElementById("down_all_medias")
                .addEventListener("click", () => {
                  teRunButtonOnce("down_all_medias", "down_all_medias", 1200, () => L(() => X("all", se), -1, !1));
                }),
              document
                .getElementById("down_filtered_medias")
                .addEventListener("click", () => {
                  teRunButtonOnce("down_filtered_medias", "down_filtered_medias", 1200, () => L(() => X("filtered", oe), -1, !1));
                }));
            const e = document.getElementById("goto");
            e &&
              e.addEventListener("click", () => {
                window.open("https://www.instagram.com/explore/", "_blank");
              });
            const t = document.getElementById("refresh");
            t &&
              t.addEventListener("click", () => {
                tePersistDownloadOptionsNow();
                const e = document.getElementById("top_n_count");
                e && (teLocalSet("filter/metric_threshold_10k", e.value || ""), teLocalSet("top_metric_units", e.value || ""), teLocalSet("top_n_count", e.value || ""));
                teRefreshRanksAndMetricCount();
                teSendToTargetTab({ type: "ndy_refresh" }, "Instagram tab not found");
              });
            const miniReread = document.getElementById("mini_reread");
            miniReread && miniReread.addEventListener("click", async () => {
              if (miniReread.disabled) return;
              const oldText = miniReread.textContent;
              miniReread.disabled = true;
              miniReread.textContent = "읽는중…";
              const requestId = `reread_${Date.now()}`;
              const ok = await teSendToTargetTab({ type: "ndy_reread", request_id: requestId }, "Instagram tab not found");
              if (ok) n("탭 새로읽기 요청: 페이지 새로고침 없이 현재 Main 데이터를 다시 읽습니다.");
              else n("탭 새로읽기 실패: 현재 Instagram 탭을 확인하세요.");
              setTimeout(() => { miniReread.disabled = false; miniReread.textContent = oldText || "탭 새로읽기"; }, 900);
            });
            const miniRefresh = document.getElementById("mini_refresh");
            miniRefresh && miniRefresh.addEventListener("click", () => {
              teRefreshRanksAndMetricCount();
              teSendToTargetTab({ type: "ndy_refresh" }, "Instagram tab not found");
            });
          })(),
          (function () {
            Promise.all([S(), M()]).catch((e) => {});
            const e = document.getElementById("settings");
            e && e.addEventListener("click", () => {
              if (!(teHasInstagramTarget || u)) return void n("인스타 홈페이지를 먼저 열어주세요");
              T();
            });
          })(),
          ((e = document.getElementById("sort_by")).value = teSelectHasValue(e, teLocalGetAny(["filter/sort_by", "sort_by"], "4")) ? teLocalGetAny(["filter/sort_by", "sort_by"], "4") : Z),
          (Z = parseInt(e.value)),
          e.addEventListener("change", function () {
            (teLocalSet("filter/sort_by", this.value),
              teLocalSet("sort_by", this.value),
              (Z = parseInt(this.value)),
              ae(se, K, Z, !1, !0),
              teRefreshRanksAndMetricCount());
          }),
          (function () {
            const e = document.getElementById("timeRange"),
              t = teLocalGetAny(["filter/timeRange", "timeRange"], e?.value || "0");
            e && teSelectHasValue(e, t) && (e.value = String(t));
            K = teRangeToTimestamp(e?.value || "0");
            e &&
              e.addEventListener("change", function () {
                (teLocalSet("filter/timeRange", this.value),
                  teLocalSet("timeRange", this.value),
                  (K = teRangeToTimestamp(this.value)),
                  ae(se, K, Z, !0, !1),
                  teRefreshRanksAndMetricCount());
              });
          })(),
          I(0, 0),
          P());
      }
      function V(e) {
        L(Y, e, !1);
      }
      function teDownloadVideoSave() {
        const e = document.getElementById("download_video_save");
        return !!e?.checked;
      }
      function teDownloadImageSave() {
        const e = document.getElementById("download_image_save");
        return !!e?.checked;
      }
      function teDownloadVideoOnly() {
        return teDownloadVideoSave() && !teDownloadImageSave();
      }
      function teDownloadIdDateName() {
        // v277: ID-date filename 선택 UI는 폐기했다. 기존 파일명 규칙을 항상 유지한다.
        return true;
      }
      function teDownloadSavePolicy() {
        const video = teDownloadVideoSave(), image = teDownloadImageSave();
        return video && image ? "video_image" : video ? "video_only" : image ? "image_only" : "metadata_only";
      }
      function teDownloadStatusFlags(assetKind = "") {
        const kind = String(assetKind || "").toLowerCase();
        return {
          metadata_collected: 1,
          video_requested: teDownloadVideoSave() ? 1 : 0,
          video_downloaded: kind === "video" || kind === "mp4" ? 1 : 0,
          image_requested: teDownloadImageSave() ? 1 : 0,
          image_downloaded: kind === "image" || kind === "jpg" || kind === "jpeg" ? 1 : 0,
          save_policy: teDownloadSavePolicy()
        };
      }
      function teDownloadDelaySec() {
        const e = document.getElementById("download_delay_sec");
        let t = parseInt(e?.value ?? teLocalGetAny(["download/delay_sec", "download_delay_sec"], "0"), 10);
        if (!Number.isFinite(t) || t < 0) t = 0;
        if (t > 99) t = 99;
        if (e && String(e.value) !== String(t)) e.value = String(t).padStart(0, "");
        return t;
      }
      function tePersistDownloadOptionsNow() {
        const e = document.getElementById("download_video_save"),
          t = document.getElementById("download_image_save"),
          r = document.getElementById("download_delay_sec");
        e && (teLocalSet("download/video_save", e.checked ? "1" : "0"), teLocalSet("download_video_save", e.checked ? "1" : "0"));
        t && (teLocalSet("download/image_save", t.checked ? "1" : "0"), teLocalSet("download_image_save", t.checked ? "1" : "0"));
        if (r) {
          const e = teDownloadDelaySec();
          teLocalSet("download/delay_sec", e);
          teLocalSet("download_delay_sec", e);
        }
      }
      function teSafeName(e) {
        return String(e || "unknown")
          .replace(/[\\/:*?"<>|]+/g, "_")
          .replace(/\s+/g, "_")
          .replace(/_+/g, "_")
          .replace(/^_+|_+$/g, "")
          .slice(0, 80) || "unknown";
      }
      function teDateStamp(e) {
        const t = Number(e?.created_at || e?.taken_at || e?.caption?.created_at || e?.media?.taken_at || e?.media?.caption?.created_at);
        if (!Number.isFinite(t) || t <= 0) return "nodate";
        const r = new Date(1e3 * t),
          n = (e) => String(e).padStart(2, "0");
        return `${r.getFullYear()}${n(r.getMonth() + 1)}${n(r.getDate())}_${n(r.getHours())}${n(r.getMinutes())}${n(r.getSeconds())}`;
      }
      function teIsVideoUrl(e) {
        const t = String(e || "").split("?")[0].toLowerCase();
        return t.includes(".mp4") || t.includes("/video/") || t.includes("video");
      }
      // IGblue local patch v18: do not use each post/media username for file/folder identity.
      // The visible channel account is decided by the current/pinned Instagram URL and is fixed
      // for the download session; a reposted/foreign media item may carry another username.
      function teDownloadChannelAccountName() {
        return teSafePath(teActiveFolderAccountName() || teAccountFromUrl(teCurrentTargetUrl()) || "instagram");
      }
      function teFilenameAccountName(e) {
        const t = teUrlMode(teCurrentTargetUrl());
        if (["explore", "p", "reel", "reels", "stories"].includes(t)) return teMediaOwnerName(e);
        const folderAccount = teCleanOwnerAccountName(teDownloadChannelAccountName().split("/").pop() || teDownloadChannelAccountName());
        return folderAccount || teMediaOwnerName(e);
      }
      function teDisplayAccountForMedia(e) {
        const t = teUrlMode(teCurrentTargetUrl());
        if (["explore", "p", "reel", "reels", "stories"].includes(t)) return teMediaOwnerName(e);
        const folderAccount = teCleanOwnerAccountName(teDownloadChannelAccountName().split("/").pop() || teDownloadChannelAccountName());
        return folderAccount || teMediaOwnerName(e);
      }
      function teOriginalInstagramFileInfo(mediaUrl) {
        const raw = String(mediaUrl || "").trim();
        let name = "";
        try {
          const u = new URL(raw, location.href);
          name = decodeURIComponent((u.pathname || "").split("/").filter(Boolean).pop() || "");
        } catch {
          name = raw.split(/[?#]/)[0].split("/").pop() || "";
        }
        name = name.replace(/[\/:*?"<>|]+/g, "_").replace(/\s+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
        const m = name.match(/^(.*)\.([a-z0-9]{2,5})$/i);
        const stem = String(m && m[1] ? m[1] : name || "media")
          .replace(/[\/:*?"<>|\u0000-\u001f]+/g, "_")
          .replace(/[. ]+$/g, "")
          .trim()
          .slice(0, 180) || "media";
        const ext = (m && m[2] ? m[2] : (teIsVideoUrl(raw) ? "mp4" : "jpg")).toLowerCase();
        return { original_filename: stem, original_filename_with_ext: stem + "." + ext, original_ext: ext, source_url: raw };
      }
      function teFirstLoadIsoFromStamp(stamp) {
        const m = String(stamp || "").match(/^(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})$/);
        return m ? `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}:${m[6]}` : "";
      }
      function teFirstLoadKey(account, shortcode, originalName) {
        // v71: first-load stamp is fixed by folder account + Instagram shortcode only.
        // CDN filename must not split jpg/mp4 into different stems.
        return "download/first_load/" + teSafePath(account || "instagram") + "/" + teSafeName(shortcode || "no_shortcode");
      }
      function teFirstLoadStamp(account, shortcode, originalName) {
        const key = teFirstLoadKey(account, shortcode, originalName);
        let saved = teLocalGet(key, "");
        if (!/^\d{8}_\d{6}$/.test(saved)) {
          saved = teCollectDateStamp();
          teLocalSet(key, saved);
        }
        return saved;
      }
      function teRankNoFromPrefix(prefix) {
        const m = String(prefix || "").match(/(?:^|_)No(\d{1,6})(?:_|$)/);
        const n = m ? Number(m[1]) : 0;
        return Number.isFinite(n) && n > 0 ? n : 0;
      }
      function teRankNoForMedia(media, fallbackIndex) {
        const code = String(media?.code || "");
        try {
          const ctx = teBuildRankContext(teMetricSourceList());
          if (ctx && ctx.active && ctx.map && ctx.map.has(code)) return Number(ctx.map.get(code)) || 0;
        } catch {}
        try {
          const list = teMetricSourceList();
          if (Array.isArray(list) && code) {
            const idx = list.findIndex((x) => String(x?.code || "") === code);
            if (idx >= 0) return idx + 1;
          }
        } catch {}
        const fb = Number(fallbackIndex);
        return Number.isFinite(fb) && fb >= 0 ? fb + 1 : 0;
      }
      function teMakeDownloadPrefix(e, t, r, mediaUrl, rankNo) {
        const account = teOwnerAccountForMedia(e || {});
        if (!account) return "";
        const code = teSafeName(e?.code || "media");
        const upload = teUploadStampFromMedia(e || {});
        const no = Number(rankNo || teRankNoForMedia(e, 0)) || 0;
        const noPart = no > 0 ? `_No${String(no).padStart(3, "0")}` : `_No없음`;
        const base = `${account}_${upload}${noPart}_${code}`;
        return Number(r || 1) > 1 ? `${base}_${Number(t || 0) + 1}` : base;
      }
      // IGblue local patch: persist side-panel options as IGblue/category/name=value.
      function teLocalKey(e) {
        return `IGblue/${e}`;
      }
      function teLocalGet(e, t = "") {
        try {
          const r = localStorage.getItem(teLocalKey(e));
          return null == r ? t : r;
        } catch {
          return t;
        }
      }
      function teLocalGetAny(e, t = "") {
        for (const r of e) {
          const e = teLocalGet(r, null);
          if (null !== e) return e;
        }
        return t;
      }
      function teLocalSet(e, t) {
        try {
          localStorage.setItem(teLocalKey(e), String(t));
        } catch {}
      }
      function teLocalBool(e, t = !1) {
        const r = teLocalGet(e, t ? "1" : "0");
        return "1" === r || "true" === r || !0 === r;
      }
      function teSelectHasValue(e, t) {
        return !!e && Array.from(e.options || []).some((e) => e.value === String(t) && !e.disabled);
      }
      function teRestoreTopN() {
        // IGblue local patch v14: top_n_count is reused as threshold units.
        // Metric threshold restore is handled by teRestoreMetricThreshold().
      }
      function teRangeToTimestamp(e) {
        let t,
          r = parseInt(e);
        if (0 === r || !Number.isFinite(r)) t = 0;
        else {
          var n = new Date(),
            i = new Date(n);
          (i.setDate(i.getDate() - r),
            i.setHours(0, 0, 0, 0),
            (t = Math.floor(i.getTime() / 1e3)));
        }
        return t;
      }
      function teInitDownloadOptions() {
        const e = document.getElementById("download_video_save"),
          t = document.getElementById("download_image_save"),
          r = document.getElementById("download_delay_sec");
        const savedVideo = teLocalGetAny(["download/video_save", "download_video_save"], null);
        const savedImage = teLocalGetAny(["download/image_save", "download_image_save"], null);
        const legacyVideoOnly = teLocalGetAny(["download/video_only", "download_video_only"], null);
        if (e) e.checked = savedVideo === null ? true : savedVideo === "1";
        if (t) t.checked = savedImage === null ? !(legacyVideoOnly === "1") : savedImage === "1";
        if (r) r.value = String(Math.max(0, Math.min(99, parseInt(teLocalGetAny(["download/delay_sec", "download_delay_sec"], r.value || "0"), 10) || 0)));
        e &&
          e.addEventListener("change", () => {
            teLocalSet("download/video_save", e.checked ? "1" : "0");
            teLocalSet("download_video_save", e.checked ? "1" : "0");
            if (Array.isArray(oe) && oe.length) teScheduleMainRankLabels(oe, teBuildRankContext(oe));
            N();
          });
        t &&
          t.addEventListener("change", () => {
            teLocalSet("download/image_save", t.checked ? "1" : "0");
            teLocalSet("download_image_save", t.checked ? "1" : "0");
            if (Array.isArray(oe) && oe.length) teScheduleMainRankLabels(oe, teBuildRankContext(oe));
            N();
          });
        r && ["change", "blur"].forEach((e) => r.addEventListener(e, () => {
          const e = teDownloadDelaySec();
          teLocalSet("download/delay_sec", e);
          teLocalSet("download_delay_sec", e);
          N();
        }));
        teRestoreTopN();
        teRestoreMetricThreshold();
      }
      function teDownloadClientId(prefix = "download") {
        return `${String(prefix || "download")}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
      }
      function teDownloadSourceMode(scope) {
        return scope === "all" ? "BULK_ALL" : scope === "filtered" ? "BULK_FILTERED" : scope === "top" ? "TOP_SORTED" : "MANUAL_SINGLE";
      }
      function tePrepareDownloadFlowMeta(meta, defaults = {}) {
        const out = Object.assign({}, meta || {}, defaults || {});
        out.source_mode = String(out.source_mode || "MANUAL_SINGLE").toUpperCase();
        out.client_batch_key = String(out.client_batch_key || teDownloadClientId("batch"));
        out.client_job_key = String(out.client_job_key || teDownloadClientId("job"));
        out.client_event_id = String(out.client_event_id || out.client_job_key);
        out.batch_requested_count = Math.max(1, Number(out.batch_requested_count || 1) || 1);
        out.account_folder_merge = false;
        out.collection_folder_name = "instagram";
        return out;
      }
      function teDirectMediaUrlsForPost(media) {
        const m = media && typeof media === "object" ? media : {};
        const video = [], image = [];
        const push = (url) => {
          const value = String(url || "").trim();
          if (!value) return;
          const bucket = teIsVideoUrl(value) ? video : image;
          if (!bucket.includes(value)) bucket.push(value);
        };
        if (typeof m.carousel_media === "string" && m.carousel_media.trim()) m.carousel_media.split(/\r?\n/).forEach(push);
        push(m.video_url);
        push(m.img_origin || m.img_large || m.img_small || m.img_b64 || m.thumbnail_url || m.display_url);
        return { video, image };
      }
      function q(e, accountFolderMergeOverride, flowMeta) {
        const accountFolderMerge = false;
        dbg("q", "entry", 58848, { media: dbgMedia(e), video_save: teDownloadVideoSave(), image_save: teDownloadImageSave(), save_policy: teDownloadSavePolicy(), active_account: teActiveFolderAccountName(), account_folder_merge: accountFolderMerge });
        if (!e || !e.code) { dbg("q", "empty_media", 58849, {media:e}); return []; }
        tePersistDownloadOptionsNow();
        const urls = teDirectMediaUrlsForPost(e);
        const selected = [];
        if (teDownloadVideoSave()) urls.video.forEach((url) => selected.push({ url, asset_kind:"video" }));
        if (teDownloadImageSave()) urls.image.forEach((url) => selected.push({ url, asset_kind:"image" }));
        const unique = [];
        const seen = new Set();
        selected.forEach((entry) => { if (!seen.has(entry.url)) { seen.add(entry.url); unique.push(entry); } });
        const normalizedFlowMeta = tePrepareDownloadFlowMeta(Object.assign({}, flowMeta || {}), {
          source_mode: (flowMeta && flowMeta.source_mode) || "MANUAL_SINGLE",
          client_batch_key: (flowMeta && flowMeta.client_batch_key) || teDownloadClientId("batch-manual"),
          batch_requested_count: (flowMeta && flowMeta.batch_requested_count) || unique.length || 1
        });
        const directMediaUrls = { video: urls.video.slice(), image: urls.image.slice() };
        const commonStatus = teDownloadStatusFlags("");
        const out = unique.map((entry, index) => {
          const prefix = teMakeDownloadPrefix(e, index, unique.length, entry.url, teRankNoForMedia(e, 0));
          if (!prefix) return null;
          const flowEntryKey = `${normalizedFlowMeta.client_batch_key}:${String(e.code || "post")}:${entry.asset_kind.toUpperCase()}:${index}`;
          const meta = tePrepareDownloadFlowMeta(Object.assign(
            {},
            teBuildMediaDownloadMeta(e, entry.url, prefix, index, unique.length),
            commonStatus,
            teDownloadStatusFlags(entry.asset_kind),
            { direct_media_urls: directMediaUrls, metadata_only:false }
          ), Object.assign({}, normalizedFlowMeta, { client_job_key: flowEntryKey, client_event_id: flowEntryKey }));
          if (meta.owner_missing || !teCleanOwnerAccountName(meta.owner_username)) return null;
          return {
            url: entry.url,
            prefix,
            account: accountFolderMerge ? "" : teSafePath(meta.owner_username || teOwnerAccountForMedia(e) || "instagram"),
            account_folder_merge: accountFolderMerge,
            metadata_only: false,
            media: e,
            meta: Object.assign({account_folder_merge: accountFolderMerge}, meta)
          };
        }).filter(Boolean);
        if (!out.length) {
          const primary = urls.video[0] || urls.image[0] || String(e.video_url || e.img_origin || "");
          const prefix = teMakeDownloadPrefix(e, 0, 1, primary, teRankNoForMedia(e, 0)) || `${teSafeName(teOwnerAccountForMedia(e) || "instagram")}_${teSafeName(e.code || "post")}`;
          const flowEntryKey = `${normalizedFlowMeta.client_batch_key}:${String(e.code || "post")}:METADATA:0`;
          const meta = tePrepareDownloadFlowMeta(Object.assign(
            {},
            teBuildMediaDownloadMeta(e, primary, prefix, 0, 1),
            commonStatus,
            {
              asset_kind:"metadata", file_name:"", filename_stem:prefix,
              direct_media_urls: directMediaUrls, metadata_only:true,
              metadata_reason: teDownloadSavePolicy() === "metadata_only" ? "file_save_disabled" : "requested_asset_unavailable"
            }
          ), Object.assign({}, normalizedFlowMeta, { client_job_key: flowEntryKey, client_event_id: flowEntryKey }));
          if (!(meta.owner_missing || !teCleanOwnerAccountName(meta.owner_username))) out.push({
            url:"", prefix, account:teSafePath(meta.owner_username || teOwnerAccountForMedia(e) || "instagram"),
            account_folder_merge:false, metadata_only:true, media:e, meta
          });
        }
        if (!out.length) {
          dbg("q", "owner_missing_or_no_assets", 58865, { code:e.code, owner:teOwnerAccountForMedia(e), video_urls:urls.video.length, image_urls:urls.image.length });
          teDownTxtStatus("계정명을 찾지 못해 저장을 보류했습니다. explore/reels/p는 계정명으로 사용하지 않습니다.");
        }
        dbg("q", "output_entries", 58866, {code:e.code, count:out.length, save_policy:teDownloadSavePolicy(), entries:out.map(x=>({prefix:x.prefix, account:x.account, metadata_only:!!x.metadata_only, url:String(x.url||"").slice(0,220), asset_kind:x.meta&&x.meta.asset_kind}))});
        return out;
      }
      async function teCollectMetadataOnlyEntry(entry) {
        const e = entry && typeof entry === "object" ? entry : {};
        const meta = Object.assign({}, e.meta || {}, teDownloadStatusFlags(""), { metadata_collected:1, metadata_only:true, asset_kind:"metadata" });
        const account = teSafePath(e.account || meta.owner_username || meta.account_name || "instagram");
        if (!meta.shortcode || !teCleanOwnerAccountName(meta.owner_username || meta.account_name)) return false;
        const ws = await teAiwkWsRequest("ig_media_metadata_collect", {
          account, source_mode:String(meta.source_mode || "MANUAL_SINGLE"), href:teCurrentTargetUrl(), meta
        }, 30000, { forceProbe:true }).catch((error) => ({ok:false, error:String(error && error.message || error)}));
        let local = { ok:false, error:"metadata_observation_missing" };
        try {
          const observation = teStatsObservationFromMedia(e.media || {}, new Date().toISOString(), "LOCAL", teCurrentTargetUrl());
          if (observation) local = await teStatsSaveLocalBatch([Object.assign({}, observation, teDownloadStatusFlags(""), { metadata_collected:1 })]);
        } catch (error) {
          local = { ok:false, error:String(error && error.message || error) };
        }
        const ok = !!(ws && ws.ok && ws.json_saved && ws.report_saved && local && local.ok);
        teDownTxtStatus(ok ? `metadata 저장 완료 ${String(meta.shortcode || "")}` : `metadata 저장 실패 ${String((ws && ws.error) || (local && local.error) || "unknown")}`);
        return ok;
      }
      async function W(e) {
        // v277: 파일 저장을 모두 해제해도 metadata/직접링크/통계를 JSON·HTML·Analyze8780에 남긴다.
        if (e && e.metadata_only) return await teCollectMetadataOnlyEntry(e);
        // IGblue local patch v39: precheck remote/local DB before down, then save JSON+MySQL after Chrome down completes.
        try {
          const pre = await teMediaPrecheckAiwk(e);
          if (pre && pre.ok && pre.exists) {
            teDownTxtStatus("skip duplicate " + (pre.match_by || "exists") + " " + (e.prefix || e.meta?.shortcode || "media"));
            return true;
          }
        } catch (err) {
          dbg("W", "precheck_failed", 59082, {prefix:e&&e.prefix, error:String(err&&err.message||err)});
        }
        return await Q(e.url, e.prefix, e.account, Object.assign({ account_folder_merge: !!e.account_folder_merge }, e.meta || {}));
      }
      async function X(e, t, resumeState) {
        dbg("bulk_X", "start", 58876, {scope:e, list_len:Array.isArray(t)?t.length:null, resume:!!resumeState, resume_queue:resumeState&&Array.isArray(resumeState.queue)?resumeState.queue.length:null});
        if (resumeState && Array.isArray(resumeState.queue)) {
          teDownloadSessionAccount = resumeState.account || teActiveFolderAccountName();
          teDownloadSessionAccountFolderMerge = (typeof resumeState.accountFolderMerge === "boolean")
            ? resumeState.accountFolderMerge
            : !!(resumeState.queue[0] && resumeState.queue[0].entry && resumeState.queue[0].entry.account_folder_merge);
        } else {
          teDownloadResumeState = null;
          if (!Array.isArray(t) || 0 === t.length) return void n("empty list");
          // v213: account and merge checkbox are frozen together when a bulk download starts.
          teDownloadSessionAccount = teActiveFolderAccountName();
          teDownloadSessionAccountFolderMerge = tePersistDownloadAccountFolderMergeNow();
        }
        teUpdateDownloadFolderPath(teDownloadSessionAccount);
        teDownloadStopRequested = !1;
        teProgressPreserveUntilNextDownload("bulk_prepare");
        const sourceMode = teDownloadSourceMode(e);
        const batchKey = String((resumeState && resumeState.batchKey) || teDownloadClientId(`batch-${String(e || "manual")}`));
        let flat = [], totalPosts = 0, skippedPosts = 0, startIndex = 0, completedFiles = 0, completedMp4 = 0, processedPosts = 0, scopeLabel = "";
        if (resumeState && Array.isArray(resumeState.queue)) {
          flat = resumeState.queue;
          totalPosts = Number(resumeState.totalPosts) || 0;
          skippedPosts = Number(resumeState.skippedPosts) || 0;
          startIndex = Number(resumeState.nextIndex) || 0;
          completedFiles = Number(resumeState.completedFiles) || 0;
          completedMp4 = Number(resumeState.completedMp4) || 0;
          processedPosts = Number(resumeState.processedPosts) || 0;
          scopeLabel = resumeState.scopeLabel || ("top" === e ? `Top ${flat.length} files` : "Down medias");
        } else {
          const flowMeta = { source_mode: sourceMode, client_batch_key: batchKey, batch_requested_count: t.length, batch_scope: String(e || ""), sort_by: teServerSortField(teTopMetricField() || String(G[Z] || "")) };
          const posts = t.map((media, idx) => ({ postIndex: idx, code: media.code, entries: q(media, teDownloadSessionAccountFolderMerge, flowMeta) }));
          totalPosts = posts.length;
          scopeLabel = "all" === e ? "All medias" : "top" === e ? `Top ${posts.length} medias` : "Filtered medias";
          posts.forEach((post) => {
            if (post.entries.length) post.entries.forEach((entry) => flat.push({ postIndex: post.postIndex, entry }));
            else skippedPosts += 1;
          });
        }
        const totalFiles = flat.length;
        flat.forEach((item) => { if (item && item.entry && item.entry.meta) item.entry.meta.batch_requested_count = totalFiles; });
        const teBulkEntryIsMp4 = (entry) => {
          const kind = String(entry && entry.meta && entry.meta.asset_kind || "").toLowerCase();
          return kind === "video" || kind === "mp4" || teIsVideoUrl(entry && entry.url || "");
        };
        const teEnsureBulkState = () => {
          if (!p) p = { scopeLabel, statusLabel: resumeState ? "Resuming..." : "Preparing...", totalPosts, processedPosts, totalFiles, completedFiles, completedMp4, skippedPosts, currentFileLabel: "", start_ts: resumeState && resumeState.start_ts || Date.now() };
          return p;
        };
        p = { scopeLabel, statusLabel: resumeState ? "Resuming..." : "Preparing...", totalPosts, processedPosts, totalFiles, completedFiles, completedMp4, skippedPosts, currentFileLabel: "", start_ts: resumeState && resumeState.start_ts || Date.now() };
        teProgressState.start_ts = p.start_ts;
        N();
        if (totalFiles <= 0) {
          p.statusLabel = "No downloadable medias";
          N();
          n("No downloadable medias found");
          return;
        }
        B(a);
        try {
          for (let idx = startIndex; idx < flat.length; idx++) {
            const item = flat[idx];
            teEnsureBulkState();
            p.statusLabel = teDownloadStopRequested ? "Stopping..." : "Downing...";
            p.currentFileLabel = item.entry.prefix;
            p.processedPosts = Math.max(p.processedPosts, Math.min(totalPosts, item.postIndex + 1));
            N();
            dbg("bulk_X", "download_item", 58923, {idx, prefix:item.entry&&item.entry.prefix, url:String(item.entry&&item.entry.url||"").slice(0,220)});
            if (await W(item.entry)) { dbg("bulk_X", "download_item_ok", 58923, {idx, prefix:item.entry&&item.entry.prefix}); teEnsureBulkState(); p.completedFiles += 1; if (teBulkEntryIsMp4(item.entry)) p.completedMp4 += 1; } else { dbg("bulk_X", "download_item_fail", 58923, {idx, prefix:item.entry&&item.entry.prefix}); }
            N();
            const teStopAndSave = () => {
              teEnsureBulkState();
              teDownloadResumeState = {
                scope: e,
                queue: flat,
                nextIndex: idx + 1,
                account: teDownloadSessionAccount,
                accountFolderMerge: !!teDownloadSessionAccountFolderMerge,
                totalPosts,
                totalFiles,
                completedFiles: p.completedFiles,
                completedMp4: p.completedMp4,
                processedPosts: p.processedPosts,
                skippedPosts: p.skippedPosts,
                scopeLabel,
                start_ts: p.start_ts || Date.now(),
                batchKey,
                sourceMode
              };
              p.statusLabel = "Stopped";
              p.currentFileLabel = "";
              teProgressState.message = "Stopped after current file.";
              N();
            };
            if (teDownloadStopRequested && idx < flat.length - 1) {
              teStopAndSave();
              return;
            }
            const delaySec = teDownloadDelaySec();
            if (delaySec > 0 && idx < flat.length - 1) {
              dbg("bulk_X", "download_delay_start", 58924, {idx, delaySec});
              teEnsureBulkState();
              p.statusLabel = `Delay ${delaySec}s...`;
              p.currentFileLabel = `Next after ${delaySec}s`;
              teProgressState.message = `Waiting ${delaySec}s before next download`;
              N();
              const delayEnd = Date.now() + delaySec * 1000;
              while (Date.now() < delayEnd) {
                if (teDownloadStopRequested) break;
                await R(Math.min(250, Math.max(0, delayEnd - Date.now())));
              }
              dbg("bulk_X", "download_delay_end", 58925, {idx, delaySec, stopped:teDownloadStopRequested});
              if (teDownloadStopRequested && idx < flat.length - 1) {
                teStopAndSave();
                return;
              }
            }
          }
          teDownloadResumeState = null;
          teEnsureBulkState();
          p.statusLabel = "Completed";
          p.currentFileLabel = "";
          teProgressState.done = !0;
          teProgressState.percent = 100;
          try { teAutoPyLastDoneText = "자동다운로드 완료\n" + teUnifiedProgressText(); n("자동다운로드 완료"); } catch (_) {}
          N();
        } finally {
          D(a);
          await R(300);
          if (!teDownloadResumeState) {
            teDownloadSessionAccount = "";
            teDownloadSessionAccountFolderMerge = null;
          }
          teDownloadStopRequested = !1;
          teUpdateDownloadControlButtons();
          P();
        }
      }
      function Y(e) {
        const t = `swipe-${Date.now()}`;
        B(t);
        teSendToTargetTab({ type: "ndy_swipe", count: e }, "Instagram tab not found").then((ok) => {
          ok || D(t);
        });
        const r = 4500 * Math.max(1, e);
        setTimeout(() => {
          D(t);
        }, r);
      }
      let K = 0,
        Z = 4;
      const G = [
        "like_count",
        "comment_count",
        "media_repost_count",
        "created_at",
        "play_count",
        "",
        "engagement_rate",
        "share_count",
      ];
      function J(e) {
        if (e)
          for (let t = 0; t < oe.length; t++) {
            const r = oe[t];
            if (r.code == e) return r;
          }
        return null;
      }
      function teNotifyManualDownloadStart(media, assetKind, mediaIndex = 0, meta = null) {
        const m = media && typeof media === "object" ? media : {};
        const code = String(m.code || m.shortcode || meta && (meta.shortcode || meta.code) || "").trim();
        if (!code) return false;
        const kind = /video|mp4/i.test(String(assetKind || "")) ? "video" : "image";
        const payload = {
          type: "ndy_down_progress",
          status: "start",
          shortcode: code,
          asset_kind: kind,
          media_index: Math.max(0, Number(mediaIndex || 0) || 0),
          percent: 0,
          manual_click: true,
          source_mode: "MANUAL_SINGLE",
          message: "수동 다운로드 시작",
          meta: Object.assign({}, meta && typeof meta === "object" ? meta : {}, { shortcode: code, asset_kind: kind, media_index: Math.max(0, Number(mediaIndex || 0) || 0), source_mode: "MANUAL_SINGLE", manual_click: true })
        };
        teSendToTargetTab(payload, "").catch(() => {});
        return true;
      }
      async function Q(e, t, r, meta) {
        // v225: every manual/bulk route carries one unified MEDIA_SAVED flow identity.
        meta = tePrepareDownloadFlowMeta(meta || {}, { source_mode: (meta && meta.source_mode) || "MANUAL_SINGLE" });
        const metaOwner = meta && typeof meta === "object" ? teCleanOwnerAccountName(meta.owner_username || meta.username || "") : "";
        if (!metaOwner) {
          dbg("Q", "owner_missing_blocked", 58990, {prefix:t, account:r, meta});
          n("계정명을 찾지 못해 다운로드를 보류했습니다. explore/reels/p는 계정명으로 사용하지 않습니다.");
          return Promise.resolve(!1);
        }
        const accountFolderMerge = false;
        const account = accountFolderMerge ? "" : teSafePath(metaOwner || r || teActiveFolderAccountName() || "instagram");
        teUpdateDownloadFolderPath(account);
        dbg("Q", "send_download_api", 58991, {prefix:t, account, account_folder_merge:accountFolderMerge, root:teDownloadRoot(), url:String(e||"").slice(0,260), is_video:teIsVideoUrl(e)});
        return teDownloadOnce(e, t, () => new Promise((resolve) => {
          try {
            chrome.runtime.sendMessage({
              type: "IGBLUE_DOWNLOAD_URL",
              url: e,
              prefix: t,
              root: tePersistDownloadRootNow(),
              account: accountFolderMerge ? "" : account,
              account_folder_merge: accountFolderMerge,
              href: teCurrentTargetUrl(),
              notify_aiwk: true,
              meta: Object.assign({href: teCurrentTargetUrl(), page_url: teCurrentTargetUrl(), account_folder_merge: accountFolderMerge}, meta || {})
            }, (res) => {
              if (chrome.runtime.lastError) { dbg("Q", "runtime_error", 59004, {prefix:t, message:chrome.runtime.lastError.message}); n("Down API failed"); return resolve(!1); }
              dbg("Q", "download_api_response", 59005, {prefix:t, res});
              if (res && res.browser_download && (res.status === "ok" || res.status === "timeout")) {
                teRecordDownloadedFile(res.filename || res.file_path || t, null);
                n("확장 수동 다운로드 완료");
                return resolve(!0);
              }
              if (res && res.status === "already_done") {
                const finalized = teAutoPyDirectReportResultFromResponse(res);
                n(res.message || (res.shortcode ? `이미 완료 shortcode=${res.shortcode}` : "이미 완료"));
                teHandleDownProgress({status:"already_done", file:res.file_path || res.file || t, file_path:res.file_path || "", file_paths:res.file_paths || [], json_path:finalized.json_path || res.json_path || "", report_ok:finalized.report_ok, report_path:finalized.report_path, shortcode:res.shortcode || (meta && meta.shortcode) || "", asset_kind:res.asset_kind || "", media_index:res.media_index || 0, percent:100, message:res.message || "이미 다운로드됨 / 메타데이터·보고서 갱신"});
                if (!finalized.ok) {
                  dbg("Q", "duplicate_report_finalize_failed", 59005, {prefix:t, finalized});
                  n("기존 파일 확인 후 JSON/보고서 갱신 실패");
                  return resolve(!1);
                }
                return resolve(!0);
              }
              if (!res || (res.status !== "ok" && res.status !== "timeout")) { n(res?.message || "Down failed"); return resolve(!1); }
              const finalized = teAutoPyDirectReportResultFromResponse(res);
              teRecordDownloadedFile(res.filename || t, finalized.notify && finalized.notify.item);
              if (!finalized.ok) {
                dbg("Q", "notify_media_downloaded_failed", 59006, {prefix:t, finalized});
                n("파일 저장 후 JSON/보고서 갱신 실패");
                return resolve(!1);
              }
              resolve(!0);
            });
          } catch (err) {
            dbg("Q", "exception", 59008, {prefix:t, message:String(err&&err.message||err)});
            n("Down API error");
            resolve(!1);
          }
        }));
      }
      const TE_SIDE_THUMB_ROOT_MARGIN = "900px 0px 1400px 0px";
      const teSideListState = { items: [], rankCtx: null, cards: new Map() };
      function teSideThumbLoad(img) {
        if (!img) return;
        const src = String(img.dataset.igblueActiveSrc || img.dataset.igblueSrc || "");
        if (!src) return teSideThumbUnload(img);
        const failedAt = Number(img.dataset.igblueErrorAt || 0) || 0;
        if (failedAt && Date.now() - failedAt < 15000) return;
        if (img.getAttribute("src") !== src) img.setAttribute("src", src);
      }
      function teSideThumbUnload(img) {
        if (!img) return;
        img.removeAttribute("src");
        img.removeAttribute("srcset");
        img.dataset.igblueLoaded = "0";
      }
      const teSideThumbObserver = typeof IntersectionObserver === "function"
        ? new IntersectionObserver((entries) => {
            entries.forEach((entry) => {
              const img = entry && entry.target;
              if (!img) return;
              img.dataset.igblueIntersecting = entry.isIntersecting ? "1" : "0";
              if (entry.isIntersecting) teSideThumbLoad(img);
              else teSideThumbUnload(img);
            });
          }, { root: null, rootMargin: TE_SIDE_THUMB_ROOT_MARGIN, threshold: 0.01 })
        : null;
      function teSideThumbBind(img) {
        if (!img || img.__igblueThumbBound) return;
        img.__igblueThumbBound = true;
        img.loading = "lazy";
        img.decoding = "async";
        try { img.fetchPriority = "low"; } catch (_) {}
        img.addEventListener("load", () => {
          const desired = String(img.dataset.igblueActiveSrc || img.dataset.igblueSrc || "");
          if (!desired || img.getAttribute("src") !== desired) return;
          img.dataset.igblueLoaded = "1";
          delete img.dataset.igblueErrorAt;
        });
        img.addEventListener("error", () => {
          const desired = String(img.dataset.igblueActiveSrc || img.dataset.igblueSrc || "");
          if (!desired || img.getAttribute("src") !== desired) return;
          const fallback = String(img.dataset.igblueFallbackSrc || "");
          if (fallback && fallback !== desired) {
            img.dataset.igblueActiveSrc = fallback;
            delete img.dataset.igblueErrorAt;
            img.setAttribute("src", fallback);
            return;
          }
          img.dataset.igblueErrorAt = String(Date.now());
          teSideThumbUnload(img);
        });
      }
      function teSideThumbAssign(img, url, fallbackUrl = "") {
        if (!img) return;
        teSideThumbBind(img);
        const next = String(url || "");
        const fallback = String(fallbackUrl || "");
        const prev = String(img.dataset.igblueSrc || "");
        const prevFallback = String(img.dataset.igblueFallbackSrc || "");
        if (prev !== next || prevFallback !== fallback) {
          img.dataset.igblueSrc = next;
          img.dataset.igblueFallbackSrc = fallback;
          img.dataset.igblueActiveSrc = next || fallback;
          img.dataset.igblueLoaded = "0";
          delete img.dataset.igblueErrorAt;
          teSideThumbUnload(img);
        }
        if (teSideThumbObserver) {
          teSideThumbObserver.observe(img);
          if (img.dataset.igblueIntersecting === "1") teSideThumbLoad(img);
        } else teSideThumbLoad(img);
      }
      function teSideThumbRelease(img) {
        if (!img) return;
        if (teSideThumbObserver) try { teSideThumbObserver.unobserve(img); } catch (_) {}
        teSideThumbUnload(img);
        delete img.dataset.igblueSrc;
        delete img.dataset.igblueFallbackSrc;
        delete img.dataset.igblueActiveSrc;
        delete img.dataset.igblueErrorAt;
        delete img.dataset.igblueLoaded;
        delete img.dataset.igblueIntersecting;
      }
      function teSideCreateCard() {
        const card = document.createElement("div");
        card.className = "post-item";
        card.innerHTML = '\n<a href="" target="_blank">\n    <img loading="lazy" decoding="async" fetchpriority="low">\n</a>\n<div class="overlay">\n    <div class="stat igblue-side-account">\n        <span>IG</span>\n    </div>\n    <div class="igblue-side-rank" style="display:none">\n        <span></span>\n    </div>\n    <div class="stat">\n        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">\n            <title>Likes</title>\n            <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>\n        </svg>\n        <span></span>\n    </div>\n    <div class="stat">\n        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">\n            <title>Comments</title>\n            <path d="M3 11.5a8.38 8.38 0 0 0 .9 3.8 8.5 8.5 0 0 0 7.6 4.7 8.38 8.38 0 0 0 3.8-.9L21 21l-1.9-5.7a8.38 8.38 0 0 0 .9-3.8 8.5 8.5 0 0 0-4.7-7.6 8.38 8.38 0 0 0-3.8-.9h-.5a8.48 8.48 0 0 0-8 8v.5z"></path>\n        </svg>\n        <span></span>\n    </div>\n    <div class="stat">\n        <svg width="16" height="16" viewBox="0 0 24 24"><title>Repost</title><path d="M4.51617 6.9986C6.13179 4.58593 8.88099 2.99979 11.9995 2.99979C15.7267 2.99979 18.9259 5.26459 20.2927 8.49676C20.5079 9.00543 21.0946 9.24341 21.6033 9.0283C22.1119 8.81318 22.3499 8.22644 22.1348 7.71777C20.466 3.7716 16.5582 0.999786 11.9995 0.999786C8.27776 0.999786 4.9897 2.84823 2.99988 5.67416V2.9986C2.99988 2.44631 2.55216 1.9986 1.99988 1.9986C1.44759 1.9986 0.999878 2.44631 0.999878 2.9986V7.9986C0.999878 8.55088 1.44759 8.9986 1.99988 8.9986H6.99988C7.55216 8.9986 7.99988 8.55088 7.99988 7.9986C7.99988 7.44631 7.55216 6.9986 6.99988 6.9986H4.51617Z" fill="currentColor"></path><path d="M2.39572 14.9713C2.90439 14.7562 3.49113 14.9942 3.70625 15.5029C5.07309 18.735 8.27228 20.9998 11.9995 20.9998C15.118 20.9998 17.8672 19.4137 19.4828 17.001H16.9991C16.4468 17.001 15.9991 16.5533 15.9991 16.001C15.9991 15.4487 16.4468 15.001 16.9991 15.001H21.9991C22.5514 15.001 22.9991 15.4487 22.9991 16.001V21.001C22.9991 21.5533 22.5514 22.001 21.9991 22.001C21.4468 22.001 20.9991 21.5533 20.9991 21.001V18.3255C19.0093 21.1514 15.7212 22.9998 11.9995 22.9998C7.44077 22.9998 3.53298 20.228 1.86419 16.2818C1.64908 15.7732 1.88705 15.1864 2.39572 14.9713Z" fill="currentColor"></path></svg>\n        <span></span>\n    </div>\n    <div class="stat">\n        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n            <title>Shares</title>\n            <line x1="22" y1="2" x2="11" y2="13"></line>\n            <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>\n        </svg>\n        <span></span>\n    </div>\n    <div class="stat">\n        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">\n            <title>Create At</title>\n            <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>\n            <line x1="16" y1="2" x2="16" y2="6"></line>\n            <line x1="8" y1="2" x2="8" y2="6"></line>\n            <line x1="3" y1="10" x2="21" y2="10"></line>\n        </svg>\n        <span></span>\n    </div>\n    <div class="stat">\n        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">\n            <title>Views(Reels)</title>\n            <path d="M23.441 11.819C23.413 11.74 20.542 4 12 4S.587 11.74.559 11.819a1 1 0 0 0 1.881.677 10.282 10.282 0 0 1 19.12 0 1 1 0 0 0 1.881-.677Zm-7.124 2.368a3.359 3.359 0 0 1-1.54-.1 3.56 3.56 0 0 1-2.365-2.362 3.35 3.35 0 0 1-.103-1.542.99.99 0 0 0-1.134-1.107 5.427 5.427 0 0 0-3.733 2.34 5.5 5.5 0 0 0 8.446 6.97 5.402 5.402 0 0 0 1.536-3.09.983.983 0 0 0-1.107-1.109Z" fill-rule="evenodd"></path>\n        </svg>\n        <span></span>\n    </div>\n    <div class="stat">\n        <span title="Engagement Rate(Formula is set in Settings)"></span>\n    </div>\n</div>\n<div class="overlay_download">\n    <div class="action stat icon">\n        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n            <title>Down Image</title>\n            <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>\n            <circle cx="8.5" cy="8.5" r="2.5"></circle>\n            <path d="M21 15l-5-5L3 21"></path>\n        </svg>\n    </div>\n    <div class="action stat icon">\n        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n            <title>Down Carousel Media</title>\n            <path fill="white" transform="scale(0.5)" d="M34.8 29.7V11c0-2.9-2.3-5.2-5.2-5.2H11c-2.9 0-5.2 2.3-5.2 5.2v18.7c0 2.9 2.3 5.2 5.2 5.2h18.7c2.8-.1 5.1-2.4 5.1-5.2zM39.2 15v16.1c0 4.5-3.7 8.2-8.2 8.2H14.9c-.6 0-.9.7-.5 1.1 1 1.1 2.4 1.8 4.1 1.8h13.4c5.7 0 10.3-4.6 10.3-10.3V18.5c0-1.6-.7-3.1-1.8-4.1-.5-.4-1.2 0-1.2.6z"></path>\n        </svg>\n    </div>\n    <div class="action stat icon">\n        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">\n            <title>Down Video</title>\n            <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>\n            <polygon points="9 8 15 12 9 16"></polygon>\n        </svg>\n    </div>\n</div>\n';
        const accountLabel = card.querySelector(".igblue-side-account");
        accountLabel && accountLabel.addEventListener("click", (event) => {
          event.preventDefault();
          event.stopPropagation();
          const code = card.getAttribute("data-code");
          teSelectSideAccountAddTarget(J(code), accountLabel);
        });
        const actions = card.querySelectorAll("div.action");
        actions?.[0].addEventListener("click", () => {
          const code = card.getAttribute("data-code");
          const media = J(code);
          if (!teLockOnce(`thumb-img-${code}`, 1200)) return void n("Ignored duplicate click");
          dbg("thumb_click", "image", 59030, {code, media:dbgMedia(media)});
          tePersistDownloadOptionsNow();
          if (media && media.img_origin) {
            const prefix = teMakeDownloadPrefix(media, 0, 1, media.img_origin, teRankNoForMedia(media, 0));
            const meta = teBuildMediaDownloadMeta(media, media.img_origin, prefix, 0, 1);
            teNotifyManualDownloadStart(media, "image", 0, meta);
            Q(media.img_origin, prefix, teActiveFolderAccountName(), meta);
          } else n("Image not found.");
        });
        actions?.[1].addEventListener("click", () => {
          const code = card.getAttribute("data-code");
          const media = J(code);
          if (!teLockOnce(`thumb-carousel-${code}`, 1200)) return void n("Ignored duplicate click");
          if (!media) return void n("Image not found.");
          const entries = q(media);
          dbg("thumb_click", "carousel_entries", 59038, {code, entries:entries.map((x) => ({prefix:x.prefix,url:String(x.url||"").slice(0,180),account:x.account}))});
          if (entries.length > 0) entries.forEach((entry, index) => {
            const meta = entry && entry.meta && typeof entry.meta === "object" ? entry.meta : {};
            if (!entry.metadata_only) teNotifyManualDownloadStart(media, meta.asset_kind || "image", meta.media_index != null ? meta.media_index : index, meta);
            W(entry);
          });
          else n("No downloadable medias found");
        });
        actions?.[2].addEventListener("click", () => {
          const code = card.getAttribute("data-code");
          const media = J(code);
          if (!teLockOnce(`thumb-video-${code}`, 1200)) return void n("Ignored duplicate click");
          dbg("thumb_click", "video", 59046, {code, media:dbgMedia(media)});
          tePersistDownloadOptionsNow();
          if (media && media.video_url) {
            const prefix = teMakeDownloadPrefix(media, 0, 1, media.video_url, teRankNoForMedia(media, 0));
            const meta = teBuildMediaDownloadMeta(media, media.video_url, prefix, 0, 1);
            teNotifyManualDownloadStart(media, "video", 0, meta);
            Q(media.video_url, prefix, teActiveFolderAccountName(), meta);
          } else n("Video not found.");
        });
        return card;
      }
      function teSideUpdateCard(card, media, rankCtx) {
        if (!card || !media) return;
        const previousCode = String(card.getAttribute("data-code") || "");
        card.setAttribute("data-code", String(media.code || ""));
        const link = card.querySelector("a");
        const img = card.querySelector("img");
        const overlay = card.querySelector(".overlay");
        const actions = card.querySelectorAll("div.action");
        if (media?.carousel_media?.length > 0) {
          actions[0].style.display = "none";
          actions[1].style.display = "flex";
        } else if (media?.img_origin) {
          actions[0].style.display = "flex";
          actions[1].style.display = "none";
        } else {
          actions[0].style.display = "none";
          actions[1].style.display = "none";
        }
        actions[2].style.display = null != media.video_url ? "flex" : "none";
        dbg("render", "button_display", 59068, {code:media.code, img:!!media.img_origin, carousel:!!media.carousel_media, video:!!media.video_url, videoButton:actions[2].style.display});
        const href = `https://www.instagram.com/p/${media.code}`;
        if (link && link.href !== href) link.href = href;
        teSideThumbAssign(img, String(media?.img_small || media?.img_origin || ""), String(media?.img_origin || ""));
        const stats = card.querySelectorAll(".stat");
        te(stats, media);
        teApplyViewVisual(card, media);
        teApplySideAccountLabel(stats[0], media);
        teApplySideRankLabel(card.querySelector(".igblue-side-rank"), rankCtx && rankCtx.active ? rankCtx.map.get(media.code) : 0, !!(rankCtx && rankCtx.active));
        if (previousCode && previousCode !== media.code && overlay) {
          overlay.classList.add("highlight");
          setTimeout(() => overlay.classList.remove("highlight"), 500);
        }
        if (img && overlay) {
          const overlayHeight = overlay.offsetHeight;
          img.style.minHeight = overlayHeight ? `${overlayHeight}px` : "";
        }
      }
      function teSideListReleaseCard(card) {
        if (!card) return;
        teSideThumbRelease(card.querySelector && card.querySelector("img"));
        if (card.parentNode) card.parentNode.removeChild(card);
      }
      function teSideListReset(box) {
        teSideListState.cards.forEach((card) => teSideListReleaseCard(card));
        teSideListState.cards.clear();
        if (box) box.textContent = "";
      }
      function teSideListRender(items) {
        const box = document.getElementById("img_collection");
        if (!box) return;
        const list = Array.isArray(items) ? items : [];
        teSideListState.items = list;
        teSideListState.rankCtx = teBuildRankContext(list);
        teScheduleMainRankLabels(list, teSideListState.rankCtx);
        if (!list.length) return teSideListReset(box);
        const desired = new Set();
        let cursor = box.firstChild;
        list.forEach((media) => {
          const code = String(media && media.code || "");
          if (!code) return;
          desired.add(code);
          let card = teSideListState.cards.get(code);
          if (!card) {
            card = teSideCreateCard();
            teSideListState.cards.set(code, card);
          }
          teSideUpdateCard(card, media, teSideListState.rankCtx);
          if (card !== cursor) box.insertBefore(card, cursor || null);
          cursor = card.nextSibling;
        });
        Array.from(teSideListState.cards.entries()).forEach(([code, card]) => {
          if (desired.has(code)) return;
          teSideListState.cards.delete(code);
          teSideListReleaseCard(card);
        });
      }
      function ee(items) {
        teSideListRender(items);
      }
      const te = (e, t) => {
        ((e[1].style.display = null != t.like_count ? "flex" : "none"),
          (e[2].style.display = null != t.comment_count ? "flex" : "none"));
        const repostPrev = e[3].style.display,
          repostDisplay = null != t.media_repost_count ? "flex" : "none";
        ((e[3].style.display = repostDisplay),
          "flex" !== repostPrev && "flex" === repostDisplay &&
            (e[3].classList.add("highlight"), setTimeout(() => e[3].classList.remove("highlight"), 500)));
        const sharePrev = e[4].style.display,
          shareDisplay = "none";
        e[4].classList.add("d-none");
        ((e[4].style.display = shareDisplay),
          "flex" !== sharePrev && "flex" === shareDisplay &&
            (e[4].classList.add("highlight"), setTimeout(() => e[4].classList.remove("highlight"), 500)));
        const datePrev = e[5].style.display,
          dateDisplay = null != t.created_at ? "flex" : "none";
        ((e[5].style.display = dateDisplay),
          "flex" !== datePrev && "flex" === dateDisplay &&
            (e[5].classList.add("highlight"), setTimeout(() => e[5].classList.remove("highlight"), 500)));
        const viewPrev = e[6].style.display,
          viewDisplay = null != t?.play_count ? "flex" : "none";
        ((e[6].style.display = viewDisplay),
          "flex" !== viewPrev && "flex" === viewDisplay &&
            (e[6].classList.add("highlight"), setTimeout(() => e[6].classList.remove("highlight"), 500)));
        const erPrev = e[7].style.display,
          erDisplay = null != t?.engagement_rate ? "flex" : "none";
        ((e[7].style.display = erDisplay),
          "flex" !== erPrev && "flex" === erDisplay &&
            (e[7].classList.add("highlight"), setTimeout(() => e[7].classList.remove("highlight"), 500)));
        const likes = null != t.like_count ? t.like_count.toLocaleString() : "",
          comments = null != t.comment_count ? t.comment_count.toLocaleString() : "",
          reposts = null != t.media_repost_count ? t.media_repost_count.toLocaleString() : "",
          shares = "",
          created = t.created_at ? ce(new Date(1e3 * t.created_at)) : "",
          views = null != t.play_count ? t.play_count.toLocaleString() : "",
          er = null != t.play_count
            ? (function (e) {
                const t = teErPercent(e);
                if (null == t) return "ER N/A";
                const r = teErEmojiForMedia(e);
                return r ? `${r.emoji} ${t.toFixed(2)}%` : `ER ${t.toFixed(2)}%`;
              })(t)
            : "";
        (e[1].querySelector("span").textContent !== likes && (e[1].querySelector("span").textContent = likes),
          e[2].querySelector("span").textContent !== comments && (e[2].querySelector("span").textContent = comments),
          e[3].querySelector("span").textContent !== reposts && (e[3].querySelector("span").textContent = reposts),
          e[4].querySelector("span").textContent !== shares && (e[4].querySelector("span").textContent = shares),
          e[5].querySelector("span").textContent !== created && (e[5].querySelector("span").textContent = created),
          teApplyDateFreshClass(e[5], t),
          teApplyMetricBars(e, t),
          e[6].querySelector("span").textContent !== views && (e[6].querySelector("span").textContent = views),
          e[7].querySelector("span").textContent !== er && (e[7].querySelector("span").textContent = er),
          e[7].querySelector("span").classList.toggle("igblue-er-badge", !!teErEmojiForMedia(t)),
          e[7].querySelector("span").classList.toggle("igblue-er-muted", !teErEmojiForMedia(t)));
      };
      let re = null,
        ne = null;
      function ie(e, t) {
        const r = document.getElementById("filter_status");
        if (e !== re || t !== ne) {
          const n = e !== t ? "red" : "";
          ((r.innerHTML = `Showing <span class="${n}">${e}</span> of ${t} unfiltered items`),
            r.classList.add("highlight"),
            setTimeout(() => r.classList.remove("highlight"), 500),
            (re = e),
            (ne = t));
        }
        I(e, t);
      }
      let se,
        oe = [];
      function ae(e, t, r, i, s) {
        if (e && e.length > 0) {
          let o;
          if (t > 0) {
            let r = {};
            ((o = (function (e, t, r) {
              let n = [];
              return (
                e.forEach((e) => {
                  null == e.created_at
                    ? (r.issue = !0)
                    : e.created_at > t && n.push(e);
                }),
                n
              );
            })(e, t, r)),
              i && r.issue && n("Some Medias do not have Date info"));
          } else o = e;
          let a = {};
          ((oe = (function (e, t, r, n) {
            t >= G.length && (t = 0);
            let i = G[t];
            if ("" == i) return e;
            const s = [...e];
            return (
              s.sort((e, t) => {
                const eHas = teMetricHasValue(e, i), tHas = teMetricHasValue(t, i);
                !r || (eHas && tHas) || (n[i] = !0);
                return teCompareMediaForSort(e, t, i);
              }),
              s
            );
          })(o, r, s, a)),
            ee(oe),
            ie(oe.length, e.length),
            teUpdateMetricThresholdCount(),
            s &&
              (a.play_count
                ? n("Some Image do not have Views info")
                : a.created_at
                  ? n("Some Meidas do not have Date info")
                  : a.engagement_rate &&
                    n("ER is calculated for reels that have 'view count'.")));
        } else ((oe = []), ee([]), ie(0, 0), teUpdateMetricThresholdCount());
      }
      async function le(e) {
        return new Promise((t, r) => {
          const n = new Image();
          ((n.onload = () => t({ width: n.width, height: n.height })),
            (n.onerror = r),
            (n.src = e));
        });
      }
      function teSortDanceDebugMedia(m, idx) {
        const code = String(m && m.code || "");
        return {
          idx: Number(idx || 0) + 1,
          code,
          username: teOwnerAccountForMedia ? teOwnerAccountForMedia(m) : String(m && (m.username || m.owner_username) || ""),
          views: teMetricNumber(m && m.play_count),
          likes: teMetricNumber(m && m.like_count),
          comments: teMetricNumber(m && m.comment_count),
          shares: 0,
          reposts: teMetricNumber(m && m.media_repost_count),
          participation_score: teParticipationScore(m),
          created_at: Number(m && m.created_at || 0) || 0,
          er: Number(m && m.engagement_rate || 0) || 0,
          has_img: !!(m && (m.img_origin || m.img_small || m.img_b64)),
          has_video: !!(m && m.video_url),
          has_carousel: !!(m && m.carousel_media),
          img_sig: String((m && (m.img_origin || m.img_small || m.img_b64)) || "").slice(0, 90),
          video_sig: String((m && m.video_url) || "").slice(0, 90)
        };
      }
      function teSortDanceDebugDomItems() {
        const box = document.getElementById("img_collection");
        if (!box) return [];
        return Array.from(box.querySelectorAll(".post-item") || []).slice(0, 20).map((el, idx) => {
          let overlay = "";
          try { overlay = String(el && el.innerText || "").replace(/\s+/g, " ").trim().slice(0, 240); } catch (_) {}
          return {
            idx: idx + 1,
            code: String(el && el.getAttribute && el.getAttribute("data-code") || ""),
            img_src_sig: String(el && el.querySelector && el.querySelector("img") && el.querySelector("img").getAttribute("src") || "").slice(0, 90),
            overlay
          };
        });
      }
      function teSortDanceDebugSignature(list) {
        if (!Array.isArray(list)) return "";
        return list.slice(0, 20).map((m, idx) => {
          const field = G[Z] || "";
          const val = field ? teMetricNumber(m && m[field]) : 0;
          return `${idx + 1}:${String(m && m.code || "")}:${val}`;
        }).join("|");
      }
      async function teCopyTextToClipboard(text) {
        const value = String(text || "");
        try {
          if (navigator.clipboard && navigator.clipboard.writeText) {
            await navigator.clipboard.writeText(value);
            return true;
          }
        } catch (_) {}
        try {
          const ta = document.createElement("textarea");
          ta.value = value;
          ta.setAttribute("readonly", "readonly");
          ta.style.position = "fixed";
          ta.style.left = "-9999px";
          ta.style.top = "0";
          document.body.appendChild(ta);
          ta.focus();
          ta.select();
          const ok = document.execCommand("copy");
          document.body.removeChild(ta);
          return !!ok;
        } catch (_) { return false; }
      }
      async function teBuildSortDanceDebugSnapshot(slot) {
        const sortEl = document.getElementById("sort_by");
        const rangeEl = document.getElementById("timeRange");
        const metricInput = document.getElementById("top_n_count");
        const metricCount = document.getElementById("top_metric_count");
        const targetCount = document.getElementById("igblue_auto_py_target_count");
        const rankCtx = teBuildRankContext(Array.isArray(oe) ? oe : []);
        const sortedTop = teAutoPyFilteredSortedTopN ? teAutoPyFilteredSortedTopN(20) : [];
        let loadedState = null, activeTarget = null;
        try { loadedState = await teSendToTargetTabResponse({ type: "igblue_auto_py_loaded_rank_state", wait_ms: 80, requested_no: 1 }, "", 900); } catch (e) { loadedState = { ok: false, error: String(e && e.message || e) }; }
        try { activeTarget = await teSendToTargetTabResponse({ type: "igblue_get_active_download_target" }, "", 900); } catch (e) { activeTarget = { ok: false, error: String(e && e.message || e) }; }
        const out = {
          schema: "igblue_sort_dance_debug_v179a13",
          slot: Number(slot || 0) || 0,
          copied_at: new Date().toISOString(),
          build_version: IGBLUE_BUILD_VERSION,
          build_tag: IGBLUE_BUILD_TAG,
          page: {
            side_href: String(location.href || ""),
            target_url: teCurrentTargetUrl ? teCurrentTargetUrl() : "",
            target_tab_id: Number(teCurrentTargetTabId || 0) || 0,
            channel_locked: teChannelLocked ? teChannelLocked() : false,
            pinned_target_url: String(tePinnedTargetUrl || "")
          },
          controls: {
            sort_index: Number(Z || 0),
            sort_field: String(G[Z] || ""),
            sort_text: sortEl ? String(sortEl.options[sortEl.selectedIndex] && sortEl.options[sortEl.selectedIndex].text || "") : "",
            time_range_days: Number(K || 0) || 0,
            time_range_value: rangeEl ? String(rangeEl.value || "") : "",
            threshold_10k: metricInput ? String(metricInput.value || "") : "",
            threshold_value: teMetricThresholdValue ? teMetricThresholdValue() : 0,
            min_comments: teMinCommentsValue ? teMinCommentsValue() : 0,
            min_likes: teMinLikesValue ? teMinLikesValue() : 0,
            top_metric_count: metricCount ? String(metricCount.dataset.count || metricCount.textContent || "") : "",
            top_target_count: targetCount ? String(targetCount.value || "") : ""
          },
          counts: {
            se_raw_count: Array.isArray(se) ? se.length : 0,
            oe_render_count: Array.isArray(oe) ? oe.length : 0,
            filtered_source_count: teAutoPyFilteredSourceList ? teAutoPyFilteredSourceList().length : 0,
            dom_thumb_count: (document.getElementById("img_collection") && document.getElementById("img_collection").querySelectorAll(".post-item") || []).length,
            page_info_all: (document.getElementById("page_info_all") || {}).textContent || "",
            page_info_filtered: (document.getElementById("page_info_filtered") || {}).textContent || "",
            filter_status: (document.getElementById("filter_status") || {}).textContent || ""
          },
          signatures: {
            se_first20: teSortDanceDebugSignature(se || []),
            oe_first20: teSortDanceDebugSignature(oe || []),
            sorted_top_first20: teSortDanceDebugSignature(sortedTop || []),
            dom_first20: teSortDanceDebugDomItems().map(x => `${x.idx}:${x.code}:${x.img_src_sig}`).join("|")
          },
          first_items: {
            se: (Array.isArray(se) ? se : []).slice(0, 20).map(teSortDanceDebugMedia),
            oe: (Array.isArray(oe) ? oe : []).slice(0, 20).map(teSortDanceDebugMedia),
            sorted_top: (Array.isArray(sortedTop) ? sortedTop : []).slice(0, 20).map(teSortDanceDebugMedia),
            dom: teSortDanceDebugDomItems()
          },
          rank_context: {
            active: !!(rankCtx && rankCtx.active),
            first10: (Array.isArray(oe) ? oe : []).slice(0, 10).map((m, idx) => ({ idx: idx + 1, code: String(m && m.code || ""), rank_no: rankCtx && rankCtx.map instanceof Map ? Number(rankCtx.map.get(m && m.code) || 0) : idx + 1 }))
          },
          content: {
            loaded_state: loadedState,
            active_target: activeTarget
          },
          note: "약 1초 간격으로 춤출 때 1번→1초후 2번→1초후 3번을 눌러 세 결과를 전달"
        };
        return JSON.stringify(out, null, 2);
      }
      async function teCopySortDanceDebug(slot) {
        const btn = document.getElementById(`igblue_sort_dance_copy_${slot}`);
        const old = btn ? btn.textContent : "";
        try {
          if (btn) { btn.disabled = true; btn.textContent = "복사중..."; }
          const text = await teBuildSortDanceDebugSnapshot(slot);
          const ok = await teCopyTextToClipboard(text);
          n(ok ? `춤춤진단${slot} 복사 완료` : `춤춤진단${slot} 복사 실패`);
          try { const st = document.getElementById("igblue_devtools_status"); if (st) st.textContent = ok ? `춤춤진단${slot} 클립보드 복사 완료` : `춤춤진단${slot} 복사 실패`; } catch (_) {}
        } catch (e) {
          n(`춤춤진단${slot} 예외`);
          try { const st = document.getElementById("igblue_devtools_status"); if (st) st.textContent = `춤춤진단${slot} 예외: ${String(e && e.message || e)}`; } catch (_) {}
        } finally {
          if (btn) { btn.disabled = false; btn.textContent = old || `춤춤복사${slot}`; }
        }
      }
      function teBindSortDanceDebugButtons() {
        [1, 2, 3].forEach((slot) => {
          const btn = document.getElementById(`igblue_sort_dance_copy_${slot}`);
          if (!btn || btn.__igblue_sort_dance_bound) return;
          btn.__igblue_sort_dance_bound = true;
          btn.addEventListener("click", () => teCopySortDanceDebug(slot));
        });
      }
      function ce(e) {
        return `${e.getFullYear()}-${String(e.getMonth() + 1).padStart(2, "0")}-${String(e.getDate()).padStart(2, "0")}`;
      }
      function teApplyDateFreshClass(e, t) {
        if (!e) return;
        e.classList.remove("igblue-date-today", "igblue-date-week", "igblue-date-month", "igblue-date-quarter", "igblue-date-border");
        e.style.removeProperty("--igblue-date-color");
        e.style.removeProperty("--igblue-date-border-width");
        const r = Number(t?.created_at || 0);
        if (!Number.isFinite(r) || r <= 0) return;
        const n = teDateBorderSettings();
        if (!n.enabled) return;
        const i = new Date(), s = new Date(1e3 * r);
        i.setHours(0, 0, 0, 0);
        s.setHours(0, 0, 0, 0);
        const o = Math.floor((i.getTime() - s.getTime()) / 86400000), a = teDateLevelByAge(o, n.levels || {});
        if (!a || !a.enabled || !a.side) return;
        e.classList.add("igblue-date-border");
        e.style.setProperty("--igblue-date-color", teSafeHexColor(a.color, "#22c55e"));
        e.style.setProperty("--igblue-date-border-width", `${Math.max(1, Math.min(9, Number(n.borderWidth) || 2))}px`);
      }

      // IGblue v179a14: Auto8700 예약 실행 요청 처리. 실제 루프는 기존 teAutoPyStartOnceFromUi()만 재사용한다.
      let teAuto8700LastStartRequestId = "";
      async function teAuto8700BringTargetAfterMouseIdle(req) {
        try {
          const d = req && typeof req === "object" ? req : {};
          const shouldActivate = !!(d.cdp_activate_before_start || /schedule|auto8700/i.test(String(d.reason || d.source || "")));
          if (!shouldActivate) return { ok: true, skipped: true, reason: "not_schedule_activation" };
          const r = await fetch("http://127.0.0.1:8707/api/cdp/ig/bring_to_front", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ reason: "auto8700_schedule", request_id: String(d.request_id || ""), timeout: 3.0 })
          });
          const j = await r.json().catch(() => ({}));
          if (!r.ok || j.ok === false) return { ok: false, error: j && (j.error || j.message) || ("HTTP " + r.status), result: j };
          return Object.assign({ ok: true }, j || {});
        } catch (err) {
          return { ok: false, error: String(err && err.message || err) };
        }
      }
      function teHandleAuto8700StartRequest(req, sendResponse) {
        try {
          const requestId = String(req && req.request_id || ("auto8700_side_" + Date.now()));
          if (requestId && requestId === teAuto8700LastStartRequestId) {
            try { sendResponse && sendResponse({ status: "duplicate", request_id: requestId }); } catch (_) {}
            return true;
          }
          teAuto8700LastStartRequestId = requestId;
          try { chrome.storage && chrome.storage.local && chrome.storage.local.set({ "IGBLUE_AUTO8700_START_REQUEST_LAST_CONSUMED": requestId }); } catch (_) {}
          const cb = document.getElementById("igblue_auto_py_down");
          if (cb && !cb.checked) {
            cb.checked = true;
            try { teLocalSet("download/auto_py", "1"); } catch (_) {}
          }
          n("Auto8700 예약 실행 요청 수신");
          setTimeout(() => {
            (async () => {
              try {
                const bring = await teAuto8700BringTargetAfterMouseIdle(req || {});
                if (bring && bring.ok === false) { try { n("CDP 탭 이동 보류/실패: " + String(bring.error || bring.reason || "unknown")); } catch (_) {} }
                try { await teAutoPyStartOnceFromUi({ new_session: true, source: "auto8700_schedule" }); } catch (err) { try { n("Auto8700 예약 실행 실패: " + String(err && err.message || err)); } catch (_) {} }
              } catch (err) {
                try { n("Auto8700 예약 실행 예외: " + String(err && err.message || err)); } catch (_) {}
              }
            })();
          }, 80);
          try { sendResponse && sendResponse({ status: "ok", started: true, request_id: requestId, cdp_tab_activation_policy: "direct_schedule_activation" }); } catch (_) {}
        } catch (err) {
          try { sendResponse && sendResponse({ status: "error", error: String(err && err.message || err) }); } catch (_) {}
        }
        return true;
      }
      function teMaybeConsumePendingAuto8700Start() {
        try {
          chrome.storage && chrome.storage.local && chrome.storage.local.get(["IGBLUE_AUTO8700_START_REQUEST", "IGBLUE_AUTO8700_START_REQUEST_LAST_CONSUMED"], (got) => {
            try {
              const req = got && got["IGBLUE_AUTO8700_START_REQUEST"];
              const consumed = String(got && got["IGBLUE_AUTO8700_START_REQUEST_LAST_CONSUMED"] || "");
              if (!req || typeof req !== "object") return;
              const rid = String(req.request_id || "");
              if (rid && rid === consumed) return;
              const age = Math.max(0, Date.now() - Number(req.ts || Date.parse(req.bg_received_at || "") || 0));
              if (age > 10 * 60 * 1000) return;
              teHandleAuto8700StartRequest(req, null);
            } catch (_) {}
          });
        } catch (_) {}
      }

      document.addEventListener("DOMContentLoaded", function () {
        (teLocalSet("filter/min_shares", "0"),
          teUpdateStatsStatus(),
          H(),
          tePruneTabSessions(),
          teUpdateTabIdBadge(),
          teMaybeConsumePendingAuto8700Start(),
          teBindSortDanceDebugButtons(),
          teApplyThumbWidth(teThumbWidthValue()),
          chrome.runtime.onMessage.addListener(function (e, t, r) {
            dbg("runtime_onMessage", "recv", 59257, {type:e&&e.type, info_count:e&&Array.isArray(e.info)?e.info.length:null, status:e&&e.status, file:e&&e.file, percent:e&&e.percent, message:e&&e.message});
            "IGBLUE_WINDOW_MOVE_STATE" === e.type
              ? (teAutoPyHandleWindowMoveState(e).catch(()=>{}), r({status:"ok"}))
              : "IGBLUE_AUTO8700_START_REQUEST" === e.type
              ? teHandleAuto8700StartRequest(e, r)
              : "ndy_side_shop" === e.type
              ? (() => {
                  const guard = teShouldAcceptSideShopPayload(e);
                  if (!(guard && guard.ok)) {
                    try { localStorage.setItem("IG_dbg/side.js/ndy_side_shop_ignored", JSON.stringify(Object.assign({ts:new Date().toISOString(), info_count:Array.isArray(e.info)?e.info.length:0}, guard || {}))); } catch (_) {}
                    r({ status: "ignored", reason: guard && guard.reason || "payload_guard" });
                    return;
                  }
                  if (Number(e.tabId || 0)) teCurrentTargetTabId = Number(e.tabId || 0);
                  u || ((u = !0), P());
                  c && clearTimeout(c);
                  c = setTimeout(F, 2e3);
                  se = e.info;
                  teQueueStatsPayload(e.info, e.source_url || e.url || "");
                  teUpdateDownloadFolderPath(teActiveFolderAccountName());
                  teTouchTabSession(!!(teAutoPyRunning && !teAutoPyStopping), "ndy_side_shop_accept");
                  ae(e.info, K, Z, !1, !1);
                          r({ status: "ok" });
                })()
              : "IGBLUE_TARGET_UPDATED" === e.type
                ? ((teCurrentTargetTabId = Number(e.tabId || 0)),
                  teSetTargetLabel(e),
                  teIsPopupMode() && teApplyTargetInfo(e),
                  r({ status: "ok" }))
                : "ndy_dq" === e.type
                  ? ((d = Number(e.count) || 0), N(), r({ status: "ok" }))
                  : "ndy_down_progress" === e.type &&
                    (teHandleDownProgress(e), r({ status: "ok" }));
          }),
          teRestoreLastInfoForPopup());
      });
  })();
})();



// IGblue v86 patch: side-panel TTS level selector.
// - 0 미사용 선택을 막지 않는다.
// - 사용자가 select를 건드린 뒤에는 늦게 도착한 init/storage 값이 다시 덮지 않는다.
// - PY TTS는 py 설정창에서 별도 관리한다.
(() => {
  if (window.__IGBLUE_SIDE_TTS_LEVEL_PATCH_V86__) return;
  window.__IGBLUE_SIDE_TTS_LEVEL_PATCH_V86__ = true;
  const STORE_KEYS = ["IGBLUE/v79/tts_level", "IGBLUE/tts_level", "igblue_tts_level"];
  const labels = { "0": "0 미사용", "1": "1 중요", "2": "2 진행", "3": "3 디버그" };
  let userTouched = false;
  let bound = false;
  const clamp = (v) => {
    const n = Number(v);
    if (!Number.isFinite(n)) return "0";
    return String(Math.max(0, Math.min(3, Math.floor(n))));
  };
  function setStatus(msg) {
    try {
      const el = document.getElementById("igblue_down_txt_status");
      if (el && msg) el.textContent = "down.txt: " + msg;
    } catch (_) {}
  }
  function cancelSpeechIfZero(level) {
    if (String(level) !== "0") return;
    try { if (window.speechSynthesis) window.speechSynthesis.cancel(); } catch (_) {}
  }
  function getTargetTabId(cb) {
    try {
      chrome.runtime.sendMessage({ type: "IGBLUE_GET_TARGET_STATE" }, (res) => {
        if (chrome.runtime.lastError || !res) return cb(0);
        const id = Number(res.tabId || 0);
        cb(Number.isFinite(id) && id > 0 ? id : 0);
      });
    } catch (_) { cb(0); }
  }
  function ensureAndSend(tabId, msg, cb) {
    if (!tabId) return cb && cb(false);
    try {
      chrome.tabs.sendMessage(tabId, msg, () => {
        if (!chrome.runtime.lastError) return cb && cb(true);
        try {
          chrome.runtime.sendMessage({ type: "IGBLUE_ENSURE_CONTENT", tabId }, (res) => {
            if (chrome.runtime.lastError || !res || res.status !== "ok") return cb && cb(false);
            chrome.tabs.sendMessage(tabId, msg, () => cb && cb(!chrome.runtime.lastError));
          });
        } catch (_) { cb && cb(false); }
      });
    } catch (_) { cb && cb(false); }
  }
  function sendToInstagram(level, reason) {
    const msg = {
      type: "igblue_set_tts_level",
      value: { tts_level: Number(level), label: labels[String(level)] || String(level), reason: reason || "side", ts: Date.now() }
    };
    getTargetTabId((tabId) => {
      if (!tabId) return setStatus("TTS " + (labels[String(level)] || level) + " / Instagram tab 없음");
      ensureAndSend(tabId, msg, (ok) => {
        setStatus(ok ? ("TTS " + (labels[String(level)] || level) + " 전달") : "TTS 전달 실패");
      });
    });
  }
  function saveLevel(level, reason) {
    level = clamp(level);
    try {
      const data = {};
      STORE_KEYS.forEach((k) => { data[k] = level; });
      chrome.storage.local.set(data, () => { chrome.runtime.lastError; });
    } catch (_) {}
    try { localStorage.setItem("IGBLUE/v79/tts_level", level); } catch (_) {}
    cancelSpeechIfZero(level);
    sendToInstagram(level, reason || "change");
  }
  function applySelectValue(sel, level, reason, fromUser) {
    const v = clamp(level);
    if (!fromUser && userTouched) return;
    if (sel.value !== v) sel.value = v;
    saveLevel(v, reason);
  }
  function init() {
    const sel = document.getElementById("igblue_side_tts_level");
    if (!sel) return;
    sel.disabled = false;
    sel.style.pointerEvents = "auto";
    sel.style.userSelect = "auto";
    sel.querySelectorAll("option").forEach((op) => { try { op.disabled = false; } catch (_) {} });
    if (bound) return;
    bound = true;
    ["pointerdown", "mousedown", "touchstart", "keydown"].forEach((name) => {
      sel.addEventListener(name, (ev) => {
        userTouched = true;
        ev.stopPropagation();
      }, true);
    });
    sel.addEventListener("click", (ev) => { userTouched = true; ev.stopPropagation(); }, true);
    sel.addEventListener("change", () => {
      userTouched = true;
      applySelectValue(sel, sel.value, "user_change", true);
    });
    try {
      chrome.storage.local.get(["IGBLUE/v81/tts_default0_done"].concat(STORE_KEYS), (got) => {
        if (userTouched) return;
        if (!got || got["IGBLUE/v81/tts_default0_done"] !== "1") {
          try { chrome.storage.local.set({"IGBLUE/v81/tts_default0_done":"1"}, () => { chrome.runtime.lastError; }); } catch (_) {}
          return applySelectValue(sel, "0", "init_default_v86", false);
        }
        const saved = got && (got[STORE_KEYS[0]] ?? got[STORE_KEYS[1]] ?? got[STORE_KEYS[2]]);
        applySelectValue(sel, saved == null || saved === "" ? "0" : saved, "init", false);
      });
    } catch (_) { applySelectValue(sel, "0", "init_fallback", false); }
  }
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init);
  else init();
  setTimeout(init, 300);
  setTimeout(init, 1000);
})();


// IGblue v172 patch: Auto8700 8707 DevTools settings bridge.
(() => {
  if (window.__IGBLUE_AUTO8700_DEVTOOLS_SETTINGS_V172__) return;
  window.__IGBLUE_AUTO8700_DEVTOOLS_SETTINGS_V172__ = true;
  const KEYS = ["devtools_network_enabled", "devtools_console", "devtools_file_log", "devtools_capture_body"];
  const IDS = { devtools_network_enabled: "igblue_devtools_network_enabled", devtools_console: "igblue_devtools_console", devtools_file_log: "igblue_devtools_file_log", devtools_capture_body: "igblue_devtools_capture_body" };
  const DEFAULTS = { devtools_network_enabled: false, devtools_console: true, devtools_file_log: false, devtools_capture_body: false };
  function $(id){ return document.getElementById(id); }
  function setStatus(text){ try { const el = $("igblue_devtools_status"); if (el) el.textContent = text; } catch(_) {} }
  function readUi(){ const out = {}; for (const k of KEYS) { const el = $(IDS[k]); out[k] = !!(el && el.checked); } return out; }
  function applyUi(values){ for (const k of KEYS) { const el = $(IDS[k]); if (el) el.checked = !!(values && values[k]); } }
  function save(reason){
    const settings = readUi();
    settings.ig_manual_click_mode_enabled = !settings.devtools_network_enabled;
    const payload = Object.assign({}, settings, { ig_auto8700_devtools_settings: settings, ig_auto8700_devtools_settings_updated_at: Date.now(), ig_auto8700_devtools_settings_reason: reason || "side_change" });
    try { chrome.storage.local.set(payload, () => { const err = chrome.runtime.lastError; setStatus(err ? ("8707 설정 저장 실패: " + err.message) : "8707 설정 저장/전송 대기 완료"); }); }
    catch (e) { setStatus("8707 설정 저장 예외: " + String(e && e.message || e)); }
  }
  function init(){
    if (!$(IDS.devtools_network_enabled)) return;
    try { chrome.storage.local.get(KEYS.concat(["ig_auto8700_devtools_settings"]), (got) => { const merged = Object.assign({}, DEFAULTS, (got && got.ig_auto8700_devtools_settings) || {}, got || {}); applyUi(merged); setStatus("8707 설정 준비됨"); }); }
    catch (_) { applyUi(DEFAULTS); }
    for (const k of KEYS) { const el = $(IDS[k]); if (el && !el.__igblue_auto8700_bound) { el.__igblue_auto8700_bound = true; el.addEventListener("change", () => save("user_change")); } }
  }
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init); else init();
  setTimeout(init, 500);
})();

// IGblue v179a13 patch: lightweight fold panels for side 공간 활용.
(() => {
  if (window.__IGBLUE_FOLD_PANELS_V179A13__) return;
  window.__IGBLUE_FOLD_PANELS_V179A13__ = true;
  const PREFIX = "IGBLUE/fold/";
  function key(el) {
    return PREFIX + String(el && (el.dataset.igblueFold || el.id) || "unknown");
  }
  function updateIcon(el) {
    try {
      const icon = el && el.querySelector && el.querySelector("[data-fold-icon]");
      if (icon) icon.textContent = el.open ? "▣" : "□";
    } catch (_) {}
  }
  function bindOne(el) {
    if (!el || el.__igblueFoldBound) return;
    el.__igblueFoldBound = true;
    try {
      const saved = localStorage.getItem(key(el));
      if (saved === "1" || saved === "0") el.open = saved === "1";
      else el.open = String(el.dataset.defaultOpen || "0") === "1";
    } catch (_) {
      el.open = String(el.dataset.defaultOpen || "0") === "1";
    }
    updateIcon(el);
    el.addEventListener("toggle", () => {
      try { localStorage.setItem(key(el), el.open ? "1" : "0"); } catch (_) {}
      updateIcon(el);
    });
    try {
      const summary = el.querySelector("summary");
      if (summary) summary.querySelectorAll("input, button, select, textarea, a").forEach((child) => {
        ["click", "mousedown", "pointerdown", "keydown"].forEach((name) => child.addEventListener(name, (ev) => ev.stopPropagation(), true));
      });
    } catch (_) {}
  }
  function init() {
    try { document.querySelectorAll("details[data-igblue-fold]").forEach(bindOne); } catch (_) {}
  }
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init);
  else init();
  setTimeout(init, 300);
  setTimeout(init, 1000);
})();

// IGblue v179a37: Auto8700 two-slot schedule UI. CDP is schedule-only; checked slots only run.
// Auto8700/8707가 살아 있으면 예약 저장/카운트다운을 유지하되, CDP:O일 때만 실제 예약 실행을 허용한다.
(() => {
  if (window.__IGBLUE_AUTO8700_SCHEDULE_UI_V188__) return;
  window.__IGBLUE_AUTO8700_SCHEDULE_UI_V188__ = true;
  const AUTO_URL = "http://127.0.0.1:8707";
  const $ = (id) => document.getElementById(id);
  let lastStatusOk = false;
  let cdpReady = false;
  let schedule = { schedule1_enabled:false, schedule1_time:"00:00", schedule2_enabled:false, schedule2_time:"00:00", revision:0, updated_at:"", updated_source:"" };
  let timeFocused = false;
  let scheduleDirty = false;
  let scheduleSaving = false;
  let scheduleLoaded = false;

  function pad2(n){ return String(Math.max(0, Math.min(99, Number(n)||0))).padStart(2,"0"); }
  function normalizeTime(v){
    const raw = String(v || "00:00").trim();
    const m = raw.match(/^(\d{1,2})(?::(\d{1,2}))?(?::(\d{1,2}))?$/);
    if (!m) return "00:00";
    const hh = Math.max(0, Math.min(23, parseInt(m[1]||"0",10)));
    const mm = Math.max(0, Math.min(59, parseInt(m[2]||"0",10)));
    return pad2(hh)+":"+pad2(mm);
  }
  function todayKey(d){ const x=d||new Date(); return x.getFullYear()+"-"+pad2(x.getMonth()+1)+"-"+pad2(x.getDate()); }
  function nextAt(timeText){
    const now = new Date(); const t = normalizeTime(timeText).split(":").map(Number);
    const target = new Date(now); target.setHours(t[0]||0, t[1]||0, 0, 0);
    if (target <= now) target.setDate(target.getDate()+1);
    return target;
  }
  function remainingText(ms){
    const sec = Math.max(0, Math.floor(ms/1000));
    return String(Math.floor(sec/3600)).padStart(2,"0")+"시간 "+String(Math.floor((sec%3600)/60)).padStart(2,"0")+"분 "+String(sec%60).padStart(2,"0")+"초";
  }
  function setStatus(text){ const el=$("igblue_auto8700_schedule_status"); if(el) el.textContent=text||""; }
  function setCdpBrief(on){
    try { if (typeof window.__IGBLUE_SET_CDP_READY_V277__ === "function") window.__IGBLUE_SET_CDP_READY_V277__(!!on, on ? "CDP 연결됨" : "CDP 실행 필요"); } catch(_) {}
  }
  function setCountdown(text){ const el=$("igblue_auto8700_schedule_countdown"); if(el) el.textContent=text||""; }
  function setScheduleVisible(on){
    const el=$("igblue_auto8700_schedule_row");
    if(el){ el.classList.toggle("is-cdp-unavailable", !on); el.style.display = "flex"; }
  }
  function scheduleAccountContext(){
    try {
      const st = (typeof teAutoPyAccountQueueStatus === "function") ? teAutoPyAccountQueueStatus() : null;
      return {
        account_queue_disabled: (typeof teAutoPyAccountQueueDisabled === "function") ? !!teAutoPyAccountQueueDisabled() : false,
        account_queue_index: st && st.has ? Number(st.index || 0) : 0,
        account_queue_total: st && st.has ? Number(st.total || 0) : 0,
        account_queue_url: st && st.has ? String(st.url || "") : "",
        account_queue_current_page: !!(st && st.current_page)
      };
    } catch (_) { return {account_queue_disabled:false, account_queue_index:0, account_queue_total:0}; }
  }
  function scheduleInputs(){
    return { e1: $("igblue_auto8700_schedule1_enabled"), t1: $("igblue_auto8700_schedule1_time"), e2: $("igblue_auto8700_schedule2_enabled"), t2: $("igblue_auto8700_schedule2_time"), save: $("igblue_auto8700_schedule_save") };
  }
  function setEnabled(on){
    ["igblue_auto8700_schedule1_enabled","igblue_auto8700_schedule1_time","igblue_auto8700_schedule2_enabled","igblue_auto8700_schedule2_time","igblue_auto8700_schedule_save"].forEach((id)=>{ const el=$(id); if(el) el.disabled=!on; });
  }
  function normalizeSchedule(sc){
    const raw = sc || {};
    const hasNew = Object.prototype.hasOwnProperty.call(raw, "schedule1_enabled") || Object.prototype.hasOwnProperty.call(raw, "schedule2_enabled");
    return {
      schedule1_enabled: hasNew ? !!raw.schedule1_enabled : !!raw.enabled,
      schedule1_time: normalizeTime(raw.schedule1_time || raw.time || "00:00"),
      schedule2_enabled: !!raw.schedule2_enabled,
      schedule2_time: normalizeTime(raw.schedule2_time || "00:00"),
      enabled: !!(hasNew ? (raw.schedule1_enabled || raw.schedule2_enabled) : raw.enabled),
      time: normalizeTime(raw.schedule1_time || raw.time || "00:00"),
      revision: Math.max(0, Number(raw.revision || 0) || 0),
      updated_at: String(raw.updated_at || ""),
      updated_source: String(raw.updated_source || ""),
      last_run_date: String(raw.last_run_date || ""),
      last_run_date_1: String(raw.last_run_date_1 || ""),
      last_run_date_2: String(raw.last_run_date_2 || "")
    };
  }
  function applyScheduleToUi(sc, force){
    const incoming = normalizeSchedule(sc);
    schedule = Object.assign({}, schedule, incoming);
    if (!force && (scheduleDirty || scheduleSaving || timeFocused)) return false;
    const i = scheduleInputs();
    if (i.e1) i.e1.checked = !!schedule.schedule1_enabled;
    if (i.e2) i.e2.checked = !!schedule.schedule2_enabled;
    if (i.t1) i.t1.value = normalizeTime(schedule.schedule1_time || "00:00");
    if (i.t2) i.t2.value = normalizeTime(schedule.schedule2_time || "00:00");
    scheduleDirty = false;
    scheduleLoaded = true;
    return true;
  }
  function markScheduleDirty(){
    scheduleDirty = true;
    const i = scheduleInputs();
    if (i.save) i.save.disabled = !lastStatusOk || scheduleSaving;
    setStatus("예약 수정중 · 예약저장 필요");
  }
  function currentSettings(extra){
    const i = scheduleInputs();
    const s1 = normalizeTime(i.t1 && i.t1.value || schedule.schedule1_time || schedule.time || "00:00");
    const s2 = normalizeTime(i.t2 && i.t2.value || schedule.schedule2_time || "00:00");
    const e1 = !!(i.e1 && i.e1.checked), e2 = !!(i.e2 && i.e2.checked);
    return Object.assign({ enabled: !!(e1 || e2), time: s1, schedule1_enabled: e1, schedule1_time: s1, schedule2_enabled: e2, schedule2_time: s2, ig_side_heartbeat: true }, scheduleAccountContext(), extra || {});
  }
  function activeSlots(){
    const s = normalizeSchedule(schedule);
    const rows = [{id:1, enabled:!!s.schedule1_enabled, time:normalizeTime(s.schedule1_time)}, {id:2, enabled:!!s.schedule2_enabled, time:normalizeTime(s.schedule2_time)}];
    const seen = new Set(), out=[];
    for (const row of rows) { if (!row.enabled) continue; if (seen.has(row.time)) continue; seen.add(row.time); out.push(row); }
    return out;
  }
  async function fetchStatus(forceApply){
    try{
      const r = await fetch(AUTO_URL + "/status", {cache:"no-store"});
      if(!r.ok) throw new Error("HTTP "+r.status);
      const j = await r.json();
      lastStatusOk = true;
      const storage = j.schedule_storage || {};
      cdpReady = !!storage.cdp_ready;
      const applied = applyScheduleToUi(j.auto_schedule || {}, !!forceApply || !scheduleLoaded);
      try {
        chrome.storage.local.set({"IGBLUE_AUTO8700_SCHEDULE": Object.assign({schema:"AUTO8700_RECEIVED_ONLY", source:"Auto8700"}, normalizeSchedule(j.auto_schedule || {}), {cdp_ready:cdpReady, auto8700_status_at:Date.now()})});
      } catch(_) {}
      setEnabled(lastStatusOk && cdpReady); setScheduleVisible(cdpReady); setCdpBrief(cdpReady);
      if (scheduleDirty || scheduleSaving || timeFocused) setStatus(scheduleSaving ? "예약 저장중" : "예약 수정중 · 예약저장 필요");
      else setStatus(cdpReady ? (storage.ig_side_active ? "IG 담당" : "예약 준비") : "CDP 실행 필요");
      return applied;
    }catch(e){ lastStatusOk = false; cdpReady = false; setEnabled(false); setScheduleVisible(false); setCdpBrief(false); setStatus("Auto8700 미실행"); setCountdown("설치/실행 필요"); return false; }
  }
  async function saveScheduleFromUi(){
    if (scheduleSaving || !lastStatusOk) return false;
    const settings = currentSettings({save_source:"ig_side"});
    scheduleSaving = true;
    setEnabled(false);
    setStatus("예약 저장중");
    try{
      const r = await fetch(AUTO_URL + "/schedule", {method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({act:"auto8700_schedule_save", settings})});
      const j = await r.json().catch(()=>({}));
      if(!r.ok || j.ok === false) throw new Error(j.error || ("HTTP "+r.status));
      scheduleDirty = false;
      applyScheduleToUi(j.auto_schedule || {}, true);
      cdpReady = !!(j.cdp_ready || (j.schedule_state && j.schedule_state.cdp_ready));
      try { chrome.storage.local.set({"IGBLUE_AUTO8700_SCHEDULE": Object.assign({schema:"AUTO8700_RECEIVED_ONLY", source:"Auto8700"}, normalizeSchedule(j.auto_schedule || {}), {cdp_ready:cdpReady, auto8700_status_at:Date.now()})}); } catch(_) {}
      setStatus(cdpReady ? "예약 저장됨" : "저장됨");
      return true;
    }catch(e){
      setStatus("예약 저장 실패: "+String(e && e.message || e));
      scheduleSaving = false;
      await fetchStatus(true);
      return false;
    }finally{
      scheduleSaving = false;
      setEnabled(lastStatusOk && cdpReady);
      setScheduleVisible(cdpReady);
      setCdpBrief(cdpReady);
    }
  }
  async function postScheduleMeta(act, extra){
    const settings = Object.assign({ig_side_heartbeat:true}, scheduleAccountContext(), extra || {});
    try{
      const r = await fetch(AUTO_URL + "/schedule", {method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({act, settings})});
      const j = await r.json().catch(()=>({}));
      if (r.ok && j && j.auto_schedule) {
        schedule = Object.assign({}, schedule, normalizeSchedule(j.auto_schedule));
        try { chrome.storage.local.set({"IGBLUE_AUTO8700_SCHEDULE": Object.assign({schema:"AUTO8700_RECEIVED_ONLY", source:"Auto8700"}, normalizeSchedule(j.auto_schedule), {cdp_ready:!!j.cdp_ready, auto8700_status_at:Date.now()})}); } catch(_) {}
      }
      return !!(r.ok && j.ok !== false);
    }catch(_){ return false; }
  }
  async function heartbeat(){
    if (!lastStatusOk) return;
    await postScheduleMeta("auto8700_schedule_heartbeat", {ig_side_heartbeat:true});
  }
  function requestAutoStartFromIg(slot){
    const requestId = "ig_schedule_" + Date.now() + "_" + Math.floor(Math.random()*100000);
    try { chrome.runtime.sendMessage(Object.assign({type:"IGBLUE_AUTO8700_START_REQUEST", source:"IG_SIDE_SCHEDULE", reason:"ig_side_schedule", request_id:requestId, open_side:false, schedule_slot_id:slot && slot.id, schedule_time:slot && slot.time, ts:Date.now()}, scheduleAccountContext()), ()=>{ chrome.runtime.lastError; }); } catch(_) {}
    const slotId = Number(slot && slot.id || 0) || 0;
    if (slotId === 1 || slotId === 2) schedule["last_run_date_" + slotId] = todayKey(new Date());
    postScheduleMeta("auto8700_schedule_ig_run_mark", {mark_run_today:true, mark_run_slot_id:slotId, mark_run_time:slot && slot.time, ig_side_heartbeat:true});
    setStatus("예약" + (slot && slot.id || "") + " 시간 도달: IG 자동시작 요청");
  }
  function tick(){
    if (!lastStatusOk) { setCountdown("Auto8700 대기"); return; }
    const slots = activeSlots();
    if (!slots.length) { setCountdown("예약 꺼짐"); return; }
    if (!cdpReady) { setCountdown("CDP 대기"); return; }
    const now = new Date(); let next = null;
    for (const slot of slots) {
      const target = nextAt(slot.time); if (!next || target < next.target) next = {slot, target};
      const parts = normalizeTime(slot.time).split(":").map(Number); const due = new Date(now); due.setHours(parts[0]||0, parts[1]||0, 0, 0);
      const delta = now.getTime() - due.getTime();
      const last = String(schedule["last_run_date_" + Number(slot.id || 0)] || schedule.last_run_date || "");
      if (delta >= 0 && delta < 90000 && last !== todayKey(now)) requestAutoStartFromIg(slot);
    }
    if (next) setCountdown("예약" + next.slot.id + " " + remainingText(next.target.getTime() - now.getTime()));
  }
  function bind(){
    const i = scheduleInputs(); if (!i.e1 || i.e1.__igblueBound) return; i.e1.__igblueBound = true;
    [i.e1, i.e2].forEach((el)=>{ if(el) el.addEventListener("change", markScheduleDirty); });
    [i.t1, i.t2].forEach((el)=>{ if(!el) return; el.addEventListener("focus", ()=>{timeFocused=true;}); el.addEventListener("input", markScheduleDirty); el.addEventListener("change", markScheduleDirty); el.addEventListener("blur", ()=>{timeFocused=false; el.value=normalizeTime(el.value);}); });
    i.save && i.save.addEventListener("click", saveScheduleFromUi);
    fetchStatus(); setInterval(fetchStatus, 3000); setInterval(heartbeat, 5000); setInterval(tick, 1000);
  }
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", bind); else bind();
  setTimeout(bind, 500);
})();
