DeepCo Statistics + Lag Fix & Optimization

same script as the one from Cosmin Deme, but no lag. performance fixes for lag.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         DeepCo Statistics + Lag Fix & Optimization
// @namespace    https://greasyfork.org/en/users/1559634-korphd
// @version      2026-01-12 v1.01
// @description  same script as the one from Cosmin Deme, but no lag. performance fixes for lag.
// @match        https://*.deepco.app/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=deepco.app
// @license      MIT
// @grant        GM.setValue
// @grant        GM.getValue
// @require      https://code.highcharts.com/highcharts.js
// @require      https://code.highcharts.com/modules/boost.js
// @require      https://code.highcharts.com/modules/mouse-wheel-zoom.js
// @run-at       document-end
// ==/UserScript==

(async function () {
  'use strict';

  /**********************
   * STORAGE / KEYS
   **********************/
  const SCHEMA = [['Timestamp', 'Tile Count', 'RC', 'Level', 'DC', 'DCIncome', 'Processing Rating']];
  const DB_KEY = 'nudgeLogs';
  const VIS_KEY = 'chartVisibility';

  const RANGE_KEY = 'deepco_range_r7';
  const AUTO_KEY  = 'deepco_range_auto_r7';

  // DC/hr from toast (keep working r3 keys)
  const DC_ACC_KEY = 'deepco_dc_acc_since_reset_r3';
  const DC_RESET_TS_KEY = 'deepco_dc_reset_ts_r3';
  const DC_TOAST_DEDUPE_KEY = 'deepco_dc_toast_dedupe_r3';
  const CHART_HIDE_KEY = 'deepco_chart_hidden_r3';

  const CHART_ID = 'deepco-chart-container';
  const PANEL_ID = 'deepco-stats-panel';
  const RANGE_BAR_ID = 'deepco-rangebar';

  // HUD (live values)
  const HUD_ID = 'deepco-live-hud';

  // Totals HUD (separate, not chart stats)
  const TOTALS_HUD_ID = 'deepco-totals-hud';
  const TOTAL_DC_KEY = 'deepco_total_dc_since_reset_r7';
  const TOTAL_RC_KEY = 'deepco_total_rc_since_reset_r7';
  const TOTAL_RC_LAST_KEY = 'deepco_total_rc_last_seen_r7';
  const TOTAL_RESET_TS_KEY = 'deepco_totals_reset_ts_r7';


  const POLL_MS = 2500;
  const MAX_CHART_POINTS = 3000;
  const RANGE_ANIM_MS = 280;

  // ✅ Anti-spike warmup (DC/hr = 0 primele N secunde după baseline start)
  const DC_WARMUP_SECONDS = 10;

  // Time-to-finish-block: average interval between DC toasts over last minute
  const BLOCK_WINDOW_MS = 60_000;
  const BLOCK_BUF_KEEP_MS = 10 * 60_000;

  // ✅ reserve top space so HUD never overlaps plot
  const CHART_TOP_SPACING = 44; // tuned for 3 lines HUD

  /**********************
   * STATE
   **********************/
  let db = await GM.getValue(DB_KEY, SCHEMA);
  fixTimestamps(db);

  let myChart = null;
  // Chart redraw throttling (prevents lag when many points accumulate)
  let _chartRedrawScheduled = false;
  let _lastChartTsForExtremes = null;

  // OPTIMIZATION: Use requestAnimationFrame for smoother UI updates logic if available, fallback to timeout
  function scheduleChartRedraw(tsForExtremes) {
    if (tsForExtremes != null) _lastChartTsForExtremes = tsForExtremes;
    if (_chartRedrawScheduled) return;
    _chartRedrawScheduled = true;
    
    // Increased throttle slightly to 300ms to group more updates
    setTimeout(() => {
      _chartRedrawScheduled = false;
      if (!myChart) return;
      try { 
          // Check if we really need to redraw? Yes, data changed.
          myChart.redraw(); 
      } catch (_) {}
      try {
        if (rangeMode !== 'all' && _lastChartTsForExtremes != null) {
          applyRangeExtremes(_lastChartTsForExtremes);
        }
      } catch (_) {}
    }, 300);
  }


  // RC/hr since recursion baseline
  let recursionTime = null;
  let startingRC = null;
  let lastRC = null;

  /** @type {{ts:number, rc:number}[]} */
  let rcBuf = [];

  // DC state
  let dcAcc = Number(await GM.getValue(DC_ACC_KEY, 0)) || 0;
  let dcResetTs = Number(await GM.getValue(DC_RESET_TS_KEY, 0)) || 0;

  // Totals since reset (separate HUD)
  let totalDcSinceReset = Number(await GM.getValue(TOTAL_DC_KEY, 0)) || 0;
  let totalRcSinceReset = Number(await GM.getValue(TOTAL_RC_KEY, 0)) || 0;
  let totalRcLastSeen = await GM.getValue(TOTAL_RC_LAST_KEY, null);
  if (totalRcLastSeen !== null) totalRcLastSeen = Number(totalRcLastSeen);

  // Totals timer ("Time since reset" for Totals HUD)
  let totalsResetTs = Number(await GM.getValue(TOTAL_RESET_TS_KEY, 0)) || 0;
  if (!Number.isFinite(totalsResetTs) || totalsResetTs <= 0) totalsResetTs = Date.now();


  // DC toast timestamps (used to estimate time to finish block)
  /** @type {number[]} */
  let dcToastTimes = [];

  // Block-time event rules
  // - multiple popups in the same observer callback count as ONE event
  // - ignore events that happen faster than 0.3s
  const BLOCK_MIN_EVENT_GAP_MS = 300;
  let lastBlockEventTs = 0;
  let lastBlockAvgSec = null;

  // Toast dedupe
  let toastKeys = await GM.getValue(DC_TOAST_DEDUPE_KEY, []);
  if (!Array.isArray(toastKeys)) toastKeys = [];
  let toastKeySet = new Set(toastKeys);

  // Range selection + AUTO
  let rangeMode = (localStorage.getItem(RANGE_KEY) || 'all').toLowerCase();
  let autoFollow = (localStorage.getItem(AUTO_KEY) || '1') === '1';

  const RANGE_MS = {
    '1m': 60_000,
    '5m': 5 * 60_000,
    '10m': 10 * 60_000,
    '30m': 30 * 60_000,
    '1h': 60 * 60_000,
    'all': null
  };

  /**********************
   * UTILS
   **********************/
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

  async function waitFor(fn, timeoutMs = 30000, stepMs = 200) {
    const t0 = Date.now();
    while (Date.now() - t0 < timeoutMs) {
      try { if (fn()) return true; } catch (_) {}
      await sleep(stepMs);
    }
    return false;
  }

  function fixTimestamps(arr) {
    if (!Array.isArray(arr) || arr.length === 0) return;
    for (let i = 1; i < arr.length; i++) {
      const ts = arr[i]?.[0];
      if (typeof ts === 'string') {
        const parsed = Date.parse(ts);
        if (Number.isFinite(parsed)) arr[i][0] = parsed;
      }
    }
  }

  function safeNumber(v) {
    const n = Number(v);
    return Number.isFinite(n) ? n : 0;
  }

  function safeText(v) {
    return (v == null) ? '' : String(v);
  }

  function parseFirstNumber(text) {
    if (!text) return 0;
    // Simple check before regex to speed up
    if (!/\d/.test(text)) return 0;
    const m = String(text).replace(/,/g, '').match(/[+-]?\d+(?:\.\d+)?/);
    return m ? safeNumber(m[0]) : 0;
  }

  function isDigPage() {
    return location.pathname === '/dig';
  }

  function clampSeries(series) {
    try {
      // OPTIMIZATION: Batch removal if significantly over limit
      const len = series.data.length;
      if (len > MAX_CHART_POINTS) {
         // remove excessive points
         // Note: shifting one by one in a loop is okay-ish because Highcharts handles array operations, 
         // but we rely on the scheduled redraw to actually render the changes.
         let toRemove = len - MAX_CHART_POINTS;
         while (toRemove > 0) {
             series.removePoint(0, false);
             toRemove--;
         }
      }
    } catch (_) {}
  }

  /**********************
   * HUD (live values) - top center, smaller, never overlaps plot
   **********************/
  function getChartAnchorRect() {
    const el = document.getElementById(CHART_ID);
    if (!el) return null;
    try {
      const r = el.getBoundingClientRect();
      if (!r || !Number.isFinite(r.top)) return null;
      return r;
    } catch (_) {
      return null;
    }
  }

  function positionOverlayHud(el, mode) {
    // Keep HUDs visible even when chart is hidden (SPA nav / toggle).
    // When chart exists, anchor near its top edge; otherwise fall back to viewport.
    const r = getChartAnchorRect();
    const topBase = r ? Math.max(6, Math.round(r.top) + 6) : 6;
    if (mode === 'center') {
      el.style.top = `${topBase}px`;
      el.style.left = '50%';
      el.style.right = '';
      el.style.transform = 'translateX(-50%)';
    } else if (mode === 'right') {
      el.style.top = `${topBase + 2}px`;
      el.style.right = '12px';
      el.style.left = '';
      el.style.transform = '';
    }
  }

  
  function hudsArePinnedInRangeBar() {
    const bar = document.getElementById(RANGE_BAR_ID);
    return !!(bar && bar.getAttribute('data-hud-mounted') === '1');
  }

function ensureHud() {
    let hud = document.getElementById(HUD_ID);
    if (hud) return hud;

    hud = document.createElement('div');
    hud.id = HUD_ID;
    hud.style.position = 'fixed';
    hud.style.zIndex = '2147483646';

    // smaller + compact to avoid touching plot
    hud.style.padding = '4px 6px';
    hud.style.borderRadius = '7px';
    hud.style.background = 'rgba(0,0,0,0.28)';
    hud.style.border = '1px solid rgba(255,255,255,0.14)';
    hud.style.pointerEvents = 'none';
    hud.style.whiteSpace = 'nowrap';
    hud.style.fontSize = '11px';
    hud.style.lineHeight = '1.18';

    document.documentElement.appendChild(hud);
    if (!hudsArePinnedInRangeBar()) positionOverlayHud(hud, 'center');
    return hud;
  }

  function fmt2(n) {
    const x = Number(n);
    return Number.isFinite(x) ? x.toFixed(2) : '0.00';
  }

  function fmt1(n) {
    const x = Number(n);
    return Number.isFinite(x) ? x.toFixed(1) : '0.0';
  }

  function fmtHMS(ms) {
    const t = Math.max(0, Math.floor(ms / 1000));
    const s = t % 60;
    const m = Math.floor(t / 60) % 60;
    const hTotal = Math.floor(t / 3600);
    const h = hTotal % 24;
    const d = Math.floor(hTotal / 24);
    const pad = (v) => String(v).padStart(2, '0');
    const core = `${pad(h)}:${pad(m)}:${pad(s)}`;
    return d > 0 ? `${d}d ${core}` : core;
  }

  function lastY(series) {
    const d = series?.data;
    if (!d || !d.length) return null;
    const p = d[d.length - 1];
    return (p && typeof p.y === 'number') ? p.y : null;
  }

  function updateHud() {
    if (!myChart) return;
    const hud = ensureHud();
    if (!hud) return;

    // Keep position synced with chart location (and still visible when chart hidden).
    // If HUD is pinned into the range bar, DON'T reposition (it would jump/disappear).
    if (!hudsArePinnedInRangeBar()) positionOverlayHud(hud, 'center');

    const s0 = myChart.series?.[0];
    const s1 = myChart.series?.[1];
    const s2 = myChart.series?.[2];
    const s3 = myChart.series?.[3];

    const lines = [];
    if (s0?.visible) lines.push(`RC/hr: ${fmt2(lastY(s0))}`);
    if (s1?.visible) lines.push(`RC/PastMinute: ${fmt2(lastY(s1))}`);
    if (s2?.visible) lines.push(`DC/hr: ${fmt2(lastY(s2))}`);
    if (s3?.visible) {
      const v = lastY(s3);
      const show = (v != null) ? v : lastBlockAvgSec;
      lines.push(`Time to finish block: ${show != null ? (fmt1(show) + 's') : '-'}`);
    }

    hud.innerHTML = lines.length ? lines.join('<br/>') : '';
  }

  /**********************
   * TOTALS HUD (separate; includes reset button)
   **********************/
  function ensureTotalsHud() {
    let box = document.getElementById(TOTALS_HUD_ID);
    if (box) return box;

    box = document.createElement('div');
    box.id = TOTALS_HUD_ID;
    box.style.position = 'fixed';
    box.style.zIndex = '2147483647';
    box.style.padding = '6px 8px';
    box.style.borderRadius = '8px';
    box.style.background = 'rgba(0,0,0,0.35)';
    box.style.border = '1px solid rgba(255,255,255,0.16)';
    box.style.whiteSpace = 'nowrap';
    box.style.fontSize = '12px';
    box.style.lineHeight = '1.25';
    box.style.display = 'flex';
    box.style.alignItems = 'flex-start';
    box.style.gap = '8px';

    const txt = document.createElement('div');
    txt.id = `${TOTALS_HUD_ID}-text`;

    const btn = document.createElement('button');
    btn.type = 'button';
    btn.textContent = 'Reset';
    btn.style.cursor = 'pointer';
    btn.style.padding = '4px 8px';
    btn.style.borderRadius = '6px';
    btn.style.border = '1px solid rgba(255,255,255,0.25)';
    btn.style.background = 'rgba(0,0,0,0.25)';
    btn.style.color = 'inherit';
    btn.style.font = 'inherit';
    btn.addEventListener('click', (e) => {
      e.preventDefault();
      e.stopPropagation();
      resetTotalsHud().catch(() => {});
    });

    box.appendChild(txt);
    box.appendChild(btn);
    document.documentElement.appendChild(box);
    if (!hudsArePinnedInRangeBar()) positionOverlayHud(box, 'right');
    return box;
  }

  function updateTotalsHud() {
    const box = ensureTotalsHud();
    if (!box) return;

    // Keep visible/positioned even when chart is hidden.
    if (!hudsArePinnedInRangeBar()) positionOverlayHud(box, 'right');
    const txt = document.getElementById(`${TOTALS_HUD_ID}-text`);
    if (!txt) return;

    const elapsedMs = Date.now() - (Number.isFinite(totalsResetTs) ? totalsResetTs : Date.now());
    txt.innerHTML =
      `Total DC since reset: <b>${safeNumber(totalDcSinceReset).toFixed(2)}</b>` +
      `<br/>Total RC since reset: <b>${safeNumber(totalRcSinceReset).toFixed(2)}</b>` +
      `<br/>Time since reset: <b>${fmtHMS(elapsedMs)}</b>`;
  }

  async function resetTotalsHud() {
    // reset only totals HUD (not chart stats, not DC/hr, not logs)
    totalDcSinceReset = 0;
    totalRcSinceReset = 0;

    // reset timer baseline for Totals HUD
    totalsResetTs = Date.now();

    // baseline RC for future diffs
    const rcNow = safeNumber(parseFirstNumber(getRCCount()));
    totalRcLastSeen = Number.isFinite(rcNow) ? rcNow : 0;

    await GM.setValue(TOTAL_DC_KEY, totalDcSinceReset);
    await GM.setValue(TOTAL_RC_KEY, totalRcSinceReset);
    await GM.setValue(TOTAL_RC_LAST_KEY, totalRcLastSeen);
    await GM.setValue(TOTAL_RESET_TS_KEY, totalsResetTs);

    updateTotalsHud();
  }

  async function addToTotalDc(delta) {
    // Rule: add ALL DC popups, regardless duplicates/time (no dedupe)
    totalDcSinceReset += delta;
    await GM.setValue(TOTAL_DC_KEY, totalDcSinceReset);
    updateTotalsHud();
  }

  async function updateTotalRcFromSnapshot(rcNow) {
    // Sum only positive increases; handle recursion drops by resetting baseline
    const rc = safeNumber(rcNow);
    if (totalRcLastSeen === null || !Number.isFinite(totalRcLastSeen)) {
      totalRcLastSeen = rc;
      await GM.setValue(TOTAL_RC_LAST_KEY, totalRcLastSeen);
      updateTotalsHud();
      return;
    }
    const d = rc - totalRcLastSeen;
    if (d > 0) {
      totalRcSinceReset += d;
      await GM.setValue(TOTAL_RC_KEY, totalRcSinceReset);
    }
    totalRcLastSeen = rc;
    await GM.setValue(TOTAL_RC_LAST_KEY, totalRcLastSeen);
    updateTotalsHud();
  }


  /**********************
   * DOM READERS
   **********************/
  function getTileCount() {
    const el = document.querySelector('#worker_tiles') || document.querySelector('[data-testid="worker_tiles"]');
    return el ? el.textContent : '';
  }

  function getRCCount() {
    const el = document.querySelector('span.flex:nth-child(2) > span:nth-child(1)');
    if (el) return el.textContent;

    const nodes = Array.from(document.querySelectorAll('span,div,p')).slice(0, 1200);
    for (const n of nodes) {
      const t = (n.textContent || '').trim();
      if ((/\bRC\b/i.test(t) || /RC:/i.test(t)) && /\d/.test(t)) return t;
    }
    return '';
  }

  function getLevel() {
    const el = document.querySelector('#worker_level') || document.querySelector('[data-testid="worker_level"]');
    return el ? el.textContent : '';
  }

  function getProcessingRating() {
    const el = document.querySelector('#processing_rating') || document.querySelector('[data-testid="processing_rating"]');
    return el ? el.textContent : '';
  }

  /**********************
   * DC FROM TOAST
   **********************/
  function extractDCDeltaFromText(text) {
    if (!text) return null;
    // OPTIMIZATION: string check before Regex
    if (!text.includes('DC') && !text.includes('dc')) return null;

    const t = String(text).replace(/,/g, ' ');
    const m = t.match(/\+?\s*([0-9]+(?:\.\d+)?)[ \t]*\[(DC)\]/i);
    if (!m) return null;
    const v = Number(m[1]);
    if (!Number.isFinite(v) || v <= 0) return null;
    return v;
  }

  function dcRateWithWarmup(ts, dcSnapshot, fallbackBaseTs) {
    if (!dcResetTs) return 0;

    const base = dcResetTs || fallbackBaseTs || ts;
    const elapsedMs = ts - base;

    if (elapsedMs < DC_WARMUP_SECONDS * 1000) return 0;

    const hours = elapsedMs / 3600_000;
    if (!(hours > 0)) return 0;

    const rate = dcSnapshot / hours;
    return Number.isFinite(rate) ? rate : 0;
  }

  function recordDcToast(ts) {
    const t = Number(ts) || Date.now();
    dcToastTimes.push(t);
    // keep only recent for performance
    const cutoff = t - BLOCK_BUF_KEEP_MS;
    while (dcToastTimes.length && dcToastTimes[0] < cutoff) dcToastTimes.shift();
  }

  // Record a "block finished" event based on DC toast timing rules:
  // - Multiple toasts in the same observer callback count as ONE event.
  // - Ignore if the last recorded event is < 0.3s ago.
  function recordBlockEvent(ts) {
    const t = Number(ts) || Date.now();
    if (lastBlockEventTs && (t - lastBlockEventTs) < BLOCK_MIN_EVENT_GAP_MS) return false;
    lastBlockEventTs = t;
    recordDcToast(t);
    return true;
  }

  function avgBlockTimeSecondsLastMinute(nowTs) {
    const now = Number(nowTs) || Date.now();
    const cutoff = now - BLOCK_WINDOW_MS;

    // find first index >= cutoff
    let startIdx = 0;
    while (startIdx < dcToastTimes.length && dcToastTimes[startIdx] < cutoff) startIdx++;

    const n = dcToastTimes.length - startIdx;
    if (n < 2) return lastBlockAvgSec;

    const first = dcToastTimes[startIdx];
    const last = dcToastTimes[dcToastTimes.length - 1];
    if (!(last > first)) return null;

    const avgMs = (last - first) / (n - 1);
    const sec = avgMs / 1000;
    if (Number.isFinite(sec)) {
      lastBlockAvgSec = sec;
      return sec;
    }
    return lastBlockAvgSec;
  }

  function countToastIfNew(delta, rawText) {
    const key = `${rawText}@@${performance.now().toFixed(3)}`;
    if (toastKeySet.has(key)) return false;

    toastKeySet.add(key);
    if (toastKeySet.size > 300) {
      const arr = Array.from(toastKeySet);
      toastKeySet = new Set(arr.slice(arr.length - 200));
    }
    GM.setValue(DC_TOAST_DEDUPE_KEY, Array.from(toastKeySet)).catch(() => {});

    if (!dcResetTs) {
      dcResetTs = Date.now();
      GM.setValue(DC_RESET_TS_KEY, dcResetTs).catch(() => {});
    }

    dcAcc += delta;
    GM.setValue(DC_ACC_KEY, dcAcc).catch(() => {});

    pushInstantDcPoint();
    return true;
  }

  // Toast observer can break on SPA navigations if <body> is replaced.
  // We keep a single observer instance and reattach whenever document.body changes.
  let __toastObserver = null;
  let __toastObservedBody = null;

  function ensureToastObserverAttached() {
    const body = document.body;
    if (!body) return;

    if (__toastObserver && __toastObservedBody === body) return;

    try { if (__toastObserver) __toastObserver.disconnect(); } catch (_) {}

    __toastObservedBody = body;
    __toastObserver = new MutationObserver((mutations) => {
      // Count all DC deltas, but record "block finish" timing at most once per callback.
      let hadAcceptedInThisCallback = false;
      
      // OPTIMIZATION: Avoid processing too many nodes if lag occurs
      let processedCount = 0;

      for (const m of mutations) {
        if (!m.addedNodes) continue;
        for (const node of m.addedNodes) {
          if (processedCount > 50) break; // Lag protection
          if (!(node instanceof HTMLElement)) continue;
          
          const text = (node.innerText || node.textContent || '').trim();
          // Fast fail
          if (!text || (!text.includes('DC') && !text.includes('dc'))) continue;
          
          processedCount++;

          const delta = extractDCDeltaFromText(text);
          if (delta == null) continue;

          // Totals HUD: count ALL DC popups (no dedupe)
          addToTotalDc(delta).catch(() => {});

          const accepted = countToastIfNew(delta, text);
          if (accepted) hadAcceptedInThisCallback = true;
        }
      }

      // Rules:
      // - multiple popups simultaneously => one event
      // - popups faster than 0.3s => ignore
      if (hadAcceptedInThisCallback) {
        const ts = Date.now();
        if (recordBlockEvent(ts)) {
          pushInstantBlockPoint(ts);
        }
      }
    });

    try {
      __toastObserver.observe(body, { childList: true, subtree: true });
    } catch (_) {}
  }

  function pushInstantDcPoint() {
    try {
      if (!myChart) return;

      const ts = Date.now();
      const fallbackBase = (db[1]?.[0] || ts);
      const dcSnap = safeNumber(dcAcc);
      const v2 = dcRateWithWarmup(ts, dcSnap, fallbackBase);

      myChart.series[2].addPoint([ts, v2], false, false);
      clampSeries(myChart.series[2]);
      
      // OPTIMIZATION: Don't redraw immediately, schedule it
      scheduleChartRedraw(ts);

      updateHud();
    } catch (_) {}
  }

  // Instant update for "Time to finish block" series when a valid event occurs.
  function pushInstantBlockPoint(ts) {
    try {
      if (!myChart) return;
      const t = Number(ts) || Date.now();
      const v3 = avgBlockTimeSecondsLastMinute(t);
      // If we still don't have enough samples, don't spam zeros.
      if (v3 == null) return;
      myChart.series[3].addPoint([t, v3], false, false);
      clampSeries(myChart.series[3]);
      
      // OPTIMIZATION: Schedule redraw instead of instant
      scheduleChartRedraw(t);
      
      updateHud();
    } catch (_) {}
  }

  /**********************
   * RC CALCS
   **********************/
  function pushRcBuf(ts, rc) {
    const prev = rcBuf.length ? rcBuf[rcBuf.length - 1].rc : null;
    if (Number.isFinite(prev) && rc < prev) rcBuf = [];
    rcBuf.push({ ts, rc });

    const cutoff = ts - (60_000 + 15_000);
    while (rcBuf.length && rcBuf[0].ts < cutoff) rcBuf.shift();
  }

  function rcHrSinceRecursion(ts, rc) {
    if (recursionTime == null || startingRC == null || (Number.isFinite(lastRC) && rc < lastRC)) {
      recursionTime = ts;
      startingRC = rc;
    }
    const hours = (ts - recursionTime) / 3600_000;
    const delta = rc - startingRC;
    if (!(hours > 0) || !(delta > 0)) return 0;
    return delta / hours;
  }

  function rcHrPastMinute(ts) {
    const cutoff = ts - 60_000;
    while (rcBuf.length && rcBuf[0].ts < cutoff) rcBuf.shift();
    if (rcBuf.length < 2) return null;

    const oldest = rcBuf[0];
    const newest = rcBuf[rcBuf.length - 1];

    const dRC = newest.rc - oldest.rc;
    if (!(dRC > 0)) return 0;

    const hours = (newest.ts - oldest.ts) / 3600_000;
    if (!(hours > 0)) return null;

    return dRC / hours;
  }

  /**********************
   * THEME
   **********************/
  function applyFontPatch() {
    try {
      const bodyStyles = getComputedStyle(document.body);
      const fontColor = bodyStyles.color || '#ccc';
      const bodyFont = bodyStyles.fontFamily || 'sans-serif';

      Highcharts.setOptions({
        chart: {
          backgroundColor: 'rgb(17, 17, 17)',
          style: { color: fontColor, fontFamily: bodyFont }
        },
        title: { style: { color: fontColor } },
        subtitle: { style: { color: fontColor } },
        xAxis: { labels: { style: { color: fontColor } } },
        yAxis: { labels: { style: { color: fontColor } } },
        legend: { itemStyle: { color: fontColor } },
        tooltip: {
          backgroundColor: 'rgba(0,0,0,0.85)',
          style: { color: fontColor }
        }
      });
    } catch (_) {}
  }

  /**********************
   * RANGE BUTTONS + AUTO
   **********************/
  function setRange(mode) {
    const m = (mode || 'all').toLowerCase();
    rangeMode = RANGE_MS[m] !== undefined ? m : 'all';
    localStorage.setItem(RANGE_KEY, rangeMode);
    mountHudsInRangeBar();
    updateRangeButtonsUI();
    applyRangeExtremes(Date.now());
  }

  function toggleAuto() {
    autoFollow = !autoFollow;
    localStorage.setItem(AUTO_KEY, autoFollow ? '1' : '0');
    updateRangeButtonsUI();
    if (autoFollow) applyRangeExtremes(Date.now());
  }

  function applyRangeExtremes(latestX) {
    if (!myChart) return;
    const ms = RANGE_MS[rangeMode];
    const xAxis = myChart.xAxis?.[0];
    if (!xAxis) return;

    if (!ms) {
      try { xAxis.setExtremes(null, null, true, false); } catch (_) {}
      return;
    }

    if (!autoFollow) return;

    const max = Number.isFinite(latestX) ? latestX : Date.now();
    const min = max - ms;

    try {
      xAxis.setExtremes(min, max, true, { duration: RANGE_ANIM_MS });
    } catch (_) {}
  }

  function updateRangeButtonsUI() {
    const bar = document.getElementById(RANGE_BAR_ID);
    if (!bar) return;


    // Keep HUDs pinned inside the range bar
    mountHudsInRangeBar();
    const btns = bar.querySelectorAll('button[data-range]');
    btns.forEach(b => {
      const r = (b.getAttribute('data-range') || '').toLowerCase();
      if (r === rangeMode) b.classList.add('active');
      else b.classList.remove('active');
    });

    const autoBtn = bar.querySelector('button[data-auto="1"]');
    if (autoBtn) {
      if (autoFollow && rangeMode !== 'all') autoBtn.classList.add('active');
      else autoBtn.classList.remove('active');
      autoBtn.textContent = autoFollow ? 'AUTO: ON' : 'AUTO: OFF';
    }

    const chartBtn = bar.querySelector('button[data-chart="1"]');
    if (chartBtn) {
      const hidden = isChartHidden();
      if (hidden) chartBtn.classList.add('active');
      else chartBtn.classList.remove('active');
      chartBtn.textContent = hidden ? 'CHART: OFF' : 'CHART: ON';
    }
  }

  
  /**********************
   * HUD PINS (keep HUDs fixed above chart, on same line as CHART toggle)
   * - Left: live series HUD (RC/hr, RC/pastminute, DC/hr, Time to finish block)
   * - Right: Totals HUD (Total DC/RC + timers + Reset)
   * Mounted inside the range bar so it doesn't "float" when scrolling.
   **********************/
  function mountHudsInRangeBar() {
    const bar = document.getElementById(RANGE_BAR_ID);
    if (!bar) return;

    // Build a 3-column grid: [left HUD] [buttons] [right HUD]
    if (bar.getAttribute('data-hud-mounted') !== '1') {
      const kids = Array.from(bar.childNodes);

      const left = document.createElement('div');
      left.id = `${RANGE_BAR_ID}-leftslot`;
      left.style.display = 'flex';
      left.style.alignItems = 'center';
      left.style.justifyContent = 'flex-start';
      left.style.minWidth = '0';

      const center = document.createElement('div');
      center.id = `${RANGE_BAR_ID}-centerslot`;
      center.style.display = 'flex';
      center.style.alignItems = 'center';
      center.style.justifyContent = 'center';
      center.style.gap = '8px';
      center.style.flexWrap = 'wrap';

      const right = document.createElement('div');
      right.id = `${RANGE_BAR_ID}-rightslot`;
      right.style.display = 'flex';
      right.style.alignItems = 'center';
      right.style.justifyContent = 'flex-end';
      right.style.minWidth = '0';

      // grid layout on bar
      bar.style.display = 'grid';
      bar.style.gridTemplateColumns = '1fr auto 1fr';
      bar.style.alignItems = 'center';
      bar.style.columnGap = '10px';
      bar.style.rowGap = '8px';
      bar.style.justifyContent = ''; // not used in grid
      bar.style.minHeight = '34px';

      // move all existing children into center
      bar.innerHTML = '';
      for (const k of kids) center.appendChild(k);

      bar.appendChild(left);
      bar.appendChild(center);
      bar.appendChild(right);

      bar.setAttribute('data-hud-mounted', '1');
    }

    const leftSlot = document.getElementById(`${RANGE_BAR_ID}-leftslot`);
    const rightSlot = document.getElementById(`${RANGE_BAR_ID}-rightslot`);
    if (!leftSlot || !rightSlot) return;

    // Ensure elements exist
    const liveHud = ensureHud();
    const totalsHud = ensureTotalsHud();

    // Re-parent HUDs into slots
    if (liveHud && liveHud.parentElement !== leftSlot) leftSlot.appendChild(liveHud);
    if (totalsHud && totalsHud.parentElement !== rightSlot) rightSlot.appendChild(totalsHud);

    // Make them non-floating (override fixed positioning)
    if (liveHud) {
      liveHud.style.position = 'relative';
      liveHud.style.top = '';
      liveHud.style.left = '';
      liveHud.style.right = '';
      liveHud.style.transform = '';
      liveHud.style.margin = '0';
    }
    if (totalsHud) {
      totalsHud.style.position = 'relative';
      totalsHud.style.top = '';
      totalsHud.style.left = '';
      totalsHud.style.right = '';
      totalsHud.style.transform = '';
      totalsHud.style.margin = '0';
    }
  }

function ensureRangeBar() {
    const panel = document.getElementById(PANEL_ID);
    if (!panel) return;

    if (document.getElementById(RANGE_BAR_ID)) return;

    const bar = document.createElement('div');
    bar.id = RANGE_BAR_ID;
    bar.style.display = 'flex';
    bar.style.justifyContent = 'center';
    bar.style.gap = '8px';
    bar.style.margin = '8px 0 10px 0';
    bar.style.flexWrap = 'wrap';

    const css = document.createElement('style');
    css.textContent = `
      #${RANGE_BAR_ID} button{
        cursor:pointer;
        padding:6px 10px;
        border-radius:6px;
        border:1px solid rgba(255,255,255,0.25);
        background: rgba(0,0,0,0.25);
        color: inherit;
        font: inherit;
        user-select:none;
      }
      #${RANGE_BAR_ID} button.active{
        border-color: rgba(255,255,255,0.8);
        background: rgba(255,255,255,0.12);
      }
      #${RANGE_BAR_ID} button[data-auto="1"]{
        border-style: dashed;
      }

      #${RANGE_BAR_ID} button[data-chart="1"]{
        border-style: dashed;
      }
    

/* Friendlier action buttons (Export / Reset) */
#deepco-action-buttons{
  display:flex;
  justify-content:center;
  gap:10px;
  flex-wrap:wrap;
  margin: 12px 0 0 0;
}
#deepco-action-buttons .deepco-action-btn{
  cursor:pointer;
  padding:8px 14px;
  border-radius:10px;
  border:1px solid rgba(255,255,255,0.25);
  background: rgba(255,255,255,0.06);
  color: inherit;
  font: inherit;
  font-weight: 600;
  letter-spacing: .2px;
  user-select:none;
  transition: transform .06s ease, background .12s ease, border-color .12s ease;
}
#deepco-action-buttons .deepco-action-btn:hover{
  background: rgba(255,255,255,0.10);
  border-color: rgba(255,255,255,0.40);
}
#deepco-action-buttons .deepco-action-btn:active{
  transform: translateY(1px);
  background: rgba(255,255,255,0.14);
}
    `;
    document.head.appendChild(css);

    const defs = [
      ['1m', '1M'],
      ['5m', '5M'],
      ['10m', '10M'],
      ['30m', '30M'],
      ['1h', '1HR'],
      ['all', 'All'],
    ];

    for (const [k, label] of defs) {
      const b = document.createElement('button');
      b.type = 'button';
      b.textContent = label;
      b.setAttribute('data-range', k);
      b.addEventListener('click', () => {
      // Period buttons force AUTO follow for the chart
      autoFollow = true;
      localStorage.setItem(AUTO_KEY, '1');
      setRange(k);
    });
      bar.appendChild(b);
    }

    const autoBtn = document.createElement('button');
    autoBtn.type = 'button';
    autoBtn.setAttribute('data-auto', '1');
    autoBtn.addEventListener('click', toggleAuto);
    bar.appendChild(autoBtn);


    // Chart toggle button (same style as range buttons)
    const chartBtn = document.createElement('button');
    chartBtn.type = 'button';
    chartBtn.id = 'deepco-toggle-chart-btn';
    chartBtn.setAttribute('data-chart', '1');
    chartBtn.addEventListener('click', () => {
      setChartHidden(!isChartHidden());
    });
    bar.appendChild(chartBtn);

    const chartDiv = document.getElementById(CHART_ID);
    if (chartDiv && chartDiv.parentElement === panel) {
      panel.insertBefore(bar, chartDiv);
    } else {
      panel.appendChild(bar);
    }

    updateRangeButtonsUI();
  }

  /**********************
   * PANEL / EXPORT / RESET
   **********************/
  async function initPanelIfNeeded() {
    const ready = await waitFor(() => document.getElementById('main-panel'), 60000);
    if (!ready) return;

    const grid = document.getElementById('main-panel');
    if (!grid) return;

    if (!document.getElementById(PANEL_ID)) {
      const panelContainer = document.createElement('div');
      panelContainer.id = PANEL_ID;
      panelContainer.className = 'grid-wrapper';

      const chartContainer = document.createElement('div');
      chartContainer.id = CHART_ID;
      chartContainer.style.minHeight = '320px';
      const btnContainer = document.createElement('div');
      btnContainer.id = 'deepco-action-buttons';
      btnContainer.style.display = 'flex';
      btnContainer.style.justifyContent = 'center';
      btnContainer.style.gap = '10px';

      const exportBtn = document.createElement('button');
      exportBtn.className = 'deepco-action-btn';
      exportBtn.textContent = 'Export Player Stats';
      exportBtn.addEventListener('click', exportStats);
      btnContainer.appendChild(exportBtn);

      const resetBlockBtn = document.createElement('button');
      resetBlockBtn.className = 'deepco-action-btn';
      resetBlockBtn.textContent = 'Reset Block Time';
      resetBlockBtn.addEventListener('click', resetBlockTimeStats);
      btnContainer.appendChild(resetBlockBtn);

      const resetBtn = document.createElement('button');
      resetBtn.className = 'deepco-action-btn';
      resetBtn.textContent = 'Reset Stats';
      resetBtn.addEventListener('click', resetStats);
      btnContainer.appendChild(resetBtn);
      panelContainer.appendChild(chartContainer);
      panelContainer.appendChild(btnContainer);
      grid.appendChild(panelContainer);
    }

    ensureRangeBar();
    applyChartHiddenState();
  }

  
  function isChartHidden() {
    try { return localStorage.getItem(CHART_HIDE_KEY) === '1'; } catch (e) { return false; }
  }

  function setChartHidden(hidden) {
    try { localStorage.setItem(CHART_HIDE_KEY, hidden ? '1' : '0'); } catch (e) {}
    applyChartHiddenState();
  }

  function applyChartHiddenState() {
    const chartEl = document.getElementById(CHART_ID);
    const hidden = isChartHidden();

    if (chartEl) {
      // Hide ONLY the Highcharts drawing area so the HUD-urile (fixed) remain visible.
      // We also collapse the chart container height to avoid leaving empty space.
      const hc = chartEl.querySelector('.highcharts-container');
      if (hidden) {
        if (!chartEl.dataset.deepcoOrigMinH) chartEl.dataset.deepcoOrigMinH = chartEl.style.minHeight || '';
        if (!chartEl.dataset.deepcoOrigH) chartEl.dataset.deepcoOrigH = chartEl.style.height || '';
        if (hc) hc.style.display = 'none';
        chartEl.style.overflow = 'hidden';
        chartEl.style.minHeight = '0px';
        chartEl.style.height = '0px';
        chartEl.style.padding = '0px';
        chartEl.style.margin = '0px';
      } else {
        if (hc) hc.style.display = '';
        chartEl.style.overflow = '';
        chartEl.style.padding = '';
        chartEl.style.margin = '';
        chartEl.style.minHeight = chartEl.dataset.deepcoOrigMinH || '320px';
        chartEl.style.height = chartEl.dataset.deepcoOrigH || '';
      }
    }

    // Update button UI (in range bar)
    updateRangeButtonsUI();

    // If we just showed the chart, Highcharts needs a reflow to size correctly.
    if (!hidden && window.myChart && typeof window.myChart.reflow === 'function') {
      setTimeout(() => { try { window.myChart.reflow(); } catch (e) {} }, 50);
    }
  }



  async function exportStats() {
    const rows = await GM.getValue(DB_KEY, SCHEMA);
    const csv = rows.map(r => r.map(v => (v == null ? '' : String(v))).join(',')).join('\n');
    const blob = new Blob([csv], { type: 'text/csv' });
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = 'deepco_player_stats.csv';
    a.click();
    URL.revokeObjectURL(a.href);
  }

  async function resetBlockTimeStats() {
    if (!confirm('Reset only the "Time to finish block" statistic?')) return;

    dcToastTimes = [];
    lastBlockEventTs = 0;
    lastBlockAvgSec = null;

    if (myChart) {
      try {
        if (myChart.series && myChart.series[3]) myChart.series[3].setData([], false);
        myChart.redraw();
        if (rangeMode !== 'all') applyRangeExtremes(Date.now());
        updateHud();
      } catch (_) {}
    }
  }

  async function resetStats() {
    if (!confirm('Are you sure you want to clear player stats?')) return;

    db = SCHEMA;
    recursionTime = null;
    startingRC = null;
    lastRC = null;
    rcBuf = [];

    dcAcc = 0;
    dcResetTs = 0;
    dcToastTimes = [];
    lastBlockEventTs = 0;
    lastBlockAvgSec = null;
    await GM.setValue(DC_ACC_KEY, dcAcc);
    await GM.setValue(DC_RESET_TS_KEY, dcResetTs);

    await GM.setValue(DB_KEY, db);

    if (myChart) {
      try {
        myChart.series.forEach(s => s.setData([], false));
        myChart.redraw();
        applyRangeExtremes(Date.now());
        updateHud();
      } catch (_) {}
    }

    alert('Tile logs have been cleared.');
  }

  /**********************
   * BUILD INITIAL SERIES
   **********************/
  function buildInitialSeries() {
    const s0 = [];
    const s1 = [];
    const s2 = [];
    const s3 = [];

    recursionTime = null;
    startingRC = null;
    lastRC = null;
    rcBuf = [];

    // rebuild toast times from stored DC snapshots (DC increases == toast event)
    dcToastTimes = [];
    lastBlockAvgSec = null;
    let prevDc = 0;

    const fallbackBase = (db[1]?.[0] || Date.now());

    for (let i = 1; i < db.length; i++) {
      const ts = db[i][0];
      const rc = db[i][2];
      const dcSnap = db[i][4];

      if (!Number.isFinite(ts) || !Number.isFinite(rc)) continue;

      pushRcBuf(ts, rc);

      const v0 = rcHrSinceRecursion(ts, rc);
      const v1 = rcHrPastMinute(ts);

      const dcVal = Number.isFinite(dcSnap) ? dcSnap : 0;
      const v2 = dcRateWithWarmup(ts, dcVal, fallbackBase);

      // infer DC toast when DC snapshot increases
      if (dcVal > prevDc) {
        recordDcToast(ts);
        prevDc = dcVal;
      } else if (dcVal < prevDc) {
        // reset detected in history
        dcToastTimes = [];
        prevDc = dcVal;
      }

      const v3 = avgBlockTimeSecondsLastMinute(ts);

      s0.push([ts, v0]);
      if (v1 != null) s1.push([ts, v1]);
      s2.push([ts, v2]);
      s3.push([ts, v3]);

      lastRC = rc;
    }

    return [s0, s1, s2, s3];
  }

  /**********************
   * SERIES VISIBILITY persistence
   **********************/
  function restoreSeriesVisibility(chart) {
    try {
      const raw = localStorage.getItem(VIS_KEY);
      if (!raw) return;
      const vis = JSON.parse(raw);
      if (!Array.isArray(vis)) return;
      chart.series.forEach((s, i) => { if (vis[i] === false) s.hide(); });
    } catch (_) {}
  }

  function attachLegendPersistence(chart) {
    chart.update({
      plotOptions: {
        series: {
          events: {
            legendItemClick: function () {
              const visibility = this.chart.series.map(s => s.visible);
              localStorage.setItem(VIS_KEY, JSON.stringify(visibility));
              setTimeout(updateHud, 0);
            }
          }
        }
      }
    }, false);
  }

  /**********************
   * CHART INIT (title left, HUD centered, top spacing reserved)
   **********************/
  async function initChartOnce() {
    if (!isDigPage()) return;

    const panelReady = await waitFor(() => document.getElementById('main-panel'), 60000);
    if (!panelReady) return;

    const container = document.getElementById(CHART_ID);
    if (!container) return;

    const hcReady = await waitFor(() => window.Highcharts && typeof Highcharts.chart === 'function', 30000);
    if (!hcReady) return;

    applyFontPatch();

    if (myChart) return;

    const [s0, s1, s2, s3] = buildInitialSeries();

    myChart = Highcharts.chart(CHART_ID, {

      boost: { enabled: true, useGPUTranslations: true, usePreAllocated: true },

      chart: {
        spacingTop: CHART_TOP_SPACING,
        zooming: { type: 'x', mouseWheel: { enabled: false, type: 'x' } }
      },

      // ✅ moved from center to left
      title: { text: 'RC/hr & DC/hr', align: 'left' },
      subtitle: {
        align: 'left',
        text: document.ontouchstart === undefined
          ? 'Click and drag in the plot area to zoom in'
          : 'Pinch the chart to zoom in'
      },

      xAxis: {
        type: 'datetime',
        events: {
          afterSetExtremes: function (e) {
            if (e && e.trigger) {
              autoFollow = false;
              localStorage.setItem(AUTO_KEY, '0');
              updateRangeButtonsUI();
            }
          }
        }
      },
      yAxis: [{ title: { text: 'per hour' } }, { title: { text: 'seconds' }, opposite: true }],
      legend: {
        enabled: true,
        layout: 'horizontal',
        align: 'center',
        verticalAlign: 'bottom'
      },
      plotOptions: {
        series: {
          animation: false,
          marker: { enabled: false },
          states: { hover: { enabled: false } },
          turboThreshold: 50000,
          boostThreshold: 1000
        }
      },
      series: [{
        name: 'RC/hr (Since Recursion)',
        data: s0,
        color: '#4aa3ff'
      }, {
        name: 'RC/hr (Past Minute)',
        data: s1,
        color: 'red'
      }, {
        name: 'DC/hr (Since Reset)',
        data: s2,
        color: '#f4c430'
      }, {
        name: 'Time to finish block (avg 1m)',
        data: s3,
        color: 'green',
        yAxis: 1
      }],
      tooltip: {
        shared: true,
        animation: false,
        hideDelay: 0,
        useHTML: true,
        outside: false,
        formatter: function () {
          const dt = new Date(this.x);
          let out = `${dt.toLocaleDateString()} ${dt.toLocaleTimeString()}`;
          const pts = this.points || [];
          for (const p of pts) {
            if (!p || !p.series) continue;
            if (p.series && String(p.series.name).startsWith('Time to finish block')) {
              out += `<br/>${p.series.name}: ${Number(p.y).toFixed(1)}s`;
            } else {
              out += `<br/>${p.series.name}: ${Number(p.y).toFixed(2)}`;
            }
          }
          return out;
        }
      },
      credits: { enabled: false }
    });
    window.myChart = myChart;
    applyChartHiddenState();

    restoreSeriesVisibility(myChart);
    attachLegendPersistence(myChart);

    ensureRangeBar();
    updateRangeButtonsUI();

    ensureHud();
    updateHud();

    ensureTotalsHud();
    updateTotalsHud();

    if (rangeMode !== 'all') applyRangeExtremes(Date.now());

    requestAnimationFrame(() => { try { myChart.reflow(); } catch (_) {} });
    setTimeout(() => { try { myChart.reflow(); } catch (_) {} }, 200);
    window.addEventListener('resize', () => { try { myChart.reflow(); } catch (_) {} });
  }

  /**********************
   * SNAPSHOT + UPDATE (poll)
   **********************/
  async function logSnapshotAndUpdate() {
    const ts = Date.now();

    const tileCount = safeNumber(getTileCount());
    const rc = safeNumber(parseFirstNumber(getRCCount()));
    // Totals HUD: sum RC increases since reset
    updateTotalRcFromSnapshot(rc).catch(() => {});
    const level = safeNumber(getLevel());
    const rating = safeText(getProcessingRating());

    const dcSinceReset = safeNumber(dcAcc);
    const row = [ts, tileCount, rc, level, dcSinceReset, 0, rating];
    db.push(row);
    await GM.setValue(DB_KEY, db);

    if (!myChart) return;

    pushRcBuf(ts, rc);

    const v0 = rcHrSinceRecursion(ts, rc);
    const v1 = rcHrPastMinute(ts);

    const fallbackBase = (db[1]?.[0] || ts);
    const v2 = dcRateWithWarmup(ts, dcSinceReset, fallbackBase);
    const v3Raw = avgBlockTimeSecondsLastMinute(ts);
    const v3 = (v3Raw != null) ? v3Raw : (lastBlockAvgSec != null ? lastBlockAvgSec : null);

    myChart.series[0].addPoint([ts, v0], false, false);
    if (v1 != null) myChart.series[1].addPoint([ts, v1], false, false);
    myChart.series[2].addPoint([ts, v2], false, false);
    // Keep the line stable: if we have no new sample, carry forward last known value.
    if (v3 != null) myChart.series[3].addPoint([ts, v3], false, false);

    clampSeries(myChart.series[0]);
    clampSeries(myChart.series[1]);
    clampSeries(myChart.series[2]);
    clampSeries(myChart.series[3]);

    // OPTIMIZATION: Do not force redraw here. Schedule it.
    scheduleChartRedraw(ts);

    lastRC = rc;
    updateHud();
  }

  async function pollLoop() {
    await logSnapshotAndUpdate();
    setInterval(async () => {
      try { await logSnapshotAndUpdate(); } catch (_) {}
    }, POLL_MS);
  }

  /**********************
   * SPA NAV FIX
   **********************/
  let lastPath = location.pathname;

  async function onRouteMaybeChanged() {
    if (location.pathname === lastPath) return;
    lastPath = location.pathname;

    if (!isDigPage()) {
      const panel = document.getElementById(PANEL_ID);
      if (panel) panel.remove();
      const hud = document.getElementById(HUD_ID);
      if (hud) hud.remove();
      const totals = document.getElementById(TOTALS_HUD_ID);
      if (totals) totals.remove();
      myChart = null;
      return;
    }

    await initPanelIfNeeded();
    await initChartOnce();
  }

  function hookHistory(fnName) {
    const original = history[fnName];
    history[fnName] = function () {
      const ret = original.apply(this, arguments);
      Promise.resolve().then(() => { ensureToastObserverAttached(); return onRouteMaybeChanged(); });
      return ret;
    };
  }

  function startSpaWatcher() {
    hookHistory('pushState');
    hookHistory('replaceState');
    window.addEventListener('popstate', () => { ensureToastObserverAttached(); onRouteMaybeChanged().catch(() => {}); });
    setInterval(() => {
      ensureToastObserverAttached();
      onRouteMaybeChanged().catch(() => {});
    }, 500);
  }

  /**********************
   * BOOT
   **********************/
  await waitFor(() => document.body, 30000);

  ensureToastObserverAttached();
  startSpaWatcher();

  await initPanelIfNeeded();
  if (isDigPage()) await initChartOnce();

  // Persist Totals HUD timer baseline if missing, and keep "Time since reset" live.
  if (!window.__deepco_totals_timer_inited_r7m__) {
    window.__deepco_totals_timer_inited_r7m__ = true;
    try {
      // ensure the baseline exists in storage (so refresh keeps the same elapsed time)
      if (!Number.isFinite(Number(await GM.getValue(TOTAL_RESET_TS_KEY, 0))) || Number(await GM.getValue(TOTAL_RESET_TS_KEY, 0)) <= 0) {
        await GM.setValue(TOTAL_RESET_TS_KEY, totalsResetTs);
      }
    } catch (_) {}

    setInterval(() => {
      try {
        if (!isDigPage()) return;
        if (document.getElementById(TOTALS_HUD_ID)) updateTotalsHud();
      } catch (_) {}
    }, 1000);
  }

  if (!window.__deepco_stats_poll_started_r7g__) {
    window.__deepco_stats_poll_started_r7g__ = true;
    await pollLoop();
  }
})();


/* === AVG POWER HUD (avg power/m) + RESET + CSV EXPORT ===
   Cerință:
   - HUD: doar "avg power/m" (cu reset + export)
   - Calcul:
       1) aduni toate cifrele negative care apar pe ecran într-o perioadă de 1 minut
       2) împarți la 60 (secunde) => medie pe secundă
       3) împarți la (atacuri/sec) derivat din "CS:" (în secunde). Exemplu CS: 0.5s => 2 atacuri/sec => împarți la 2.
          Echivalent: înmulțești cu CS (secunde/atac).
     CS se poate schimba; aplicăm ajustarea pe fiecare secundă și facem media pe ultimele 60 secunde.
*/
(function () {
  'use strict';

  const ROOT_ID   = 'deepco-avg-power-root';
  const SEC_ID    = 'deepco-avg-power-sec';
  const MIN_ID    = 'deepco-avg-power-min';
  const RESET_ID  = 'deepco-avg-power-reset';
  const EXPORT_ID = 'deepco-avg-power-export';

  // ---- capture negatives (fast floaters) ----
  let secSum = 0; // sum of negative numbers observed within current 1s bucket

  // Matches: "-97", "- 97", "-1,234.56", "-1234,56"
  // OPTIMIZATION: Created once, reused.
  const NEG_RE = /-\s*\d{1,3}(?:[.,]\d{3})*(?:[.,]\d+)?|-\s*\d+(?:[.,]\d+)?/g;

  function parseNegatives(text) {
    if (!text) return [];
    // OPTIMIZATION: Critical check. If no minus sign, don't run Regex. 
    // This avoids checking every timer, resource counter, etc.
    if (text.indexOf('-') === -1) return [];

    const m = String(text).match(NEG_RE);
    if (!m) return [];
    const out = [];
    for (let s of m) {
      s = s.replace(/\s+/g, '');
      const hasComma = s.includes(',');
      const hasDot = s.includes('.');
      if (hasComma && hasDot) {
        // comma thousands, dot decimals
        s = s.replace(/,/g, '');
      } else if (hasComma && !hasDot) {
        // comma decimals
        s = s.replace(',', '.');
      }
      const v = Number(s);
      if (Number.isFinite(v) && v < 0) out.push(v);
    }
    return out;
  }

  function startObserver() {
    const mo = new MutationObserver((mutations) => {
      // OPTIMIZATION: If too many mutations pile up (lag), process fewer.
      if (mutations.length > 500) return; 

      for (const mut of mutations) {
        if (mut.type === 'characterData') {
          const vals = parseNegatives(mut.target && mut.target.data);
          for (const v of vals) secSum += v;
          continue;
        }
        if (mut.addedNodes && mut.addedNodes.length) {
          for (const n of mut.addedNodes) {
            if (!n) continue;
            // Check node type to avoid property access on irrelevant nodes
            if (n.nodeType === 3) { // Text node
              const vals = parseNegatives(n.nodeValue);
              for (const v of vals) secSum += v;
            } else if (n.nodeType === 1) { // Element
              const vals = parseNegatives(n.textContent);
              for (const v of vals) secSum += v;
            }
          }
        }
      }
    });

    // OPTIMIZATION: Target specific container if known, otherwise Body. 
    // Removed documentElement (don't need to watch <head>)
    mo.observe(document.body || document.documentElement, {
      subtree: true,
      childList: true,
      characterData: true
    });
  }

  // ---- CS (attack period in seconds) ----
  let lastCsSeconds = 1.0;

  function readCsSeconds() {
    // Robust scan for: "CS: 0.5s" / "CS : 0.5 s" / "CS 0.5s"
    // We intentionally keep it tolerant to UI changes.
    const re = /\bCS\s*:\s*([0-9]+(?:\.[0-9]+)?)\s*s\b/i;

    try {
      // OPTIMIZATION: Limit tree walker depth or assume CS is visible
      const walker = document.createTreeWalker(
        document.body || document.documentElement,
        NodeFilter.SHOW_TEXT,
        {
          acceptNode(node) {
            const t = node && node.nodeValue;
            if (!t) return NodeFilter.FILTER_REJECT;
            // Simple string check before Regex
            return t.includes('CS') ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
          }
        }
      );

      let node;
      let count = 0;
      while ((node = walker.nextNode())) {
        if (count++ > 50) break; // Don't scan forever if not found
        const t = node.nodeValue;
        const m = t.match(re);
        if (m) {
          const v = Number(m[1]);
          if (Number.isFinite(v) && v > 0) {
            lastCsSeconds = v;
            return lastCsSeconds;
          }
        }
      }
    } catch (_) {}

    return lastCsSeconds;
  }

  // ---- rolling 60s window (per-second, CS-adjusted) ----
  const WIN = 60;
  const adjRing = new Array(WIN).fill(0); // each entry: secSum * csSeconds (negative)
  let ringIdx = 0;
  let adjSumTot = 0;

  // session log (one row per second)
  const sessionRows = [];

  function resetAll() {
    secSum = 0;
    lastCsSeconds = 1.0;

    adjSumTot = 0;
    for (let i = 0; i < WIN; i++) adjRing[i] = 0;
    ringIdx = 0;

    sessionRows.length = 0;

    const secEl = document.getElementById(SEC_ID);
    const minEl = document.getElementById(MIN_ID);
    if (minEl) minEl.textContent = '0.00';
    if (secEl) secEl.textContent = '0.00';
  }

  function exportSessionCsv() {
    // Includes CS to help validate calculations when CS changes.
    const header = ['ts_iso','cs_s','sum_neg_1s','avg_power_s','adj_sum_1s','avg_power_m'];
    const lines = [header.join(',')];

    for (const r of sessionRows) {
      lines.push([
        r.ts,
        String(r.cs),
        String(r.sum1s),
        String(r.avgS),
        String(r.adj1s),
        String(r.avgM)
      ].join(','));
    }

    const csv = lines.join('\n');
    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
    const url = URL.createObjectURL(blob);

    const a = document.createElement('a');
    const stamp = new Date().toISOString().replace(/[:.]/g, '-');
    a.href = url;
    a.download = `avg-power-m-session-${stamp}.csv`;
    document.body.appendChild(a);
    a.click();
    a.remove();

    setTimeout(() => URL.revokeObjectURL(url), 1000);
  }

  // ---- HUD creation + placement ----
  function makeHud() {
    let root = document.getElementById(ROOT_ID);
    if (root) return root;

    root = document.createElement('div');
    root.id = ROOT_ID;

    // Root: row with left stacked metrics + right buttons
    root.style.display = 'inline-flex';
    root.style.flexDirection = 'row';
    root.style.alignItems = 'center';
    root.style.gap = '10px';
    root.style.padding = '4px 8px';
    root.style.borderRadius = '8px';
    root.style.background = 'rgba(0,0,0,0.35)';
    root.style.border = '1px solid rgba(255,255,255,0.16)';
    root.style.fontSize = '12px';
    root.style.whiteSpace = 'nowrap';
    root.style.color = '#ffb3b3';
    root.style.pointerEvents = 'auto';

    const left = document.createElement('div');
    left.style.display = 'flex';
    left.style.flexDirection = 'column';
    left.style.gap = '2px';
    left.style.alignItems = 'flex-start';

    const row = (labelText, valueId) => {
      const r = document.createElement('div');
      r.style.display = 'flex';
      r.style.alignItems = 'center';
      r.style.gap = '6px';

      const lab = document.createElement('span');
      lab.textContent = labelText;

      const val = document.createElement('span');
      val.id = valueId;
      val.textContent = '0.00';
      val.style.fontWeight = '700';

      r.appendChild(lab);
      r.appendChild(val);
      return r;
    };

    left.appendChild(row('avg power/s:', SEC_ID));
    left.appendChild(row('avg power/m:', MIN_ID));

    const right = document.createElement('div');
    right.style.display = 'flex';
    right.style.flexDirection = 'row';
    right.style.gap = '6px';
    right.style.alignItems = 'center';

    const btn = (id, text) => {
      const b = document.createElement('button');
      b.id = id;
      b.type = 'button';
      b.textContent = text;
      b.style.fontSize = '11px';
      b.style.padding = '2px 6px';
      b.style.borderRadius = '6px';
      b.style.border = '1px solid rgba(255,255,255,0.25)';
      b.style.background = 'rgba(255,255,255,0.08)';
      b.style.color = 'inherit';
      b.style.cursor = 'pointer';
      return b;
    };

    const resetBtn = btn(RESET_ID, 'reset');
    resetBtn.addEventListener('click', resetAll);

    const exportBtn = btn(EXPORT_ID, 'export CSV');
    exportBtn.addEventListener('click', exportSessionCsv);

    right.appendChild(resetBtn);
    right.appendChild(exportBtn);

    root.appendChild(left);
    root.appendChild(right);

    return root;
  }

  function attachHud() {
    const root = makeHud();

    const totals = document.getElementById('deepco-totals-hud');
    if (totals && totals.parentElement) {
      // Insert immediately before TOTAL DC/RC HUD
      if (root.parentElement !== totals.parentElement || root.nextSibling !== totals) {
        totals.parentElement.insertBefore(root, totals);
      }
      // ensure normal flow (inline, same row)
      root.style.position = '';
      root.style.right = '';
      root.style.top = '';
      root.style.zIndex = '';
      return true;
    }

    // Fallback: keep it visible in top-right
    if (!root.parentElement) document.body.appendChild(root);
    root.style.position = 'fixed';
    root.style.right = '12px';
    root.style.top = '110px';
    root.style.zIndex = '2147483647';
    return false;
  }

  function tick1s() {
    const cs = readCsSeconds();           // seconds/attack
    const adj1s = secSum * cs;            // adjust for attacks/sec (divide by aps)
    // maintain ring
    adjSumTot -= adjRing[ringIdx];
    adjRing[ringIdx] = adj1s;
    adjSumTot += adjRing[ringIdx];
    ringIdx = (ringIdx + 1) % WIN;

    const avgM = adjSumTot / WIN;         // already "sum over 1 min / 60", with per-second CS adjustment

    // update HUD
    const secEl = document.getElementById(SEC_ID);
    const minEl = document.getElementById(MIN_ID);
    if (secEl) secEl.textContent = adj1s.toFixed(2);
    if (minEl) minEl.textContent = avgM.toFixed(2);

    // session row (one per second)
    sessionRows.push({
      ts: new Date().toISOString(),
      cs: Number(cs.toFixed(4)),
      sum1s: Number(secSum.toFixed(4)),
      avgS: Number(adj1s.toFixed(4)),
      adj1s: Number(adj1s.toFixed(4)),
      avgM: Number(avgM.toFixed(4))
    });

    // reset bucket for next second
    secSum = 0;
  }

  function boot() {
    startObserver();

    // Re-attach frequently (UI can rerender)
    let tries = 0;
    const attachTimer = setInterval(() => {
      attachHud();
      tries++;
      if (document.getElementById(ROOT_ID)?.parentElement && document.getElementById('deepco-totals-hud')) {
        if (tries > 10) {
          clearInterval(attachTimer);
          setInterval(attachHud, 2000);
        }
      }
      if (tries > 120) {
        clearInterval(attachTimer);
        setInterval(attachHud, 2000);
      }
    }, 500);

    setInterval(tick1s, 1000);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', boot, { once: true });
  } else {
    boot();
  }
})();