DeepCo Statistics + Lag Fix & Optimization

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

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey to install this script.

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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();
  }
})();