Changeset c26cc18 in Klonkt for src/assets/js/guardian.js


Ignore:
Timestamp:
07/24/2026 06:04:03 PM (7 weeks ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
780a7c6
Parents:
1a2d8ac
Message:

Guardian-PWA: duidelijker + verstuurde offer altijd zichtbaar

Twee klachten opgelost.

"Ik zie niet dat ik verstuurd heb": de offer werd wél opgeslagen, maar als de
directe bezorging aan de remote ward faalde gaf de flow 502 en ververste de
PWA niet, dus de kaart verscheen niet. Nu: bezorging is async met retry (de
delivery-queue), de handshake geeft altijd 202 (opgeslagen) met een
delivered-vlag, en de PWA ververst altijd en toont de offer onder een eigen
sectie "Verzonden aanvragen · wacht op antwoord". Faalt de eerste bezorging,
dan meldt de UI dat we blijven proberen.

"Mag allemaal duidelijker": de pagina is herzien met vaste secties met kop en
uitleg: Hulpverzoeken (met teller), Ward adopteren (met uitleg wat er gebeurt),
Verzonden aanvragen, Mijn wards (actief-tag), Meldingen. Statussen als tags,
nette lege staten, alles via i18n (nl/en/de).

Changed files:
src/services/ActivityPubService.js

  • deliverToActor: enqueue+retry+logging, geeft {delivered, inbox}

src/services/guardianship/handshake.js

src/routes/guardian.js

  • /adopt niet meer hard falen op bezorg-hik; delivered terug; meer strings

src/views/pages/guardian.ejs

  • herziene, duidelijke secties

src/assets/js/guardian.js

  • aparte "Verzonden aanvragen"-sectie, altijd verversen na adopt

src/assets/css/guardian.css

  • sectie-koppen, status-tags, nette lege staten

src/services/i18n.js

  • uitgebreide guardian.*-teksten nl/en/de

remarks: als de offer "niet binnenkomt" bij de ward, moet diens Klonkt de
nieuwe code draaien (demo/beta/demohub zijn bij); een oude Klonkt of Mastodon
snapt de Offer-Relationship niet.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/assets/js/guardian.js

    r1a2d8ac rc26cc18  
    1 /* Guardian PWA client (FEP-633c): renders the dashboard state, adopts wards,
    2    and manages the guardian push channel (web-push slice reused: alert types
    3    'help' + 'guardian'). No framework, no inline scripts (CSP). */
     1/* Guardian PWA client (FEP-633c): renders the dashboard, adopts wards, and
     2   manages the guardian push channel. No framework, no inline scripts (CSP).
     3   All user-facing text comes from state.strings (server i18n). */
    44(function () {
    55  'use strict';
    6   var state = JSON.parse(document.getElementById('guardian-state').textContent || '{}');
     6  var S = JSON.parse(document.getElementById('guardian-state').textContent || '{}');
     7  var T = S.strings || {};
    78
    89  function el(tag, cls, text) {
     
    1718    catch (e) { return uri; }
    1819  }
    19   function when(s) {
    20     return String(s || '').slice(0, 16).replace('T', ' ');
    21   }
     20  function when(s) { return String(s || '').slice(0, 16).replace('T', ' '); }
     21  function show(id, on) { document.getElementById(id).hidden = !on; }
    2222
    23   // ── Message centre: help requests ──────────────────────────────────────
     23  // ── 1. Help requests ───────────────────────────────────────────────────
    2424  function renderHelp() {
    2525    var list = document.getElementById('help-list');
    2626    list.textContent = '';
    27     (state.help || []).forEach(function (h) {
     27    var help = S.help || [];
     28    help.forEach(function (h) {
    2829      var card = el('div', 'g-card help');
    2930      var row = el('div', 'row');
    30       var who = el('span', 'who', h.actor_name || handleOf(h.actor_uri, h.actor_handle));
    31       var at = el('span', 'when', when(h.published || h.created_at));
    32       row.appendChild(who); row.appendChild(at);
     31      row.appendChild(el('span', 'who grow', h.actor_name || handleOf(h.actor_uri, h.actor_handle)));
     32      row.appendChild(el('span', 'when', when(h.published || h.created_at)));
    3333      card.appendChild(row);
    3434      var body = el('div', 'body');
    35       body.innerHTML = h.content || '';           // sanitized server-side on ingest
     35      body.innerHTML = h.content || '';          // sanitized server-side on ingest
    3636      card.appendChild(body);
    3737      if (h.note_url) {
    38         var link = el('a', 'when', 'open');
    39         link.href = h.note_url; link.target = '_blank'; link.rel = 'noopener';
    40         card.appendChild(link);
     38        var a = el('a', 'g-link', T.open || 'open');
     39        a.href = h.note_url; a.target = '_blank'; a.rel = 'noopener';
     40        card.appendChild(a);
    4141      }
    4242      list.appendChild(card);
    4343    });
    44     document.getElementById('help-empty').hidden = (state.help || []).length > 0;
     44    var badge = document.getElementById('help-count');
     45    badge.textContent = help.length; badge.hidden = help.length === 0;
     46    show('help-empty', help.length === 0);
    4547  }
    4648
    47   // ── Wards + pending offers ─────────────────────────────────────────────
    48   function wardCard(w, pending) {
    49     var card = el('div', 'g-card');
    50     var row = el('div', 'row');
    51     var who = el('span', 'who grow', handleOf(w.other_uri, w.other_handle));
    52     row.appendChild(who);
    53     if (pending) row.appendChild(el('span', 'pend', state.strings.pending));
    54     var btn = el('button', 'quiet', pending ? state.strings.retract : state.strings.release);
    55     btn.addEventListener('click', function () {
    56       fetch('/guardian/wards/remove', {
    57         method: 'POST', headers: { 'Content-Type': 'application/json' },
    58         body: JSON.stringify({ uri: w.other_uri, site: state.site }),
    59       }).then(refresh);
     49  // ── 3. Pending offers I sent ───────────────────────────────────────────
     50  function renderPending() {
     51    var list = document.getElementById('pending-list');
     52    list.textContent = '';
     53    var pend = S.pendingOffers || [];
     54    pend.forEach(function (w) {
     55      var card = el('div', 'g-card');
     56      var row = el('div', 'row');
     57      row.appendChild(el('span', 'who grow', handleOf(w.other_uri, w.other_handle)));
     58      row.appendChild(el('span', 'tag wait', T.pending));
     59      var btn = el('button', 'quiet small', T.retract);
     60      btn.addEventListener('click', function () { remove(w.other_uri, btn); });
     61      row.appendChild(btn);
     62      card.appendChild(row);
     63      list.appendChild(card);
    6064    });
    61     row.appendChild(btn);
    62     card.appendChild(row);
    63     return card;
    64   }
    65   function renderWards() {
    66     var wl = document.getElementById('wards-list');
    67     var ol = document.getElementById('offers-list');
    68     wl.textContent = ''; ol.textContent = '';
    69     (state.wards || []).forEach(function (w) { wl.appendChild(wardCard(w, false)); });
    70     (state.pendingOffers || []).forEach(function (w) { ol.appendChild(wardCard(w, true)); });
    71     document.getElementById('wards-empty').hidden =
    72       (state.wards || []).length + (state.pendingOffers || []).length > 0;
     65    show('pending-section', pend.length > 0);
    7366  }
    7467
    75   function refresh() {
    76     fetch('/guardian/api/state?site=' + encodeURIComponent(state.site))
    77       .then(function (r) { return r.json(); })
    78       .then(function (s) { if (s && !s.error) { state = s; renderHelp(); renderWards(); } });
     68  // ── 4. Accepted wards ──────────────────────────────────────────────────
     69  function renderWards() {
     70    var list = document.getElementById('wards-list');
     71    list.textContent = '';
     72    var wards = S.wards || [];
     73    wards.forEach(function (w) {
     74      var card = el('div', 'g-card');
     75      var row = el('div', 'row');
     76      row.appendChild(el('span', 'who grow', handleOf(w.other_uri, w.other_handle)));
     77      row.appendChild(el('span', 'tag ok', T.active));
     78      var btn = el('button', 'quiet small', T.release);
     79      btn.addEventListener('click', function () { remove(w.other_uri, btn); });
     80      row.appendChild(btn);
     81      card.appendChild(row);
     82      list.appendChild(card);
     83    });
     84    show('wards-empty', wards.length === 0);
    7985  }
    8086
    81   // ── Adopt ──────────────────────────────────────────────────────────────
    82   document.getElementById('adopt-form').addEventListener('submit', function (ev) {
     87  function remove(uri, btn) {
     88    btn.disabled = true;
     89    fetch('/guardian/wards/remove', {
     90      method: 'POST', headers: { 'Content-Type': 'application/json' },
     91      body: JSON.stringify({ uri: uri, site: S.site }),
     92    }).then(refresh);
     93  }
     94
     95  function renderAll() { renderHelp(); renderPending(); renderWards(); }
     96
     97  function refresh() {
     98    return fetch('/guardian/api/state?site=' + encodeURIComponent(S.site))
     99      .then(function (r) { return r.json(); })
     100      .then(function (s) { if (s && !s.error) { S = s; T = s.strings || T; renderAll(); } });
     101  }
     102
     103  // ── 2. Adopt ───────────────────────────────────────────────────────────
     104  var form = document.getElementById('adopt-form');
     105  var input = document.getElementById('adopt-handle');
     106  var adoptBtn = document.getElementById('adopt-btn');
     107  var msg = document.getElementById('adopt-msg');
     108  function setMsg(text, isErr) { msg.hidden = false; msg.className = 'g-msg' + (isErr ? ' err' : ''); msg.textContent = text; }
     109
     110  form.addEventListener('submit', function (ev) {
    83111    ev.preventDefault();
    84     var input = document.getElementById('adopt-handle');
    85     var msg = document.getElementById('adopt-msg');
    86112    var handle = input.value.trim();
    87113    if (!handle) return;
    88     msg.hidden = false; msg.classList.remove('err'); msg.textContent = '…';
     114    adoptBtn.disabled = true;
     115    setMsg(T.sending || '…', false);
    89116    fetch('/guardian/adopt', {
    90117      method: 'POST', headers: { 'Content-Type': 'application/json' },
    91       body: JSON.stringify({ handle: handle, site: state.site }),
     118      body: JSON.stringify({ handle: handle, site: S.site }),
    92119    }).then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); })
    93120      .then(function (res) {
    94         if (res.ok) { msg.textContent = state.strings.sent; input.value = ''; refresh(); }
    95         else { msg.classList.add('err'); msg.textContent = state.strings.failed + ': ' + (res.j.error || '?'); }
     121        adoptBtn.disabled = false;
     122        if (res.ok) {
     123          input.value = '';
     124          // Always refresh: the offer is recorded even if delivery is still
     125          // in flight. Show it under "Verzonden aanvragen".
     126          setMsg(res.j.delivered === false ? T.sent_retry : T.sent, false);
     127          refresh();
     128        } else {
     129          setMsg((res.j.error === 'not_found' ? T.not_found : T.failed) , true);
     130        }
    96131      })
    97       .catch(function () { msg.classList.add('err'); msg.textContent = state.strings.network; });
     132      .catch(function () { adoptBtn.disabled = false; setMsg(T.network, true); });
    98133  });
    99134
     
    104139  });
    105140
    106   // ── Push: the guardian channel (help + guardian alerts) ────────────────
     141  // ── 5. Push ────────────────────────────────────────────────────────────
    107142  var toggle = document.getElementById('push-toggle');
    108143  var pmsg = document.getElementById('push-msg');
     
    115150        toggle.textContent = sub ? toggle.dataset.onLabel : toggle.dataset.offLabel;
    116151        toggle.dataset.subscribed = sub ? '1' : '';
     152        toggle.classList.toggle('is-on', !!sub);
    117153      });
    118154  }
     
    145181          body: JSON.stringify({
    146182            subscription: sub.toJSON(),
    147             // The guardian channel: calls for help + adoption traffic.
    148183            alerts: { help: 1, guardian: 1, dm: 1, follow: 0, reply: 0, like: 0, boost: 0 },
    149184            uaLabel: 'Guardian PWA',
     
    151186        });
    152187      }).then(pushState).catch(function (e) {
    153         pmsg.hidden = false; pmsg.classList.add('err');
    154         pmsg.textContent = 'Push niet beschikbaar: ' + e.message;
     188        pmsg.hidden = false; pmsg.className = 'g-msg err';
     189        pmsg.textContent = (T.push_unavailable || 'Push unavailable') + ': ' + e.message;
    155190      });
    156191    });
    157192  });
    158193
    159   renderHelp(); renderWards(); pushState();
    160   // Live-ish: poll the state every 45s while the PWA is open.
    161   setInterval(refresh, 45000);
     194  renderAll(); pushState();
     195  setInterval(refresh, 45000);   // live-ish while open
    162196})();
Note: See TracChangeset for help on using the changeset viewer.