Cannabis Drying Interactive Dashboard — test.1

HTML

<h1>Cannabis Drying — Scientifically Tuned Reference Dashboard (test.1)</h1>
  <p class="lead">Revised thresholds based on known cannabis drying data — only truly optimal conditions marked as optimal.</p>

  <div>
    <label>Manual Entry — check any condition:</label><br>
    Temp (°F): <input id="tIn" type="number" value="60" min="30" max="70"> RH (%): <input id="rhIn" type="number" value="55" min="30" max="70">
    <button onclick="calcOne()">Calculate</button>
    <div id="calcResult">Enter temperature and humidity, then click Calculate to view drying conditions.</div>
  </div>

  <div class="legend">
    <span class="badge g">Optimal</span>
    <span class="badge y">Sub-optimal / Fast Drying</span>
    <span class="badge r">High Risk / Mold</span>
  </div>

  <div class="grid" id="heatWrap"></div>
  <div class="data" id="dataWrap"></div>

CSS

body{font-family:Inter,system-ui,Arial,sans-serif;background:#fbfbfb;color:#111;margin:0;padding:24px}
    h1{font-size:22px;margin:0}
    p.lead{margin:4px 0 20px;color:#6b7280}
    .grid, .data{overflow:auto;background:#fff;border-radius:10px;box-shadow:0 2px 8px rgba(2,6,23,0.06);padding:14px;margin-top:20px}
    table{border-collapse:collapse;width:100%;font-size:12px}
    th,td{border:1px solid #eee;padding:6px;text-align:center}
    th{background:#f3f4f6;position:sticky;top:0}
    .legend{margin:12px 0;display:flex;gap:8px;align-items:center}
    .badge{padding:4px 8px;border-radius:6px;font-size:12px;font-weight:600}
    .g{background:#22c55e;color:white}.y{background:#fde047;color:black}.r{background:#ef4444;color:white}
    .cond-opt{background:#22c55e;color:white;border-radius:4px;padding:2px 4px;margin-right:4px;}
    .cond-fast{background:#fde047;color:black;border-radius:4px;padding:2px 4px;margin-right:4px;}
    .cond-sub{background:#fde047;color:black;border-radius:4px;padding:2px 4px;}
    .mold-low{background:#22c55e;color:white;border-radius:4px;padding:2px 4px;}
    .mold-mod{background:#fde047;color:black;border-radius:4px;padding:2px 4px;}
    .mold-high{background:#ef4444;color:white;border-radius:4px;padding:2px 4px;}
    input[type=number]{width:90px;padding:6px;margin:4px;border:1px solid #ddd;border-radius:6px}
    button{background:#2f855a;color:white;border:0;border-radius:6px;padding:6px 10px;cursor:pointer}
    #calcResult{margin-top:12px;padding:10px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,0.1);font-size:13px}

JavaScript

function es_kpa(T_c){return 0.6108*Math.exp((17.27*T_c)/(T_c+237.3));}
    function dewpoint_from_ea(ea_kpa){if(ea_kpa<=0)return NaN;let ln=Math.log(ea_kpa/0.6108);return(237.3*ln)/(17.27-ln);}

    function dryingDays(vpd){
      if(vpd < 0.35) return 16;
      if(vpd < 0.5) return 13;
      if(vpd < 0.9) return 10;
      if(vpd < 1.3) return 8;
      if(vpd < 1.6) return 6;
      return 4;
    }

    function moldDays(RH,vpd){
      if(RH >= 90 || vpd < 0.20) return 1;
      if(RH >= 85 || vpd < 0.25) return 2;
      if(RH >= 80 || vpd < 0.30) return 3;
      if(RH >= 75 || vpd < 0.35) return 5;
      if(RH >= 70 || vpd < 0.40) return 7;
      return '—';
    }

    function terpPreservation(vpd){
      if(vpd < 0.4) return 88;
      if(vpd < 0.9) return 96;
      if(vpd < 1.4) return 93;
      return 75;
    }

    function cannabinoidPreservation(vpd){
      if(vpd < 0.4) return 90;
      if(vpd < 1.2) return 95;
      return 88;
    }

    function conditionReason(vpd,RH){
      if(vpd < 0.4) return 'Too humid / low VPD — drying too slow, risk of mold.';
      if(vpd >= 0.4 && vpd <= 1.2 && RH >= 45 && RH <= 60) return 'Balanced VPD and RH — steady, even drying.';
      if(vpd > 1.2 && vpd <= 1.6) return 'Upper optimal range — faster drying, watch internal moisture balance.';
      if(vpd > 1.6) return 'Too dry / high VPD — drying too fast, uneven drying and terpene loss risk.';
      if(RH > 65) return 'High RH — moisture equalization slowed, potential mold.';
      if(RH < 40) return 'Low RH — outer layers dry too quickly, uneven drying.';
      return 'Moderate drying — acceptable but not ideal.';
    }

    function computeMetrics(T_f,RH){
      const T_c=(T_f-32)*5/9;
      const es=es_kpa(T_c);
      const ea=es*(RH/100);
      const vpd=Math.max(es-ea,0);
      const Td_c=dewpoint_from_ea(ea);
      const Td_f=(Td_c*9/5)+32;
      const emc=Math.max(2,(0.12*RH)+(0.02*(T_f-40))+4).toFixed(1);

     ...