Ignore:
Timestamp:
07/29/2026 12:36:50 PM (6 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
7a93fcf
Parents:
6100ce9
Message:

Een voorstel krijgt een antwoord, een status en een zichtbare kring

Robin meldde drie dingen na de voorstelronde: er lijkt niets te gebeuren, de
status van een voorstel is nergens te volgen, en in /guardian zie je niet wie
de mede-guardians van een kind zijn. Onderzoek op beta wees de kern aan: de
ronde is aangekomen en geteld, maar elk voorstel was shaer:externalEmbeds, en
die stond al aan. Afspelen is nooit voorgesteld, en dat KON ook niet: de knop
"Afspelen voorstellen" verscheen alleen bij embeds === true, en voor een ward
op een andere server is die waarde onbekend, dus null, dus geen knop. Onbekend
is niet uit.

Drie reparaties, een per klacht.

Een: de lus sluit. De server van het kind beantwoordt de Offer die de
beslissing opende, terug naar de voorsteller: Accept als hij settelde op het
voorgestelde, Reject bij het tegendeel. Alleen de stem van het kind-account
telt als uitkomst; een vreemde die onze boeken wil sluiten wordt genegeerd.
Zonder dit kon het scherm van de voorsteller alleen maar eeuwig "wacht"
zeggen, wat er ook gebeurd was: de telling is het prive-grootboek van de
server van het kind.

Twee: de voorsteller houdt een eigen boek bij (ap_gated_sent) en het paneel
toont per ward de stand: wacht, aangenomen, afgewezen, of eerlijk verlopen als
het venster leegliep. Stilte na het venster is geen "loopt nog".

Drie: voor een ward op een andere server toont het paneel nu wel wie de
guardians zijn. Het lidmaatschap is publiek op het actordocument
(shaer:guardians, 2.1); de beschikbaarheid is en blijft het prive-grootboek
van hun server (3.6.1) en wordt alleen benoemd, niet getoond.

Changed files:
src/config/database.js

  • tabel ap_gated_sent (het boek van de voorsteller)
  • kolom proposer op ap_gated_offers, voor het antwoord naar huis

src/services/guardianship/gated.js

  • recordSent/recallSent/settleSent/listSent en sentStatus (puur, getest)
  • rememberGatedOffer onthoudt de voorsteller

src/services/guardianship/handshake.js

  • answerGatedProposer: het settle-antwoord naar de voorsteller
  • de inbox herkent dat antwoord en settelt het eigen boek; alleen het kind-account mag dat

src/routes/guardian.js

  • proposeGated schrijft het boek; dashboardState serveert per ward de voorstellen met status
  • GET /wards/guardians: lokaal met beschikbaarheid, remote de publieke lijst van hun actordocument

src/assets/js/guardian.js

  • de afspeel-knop verschijnt ook bij onbekende embeds-stand (de bug)
  • statusregels onder de instellingen-knoppen
  • remote guardians opgehaald bij het openen van het paneel

src/assets/css/guardian.css

  • g-prop-kleuren: de staat spreekt voor de woorden uit

src/services/i18n.js

  • prop_*- en panel_guards_far-strings, NL/EN/DE

test/gated-settings.test.js

  • de lus sluit: de voorsteller krijgt het antwoord, van alleen het kind
  • het boek: open, aangenomen, afgewezen, verlopen

remarks: 334 tests groen, server start op een schone database. De guardian-kant
(sound-fabrics) en de ward-kant (beta) hebben allebei deze commit nodig: de
knop en de status leven bij de guardian, het antwoord bij het kind.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/guardianship/handshake.js

    r6100ce9 rd56d471  
    8181  }
    8282  return anyDelivered;
     83}
     84
     85/**
     86 * §5.6, the closing of the loop: a settled gated decision answers the Offer
     87 * that opened it. Accept when it settled on the proposed value, Reject when on
     88 * the opposite. Without this the proposer's screen can only ever say
     89 * "waiting", forever, whatever actually happened: the tally lives on the
     90 * ward's server and nobody else may read it, so the ward's server must speak.
     91 */
     92function answerGatedProposer(site, offerId, r) {
     93  const o = gated.recallGatedOffer(offerId);
     94  if (!o || !o.proposer) return;
     95  const me = deps.selfId(site.slug);
     96  if (o.proposer === me) return;   // the ward proposed to itself: nothing to write home
     97  const agreed = r.value === !!o.value;
     98  deps.deliverTo(site, o.proposer, {
     99    id: `${me}#gatedanswer-${Date.now().toString(36)}${Math.floor(Math.random() * 1e4).toString(36)}`,
     100    type: agreed ? 'Accept' : 'Reject',
     101    actor: me, to: [o.proposer], object: offerId,
     102  }).catch(() => { /* the delivery queue retries */ });
    83103}
    84104
     
    301321      // ── I am the WARD: record, tally, and forward to the other guardians.
    302322      if (gs.ward === me) {
    303         gated.rememberGatedOffer(offerId, site.slug, gs.feature, gs.value);
     323        gated.rememberGatedOffer(offerId, site.slug, gs.feature, gs.value, actor);
    304324        // The proposer's Offer carries its own agreement (§3.1's one-step clause).
    305325        const r = gated.recordGatedVote(site.slug, gs.feature, actor, gs.value);
     
    326346        } else {
    327347          gated.clearGatedReviews(offerId);   // settled at once: nothing left to ask
     348          answerGatedProposer(site, offerId, r);
    328349        }
    329350        notify(site.slug, { kind: 'gated_setting', feature: gs.feature, value: gs.value, state: r.state });
     
    385406  // Accept / Reject of an offer we (also) track.
    386407  const offerId = idOf(activity.object);
     408  // §5.6, the answer coming HOME: the ward's server settled a decision we
     409  // proposed and answers our Offer. Accept = it settled on what we proposed,
     410  // Reject = on the opposite. Only the ward may say so: the answer must come
     411  // from the ward the proposal was about, or anyone could close our books.
     412  const sent = gated.recallSent(offerId);
     413  if (sent && sent.guardian_slug === site.slug) {
     414    if (actor !== sent.ward_uri) return false;   // not the ward's voice: not an outcome
     415    const outcome = type === 'Accept' ? 'accepted' : 'rejected';
     416    gated.settleSent(offerId, outcome);
     417    notify(site.slug, { kind: 'gated_outcome', feature: sent.feature, value: !!sent.value, outcome, ward: sent.ward_uri });
     418    return true;
     419  }
    387420  // §5.6: a fellow guardian answering a gated-setting proposal. The Accept only
    388421  // references the offer, so the value comes from the proposal we stored. A
     
    392425    const value = type === 'Accept' ? !!gsOffer.value : !gsOffer.value;
    393426    const r = gated.recordGatedVote(site.slug, gsOffer.feature, actor, value);
     427    if (r.state === 'settled') answerGatedProposer(site, offerId, r);
    394428    notify(site.slug, { kind: 'gated_setting', feature: gsOffer.feature, value, state: r.state });
    395429    return true;
Note: See TracChangeset for help on using the changeset viewer.