Changeset f3a2556 in Klonkt


Ignore:
Timestamp:
08/08/2026 06:35:21 AM (5 weeks ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
2bfe26c
Parents:
cf5073b
git-author:
Robin <roboburr@…> (08/08/2026 06:35:19 AM)
git-committer:
roboburr <roboburr@…> (08/08/2026 06:35:21 AM)
Message:

De apps kunnen nu zien wat er voor een ward gated is (shaer-ahy.1)

Bart: iOS en Android op hetzelfde plan als de PWA. Dit is het blokkerende stuk --
zonder dit kan geen app een poort tonen.

wardsCollection gaf {id, handle, since}. Een app kon een ward dus wel TONEN maar
niets over hem zeggen, en dat is precies de helft van het antwoord op "wat mag
dit kind". Er zit nu shaer:gates bij: dezelfde rijen als het PWA-paneel, met
soort, drempel, lopend voorstel en omkeerbaarheid.

UIT DEZELFDE FUNCTIE, en dat is de kern. wardGates stond in routes/guardian.js
en daar kon alleen de PWA erbij. Een tweede berekening ernaast zou vroeg of laat
een ander antwoord geven op dezelfde vraag -- geen schoonheidsfoutje, maar twee
guardians die een verschillend beeld van hetzelfde kind krijgen. Verplaatst naar
queues.js; beide schermen lezen er nu uit, en een toets vergelijkt de twee
uitkomsten letterlijk met deepEqual.

Suite 688/688; zonder de nieuwe regel vallen er twee om.

Files:
4 edited

Legend:

Unmodified
Added
Removed
  • src/routes/guardian.js

    rcf5073b rf3a2556  
    123123      embeds: wardEmbedSetting(w.other_uri),
    124124      playback: wardPlaybackSetting(w.other_uri),
    125       guardians: wardGuardianStatuses(w.other_uri),
     125      guardians: Guardianship.queues.wardGuardianStatuses(w.other_uri),
    126126      // What THIS guardian proposed for this ward and how it stands (5.6):
    127127      // open, accepted, rejected, or expired when the window ran out and the
     
    135135      // de drempel (shaer-ahy.1). Losse knoppen lieten een guardian zelf
    136136      // uitzoeken wat er allemaal geldt; wat niet verstelbaar is stond nergens.
    137       gates: wardGates(site.slug, w.other_uri),
     137      gates: Guardianship.queues.wardGates(site.slug, w.other_uri),
    138138    })),
    139139    offers: Guardianship.offersCollection(`${me}/queues/offers`, site.slug, me).orderedItems,
     
    155155}
    156156
    157 /** The guardians of a ward WE host, with availability (3.6.1: owner-only in
    158  *  spirit; the co-guardians are among the owners of the relationship). Null
    159  *  for a remote ward: its server tracks availability, not us. */
    160 function wardGuardianStatuses(wardUri) {
    161   const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
    162   if (!base || !String(wardUri || '').startsWith(`${base}/`)) return null;
    163   const slug = String(wardUri).trim().replace(/\/+$/, '').split('/').pop();
    164   try {
    165     const uris = Guardianship.listGuardians(slug).map((g) => ({ uri: g.other_uri, handle: g.other_handle }));
    166     const st = Object.fromEntries(
    167       Guardianship.availability.statusesFor(slug, uris.map((u) => u.uri), Date.now()).map((s) => [s.id, s]),
    168     );
    169     return uris.map((u) => ({
    170       uri: u.uri,
    171       handle: u.handle,
    172       availability: (st[u.uri] || {})['shaer:availability'] || 'active',
    173       awayUntil: (st[u.uri] || {})['shaer:awayUntil'] || null,
    174       lapse: (st[u.uri] || {})['shaer:lapse'] || null,
    175     }));
    176   } catch { return null; }
    177 }
    178157
    179158// ── The PWA page ─────────────────────────────────────────────────────────
     
    578557    return res.status(403).json({ error: 'not_my_ward' });
    579558  }
    580   const local = wardGuardianStatuses(uri);
     559  const local = Guardianship.queues.wardGuardianStatuses(uri);
    581560  if (local) return res.json({ local: true, guardians: local });
    582561  const doc = await AP.fetchActor(uri).catch(() => null);
     
    613592/** The playback gate of a ward we host (5.6): the heavier sibling. */
    614593function wardPlaybackSetting(uri) { return wardGateSetting(uri, 'external_playback'); }
    615 /**
    616  * De gate-rijen van een ward voor het paneel.
    617  *
    618  * De standen komen uit onze eigen kolommen als we het kind hosten; bij een ward
    619  * elders weten we ze niet en blijft het NULL -- onbekend, niet uit. Het aantal
    620  * guardians idem: dat wordt op de server van die ward bijgehouden, en zonder dat
    621  * getal wordt er geen drempel verzonnen.
    622  */
    623 function wardGates(mySlug, wardUri) {
    624   const statuses = wardGuardianStatuses(wardUri);
    625   const wachtend = Guardianship.follows.listReviewsByDirection(mySlug, 'incoming')
    626     .filter((r) => r.ward_uri === wardUri).length;
    627   return Guardianship.gated.gateRows({
    628     // Uit de BESLUITEN, niet uit onze eigen kolom. Er zijn geen lokale accounts:
    629     // elke ward woont elders, dus wardEmbedSetting() gaf voor iedere ward null en
    630     // stond er in het paneel overal "onbekend". Wat een guardian wel heeft is de
    631     // uitslag van wat hij voorstelde.
    632     settings: Object.fromEntries(Guardianship.gated.GATE_CATALOGUE
    633       .filter((g) => g.available !== false && Guardianship.gated.featureColumn(g.feature))
    634       .map((g) => [g.feature, Guardianship.gated.knownSetting(mySlug, wardUri, g.feature)])),
    635     guardianCount: statuses ? statuses.length : null,
    636     proposals: Guardianship.gated.listSent(mySlug, wardUri).map((p) => ({
    637       feature: p.feature, value: !!p.value, status: Guardianship.gated.sentStatus(p, Date.now()),
    638     })),
    639     waiting: { 'shaer:follows': wachtend || undefined },
    640   });
    641 }
    642594
    643595function wardGateSetting(uri, column) {
  • src/services/guardianship/index.js

    rcf5073b rf3a2556  
    3232// §5.6 gated settings (decided by the guardians, enforced by the ward's server)
    3333export * as gated from './gated.js';
     34export * as queues from './queues.js';
    3435// 5.2.1: wie er op een hulpvraag af is en wanneer hij is afgesloten (shaer-lgo)
    3536export * as help from './help.js';
  • src/services/guardianship/queues.js

    rcf5073b rf3a2556  
    1414import * as outgoing from './outgoing.js';
    1515import * as follows from './follows.js';
     16import * as gated from './gated.js';
    1617import * as handshake from './handshake.js';
    1718
     
    9495export function wardsCollection(id, slug) {
    9596  const items = relations.listWards(slug)
    96     .map((r) => ({ id: r.other_uri, 'shaer:handle': r.other_handle || undefined, since: r.created_at }));
     97    .map((r) => ({
     98      id: r.other_uri,
     99      'shaer:handle': r.other_handle || undefined,
     100      since: r.created_at,
     101      // Alles wat voor dit kind gated is, met soort, drempel en lopend voorstel
     102      // (shaer-ahy.1). Zonder dit kon een app wel een ward TONEN maar niets over
     103      // hem zeggen -- en dat is precies de helft van het antwoord op "wat mag
     104      // dit kind". Dezelfde rijen als het PWA-paneel, uit dezelfde functie.
     105      'shaer:gates': wardGates(slug, r.other_uri),
     106    }));
    97107  return collection(id, items);
    98108}
     
    105115}
    106116
    107 export default { offersCollection, followsCollection, outgoingFollowsCollection, wardsCollection, guardiansCollection };
     117export default { offersCollection, followsCollection, outgoingFollowsCollection, wardsCollection, guardiansCollection, wardGates, wardGuardianStatuses };
     118
     119// ── Wat er voor een ward gated is (shaer-ahy.1) ─────────────────────────
     120//
     121// STOND IN routes/guardian.js, en daar kon alleen de PWA erbij. De Shaer-apps
     122// lezen dezelfde toestand via de wards-queue, en een tweede berekening naast
     123// deze zou vroeg of laat een ander antwoord geven op dezelfde vraag -- dat is
     124// hier geen schoonheidsfoutje maar twee guardians die een verschillend beeld
     125// van hetzelfde kind krijgen. Een plek dus, en beide schermen lezen eruit.
     126/** The guardians of a ward WE host, with availability (3.6.1: owner-only in
     127 *  spirit; the co-guardians are among the owners of the relationship). Null
     128 *  for a remote ward: its server tracks availability, not us. */
     129export function wardGuardianStatuses(wardUri) {
     130  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     131  if (!base || !String(wardUri || '').startsWith(`${base}/`)) return null;
     132  const slug = String(wardUri).trim().replace(/\/+$/, '').split('/').pop();
     133  try {
     134    const uris = relations.listGuardians(slug).map((g) => ({ uri: g.other_uri, handle: g.other_handle }));
     135    const st = Object.fromEntries(
     136      availability.statusesFor(slug, uris.map((u) => u.uri), Date.now()).map((s) => [s.id, s]),
     137    );
     138    return uris.map((u) => ({
     139      uri: u.uri,
     140      handle: u.handle,
     141      availability: (st[u.uri] || {})['shaer:availability'] || 'active',
     142      awayUntil: (st[u.uri] || {})['shaer:awayUntil'] || null,
     143      lapse: (st[u.uri] || {})['shaer:lapse'] || null,
     144    }));
     145  } catch { return null; }
     146}
     147/**
     148 * De gate-rijen van een ward voor het paneel.
     149 *
     150 * De standen komen uit onze eigen kolommen als we het kind hosten; bij een ward
     151 * elders weten we ze niet en blijft het NULL -- onbekend, niet uit. Het aantal
     152 * guardians idem: dat wordt op de server van die ward bijgehouden, en zonder dat
     153 * getal wordt er geen drempel verzonnen.
     154 */
     155export function wardGates(mySlug, wardUri) {
     156  const statuses = wardGuardianStatuses(wardUri);
     157  const wachtend = follows.listReviewsByDirection(mySlug, 'incoming')
     158    .filter((r) => r.ward_uri === wardUri).length;
     159  return gated.gateRows({
     160    // Uit de BESLUITEN, niet uit onze eigen kolom. Er zijn geen lokale accounts:
     161    // elke ward woont elders, dus wardEmbedSetting() gaf voor iedere ward null en
     162    // stond er in het paneel overal "onbekend". Wat een guardian wel heeft is de
     163    // uitslag van wat hij voorstelde.
     164    settings: Object.fromEntries(gated.GATE_CATALOGUE
     165      .filter((g) => g.available !== false && gated.featureColumn(g.feature))
     166      .map((g) => [g.feature, gated.knownSetting(mySlug, wardUri, g.feature)])),
     167    guardianCount: statuses ? statuses.length : null,
     168    proposals: gated.listSent(mySlug, wardUri).map((p) => ({
     169      feature: p.feature, value: !!p.value, status: gated.sentStatus(p, Date.now()),
     170    })),
     171    waiting: { 'shaer:follows': wachtend || undefined },
     172  });
     173}
  • test/guardian-gate-panel.test.js

    rcf5073b rf3a2556  
    145145  }
    146146});
     147
     148// ── Dezelfde poorten voor het paneel en voor de apps (shaer-ahy.1) ───────
     149
     150test('de wards-queue draagt de poorten mee', async () => {
     151  // Zonder dit kon een app een ward wel TONEN maar niets over hem zeggen. De
     152  // apps lezen deze collectie; het PWA-paneel leest wardGates rechtstreeks.
     153  const q = await import('../src/services/guardianship/queues.js');
     154  const rel = await import('../src/services/guardianship/relations.js');
     155  const dbm = await import('../src/config/database.js');
     156  dbm.initializeDatabase();
     157  dbm.default.prepare('INSERT OR IGNORE INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
     158    .run('gu', 'gu', 'gu@t', 'x', 'god');
     159  dbm.default.prepare('INSERT OR IGNORE INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('gs', 'oma', 'Oma', 'gu');
     160  rel.commitWardForGuardian('oma', 'https://elders/ap/users/kind', { handle: '@kind' });
     161
     162  const coll = q.wardsCollection('https://x/queues/wards', 'oma');
     163  const ward = coll.orderedItems.find((w) => w.id === 'https://elders/ap/users/kind');
     164  assert.ok(ward, 'de ward staat in de collectie');
     165  assert.ok(Array.isArray(ward['shaer:gates']), 'en draagt zijn poorten');
     166  assert.equal(ward['shaer:gates'].length, gated.GATE_CATALOGUE.length);
     167});
     168
     169test('de app krijgt PRECIES dezelfde rijen als het paneel', async () => {
     170  // Twee berekeningen naast elkaar geven vroeg of laat een ander antwoord op
     171  // dezelfde vraag, en dat is hier geen schoonheidsfoutje: dan krijgen twee
     172  // guardians een verschillend beeld van hetzelfde kind.
     173  const q = await import('../src/services/guardianship/queues.js');
     174  const coll = q.wardsCollection('https://x/queues/wards', 'oma');
     175  const uitQueue = coll.orderedItems.find((w) => w.id === 'https://elders/ap/users/kind')['shaer:gates'];
     176  const uitPaneel = q.wardGates('oma', 'https://elders/ap/users/kind');
     177  assert.deepEqual(uitQueue, uitPaneel);
     178});
Note: See TracChangeset for help on using the changeset viewer.