| 1 | /**
|
|---|
| 2 | * Guardianship (FEP-633c) — the owner-only dashboard queues.
|
|---|
| 3 | *
|
|---|
| 4 | * Three OrderedCollections on the actor (shaer:queues), same contract as the
|
|---|
| 5 | * Shaer test daemon so the iOS/Android dashboards read them as-is:
|
|---|
| 6 | * - offers: pending handshake offers where I am a party (§3), with the full
|
|---|
| 7 | * accept tally so the client shows the right action
|
|---|
| 8 | * - follows: pending gated follows ON my wards (§5.3), Fase 2 (shaer-jdb)
|
|---|
| 9 | * - wards: my committed wards
|
|---|
| 10 | */
|
|---|
| 11 | import * as offers from './offers.js';
|
|---|
| 12 | import * as relations from './relations.js';
|
|---|
| 13 | import * as availability from './availability.js';
|
|---|
| 14 | import * as outgoing from './outgoing.js';
|
|---|
| 15 | import * as follows from './follows.js';
|
|---|
| 16 | import * as gated from './gated.js';
|
|---|
| 17 | import * as gatereq from './gatereq.js';
|
|---|
| 18 | import * as handshake from './handshake.js';
|
|---|
| 19 |
|
|---|
| 20 | const collection = (id, items) => ({
|
|---|
| 21 | id, type: 'OrderedCollection', totalItems: items.length, orderedItems: items,
|
|---|
| 22 | });
|
|---|
| 23 |
|
|---|
| 24 | /** Pending offers where the local site is a party, each with its accept
|
|---|
| 25 | * tally. The same collection carries the running lapses (§3.6.3) this
|
|---|
| 26 | * account is a party to, exactly as the daemon serves them, so the Shaer
|
|---|
| 27 | * clients render both without a second fetch. */
|
|---|
| 28 | export function offersCollection(id, slug, me) {
|
|---|
| 29 | // §4.2: a handshake whose candidate could not be dereferenced is deferred,
|
|---|
| 30 | // not decided, and the last Accept may already have landed — so nothing else
|
|---|
| 31 | // would ever retry it. This poll is the schedule. Not awaited: the read
|
|---|
| 32 | // answers with what is true now, and a retry that succeeds surfaces in the
|
|---|
| 33 | // next one. `listForParty` settles closed windows on the way past.
|
|---|
| 34 | handshake.retryDeferred(slug).catch(() => { /* the next read tries again */ });
|
|---|
| 35 | const items = offers.listForParty(slug, me).map((o) => offers.queueItem(o, me));
|
|---|
| 36 | items.push(...availability.lapseQueueItems(slug, me, Date.now()));
|
|---|
| 37 | return collection(id, items);
|
|---|
| 38 | }
|
|---|
| 39 |
|
|---|
| 40 | /**
|
|---|
| 41 | * Gate-verzoeken OP mijn wards die op mijn antwoord wachten (Guardianship Fase 2,
|
|---|
| 42 | * shaer-jdb). Dit was een lege stub: de gating zelf werkt sinds shaer-hxg, maar
|
|---|
| 43 | * werd nooit aan een C2S-client doorgegeven omdat de koers toen op de PWA lag.
|
|---|
| 44 | *
|
|---|
| 45 | * Twee bronnen, want een guardian kan wards op andere servers hebben en (nog)
|
|---|
| 46 | * op deze:
|
|---|
| 47 | * - ap_follow_reviews: de doorgestuurde kopie van een REMOTE ward
|
|---|
| 48 | * - ap_pending_follows: een ward op deze instance
|
|---|
| 49 | * Zie shaer-h6u: die tweede hoort op termijn ook over de lijn te gaan.
|
|---|
| 50 | */
|
|---|
| 51 | export function followsCollection(id, slug, me) {
|
|---|
| 52 | const items = follows.listReviewsByDirection(slug, 'incoming')
|
|---|
| 53 | .map((r) => follows.reviewQueueItem(r, me));
|
|---|
| 54 | for (const w of relations.listWards(slug)) {
|
|---|
| 55 | const wardSlug = slugOf(w.other_uri);
|
|---|
| 56 | if (!wardSlug) continue;
|
|---|
| 57 | for (const p of follows.listForWard(wardSlug)) {
|
|---|
| 58 | items.push({
|
|---|
| 59 | id: p.id, type: 'Follow', actor: p.follower_uri, object: w.other_uri,
|
|---|
| 60 | 'shaer:direction': 'incoming', 'shaer:ward': w.other_uri,
|
|---|
| 61 | 'shaer:follower': p.follower_uri, 'shaer:followerHandle': p.follower_handle || undefined,
|
|---|
| 62 | 'shaer:quorum': p.quorum || 'any', published: p.created_at,
|
|---|
| 63 | });
|
|---|
| 64 | }
|
|---|
| 65 | }
|
|---|
| 66 | return collection(id, items);
|
|---|
| 67 | }
|
|---|
| 68 |
|
|---|
| 69 | /** De slug van een actor-uri op DEZE instance, of null als hij elders woont. */
|
|---|
| 70 | function slugOf(uri) {
|
|---|
| 71 | const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
|
|---|
| 72 | if (!base || !String(uri || '').startsWith(`${base}/ap/users/`)) return null;
|
|---|
| 73 | return decodeURIComponent(String(uri).slice(`${base}/ap/users/`.length).split(/[/?#]/)[0]) || null;
|
|---|
| 74 | }
|
|---|
| 75 |
|
|---|
| 76 | /**
|
|---|
| 77 | * §5.3 uitgaand. Twee lezers, een wachtrij, en dat kan omdat §1 een ward en een
|
|---|
| 78 | * guardian wederzijds uitsluit: je bent het een of het ander.
|
|---|
| 79 | *
|
|---|
| 80 | * ALS WARD wat IK wil volgen en waar mijn guardians nog over moeten
|
|---|
| 81 | * ALS GUARDIAN wat mijn WARDS willen volgen en waar IK over moet (shaer-jdb)
|
|---|
| 82 | *
|
|---|
| 83 | * Dat tweede ontbrak. De wachtrij serveerde alleen listForWard(slug), en voor
|
|---|
| 84 | * een guardian is dat per definitie leeg -- dus het scherm "Your wards want to
|
|---|
| 85 | * follow" kon nooit iets tonen.
|
|---|
| 86 | */
|
|---|
| 87 | export function outgoingFollowsCollection(id, slug, me) {
|
|---|
| 88 | const items = outgoing.listForWard(slug).map((o) => outgoing.queueItem(o, me));
|
|---|
| 89 | for (const r of follows.listReviewsByDirection(slug, 'outgoing')) {
|
|---|
| 90 | items.push(follows.reviewQueueItem(r, me));
|
|---|
| 91 | }
|
|---|
| 92 | return collection(id, items);
|
|---|
| 93 | }
|
|---|
| 94 |
|
|---|
| 95 | /** The guardian's committed wards, with cached handle for display. */
|
|---|
| 96 | export function wardsCollection(id, slug) {
|
|---|
| 97 | const items = relations.listWards(slug)
|
|---|
| 98 | .map((r) => ({
|
|---|
| 99 | id: r.other_uri,
|
|---|
| 100 | 'shaer:handle': r.other_handle || undefined,
|
|---|
| 101 | since: r.created_at,
|
|---|
| 102 | // Alles wat voor dit kind gated is, met soort, drempel en lopend voorstel
|
|---|
| 103 | // (shaer-ahy.1). Zonder dit kon een app wel een ward TONEN maar niets over
|
|---|
| 104 | // hem zeggen -- en dat is precies de helft van het antwoord op "wat mag
|
|---|
| 105 | // dit kind". Dezelfde rijen als het PWA-paneel, uit dezelfde functie.
|
|---|
| 106 | 'shaer:gates': wardGates(slug, r.other_uri),
|
|---|
| 107 | }));
|
|---|
| 108 | return collection(id, items);
|
|---|
| 109 | }
|
|---|
| 110 |
|
|---|
| 111 | /** The ward's guardians with their availability (§3.6.1: never public,
|
|---|
| 112 | * owner-only): the real size of the safety net. Same shape as the daemon. */
|
|---|
| 113 | export function guardiansCollection(id, slug) {
|
|---|
| 114 | const uris = relations.listGuardians(slug).map((r) => r.other_uri);
|
|---|
| 115 | return collection(id, availability.statusesFor(slug, uris, Date.now()));
|
|---|
| 116 | }
|
|---|
| 117 |
|
|---|
| 118 | export default { offersCollection, followsCollection, outgoingFollowsCollection, wardsCollection, guardiansCollection, wardGates, wardGuardianStatuses };
|
|---|
| 119 |
|
|---|
| 120 | // ── Wat er voor een ward gated is (shaer-ahy.1) ─────────────────────────
|
|---|
| 121 | //
|
|---|
| 122 | // STOND IN routes/guardian.js, en daar kon alleen de PWA erbij. De Shaer-apps
|
|---|
| 123 | // lezen dezelfde toestand via de wards-queue, en een tweede berekening naast
|
|---|
| 124 | // deze zou vroeg of laat een ander antwoord geven op dezelfde vraag -- dat is
|
|---|
| 125 | // hier geen schoonheidsfoutje maar twee guardians die een verschillend beeld
|
|---|
| 126 | // van hetzelfde kind krijgen. Een plek dus, en beide schermen lezen eruit.
|
|---|
| 127 | /** The guardians of a ward WE host, with availability (3.6.1: owner-only in
|
|---|
| 128 | * spirit; the co-guardians are among the owners of the relationship). Null
|
|---|
| 129 | * for a remote ward: its server tracks availability, not us. */
|
|---|
| 130 | export function wardGuardianStatuses(wardUri) {
|
|---|
| 131 | const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
|
|---|
| 132 | if (!base || !String(wardUri || '').startsWith(`${base}/`)) return null;
|
|---|
| 133 | const slug = String(wardUri).trim().replace(/\/+$/, '').split('/').pop();
|
|---|
| 134 | try {
|
|---|
| 135 | const uris = relations.listGuardians(slug).map((g) => ({ uri: g.other_uri, handle: g.other_handle }));
|
|---|
| 136 | const st = Object.fromEntries(
|
|---|
| 137 | availability.statusesFor(slug, uris.map((u) => u.uri), Date.now()).map((s) => [s.id, s]),
|
|---|
| 138 | );
|
|---|
| 139 | return uris.map((u) => ({
|
|---|
| 140 | uri: u.uri,
|
|---|
| 141 | handle: u.handle,
|
|---|
| 142 | availability: (st[u.uri] || {})['shaer:availability'] || 'active',
|
|---|
| 143 | awayUntil: (st[u.uri] || {})['shaer:awayUntil'] || null,
|
|---|
| 144 | lapse: (st[u.uri] || {})['shaer:lapse'] || null,
|
|---|
| 145 | }));
|
|---|
| 146 | } catch { return null; }
|
|---|
| 147 | }
|
|---|
| 148 | /**
|
|---|
| 149 | * De gate-rijen van een ward voor het paneel.
|
|---|
| 150 | *
|
|---|
| 151 | * De standen komen uit onze eigen kolommen als we het kind hosten; bij een ward
|
|---|
| 152 | * elders weten we ze niet en blijft het NULL -- onbekend, niet uit. Het aantal
|
|---|
| 153 | * guardians idem: dat wordt op de server van die ward bijgehouden, en zonder dat
|
|---|
| 154 | * getal wordt er geen drempel verzonnen.
|
|---|
| 155 | */
|
|---|
| 156 | export function wardGates(mySlug, wardUri) {
|
|---|
| 157 | const statuses = wardGuardianStatuses(wardUri);
|
|---|
| 158 | const wachtend = follows.listReviewsByDirection(mySlug, 'incoming')
|
|---|
| 159 | .filter((r) => r.ward_uri === wardUri).length;
|
|---|
| 160 | return gated.gateRows({
|
|---|
| 161 | // Uit de BESLUITEN, niet uit onze eigen kolom. Er zijn geen lokale accounts:
|
|---|
| 162 | // elke ward woont elders, dus wardEmbedSetting() gaf voor iedere ward null en
|
|---|
| 163 | // stond er in het paneel overal "onbekend". Wat een guardian wel heeft is de
|
|---|
| 164 | // uitslag van wat hij voorstelde.
|
|---|
| 165 | settings: Object.fromEntries(gated.GATE_CATALOGUE
|
|---|
| 166 | .filter((g) => g.available !== false && gated.featureColumn(g.feature))
|
|---|
| 167 | .map((g) => [g.feature, gated.knownSetting(mySlug, wardUri, g.feature)])),
|
|---|
| 168 | guardianCount: statuses ? statuses.length : null,
|
|---|
| 169 | proposals: gated.listSent(mySlug, wardUri).map((p) => ({
|
|---|
| 170 | feature: p.feature, value: !!p.value, status: gated.sentStatus(p, Date.now()),
|
|---|
| 171 | })),
|
|---|
| 172 | // Wat er op deze poort wacht: volgverzoeken bij de volgpoort, en de vraag
|
|---|
| 173 | // van het kind zelf bij de poort waar hij over gaat (shaer-8ru). Zo staat
|
|---|
| 174 | // hij waar je hem nodig hebt, en niet in een aparte lijst die je apart moet
|
|---|
| 175 | // openen -- en die je dus vergeet.
|
|---|
| 176 | waiting: { ...gatereq.waitingFor(mySlug, wardUri), 'shaer:follows': wachtend || undefined },
|
|---|
| 177 | });
|
|---|
| 178 | }
|
|---|