<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>XOY calibration tool powered by DroidCamX</title>
  <style type="text/css">
	body { background:#222222; color: darkgray;}
	.btn { height: 3em; min-width: 3em; border-radius:5px; color: darkgray; background-color: #222222; border: 1px solid darkgray; padding: 0.2rem 0.5rem ; margin: 0.5rem; } 
	.btn:hover { border-color: white; }
  #feedimg { overflow:hidden;float:left;position:relative }
  #feedimg img {
    transform-origin: top left; /* IE 10+, Firefox, etc. */
    -webkit-transform-origin: top left; /* Chrome */
    -ms-transform-origin: top left; /* IE 9 */
    position: relative;
    z-index: -1;
    top: 2px;
    left: 2px;
  }
  .invisible { visibility: hidden; }
  #x-container {
    display: flex;
    justify-content: center;
    align-items: center;
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    border: 2px solid red;
  }
  th, td { padding: 0.2rem; width: 25%; text-align: center;}
  table input[type=number] { text-align: right; font-size: large; }
  </style>
</head>
<body>
  <div id="buttons" style="display: flex; justify-content: center; flex-direction: row;">
    <button type="button" class="btn" onclick="feed.setDroid()" title="Set DroidCamX address">
      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M5 4q0-.825.588-1.413T7 2h10q.825 0 1.413.588T19 4v12q0 .425-.288.713T18 17h-1V4H7v12.925L6.925 17H6q-.425 0-.713-.288T5 16V4Zm7 6q.825 0 1.413-.588T14 8q0-.825-.588-1.413T12 6q-.825 0-1.413.588T10 8q0 .825.588 1.413T12 10ZM9.55 20H6q-.425 0-.713-.288T5 19q0-.425.288-.713T6 18h3.55l-.4-.4q-.275-.275-.275-.7t.275-.7q.275-.275.7-.275t.7.275l2.1 2.1q.3.3.3.7t-.3.7l-2.1 2.1q-.275.275-.7.275t-.7-.275q-.275-.275-.275-.7t.275-.7l.4-.4ZM15 20q-.425 0-.713-.288T14 19q0-.425.288-.713T15 18h3q.425 0 .713.288T19 19q0 .425-.288.713T18 20h-3ZM12 8Z"/></svg>
    </button>
    <button type="button" class="btn" onclick="feed.reload()" title="Reload">
      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 13.5A7.5 7.5 0 1 1 11.5 6H20m0 0l-3-3m3 3l-3 3"/></svg>
    </button>
    <button type="button" id="get-droid" onclick="window.open('https://www.dev47apps.com/', '_blank')" class="btn">Get <b>DroidCam</b>
      <svg version="1.2" width="24" height="24" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" style="vertical-align: middle;">
        <path d="m47.49 39.57a6.19 6.19 0 0 1-6.19 6.19h-34.1a6.19 6.19 0 0 1-6.19-6.19v-31.62a6.19 6.19 0 0 1 6.19-6.19h34.1a6.19 6.19 0 0 1 6.19 6.19v31.62z" fill="#74aa03"/>
        <path d="m19.84 10.28q4.37-1.48 8.77 0a0.62 0.61-62.5 0 0 0.7-0.23l2.05-2.92a0.33 0.32-14.1 0 1 0.56 0.33l-1.42 2.73a0.84 0.83-62 0 0 0.34 1.12q4.03 2.19 4.93 6.73a0.81 0.8-5.6 0 1-0.79 0.96h-21.75q-0.81 0-0.66-0.79 0.84-4.46 4.72-6.76a1.16 1.15 59.7 0 0 0.41-1.57l-1.23-2.12a0.46 0.46 0 0 1 0.78-0.48l1.84 2.75q0.28 0.41 0.75 0.25z" fill="#fff"/>
        <path d="m20.43 14.5a1.23 1.23 0 0 1-1.23 1.23 1.23 1.23 0 0 1-1.23-1.23 1.23 1.23 0 0 1 1.23-1.23 1.23 1.23 0 0 1 1.23 1.23z" fill="#74aa03"/>
        <path d="m30.42 14.5a1.23 1.23 0 0 1-1.23 1.23 1.23 1.23 0 0 1-1.23-1.23 1.23 1.23 0 0 1 1.23-1.23 1.23 1.23 0 0 1 1.23 1.23z" fill="#74aa03"/>
        <g fill="#fff">
          <path d="m10.741 33.874a2.39 2.39 0 0 1-2.3942 2.3858l-0.18-3e-4a2.39 2.39 0 0 1-2.3858-2.3941l0.0186-10.66a2.39 2.39 0 0 1 2.3942-2.3858l0.18 3e-4a2.39 2.39 0 0 1 2.3858 2.3941l-0.0186 10.66z"/>
          <path d="m30.03 25.45c-10.55-9-20.82 9.99-7.6 13.94a0.57 0.57 0 0 1-0.16 1.11h-2.3q-0.53 0-0.98 0.29-2.3 1.48-4.96 0.58a2.26 2.26 0 0 1-1.53-2.14v-17.73q0-0.75 0.75-0.75h22a0.5 0.5 0 0 1 0.5 0.5v18.24a1.83 1.83 0 0 1-1.1 1.68q-2.61 1.13-4.78-0.32-0.52-0.35-1.14-0.35h-2.26a0.55 0.55 0 0 1-0.17-1.07c5.74-1.88 9.03-9.45 3.73-13.98z"/>
          <path d="m42.479 34.016a2.38 2.38 0 0 1-2.3758 2.3841l-0.16 3e-4a2.38 2.38 0 0 1-2.3842-2.3758l-0.0188-10.82a2.38 2.38 0 0 1 2.3758-2.3841l0.16-3e-4a2.38 2.38 0 0 1 2.3842 2.3758l0.0188 10.82z"/>
          <path d="m26 32.91q1.22-2.16-0.81-3.36-0.77-0.45-0.73 0.44l0.05 0.89q0.05 0.99-0.9 0.69l-0.51-0.16q-0.68-0.22-0.97 0.44l-0.53 1.24a0.55 0.54-37.8 0 1-1.04-0.13c-1.15-7.43 8.51-6.48 7.66-1.23q-0.46 2.81-4.53 3.85a0.93 0.92 28.4 0 1-0.53-1.77q1.09-0.37 2.18-0.47a0.85 0.84-77.6 0 0 0.66-0.43z"/>
        </g>
      </svg>
    </button>
    <div id="button-bar" style="display: none; justify-content: center;flex-direction: row;align-items: start;" role="group">
      <button type="button" class="btn" onclick="feed.flipX()" title="Flip X">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16"><path fill="currentColor" d="m0 15l6-5l-6-4.9zm9-4.9l6 4.9V5l-6 5.1zm5 2.8l-3.4-2.8l3.4-3v5.8zM7 5h1v1H7V5zm0-2h1v1H7V3zm0 4h1v1H7V7zm0 2h1v1H7V9zm0 2h1v1H7v-1zm0 2h1v1H7v-1zm0 2h1v1H7v-1z"/><path fill="currentColor" d="M7.5 1c1.3 0 2.6.7 3.6 1.9L10 4h3V1l-1.2 1.2C10.6.8 9.1 0 7.5 0C5.6 0 3.9 1 2.6 2.9l.8.6C4.5 1.9 5.9 1 7.5 1z"/></svg>
      </button>
      <button type="button" class="btn" onclick="feed.flipY()" title="Flip Y">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16"><path fill="currentColor" d="m1 1l5 6l4.94-6H1zm4.94 9L1 16h10zm-2.82 5l2.83-3.44l3 3.44H3.12zM10 8h1v1h-1V8zm2 0h1v1h-1V8zM8 8h1v1H8V8zM6 8h1v1H6V8zM4 8h1v1H4V8zM2 8h1v1H2V8zM0 8h1v1H0V8z"/><path fill="currentColor" d="M15 8.47a4.807 4.807 0 0 1-1.879 3.632L12 11v3h3l-1.18-1.18A5.757 5.757 0 0 0 16 8.478V8.47a6.062 6.062 0 0 0-2.884-4.905l-.596.805a5.095 5.095 0 0 1 2.479 4.087z"/></svg>
      </button>
      <button type="button" class="btn" onclick="feed.zoomIn()" title="Zoom In">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13v4m-2-2h4m-7 0a5 5 0 1 0 10 0a5 5 0 1 0-10 0m12 7l-3-3M6 18H5a2 2 0 0 1-2-2v-1m0-4v-1m0-4V5a2 2 0 0 1 2-2h1m4 0h1m4 0h1a2 2 0 0 1 2 2v1"/></svg>
      </button>
      <button type="button" class="btn" onclick="feed.zoomOut()" title="Zoom Out">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 15h4m-7 0a5 5 0 1 0 10 0a5 5 0 1 0-10 0m12 7l-3-3M6 18H5a2 2 0 0 1-2-2v-1m0-4v-1m0-4V5a2 2 0 0 1 2-2h1m4 0h1m4 0h1a2 2 0 0 1 2 2v1"/></svg>
      </button>
      <button type="button" class="btn" onclick="feed.toggleLed()" title="Toggle LED">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12a4 4 0 1 0 8 0a4 4 0 1 0-8 0m-5 0h1m8-9v1m8 8h1m-9 8v1M5.6 5.6l.7.7m12.1-.7l-.7.7m0 11.4l.7.7m-12.1-.7l-.7.7"/></svg>
      </button>
      <button type="button" class="btn" onclick="feed.setAutoFocus()" title="Auto Focus">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M3 7V5a2 2 0 0 1 2-2h2m10 0h2a2 2 0 0 1 2 2v2m0 10v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2"/></g></svg>
      </button>
      <button type="button" class="btn" onclick="feed.lockExposure()" title="Lock Exposure">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M11 6H7a1 1 0 0 0 0 2h4a1 1 0 0 0 0-2Zm8-4H5a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3ZM4 18.59V5a1 1 0 0 1 1-1h13.59ZM20 19a1 1 0 0 1-1 1H5.41L20 5.41Zm-7-2h1v1a1 1 0 0 0 2 0v-1h1a1 1 0 0 0 0-2h-1v-1a1 1 0 0 0-2 0v1h-1a1 1 0 0 0 0 2Z"/></svg>
      </button>
      <button type="button" class="btn" onclick="feed.unlockExposure()" title="Unlock Exposure">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M4.6 19.4L12 12m2-2l5.4-5.4M8 4h10a2 2 0 0 1 2 2v10m-.586 3.414A2 2 0 0 1 18 20H6a2 2 0 0 1-2-2V6c0-.547.22-1.043.576-1.405"/><path d="M7 9h2v2m4 5h3M3 3l18 18"/></g></svg>
      </button>
      <button type="button" class="btn" onclick="feed.resize(-1)" title="Smaller">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 20 20"><path fill="currentColor" d="M4.1 14.1L1 17l2 2l2.9-3.1L8 18v-6H2l2.1 2.1zM19 3l-2-2l-2.9 3.1L12 2v6h6l-2.1-2.1L19 3z"/></svg>
      </button>
      <button type="button" class="btn" onclick="feed.resize(1)" title="Larger">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 20 20"><path fill="currentColor" d="m6.987 10.987l-2.931 3.031L2 11.589V18h6.387l-2.43-2.081l3.03-2.932l-2-2zM11.613 2l2.43 2.081l-3.03 2.932l2 2l2.931-3.031L18 8.411V2h-6.387z"/></svg>
      </button>
      <button type="button" class="btn" style="background-color: green;" onclick="feed.setColor('green')">&nbsp;</button>
      <button type="button" class="btn" style="background-color: blue;" onclick="feed.setColor('blue')">&nbsp;</button>
      <button type="button" class="btn" style="background-color: red;" onclick="feed.setColor('red')">&nbsp;</button>
      <button type="button" class="btn" style="background-color: yellow;" onclick="feed.setColor('yellow')">&nbsp;</button>
      <button type="button" class="btn" style="background-color: white;" onclick="feed.setColor('white')">&nbsp;</button>
    </div>
  </div>
<div style="display: flex; justify-content: center;">
	<div id="feedimg">
		<div id="x-container">
			<svg width="36.71mm" height="36.71mm" version="1.1" viewBox="0 0 36.71 36.71" xmlns="http://www.w3.org/2000/svg">
			 <g id="cross" transform="translate(-64.763 -72.035)" fill="none" stroke="#F00">
			  <path d="m73.714 90.274a9.4037 9.4037 0 0 1 9.4674-9.2867 9.4037 9.4037 0 0 1 9.3392 9.4156 9.4037 9.4037 0 0 1-9.3634 9.3915 9.4037 9.4037 0 0 1-9.4435-9.311" stop-color="#000000" style="paint-order:stroke fill markers"/>
			  <path d="m83.118 72.035v36.71" stroke-width=".3"/>
			  <path d="m101.47 90.39h-36.71" stroke-width=".3"/>
			 </g>
			</svg>
		</div>
	</div>
</div>

<fieldset>
  <legend>Head positions</legend>
  <table>
    <thead>
      <tr>
        <th>Head # <button class="btn" onclick="addHead()">Add head</button></th>
        <th>X</th>
        <th>Y</th>
        <th>dX</th>
        <th>dY</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th>1</th>
        <td><input type="number" id="x1" value="0" /></td>
        <td><input type="number" id="y1" value="0" /></td>
        <td colspan="2">
          With precision:
          <input type="number" id="precision" value="1" min="0" max="3"/>
        </td>
      </tr>
      <tr class="template">
        <th>2</th>
        <td><input type="number" class="x" value="0" /></td>
        <td><input type="number" class="y" value="0" /></td>
        <td><input type="number" readonly class="dx" value="0" /></td>
        <td><input type="number" readonly class="dy" value="0" /></td>
      </tr>
    </tbody>
  </table>
</fieldset>

<div style="position:absolute;bottom:10px;left:10px;z-index:9;">
  <p><span id="remote-req" class="invisible" style="padding: .3rem 1rem;color: darkblue; background-color: aquamarine;">Request Sent</span></p>
</div>

<script type="text/javascript">

var sizes = {
 "240p" : [320, 240],
 "480p" : [640, 480],
 "720p" : [960, 720],
 "FHD 720p" : [1280, 720],
 "FHD 1080p" : [1920, 1080],
};
const dummy = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzcuNTk3IiBoZWlnaHQ9IjM2LjY3MyIgdmlld0JveD0iMCAwIDIwLjUzMSA5LjcwMyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNi45OTYgMS4zMjUgNS4xNiA0Ljc1NWwxLjg5NyAzLjYyM0g0Ljg1bC0uODY4LTEuNzEtLjczNCAxLjcxaC0yLjE3bDEuOTM4LTMuNTY1LTEuODM1LTMuNDg4aDIuMTg2bC44MjIgMS42MDIuNjYxLTEuNjAyeiIgZmlsbD0idGVhbCIvPjxwYXRoIGQ9Ik0xMC40MzMgMS41ODNxMS4zMjIgMCAyLjI2Ny45NjIuOTQ4Ljk2Mi45NDggMi4zMDQgMCAxLjM2My0uOTUzIDIuMzE3LS45NTMuOTUzLTIuMzE3Ljk1My0xLjM1NSAwLTIuMzEzLS45NTgtLjk1OC0uOTU4LS45NTgtMi4zMTIgMC0xLjM2OS45NjMtMi4zMTguOTY3LS45NDggMi4zNjMtLjk0OHptLS4wNTUgMS44MzNxLS41NyAwLS45NzIuNDJ0LS40MDEgMS4wMTNxMCAuNTkzLjQwMSAxLjAxMi40MDYuNDIuOTcyLjQyLjU3NSAwIC45NzEtLjQxNS40MDItLjQyLjQwMi0xLjAxNyAwLS41OTgtLjQwMi0xLjAxMy0uMzk2LS40Mi0uOTcxLS40MnoiIGZpbGw9IiNmZmYiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIuMjM0Ii8+PHBhdGggZD0iTTE5LjQ1MyAxLjMyNSAxNy40NTggNC40NnYzLjkxN0gxNS4zNlY0LjQ2MmwtMi4wMS0zLjEzNmgyLjEyOWwuOTEgMS40NTIuOTc2LTEuNDUyeiIgZmlsbD0ibWFyb29uIi8+PHBhdGggZmlsbD0ibm9uZSIgc3R5bGU9InBhaW50LW9yZGVyOnN0cm9rZSBmaWxsIG1hcmtlcnMiIGQ9Ik0wIDBoMjAuNTMxdjkuNzAzSDB6Ii8+PC9zdmc+";

function addHead() {
  let row = document.querySelector('.template').cloneNode(true);
  row.classList.remove('template');
  document.querySelector('tbody').appendChild(row);
  let headNum = document.querySelectorAll('tbody tr').length;
  row.querySelector('th').textContent = headNum;
  row.querySelectorAll('input').forEach((el) => {
    el.value = 0;
    if(!el.readOnly) el.addEventListener('change', updateDeltas);
  });
}

function updateDeltas() {
  let x1 = document.getElementById('x1').value;
  let y1 = document.getElementById('y1').value;
  document.querySelectorAll('tbody tr:not(:first-child)').forEach((row) => {
    let x = row.querySelector('input.x').value;
    let y = row.querySelector('input.y').value;
    let precision = document.getElementById('precision').value;
    row.querySelector('input.dx').value = (x1 - x).toFixed(precision);
    row.querySelector('input.dy').value = (y1 - y).toFixed(precision);
  });
}


class DroidCamXView {
    source = '';
    feedimg = null;
    currimg = null;
    Xflip = 0;
    Yflip = 0;
    aspect = 1;
    angle = 0;

    constructor() {
        this.feedimg = document.getElementById('feedimg');

        this.currimg = new Image(640,480);
        this.currimg.src = dummy;
        this.feedimg.appendChild(this.currimg);

        this.cursz = (typeof(Storage) !== "undefined" && localStorage.cursz && sizes[localStorage.cursz]) ? localStorage.cursz : ((window.screen.width < 640) ? "240p" : "480p");
        this.setSource( (typeof(Storage) !== "undefined" && localStorage.source) || this.askDroid());
    }

    setSource(source) {
		    if(!source) return;
		
        if(!(source.startsWith("http://") || source.startsWith("https://"))) 
          source = (window.location.startsWith("https:") ? "https://" : "http://") + source;
        
        this.source = source;
        
        if(typeof(Storage) !== "undefined") {
            localStorage.setItem("source", this.source);
        }
    }

    reload() {
        if(!this.source) this.setSource(this.askDroid());
        this.exec('/override');
        this.hidectls();
        setTimeout(() => this.startfeed(),2500);
    }

    resize(d) {
      const s = Object.keys(sizes);
      const c = s.indexOf(this.cursz);
      const nc = c + d;
      if(0 <= nc && s.length-1 >= nc) this.setCurrentSize(s[nc]);
    }

    setCurrentSize(size) {
        this.cursz = size;
        this.reload();
    }

    startfeed() {
        const oldImg = this.feedimg.querySelector('img');
        if (oldImg) this.feedimg.removeChild(oldImg);

        this.feedimg.style.width = "";
        this.feedimg.style.height = "";
        const w = sizes[this.cursz][0];
        const h = sizes[this.cursz][1];
        this.aspect = w / h;
        this.currimg = new Image(w, h);
        this.currimg.src = this.source + "/video?" + w + "x" + h;
        this.currimg.onload = this.showctls;
        this.currimg.onerror = this.hidectls;
        this.feedimg.appendChild(this.currimg);

        if(typeof(Storage) !== "undefined") {
            localStorage.setItem("cursz", this.cursz);
        }
    }

    // valid option values are: auto, incandescent, warm-fluorescent, twilight, fluorescent, daylight, cloudy-daylight, shade
    set_wb(el, option) {
      return this.exec('cam/1/set_wb/' + option)
    }

    lockExposure() {
      return this.exec('/cam/1/set_exposure_lock')
    }

    unlockExposure() {
      return this.exec('/cam/1/unset_exposure_lock')
    }

    setAutoFocus() {
      return this.exec('/cam/1/af')
    }
	
    limitFPS() {
      return this.exec('/cam/1/fpslimit');
    }

    toggleLed() {
      return this.exec('/cam/1/led_toggle')
    }

    zoomIn() {
      return this.exec('/cam/1/zoomin')
    }

    zoomOut() {
      return this.exec('/cam/1/zoomout')
    }

    flipX() {
        this.Xflip = !this.Xflip;
        this.setFlip();
    }

    flipY() {
        this.Yflip = !this.Yflip;
        this.setFlip();
    }

    setColor(color) {
        document.getElementById('x-container').style.borderColor = color;
        document.getElementById('cross').style.stroke=color
    }

    setDroid(){
        let droid = this.askDroid();
        if (droid != null) {
            this.setSource(droid);
            this.reload();
        }
    }

    askDroid = () => prompt("Enter DroidCamX address", this.source);

    setFlip = () => this.feedimg.style.transform = `scaleX(${this.Xflip?-1:1}) scaleY(${this.Yflip?-1:1})`;

    imgWidth = () => this.currimg.offsetWidth ?? this.currimg.naturalWidth;

    imgHeight = () => this.currimg.offsetHeight ?? this.currimg.naturalHeight;

    showctls = () => {
      document.getElementById('button-bar').style.display = 'flex';
      document.getElementById('get-droid').remove();
    }

    hidectls = () => {
        this.currimg.onload = null;
        this.currimg.src = dummy;
        document.getElementById('button-bar').style.display = 'none';
    }

    showRemote = () => {
        let el = document.getElementById('remote-req'); 
        el.classList.remove('invisible');
        setTimeout(function(){ el.classList.add('invisible') }, 2200);
    }

    exec = (cmd) => {
	  let res =	fetch(
      this.source + cmd,
      {
        method: 'GET', 
        mode: "no-cors"
      })
      .catch(() => this.hidectls())
      ;
      this.showRemote();
	  return res;
    }
}

let feed = new DroidCamXView();
document.addEventListener('DOMContentLoaded', () => {
  feed.startfeed();
  document.querySelectorAll('table input').forEach((el) => {
    el.addEventListener('change', updateDeltas);
  });   
});
</script>
</body></html>