source: Klonkt/src/services/guardianship/queues.js@ 86e6a45

main
Last change on this file since 86e6a45 was 86e6a45, checked in by roboburr <roboburr@…>, 5 weeks ago

De apps krijgen de hulpstaat, en kunnen afhandelen (shaer-lgo)

Barts melding: afgehandelde hulpverzoeken blijven zichtbaar in de Shaer
GuardianshipView, en kan het afhandelen daar ook?

DIE TWEE ZIJN HETZELFDE PROBLEEM. De apps lazen hulpvragen uit de FEED -- losse
notes met een helpRequest-vlag -- en kregen de staat helemaal niet. Ze konden dus
niet weten of er al iemand op af was, en dan is een afgehandeld verzoek laten
staan nog het eerlijkste dat een app kan doen. Klonkt bewaarde de staat wel; hij
reisde alleen nergens heen.

Nu een queue help op de actor, met de staat PLAT erin: open, wie hem oppakte,
wie hem afsloot en wanneer. Een app hoeft hem niet af te leiden en kan hem dus
ook niet anders afleiden dan het paneel -- helpItemsFor is een plek, net als
wardGates. Twee berekeningen zouden twee guardians een ander beeld geven van
hetzelfde kind, en bij een reddingsboei is dat het gevaarlijkste dat er mis kan
gaan.

AFHANDELEN HOEFDE GEEN NIEUWE VORM. De markering IS al een gewone directe note
met shaer:helpPickup of shaer:helpHandled, precies zoals de zwaai. De outbox
herkent hem nu, dus de app stuurt letterlijk wat de PWA stuurt en het reist over
dezelfde bezorging naar de mede-guardians. Geen tweede weg.

Wel LOKAAL boeken, en daar staat een toets op: zonder dat zag de guardian die de
knop indrukt zijn eigen markering pas als hij bij zichzelf terugkwam, en die weg
bestaat niet.

Oppikken blijft OPEN. De faalstand hier is "iedereen denkt dat het geregeld is",
en die is gevaarlijker dan geen markering.

De AS2-toets ving dat help nog niet gedeclareerd stond op de actor -- precies
waarvoor die toets er is. Suite 718/718; zonder de lokale boeking valt er een om.

  • Property mode set to 100644
File size: 11.0 KB
Line 
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 */
11import * as offers from './offers.js';
12import * as relations from './relations.js';
13import * as availability from './availability.js';
14import * as outgoing from './outgoing.js';
15import * as follows from './follows.js';
16import * as gated from './gated.js';
17import * as gatereq from './gatereq.js';
18import * as help from './help.js';
19import db from '../../config/database.js';
20import * as handshake from './handshake.js';
21
22const collection = (id, items) => ({
23 id, type: 'OrderedCollection', totalItems: items.length, orderedItems: items,
24});
25
26/** Pending offers where the local site is a party, each with its accept
27 * tally. The same collection carries the running lapses (§3.6.3) this
28 * account is a party to, exactly as the daemon serves them, so the Shaer
29 * clients render both without a second fetch. */
30export function offersCollection(id, slug, me) {
31 // §4.2: a handshake whose candidate could not be dereferenced is deferred,
32 // not decided, and the last Accept may already have landed — so nothing else
33 // would ever retry it. This poll is the schedule. Not awaited: the read
34 // answers with what is true now, and a retry that succeeds surfaces in the
35 // next one. `listForParty` settles closed windows on the way past.
36 handshake.retryDeferred(slug).catch(() => { /* the next read tries again */ });
37 const items = offers.listForParty(slug, me).map((o) => offers.queueItem(o, me));
38 items.push(...availability.lapseQueueItems(slug, me, Date.now()));
39 return collection(id, items);
40}
41
42/**
43 * Gate-verzoeken OP mijn wards die op mijn antwoord wachten (Guardianship Fase 2,
44 * shaer-jdb). Dit was een lege stub: de gating zelf werkt sinds shaer-hxg, maar
45 * werd nooit aan een C2S-client doorgegeven omdat de koers toen op de PWA lag.
46 *
47 * Twee bronnen, want een guardian kan wards op andere servers hebben en (nog)
48 * op deze:
49 * - ap_follow_reviews: de doorgestuurde kopie van een REMOTE ward
50 * - ap_pending_follows: een ward op deze instance
51 * Zie shaer-h6u: die tweede hoort op termijn ook over de lijn te gaan.
52 */
53export function followsCollection(id, slug, me) {
54 const items = follows.listReviewsByDirection(slug, 'incoming')
55 .map((r) => follows.reviewQueueItem(r, me));
56 for (const w of relations.listWards(slug)) {
57 const wardSlug = slugOf(w.other_uri);
58 if (!wardSlug) continue;
59 for (const p of follows.listForWard(wardSlug)) {
60 items.push({
61 id: p.id, type: 'Follow', actor: p.follower_uri, object: w.other_uri,
62 'shaer:direction': 'incoming', 'shaer:ward': w.other_uri,
63 'shaer:follower': p.follower_uri, 'shaer:followerHandle': p.follower_handle || undefined,
64 'shaer:quorum': p.quorum || 'any', published: p.created_at,
65 });
66 }
67 }
68 return collection(id, items);
69}
70
71/** De slug van een actor-uri op DEZE instance, of null als hij elders woont. */
72function slugOf(uri) {
73 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
74 if (!base || !String(uri || '').startsWith(`${base}/ap/users/`)) return null;
75 return decodeURIComponent(String(uri).slice(`${base}/ap/users/`.length).split(/[/?#]/)[0]) || null;
76}
77
78/**
79 * §5.3 uitgaand. Twee lezers, een wachtrij, en dat kan omdat §1 een ward en een
80 * guardian wederzijds uitsluit: je bent het een of het ander.
81 *
82 * ALS WARD wat IK wil volgen en waar mijn guardians nog over moeten
83 * ALS GUARDIAN wat mijn WARDS willen volgen en waar IK over moet (shaer-jdb)
84 *
85 * Dat tweede ontbrak. De wachtrij serveerde alleen listForWard(slug), en voor
86 * een guardian is dat per definitie leeg -- dus het scherm "Your wards want to
87 * follow" kon nooit iets tonen.
88 */
89export function outgoingFollowsCollection(id, slug, me) {
90 const items = outgoing.listForWard(slug).map((o) => outgoing.queueItem(o, me));
91 for (const r of follows.listReviewsByDirection(slug, 'outgoing')) {
92 items.push(follows.reviewQueueItem(r, me));
93 }
94 return collection(id, items);
95}
96
97/** The guardian's committed wards, with cached handle for display. */
98export function wardsCollection(id, slug) {
99 const items = relations.listWards(slug)
100 .map((r) => ({
101 id: r.other_uri,
102 'shaer:handle': r.other_handle || undefined,
103 since: r.created_at,
104 // Alles wat voor dit kind gated is, met soort, drempel en lopend voorstel
105 // (shaer-ahy.1). Zonder dit kon een app wel een ward TONEN maar niets over
106 // hem zeggen -- en dat is precies de helft van het antwoord op "wat mag
107 // dit kind". Dezelfde rijen als het PWA-paneel, uit dezelfde functie.
108 'shaer:gates': wardGates(slug, r.other_uri),
109 }));
110 return collection(id, items);
111}
112
113/** The ward's guardians with their availability (§3.6.1: never public,
114 * owner-only): the real size of the safety net. Same shape as the daemon. */
115export function guardiansCollection(id, slug) {
116 const uris = relations.listGuardians(slug).map((r) => r.other_uri);
117 return collection(id, availability.statusesFor(slug, uris, Date.now()));
118}
119
120export default { offersCollection, followsCollection, outgoingFollowsCollection, wardsCollection, guardiansCollection, helpCollection, helpItemsFor, wardGates, wardGuardianStatuses };
121
122// ── Wat er voor een ward gated is (shaer-ahy.1) ─────────────────────────
123//
124// STOND IN routes/guardian.js, en daar kon alleen de PWA erbij. De Shaer-apps
125// lezen dezelfde toestand via de wards-queue, en een tweede berekening naast
126// deze zou vroeg of laat een ander antwoord geven op dezelfde vraag -- dat is
127// hier geen schoonheidsfoutje maar twee guardians die een verschillend beeld
128// van hetzelfde kind krijgen. Een plek dus, en beide schermen lezen eruit.
129/** The guardians of a ward WE host, with availability (3.6.1: owner-only in
130 * spirit; the co-guardians are among the owners of the relationship). Null
131 * for a remote ward: its server tracks availability, not us. */
132export function wardGuardianStatuses(wardUri) {
133 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
134 if (!base || !String(wardUri || '').startsWith(`${base}/`)) return null;
135 const slug = String(wardUri).trim().replace(/\/+$/, '').split('/').pop();
136 try {
137 const uris = relations.listGuardians(slug).map((g) => ({ uri: g.other_uri, handle: g.other_handle }));
138 const st = Object.fromEntries(
139 availability.statusesFor(slug, uris.map((u) => u.uri), Date.now()).map((s) => [s.id, s]),
140 );
141 return uris.map((u) => ({
142 uri: u.uri,
143 handle: u.handle,
144 availability: (st[u.uri] || {})['shaer:availability'] || 'active',
145 awayUntil: (st[u.uri] || {})['shaer:awayUntil'] || null,
146 lapse: (st[u.uri] || {})['shaer:lapse'] || null,
147 }));
148 } catch { return null; }
149}
150/**
151 * De gate-rijen van een ward voor het paneel.
152 *
153 * De standen komen uit onze eigen kolommen als we het kind hosten; bij een ward
154 * elders weten we ze niet en blijft het NULL -- onbekend, niet uit. Het aantal
155 * guardians idem: dat wordt op de server van die ward bijgehouden, en zonder dat
156 * getal wordt er geen drempel verzonnen.
157 */
158export function wardGates(mySlug, wardUri) {
159 const statuses = wardGuardianStatuses(wardUri);
160 const wachtend = follows.listReviewsByDirection(mySlug, 'incoming')
161 .filter((r) => r.ward_uri === wardUri).length;
162 return gated.gateRows({
163 // Uit de BESLUITEN, niet uit onze eigen kolom. Er zijn geen lokale accounts:
164 // elke ward woont elders, dus wardEmbedSetting() gaf voor iedere ward null en
165 // stond er in het paneel overal "onbekend". Wat een guardian wel heeft is de
166 // uitslag van wat hij voorstelde.
167 settings: Object.fromEntries(gated.GATE_CATALOGUE
168 .filter((g) => g.available !== false && gated.featureColumn(g.feature))
169 .map((g) => [g.feature, gated.knownSetting(mySlug, wardUri, g.feature)])),
170 guardianCount: statuses ? statuses.length : null,
171 proposals: gated.listSent(mySlug, wardUri).map((p) => ({
172 feature: p.feature, value: !!p.value, status: gated.sentStatus(p, Date.now()),
173 })),
174 waiting: { 'shaer:follows': wachtend || undefined },
175 // De vraag van het kind zelf staat APART van wat er in een wachtrij staat
176 // (shaer-8ru). Allebei "n waiting" noemen maakt van twee verschillende
177 // dingen een getal: drie onbekenden die je kind willen volgen is iets heel
178 // anders dan je kind dat een keer vraagt of muziek aan mag. Wel bij de poort
179 // waar het over gaat, want een aparte lijst vergeet je.
180 requested: gatereq.waitingFor(mySlug, wardUri),
181 });
182}
183
184
185// ── Hulpvragen met hun staat (shaer-lgo, shaer-ahy.1) ───────────────────
186//
187// De PWA had dit al; de apps kregen alleen de losse notes uit de feed en wisten
188// dus NIET of er al iemand op af was. Daarom bleef een afgehandeld verzoek daar
189// gewoon staan -- Barts melding. De staat wordt hier een keer berekend, zoals bij
190// wardGates: twee berekeningen zouden twee guardians een ander beeld geven van
191// hetzelfde kind.
192
193/** De hulpvragen van deze guardian, met wie erop af is en of het dicht is. */
194export function helpItemsFor(slug, limit = 50) {
195 let rijen = [];
196 try {
197 rijen = db.prepare(
198 `SELECT object_uri, actor_uri, actor_name, actor_handle, actor_icon, content, published, created_at
199 FROM ap_mentions WHERE slug = ? AND help_request = 1 ORDER BY created_at DESC LIMIT ?`,
200 ).all(slug, limit);
201 } catch { return []; }
202 const staat = help.statusFor(rijen.map((r) => r.object_uri));
203 const mijn = new Set(relations.listWards(slug).map((w) => w.other_uri));
204 return rijen.map((r) => ({
205 ...r,
206 // Bij twijfel OPEN. Een hulpvraag die er afgehandeld uitziet terwijl hij dat
207 // niet is, is de gevaarlijke fout -- niet andersom.
208 state: help.withWardship(
209 staat.get(r.object_uri) || { open: true, pickedUpBy: [], handled: null, ageMs: null },
210 mijn.has(r.actor_uri),
211 ),
212 }));
213}
214
215/** Dezelfde vragen als collectie voor de apps (5.2.1). */
216export function helpCollection(id, slug) {
217 const items = helpItemsFor(slug).map((h) => ({
218 id: h.object_uri,
219 type: 'Note',
220 attributedTo: h.actor_uri,
221 'shaer:handle': h.actor_handle || undefined,
222 content: h.content || '',
223 published: h.published || h.created_at,
224 'shaer:helpRequest': true,
225 // De staat als platte velden: een app hoeft hem niet af te leiden, en kan
226 // hem dus ook niet anders afleiden dan het paneel.
227 'shaer:open': h.state.open,
228 'shaer:handledBy': h.state.handled ? (h.state.handled.handle || h.state.handled.uri) : undefined,
229 'shaer:handledAt': h.state.handled ? h.state.handled.at : undefined,
230 'shaer:pickedUpBy': h.state.pickedUpBy.map((p) => p.handle || p.uri),
231 'shaer:formerWard': h.state.formerWard || undefined,
232 }));
233 return collection(id, items);
234}
Note: See TracBrowser for help on using the repository browser.