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/gated.js

    r6100ce9 rd56d471  
    182182export function clearGatedReviews(id) { rstmts().delAll.run(id); }
    183183
    184 export function rememberGatedOffer(offerId, slug, feature, value) {
     184export function rememberGatedOffer(offerId, slug, feature, value, proposer) {
    185185  try {
    186     db.prepare('INSERT OR REPLACE INTO ap_gated_offers (offer_id, slug, feature, value) VALUES (?,?,?,?)')
    187       .run(offerId, slug, feature, value ? 1 : 0);
     186    db.prepare('INSERT OR REPLACE INTO ap_gated_offers (offer_id, slug, feature, value, proposer) VALUES (?,?,?,?,?)')
     187      .run(offerId, slug, feature, value ? 1 : 0, proposer || null);
    188188  } catch { /* non-fatal */ }
    189189}
     
    192192  try { return db.prepare('SELECT * FROM ap_gated_offers WHERE offer_id = ?').get(offerId) || null; }
    193193  catch { return null; }
     194}
     195
     196// ── The proposer's own record (5.6) ───────────────────────────────
     197// "Where did my proposal go?" had no answer: the status was a button caption
     198// that did not survive a refresh. The ward's server tallies elsewhere, so the
     199// proposer keeps its own row and the ward's server ANSWERS the Offer when the
     200// decision settles: Accept when it settled on the proposed value, Reject when
     201// it settled on the opposite. An open row past the window renders as expired,
     202// because an expired decision settles on nothing and nobody writes home.
     203
     204export function recordSent(offerId, guardianSlug, wardUri, feature, value) {
     205  try {
     206    db.prepare(`INSERT OR REPLACE INTO ap_gated_sent (offer_id, guardian_slug, ward_uri, feature, value)
     207                VALUES (?,?,?,?,?)`).run(offerId, guardianSlug, wardUri, feature, value ? 1 : 0);
     208  } catch { /* non-fatal */ }
     209}
     210
     211export function recallSent(offerId) {
     212  try { return db.prepare('SELECT * FROM ap_gated_sent WHERE offer_id = ?').get(offerId) || null; }
     213  catch { return null; }
     214}
     215
     216export function settleSent(offerId, outcome) {
     217  try { db.prepare('UPDATE ap_gated_sent SET status = ? WHERE offer_id = ?').run(outcome, offerId); } catch { /* non-fatal */ }
     218}
     219
     220/** The latest proposal per feature this guardian sent to this ward. */
     221export function listSent(guardianSlug, wardUri) {
     222  try {
     223    return db.prepare(`SELECT * FROM ap_gated_sent WHERE guardian_slug = ? AND ward_uri = ?
     224                       GROUP BY feature HAVING MAX(created_at) ORDER BY created_at DESC`).all(guardianSlug, wardUri);
     225  } catch { return []; }
     226}
     227
     228/**
     229 * What a sent row means on a screen. Pure, so the rule is testable: an answer
     230 * wins, and silence past the window is not "still running", it is over.
     231 */
     232export function sentStatus(row, now) {
     233  if (!row) return null;
     234  if (row.status === 'accepted' || row.status === 'rejected') return row.status;
     235  const opened = new Date(String(row.created_at).includes('T') ? row.created_at : `${row.created_at}Z`.replace(' ', 'T')).getTime();
     236  if (Number.isFinite(opened) && now - opened >= GATED_WINDOW_MS) return 'expired';
     237  return 'open';
    194238}
    195239
     
    198242  parseGatedSetting, buildGatedOffer, rememberGatedOffer, recallGatedOffer,
    199243  recordGatedReview, getGatedReview, listGatedReviews, removeGatedReview, clearGatedReviews,
     244  recordSent, recallSent, settleSent, listSent, sentStatus,
    200245};
Note: See TracChangeset for help on using the changeset viewer.