Changeset d56d471 in Klonkt for src/routes/guardian.js


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/routes/guardian.js

    r6100ce9 rd56d471  
    5555    // A gated-setting proposal from a fellow guardian (5.6).
    5656    'gated_title', 'gated_line_on', 'gated_line_off', 'gated_agree', 'gated_disagree',
    57     'play_propose', 'play_on', 'play_off'];
     57    'play_propose', 'play_on', 'play_off',
     58    // The status of a proposal this guardian sent (5.6).
     59    'prop_line', 'prop_embeds', 'prop_play', 'prop_on', 'prop_off',
     60    'prop_st_open', 'prop_st_accepted', 'prop_st_rejected', 'prop_st_expired',
     61    'panel_guards_far'];
    5862  const s = Object.fromEntries(keys.map((k) => [k, i18nT(L, `guardian.${k}`)]));
    5963  s.wave = i18nT(L, 'guardian.wave');
     
    9397      playback: wardPlaybackSetting(w.other_uri),
    9498      guardians: wardGuardianStatuses(w.other_uri),
     99      // What THIS guardian proposed for this ward and how it stands (5.6):
     100      // open, accepted, rejected, or expired when the window ran out and the
     101      // ward's server had nothing to write home. The answer is a real
     102      // Accept/Reject from the ward's server, not a guess from here.
     103      proposals: Guardianship.gated.listSent(site.slug, w.other_uri).map((p) => ({
     104        feature: p.feature, value: !!p.value, created: p.created_at,
     105        status: Guardianship.gated.sentStatus(p, Date.now()),
     106      })),
    95107    })),
    96108    offers: Guardianship.offersCollection(`${me}/queues/offers`, site.slug, me).orderedItems,
     
    443455});
    444456
     457// ── The fellow guardians of a ward, wherever it lives ─────────────────────
     458// A guardian looking at a ward's panel should see who else holds a seat: that
     459// is the child's safety net, and "dit kind woont op een andere server" is not
     460// an answer. For a local ward the availability rides along (we do that
     461// bookkeeping). For a remote ward we read the PUBLIC membership from its
     462// actor document (shaer:guardians, §2.1) and nothing more: availability is
     463// the ward's server's private ledger (§3.6.1) and stays there. Fetched on
     464// panel-open rather than into the dashboard, so one slow remote server does
     465// not hold the whole screen hostage.
     466router.get('/wards/guardians', requireAuth, async (req, res) => {
     467  const site = siteForUser(req);
     468  if (!site) return res.status(404).json({ error: 'no_site' });
     469  const uri = String(req.query.uri || '').trim();
     470  if (!Guardianship.listWards(site.slug).some((w) => w.other_uri === uri)) {
     471    return res.status(403).json({ error: 'not_my_ward' });
     472  }
     473  const local = wardGuardianStatuses(uri);
     474  if (local) return res.json({ local: true, guardians: local });
     475  const doc = await AP.fetchActor(uri).catch(() => null);
     476  let g = doc && doc['shaer:guardians'];
     477  if (g && Array.isArray(g.items)) g = g.items;             // a Collection
     478  const guardians = (Array.isArray(g) ? g : (typeof g === 'string' ? [g] : []))
     479    .filter((x) => typeof x === 'string')
     480    .map((u) => {
     481      try { const p = new URL(u); return { uri: u, handle: `@${p.pathname.replace(/\/+$/, '').split('/').pop()}@${p.host}` }; }
     482      catch { return { uri: u, handle: u }; }
     483    });
     484  res.json({ local: false, guardians });
     485});
     486
    445487router.post('/wards/remove', requireAuth, express.json({ limit: '4kb' }), async (req, res) => {
    446488  const site = siteForUser(req);
     
    507549  // nothing else. The old shortcut recorded the vote here directly, which is
    508550  // how the remote path stayed broken for a month without anyone noticing.
     551  // Our own record of what we sent (5.6): the ward's server answers this Offer
     552  // once the decision settles, and that answer needs a row to land in. It is
     553  // also the only way the proposer's screen can say more than a button caption.
     554  Guardianship.gated.recordSent(offerId, site.slug, uri, feature, allow);
    509555  AP.deliverToActor(site, uri, offer).catch(() => { /* queued, best-effort */ });
    510556  const localSlug = (base && uri.startsWith(`${base}/`)) ? uri.replace(/\/+$/, '').split('/').pop() : null;
Note: See TracChangeset for help on using the changeset viewer.