Index: src/assets/js/guardian2.js
===================================================================
--- src/assets/js/guardian2.js	(revision 5c373b8e846c6510a176f2fa06bfe3e2b6753102)
+++ src/assets/js/guardian2.js	(revision c66cbb4536b30530330e60aecad2a6de103f0375)
@@ -114,4 +114,7 @@
       row.appendChild(el('span', 'who grow', handleOf(w.other_uri, w.other_handle)));
       row.appendChild(el('span', 'tag ok', T.active));
+      var wave = el('button', 'small', T.wave || '👋 Wave');
+      wave.addEventListener('click', function () { sendWave(w.other_uri, wave); });
+      row.appendChild(wave);
       var btn = el('button', 'quiet small', T.release);
       btn.addEventListener('click', function () { remove(w.other_uri, btn); });
@@ -121,4 +124,14 @@
     });
     show('wards-empty', wards.length === 0);
+  }
+
+  function sendWave(uri, btn) {
+    btn.disabled = true;
+    fetch('/guardian2/api/wave', {
+      method: 'POST', headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ ward: uri, site: S.site }),
+    }).then(function (r) { return r.json(); })
+      .then(function (j) { btn.disabled = false; btn.textContent = (j && j.ok) ? (T.waved || '👋 sent') : (T.wave || '👋 Wave'); })
+      .catch(function () { btn.disabled = false; });
   }
 
