| 1 | /**
|
|---|
| 2 | * Wie er op een hulpvraag af is, en wanneer hij is afgesloten (shaer-lgo).
|
|---|
| 3 | *
|
|---|
| 4 | * Een hulpvraag (FEP-633c 5.2.1) gaat naar ALLE guardians van een kind, die op
|
|---|
| 5 | * verschillende servers zitten. Zonder gedeelde staat denken er twee dat de
|
|---|
| 6 | * ander het oppakt -- en dat is precies het scenario waar de reddingsboei voor
|
|---|
| 7 | * bestaat.
|
|---|
| 8 | *
|
|---|
| 9 | * DE FAALSTAND IS HIER NIET VEILIG, en dat maakt dit anders dan elke gate. Bij
|
|---|
| 10 | * een gate is "dicht" het veilige antwoord. Hier is de faalstand "iedereen denkt
|
|---|
| 11 | * dat het geregeld is", en dat is gevaarlijker dan geen markering. Daaruit volgt
|
|---|
| 12 | * de regel die overal in dit bestand terugkomt: bij twijfel is een hulpvraag
|
|---|
| 13 | * OPEN.
|
|---|
| 14 | *
|
|---|
| 15 | * Twee besluiten van Bart (7-8) zitten in de vorm:
|
|---|
| 16 | *
|
|---|
| 17 | * OPGEPIKT mag stapelen en vervalt niet, maar VEROUDERT zichtbaar. Twee mensen
|
|---|
| 18 | * die tegelijk reageren op een kind is geen probleem; twee die allebei niets
|
|---|
| 19 | * doen omdat de ander het "geclaimd" had, wel. En een signaal dat vanzelf
|
|---|
| 20 | * verdwijnt laat een hulpvraag er onaangeroerd uitzien terwijl er iemand mee
|
|---|
| 21 | * bezig is.
|
|---|
| 22 | *
|
|---|
| 23 | * AFGEHANDELD kent geen terugdraai. Sluiten gebeurt met een stevige
|
|---|
| 24 | * bevestiging, en leeft de vraag daarna nog, dan wordt hij OPNIEUW GESTELD --
|
|---|
| 25 | * een nieuwe hulpvraag. Er wordt niets herschreven, er wordt toegevoegd.
|
|---|
| 26 | */
|
|---|
| 27 |
|
|---|
| 28 | import db from '../../config/database.js';
|
|---|
| 29 |
|
|---|
| 30 | let _s = null;
|
|---|
| 31 | function stmts() {
|
|---|
| 32 | if (!_s) {
|
|---|
| 33 | _s = {
|
|---|
| 34 | ins: db.prepare(`INSERT OR IGNORE INTO ap_help_state (note_uri, guardian_uri, kind, guardian_handle)
|
|---|
| 35 | VALUES (?,?,?,?)`),
|
|---|
| 36 | forNote: db.prepare('SELECT * FROM ap_help_state WHERE note_uri = ? ORDER BY created_at ASC'),
|
|---|
| 37 | forNotes: db.prepare('SELECT * FROM ap_help_state WHERE note_uri IN (SELECT value FROM json_each(?))'),
|
|---|
| 38 | };
|
|---|
| 39 | }
|
|---|
| 40 | return _s;
|
|---|
| 41 | }
|
|---|
| 42 |
|
|---|
| 43 | /** Leg vast dat iemand deze hulpvraag heeft opgepikt of afgesloten. */
|
|---|
| 44 | export function record(noteUri, guardianUri, kind, handle = null) {
|
|---|
| 45 | if (!noteUri || !guardianUri) return;
|
|---|
| 46 | const k = kind === 'handled' ? 'handled' : 'pickup';
|
|---|
| 47 | try { stmts().ins.run(noteUri, guardianUri, k, handle); } catch { /* nooit dragend */ }
|
|---|
| 48 | }
|
|---|
| 49 |
|
|---|
| 50 | /**
|
|---|
| 51 | * De staat van een hulpvraag, uit zijn rijen. Puur, zodat de regels te toetsen
|
|---|
| 52 | * zijn zonder database of scherm.
|
|---|
| 53 | *
|
|---|
| 54 | * `oldestPickupAt` is het TIJDSTIP van de oudste oppik, niet de leeftijd. Daar
|
|---|
| 55 | * tekent het scherm mee dat een signaal oud wordt -- niets verdwijnt, maar je
|
|---|
| 56 | * ziet wel dat er misschien niets meer gebeurt.
|
|---|
| 57 | *
|
|---|
| 58 | * EEN TIJDSTIP, GEEN LEEFTIJD, en dat is geen smaak. Hier stond `ageMs`, een
|
|---|
| 59 | * verschil met `now`, en dus veranderde dit antwoord elke milliseconde. Zodra
|
|---|
| 60 | * het paneel een ETag kreeg (9-8) kon die daardoor nooit meer gelijk zijn: de
|
|---|
| 61 | * 304 kwam nooit, de lange poll keerde meteen terug, en de browser kwam in een
|
|---|
| 62 | * lus van ongeveer een seconde waarin de scrollpositie werd vermalen. Een
|
|---|
| 63 | * levende klok in een antwoord maakt dat antwoord onvergelijkbaar met zichzelf.
|
|---|
| 64 | *
|
|---|
| 65 | * De leeftijd is een weergavedetail en wordt in de client uitgerekend.
|
|---|
| 66 | */
|
|---|
| 67 | export function helpStatus(rows, now = Date.now()) {
|
|---|
| 68 | const list = rows || [];
|
|---|
| 69 | const pickups = list.filter((r) => r.kind === 'pickup');
|
|---|
| 70 | const done = list.find((r) => r.kind === 'handled') || null;
|
|---|
| 71 | const stamp = (r) => { const t = Date.parse(r.created_at); return isNaN(t) ? null : t; };
|
|---|
| 72 | const oudste = pickups.map(stamp).filter((t) => t !== null).sort((a, b) => a - b)[0];
|
|---|
| 73 | return {
|
|---|
| 74 | // Namen erbij: "door wie" was de hele vraag. Zonder dat is het een vinkje.
|
|---|
| 75 | pickedUpBy: pickups.map((r) => ({ uri: r.guardian_uri, handle: r.guardian_handle || null, at: r.created_at })),
|
|---|
| 76 | handled: done ? { uri: done.guardian_uri, handle: done.guardian_handle || null, at: done.created_at } : null,
|
|---|
| 77 | // Alleen betekenisvol zolang er niets is afgesloten.
|
|---|
| 78 | oldestPickupAt: (!done && oudste) ? new Date(oudste).toISOString() : null,
|
|---|
| 79 | // Waar het scherm op afgaat. Bij twijfel OPEN: een lege lijst, een rij die we
|
|---|
| 80 | // niet kunnen lezen, wat dan ook -- alles wat geen expliciete afsluiting is,
|
|---|
| 81 | // is een hulpvraag die nog op iemand wacht.
|
|---|
| 82 | open: !done,
|
|---|
| 83 | };
|
|---|
| 84 | }
|
|---|
| 85 |
|
|---|
| 86 | /**
|
|---|
| 87 | * Een hulpvraag van iemand die je NIET MEER bewaakt.
|
|---|
| 88 | *
|
|---|
| 89 | * Het loslaat-scherm belooft dit al letterlijk: "je krijgt geen hulpvragen meer
|
|---|
| 90 | * van ze". Nieuwe komen inderdaad niet meer binnen, maar wat er al lag bleef in
|
|---|
| 91 | * de open lijst staan -- en was niet af te sluiten, want de markeerroute eist
|
|---|
| 92 | * dat het nog je ward is en antwoordt anders met 403. De knop stond er dus wel
|
|---|
| 93 | * en deed niets.
|
|---|
| 94 | *
|
|---|
| 95 | * Zo'n vraag is niet AFGEHANDELD -- dat zou een claim zijn over een kind waar je
|
|---|
| 96 | * niets meer over te zeggen hebt, en die claim wordt ook nog rondgestuurd. Hij is
|
|---|
| 97 | * niet meer van jou. Dat is een derde uitkomst en die hoort als zodanig te lezen.
|
|---|
| 98 | *
|
|---|
| 99 | * Veilig omdat een guardianship nooit bij de LAATSTE guardian eindigt (3.4,
|
|---|
| 100 | * emancipatie): er blijft altijd iemand over voor wie de vraag wel open staat.
|
|---|
| 101 | */
|
|---|
| 102 | export function withWardship(status, stillWard) {
|
|---|
| 103 | if (stillWard) return status;
|
|---|
| 104 | return { ...status, open: false, formerWard: true };
|
|---|
| 105 | }
|
|---|
| 106 |
|
|---|
| 107 | /** De staat van een hulpvraag zoals die nu is opgeslagen. */
|
|---|
| 108 | export function statusOf(noteUri, now = Date.now()) {
|
|---|
| 109 | if (!noteUri) return helpStatus([], now);
|
|---|
| 110 | try { return helpStatus(stmts().forNote.all(noteUri), now); } catch { return helpStatus([], now); }
|
|---|
| 111 | }
|
|---|
| 112 |
|
|---|
| 113 | /** Idem voor een hele lijst in een query, zodat een paneel geen N+1 wordt. */
|
|---|
| 114 | export function statusFor(noteUris, now = Date.now()) {
|
|---|
| 115 | const uit = new Map();
|
|---|
| 116 | const lijst = [...new Set((noteUris || []).filter(Boolean))];
|
|---|
| 117 | if (!lijst.length) return uit;
|
|---|
| 118 | let rijen = [];
|
|---|
| 119 | try { rijen = stmts().forNotes.all(JSON.stringify(lijst)); } catch { rijen = []; }
|
|---|
| 120 | const perNote = new Map();
|
|---|
| 121 | for (const r of rijen) {
|
|---|
| 122 | if (!perNote.has(r.note_uri)) perNote.set(r.note_uri, []);
|
|---|
| 123 | perNote.get(r.note_uri).push(r);
|
|---|
| 124 | }
|
|---|
| 125 | for (const uri of lijst) uit.set(uri, helpStatus(perNote.get(uri) || [], now));
|
|---|
| 126 | return uit;
|
|---|
| 127 | }
|
|---|
| 128 |
|
|---|
| 129 | /**
|
|---|
| 130 | * De markering als bericht. Bewust een gewone directe note met een
|
|---|
| 131 | * shaer:-markering, zoals de zwaai en de afwezigheidsmelding: dan reist het over
|
|---|
| 132 | * de bestaande bezorging, en de WARD leest het als wat het is -- er komt iemand.
|
|---|
| 133 | */
|
|---|
| 134 | export function markerNote({ id, me, noteUri, kind, to }) {
|
|---|
| 135 | const k = kind === 'handled' ? 'handled' : 'pickup';
|
|---|
| 136 | return {
|
|---|
| 137 | id, type: 'Note', attributedTo: me, to,
|
|---|
| 138 | [k === 'handled' ? 'shaer:helpHandled' : 'shaer:helpPickup']: noteUri,
|
|---|
| 139 | content: k === 'handled'
|
|---|
| 140 | ? '<p>Deze hulpvraag is afgehandeld.</p>'
|
|---|
| 141 | : '<p>Ik kijk hiernaar.</p>',
|
|---|
| 142 | };
|
|---|
| 143 | }
|
|---|
| 144 |
|
|---|
| 145 | /** Leest een binnengekomen note als markering, of null als hij er geen is. */
|
|---|
| 146 | export function parseMarker(object) {
|
|---|
| 147 | if (!object || typeof object !== 'object') return null;
|
|---|
| 148 | const pickup = object['shaer:helpPickup'];
|
|---|
| 149 | const handled = object['shaer:helpHandled'];
|
|---|
| 150 | if (typeof handled === 'string' && handled) return { kind: 'handled', noteUri: handled };
|
|---|
| 151 | if (typeof pickup === 'string' && pickup) return { kind: 'pickup', noteUri: pickup };
|
|---|
| 152 | return null;
|
|---|
| 153 | }
|
|---|
| 154 |
|
|---|
| 155 | export default { record, helpStatus, withWardship, statusOf, statusFor, markerNote, parseMarker };
|
|---|