Changeset 952baf3 in Klonkt for src/assets/js/guardian.js


Ignore:
Timestamp:
08/07/2026 05:15:52 PM (5 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
ba76bf5
Parents:
f85b2c3 (diff), 0d5bd2c (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent.
Message:

Merge GitHub-main (1.7.0) met de VPS-lijn

De twee mains waren een dag gedivergeerd en bevatten elk echt werk. GitHub had 66
commits die nooit langs prutfolio.git zijn gekomen, omdat een parallelle sessie
rechtstreeks naar GitHub pushte vanaf een kloon in /tmp op de VPS. De VPS had twee
commits die GitHub niet had. Geen van beide bevatte de ander, en stable had geen van
de twee.

Bewust een merge en geen rebase: dan blijft beide historie intact en wordt er niets
herschreven waar iemand anders al op voortbouwt.

Drie bestanden raakten beide kanten. Alle drie zijn nagekeken, want dat een merge
automatisch slaagt zegt niets over of hij inhoudelijk klopt:

src/services/ActivityPubService.js

  • de sleutelbinding staat nu boven de nieuwe asSlug-aanroep van 1.7.0, dus de controle komt nog steeds voor de handtekeningcontrole

scripts/klonkt-refresh-updater.sh

  • alleen de opzij-aanpak overleefde; systemctl mask staat nergens meer als code

deploy/MULTI-INSTANCE.md

  • spreekt zichzelf niet tegen: beschrijft opzij zetten, met de reden waarom mask weigert

remarks: het gat dat in de review naar boven kwam staat hiermee ook op de 1.7.0-lijn.
De andere bevindingen uit die review staan nog open en zijn niet in deze merge
opgelost; die horen als beads. Ook nog te doen: dezelfde sleutelbinding op stable
als 1.6.1, want daar is het gat nog open bij self-hosters.

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

File:
1 edited

Legend:

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

    rf85b2c3 r952baf3  
    4949  // child's panel.
    5050  var HELP_TOP = 5;
     51
     52  /**
     53   * Wacht deze hulpvraag nog op iemand?
     54   *
     55   * EEN plek, met opzet. Dit werd eerst op twee plekken los beslist -- de lijst
     56   * en de teller op de wardregel -- en toen de lijst gefilterd werd bleef die
     57   * teller een afgehandelde vraag meetellen. Een derde plek komt er vast, en dan
     58   * hoort hij hier langs te komen.
     59   *
     60   * Bij TWIJFEL open: alles wat geen expliciete afsluiting draagt telt als
     61   * wachtend. De omgekeerde fout -- iets als afgehandeld tonen dat het niet is --
     62   * is hier de gevaarlijke.
     63   */
     64  function helpOpen(h) { return !h || !h.state || h.state.open; }
    5165
    5266  function helpCard(h) {
     
    7286      card.appendChild(a);
    7387    }
     88    card.appendChild(helpState(h));
    7489    return card;
     90  }
     91
     92  /** Hoe lang geleden, grof. Een oppik vervalt niet maar hoort wel te verouderen. */
     93  function ago(ms) {
     94    var u = Math.floor(ms / 3600000);
     95    if (u < 1) return T.help_just_now || 'just now';
     96    if (u < 24) return (T.help_hours || '{n}h ago').replace('{n}', u);
     97    return (T.help_days || '{n}d ago').replace('{n}', Math.floor(u / 24));
     98  }
     99
     100  /**
     101   * De gedeelde staat van een hulpvraag (shaer-lgo): wie er al op af is, of het
     102   * is afgesloten, en de twee knoppen.
     103   *
     104   * De faalstand hier is NIET veilig: 'iedereen denkt dat het geregeld is' is
     105   * gevaarlijker dan geen markering. Dus bij twijfel toont dit OPEN, en een oude
     106   * oppik verkleurt in plaats van te verdwijnen -- anders ziet een hulpvraag er
     107   * onaangeroerd uit terwijl er iemand mee bezig is.
     108   */
     109  function helpState(h) {
     110    var st = h.state || { open: true, pickedUpBy: [], handled: null, ageMs: null };
     111    var box = el('div', 'g-help-state');
     112
     113    if (st.handled) {
     114      var wie = st.handled.handle || handleOf(st.handled.uri);
     115      box.appendChild(el('div', 'g-help-done', (T.help_handled_by || 'Handled by {who}').replace('{who}', wie)));
     116      // Geen terugdraaiknop: leeft de vraag nog, dan wordt hij opnieuw gesteld.
     117      box.appendChild(el('p', 'small g-empty', T.help_handled_note || ''));
     118      return box;
     119    }
     120
     121    (st.pickedUpBy || []).forEach(function (p) {
     122      var line = (T.help_picked_by || '{who} is looking into this').replace('{who}', p.handle || handleOf(p.uri));
     123      box.appendChild(el('div', 'g-help-pick', line));
     124    });
     125    // Oud, maar niet weg. Dit is het verschil tussen 'er is iemand mee bezig' en
     126    // 'er was ooit iemand mee bezig'.
     127    if (st.ageMs != null && st.ageMs > 3600000) {
     128      box.appendChild(el('div', 'g-help-age small', ago(st.ageMs)));
     129    }
     130
     131    var row = el('div', 'row');
     132    var mij = (st.pickedUpBy || []).some(function (p) { return p.uri === S.me; });
     133    if (!mij) {
     134      var pick = el('button', 'small', T.help_pick || 'I am on it');
     135      pick.addEventListener('click', function () { markHelp(h, 'pickup', pick); });
     136      row.appendChild(pick);
     137    }
     138    var done = el('button', 'quiet small', T.help_close || 'Mark handled');
     139    // Een stevige bevestiging, en nooit een window.confirm: dat verstopt een
     140    // uitleg achter een OK die mensen wegklikken. Zelfde lijn als het loslaten
     141    // van een ward.
     142    done.addEventListener('click', function () {
     143      done.hidden = true;
     144      var ask = el('div', 'g-confirm');
     145      ask.appendChild(el('p', 'small', T.help_close_ask || ''));
     146      var ja = el('button', 'small', T.help_close_yes || 'Yes, handled');
     147      ja.addEventListener('click', function () { markHelp(h, 'handled', ja); });
     148      var nee = el('button', 'quiet small', T.release_no || 'No');
     149      nee.addEventListener('click', function () { ask.remove(); done.hidden = false; });
     150      ask.appendChild(ja); ask.appendChild(nee);
     151      box.appendChild(ask);
     152    });
     153    row.appendChild(done);
     154    box.appendChild(row);
     155    return box;
     156  }
     157
     158  function markHelp(h, kind, btn) {
     159    if (btn) btn.disabled = true;
     160    fetch('/guardian/api/help/' + kind, {
     161      method: 'POST', headers: { 'Content-Type': 'application/json' },
     162      body: JSON.stringify({ note: h.object_uri, ward: h.actor_uri, site: S.site }),
     163    }).then(refresh).catch(function () { if (btn) btn.disabled = false; });
    75164  }
    76165
     
    78167    var list = document.getElementById('help-list');
    79168    list.textContent = '';
    80     var help = S.help || [];
    81     help.slice(0, HELP_TOP).forEach(function (h) { list.appendChild(helpCard(h)); });
    82     if (help.length > HELP_TOP) {
    83       list.appendChild(el('p', 'g-sec-sub', '+ ' + (help.length - HELP_TOP) + ' — ' + (T.panel_help || '')));
    84     }
     169    var alle = S.help || [];
     170    // Afgehandeld hoort niet meer in de lijst die om aandacht vraagt, en niet
     171    // meer in de teller: anders blijft het cijfer alarm slaan voor iets dat af
     172    // is, en neemt een gesloten vraag de ruimte in van een open vraag.
     173    var open = alle.filter(helpOpen);
     174    var klaar = alle.filter(function (h) { return !helpOpen(h); });
     175
     176    open.slice(0, HELP_TOP).forEach(function (h) { list.appendChild(helpCard(h)); });
     177    if (open.length > HELP_TOP) {
     178      list.appendChild(el('p', 'g-sec-sub', '+ ' + (open.length - HELP_TOP) + ' — ' + (T.panel_help || '')));
     179    }
     180
     181    // WEG is niet hetzelfde als AF. Een afgehandelde hulpvraag blijft bestaan --
     182    // er wordt in dit systeem niets herschreven -- maar hij hoort achter een
     183    // klik, niet voor je neus. De volledige geschiedenis per kind staat sowieso
     184    // in het paneel van dat kind.
     185    if (klaar.length) {
     186      var doos = el('div', 'g-help-archive');
     187      doos.hidden = true;
     188      klaar.forEach(function (h) { doos.appendChild(helpCard(h)); });
     189      var knop = el('button', 'quiet small', (T.help_archive || '{n} handled').replace('{n}', klaar.length));
     190      knop.addEventListener('click', function () {
     191        doos.hidden = !doos.hidden;
     192        knop.textContent = doos.hidden
     193          ? (T.help_archive || '{n} handled').replace('{n}', klaar.length)
     194          : (T.help_archive_hide || 'hide');
     195      });
     196      list.appendChild(knop);
     197      list.appendChild(doos);
     198    }
     199
    85200    var badge = document.getElementById('help-count');
    86     badge.textContent = help.length; badge.hidden = help.length === 0;
    87     show('help-empty', help.length === 0);
     201    badge.textContent = open.length; badge.hidden = open.length === 0;
     202    // "Geen hulpverzoeken. Mooi zo." mag ook staan als er wel een archief is:
     203    // er wacht dan immers niets.
     204    show('help-empty', open.length === 0);
    88205  }
    89206
     
    351468  }
    352469
     470  /** Het woord dat we voor een gate gebruiken; onbekend valt terug op de naam. */
     471  function gateLabel(feature) {
     472    return T['gate_' + feature.replace('shaer:', '')] || feature.replace('shaer:', '');
     473  }
     474
     475  /**
     476   * Een rij in het gate-paneel. Toont WAT er geldt, van welk SOORT het is, welke
     477   * drempel er hoort en wat er loopt -- en pas daarna een knop, als er iets te
     478   * verzetten valt.
     479   *
     480   * Het soort staat erbij omdat de gates niet hetzelfde werken: een stand is aan
     481   * of uit, volgverzoeken zijn een stroom beslissingen, en een overdracht is
     482   * onomkeerbaar zodra het kind hem gebruikt. Vier rijen met dezelfde schakelaar
     483   * zouden dat verschil wegpoetsen.
     484   */
     485  function gateRow(w, g) {
     486    var row = el('div', 'g-gate');
     487    var head = el('div', 'g-gate-head');
     488    head.appendChild(el('span', 'g-gate-name', gateLabel(g.feature)));
     489    head.appendChild(el('span', 'g-gate-kind', T['gate_kind_' + g.kind] || g.kind));
     490    row.appendChild(head);
     491
     492    // De stand. NULL is onbekend en dat is iets anders dan uit: bij een ward op
     493    // een andere server wordt die kolom daar bijgehouden.
     494    var stand = g.value === null || g.value === undefined
     495      ? (T.gate_unknown || 'unknown')
     496      : (g.value ? (T.prop_on || 'on') : (T.prop_off || 'off'));
     497    var meta = el('div', 'g-gate-meta small');
     498    meta.appendChild(el('span', null, stand));
     499    if (g.threshold) {
     500      meta.appendChild(el('span', null, (T.gate_threshold || '{need} of {of} guardians')
     501        .replace('{need}', g.threshold.need).replace('{of}', g.threshold.of)));
     502    } else {
     503      // Geen drempel verzinnen die we niet kennen.
     504      meta.appendChild(el('span', 'g-dim', T.gate_threshold_unknown || ''));
     505    }
     506    if (!g.reversible) meta.appendChild(el('span', 'g-gate-warn', T.gate_irreversible || ''));
     507    if (g.waiting) {
     508      meta.appendChild(el('span', 'g-gate-wait', (T.gate_waiting || '{n} waiting').replace('{n}', g.waiting)));
     509    }
     510    row.appendChild(meta);
     511
     512    if (g.proposal) {
     513      row.appendChild(el('p', 'small g-prop g-prop-' + g.proposal.status,
     514        (T.prop_line || 'Proposal {what} {value}: {status}')
     515          .replace('{what}', gateLabel(g.feature))
     516          .replace('{value}', g.proposal.value ? (T.prop_on || 'on') : (T.prop_off || 'off'))
     517          .replace('{status}', T['prop_st_' + g.proposal.status] || g.proposal.status)));
     518    }
     519    if (g.blockedBy) {
     520      row.appendChild(el('p', 'small g-empty', (T.gate_blocked || 'needs {what} first')
     521        .replace('{what}', gateLabel(g.blockedBy))));
     522    }
     523    if (g.adjustable) {
     524      row.appendChild(gateButton(w, g.feature, g.value, T.gate_propose || T.embeds_propose,
     525        T.prop_on || 'on', T.prop_off || 'off'));
     526    }
     527    return row;
     528  }
     529
    353530  function wardPanel(w) {
    354531    var uri = w.other_uri;
     
    358535    var set = el('div', 'g-panel-sec');
    359536    set.appendChild(el('h3', null, T.settings_title || 'Settings'));
    360     var setRow = el('div', 'row');
    361     setRow.appendChild(gateButton(w, 'shaer:externalEmbeds', w.embeds, T.embeds_propose, T.embeds_on, T.embeds_off));
    362     // The heavier sibling (5.6): seeing that a video exists is one decision,
    363     // letting a third party's player run inside the app is another. Hidden
    364     // only when previews are known-OFF: for a ward on another server the
    365     // value is unknown, and unknown is not off. Hiding it there meant a
    366     // guardian elsewhere could never even propose playback, which is how a
    367     // whole proposal round went into the wrong gate. The ward's server
    368     // enforces play-needs-previews at serve time regardless.
    369     if (w.embeds !== false) {
    370       setRow.appendChild(gateButton(w, 'shaer:externalPlayback', w.playback, T.play_propose, T.play_on, T.play_off));
    371     }
    372     set.appendChild(setRow);
    373     // What this guardian proposed and how it stands (5.6). This used to be a
    374     // button caption that vanished on refresh, so a running decision was
    375     // invisible: you could not tell "waiting", "done" and "expired" apart.
    376     (w.proposals || []).forEach(function (p) {
    377       var what = p.feature === 'shaer:externalPlayback' ? (T.prop_play || 'playback') : (T.prop_embeds || 'link previews');
    378       var line = (T.prop_line || 'Proposal {what} {value}: {status}')
    379         .replace('{what}', what)
    380         .replace('{value}', p.value ? (T.prop_on || 'on') : (T.prop_off || 'off'))
    381         .replace('{status}', T['prop_st_' + p.status] || p.status);
    382       set.appendChild(el('p', 'small g-prop g-prop-' + p.status, line));
    383     });
     537    // Een rij per gate, uit de catalogus van de server (shaer-ahy.1). Losse
     538    // knoppen lieten een guardian zelf uitzoeken wat er allemaal geldt, en wat
     539    // niet verstelbaar is stond nergens -- terwijl dat de helft van het antwoord
     540    // is op "wat mag dit kind".
     541    (w.gates || []).forEach(function (g) { set.appendChild(gateRow(w, g)); });
     542    // Terugval voor een server die de catalogus nog niet stuurt: dan de twee
     543    // knoppen zoals ze waren, zodat een oudere Klonkt niet met een leeg vak zit.
     544    if (!(w.gates || []).length) {
     545      var setRow = el('div', 'row');
     546      setRow.appendChild(gateButton(w, 'shaer:externalEmbeds', w.embeds, T.embeds_propose, T.embeds_on, T.embeds_off));
     547      if (w.embeds !== false) {
     548        setRow.appendChild(gateButton(w, 'shaer:externalPlayback', w.playback, T.play_propose, T.play_on, T.play_off));
     549      }
     550      set.appendChild(setRow);
     551      (w.proposals || []).forEach(function (p) {
     552        var what = p.feature === 'shaer:externalPlayback' ? (T.prop_play || 'playback') : (T.prop_embeds || 'link previews');
     553        set.appendChild(el('p', 'small g-prop g-prop-' + p.status, (T.prop_line || 'Proposal {what} {value}: {status}')
     554          .replace('{what}', what).replace('{value}', p.value ? (T.prop_on || 'on') : (T.prop_off || 'off'))
     555          .replace('{status}', T['prop_st_' + p.status] || p.status)));
     556      });
     557    }
    384558    panel.appendChild(set);
    385559
     
    420594      T.panel_follow_empty || '', followCard);
    421595
     596    // Hier juist de VOLLEDIGE geschiedenis van dit kind -- dat is waar dit
     597    // paneel voor is. Wel wat nog wacht bovenaan, want dat is waar je naar zoekt
     598    // als je het opent.
    422599    sectionInto(panel, T.panel_help || 'Calls for help',
    423       (S.help || []).filter(function (h) { return h.actor_uri === uri; }),
     600      (S.help || []).filter(function (h) { return h.actor_uri === uri; })
     601        .sort(function (a, b) { return (helpOpen(b) ? 1 : 0) - (helpOpen(a) ? 1 : 0); }),
    424602      T.panel_help_empty || '', helpCard);
    425603
     
    453631    actRow.appendChild(rel);
    454632    act.appendChild(actRow);
    455     panel.appendChild(act);
     633    // Bovenaan, niet onderaan: zwaaien en loslaten zijn de dingen die je DOET.
     634    // De rest van het paneel is lezen -- instellingen, wie er nog meer op let,
     635    // wat er binnenkwam. Wie het paneel opent om iets te doen hoorde eerst langs
     636    // vijf secties te scrollen.
     637    panel.insertBefore(act, panel.firstChild);
    456638    return panel;
    457639  }
     
    467649      row.appendChild(el('span', 'who grow', handleOf(uri, w.other_handle)));
    468650      // Counts on the row: whatever is waiting must be visible with the panel shut.
    469       var nHelp = (S.help || []).filter(function (h) { return h.actor_uri === uri; }).length;
     651      // Alleen wat nog WACHT, zoals het commentaar hierboven al zei: een
     652      // afgehandelde vraag hoorde de boei niet te laten staan.
     653      var nHelp = (S.help || []).filter(function (h) { return h.actor_uri === uri && helpOpen(h); }).length;
    470654      var nFollow = FOLLOWS.filter(function (f) { return f.wardUri === uri; }).length;
    471655      if (nHelp) row.appendChild(el('span', 'tag help', '🛟 ' + nHelp));
     
    553737    card.appendChild(head);
    554738    var body = el('div', 'feed-body');
     739    // body_html is de gedeelde note-body-partial, serverside gerenderd: opmaak,
     740    // media, quote-kaart en embed, precies als in de Krant en in Berichten.
     741    // Valt terug op de kale content voor een client uit de cache.
     742    var html = p.body_html || p.content || '';
    555743    if (p.cw) {
     744      // De content warning blijft van de PWA zelf: note-body versluiert alleen
     745      // bij nsfw, en een ward-post met alleen een cw hoort hier dicht te staan.
    556746      var d = document.createElement('details');
    557747      var sum = document.createElement('summary'); sum.textContent = p.cw; d.appendChild(sum);
    558       var inner = el('div'); inner.innerHTML = p.content || ''; d.appendChild(inner);
     748      var inner = el('div'); inner.innerHTML = html; d.appendChild(inner);
    559749      body.appendChild(d);
    560750    } else {
    561       body.innerHTML = p.content || '';   // server-sanitized HTML (same as Berichten)
     751      body.innerHTML = html;   // server-sanitized HTML (same as Berichten)
    562752    }
    563753    card.appendChild(body);
     
    700890
    701891  renderAll(); pushState(); loadFeed(); loadFollowReqs();
    702   setInterval(refresh, 45000);   // live-ish while open
     892
     893  // De push die de melding brengt is meteen het teken dat de staat veranderd is.
     894  // Daarmee hoeft er geen tweede, open verbinding bij: hetzelfde kanaal doet het
     895  // werk, en het werkt ook als de app dicht is.
     896  if ('serviceWorker' in navigator) {
     897    navigator.serviceWorker.addEventListener('message', function (e) {
     898      if (e.data && e.data.klonkt === 'push') refresh();
     899    });
     900  }
     901  // Het tikje blijft als vangnet -- niet elke verandering geeft een melding, en
     902  // niet iedereen heeft meldingen aanstaan. Maar niet tikken terwijl niemand
     903  // kijkt: dat waren verzoeken voor een tabblad op de achtergrond. Bij terugkomen
     904  // meteen een keer, want dan is de kans op nieuws het grootst.
     905  setInterval(function () { if (!document.hidden) refresh(); }, 45000);
     906  document.addEventListener('visibilitychange', function () { if (!document.hidden) refresh(); });
    703907  } catch (e) {
    704908    fatal((e && e.message) || String(e));
Note: See TracChangeset for help on using the changeset viewer.