Changeset 0202104 in Klonkt for src/routes/guardian.js


Ignore:
Timestamp:
07/28/2026 11:40:37 PM (6 weeks ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
439f095
Parents:
6eab7e9
Message:

De gebruikerskant van 3.6: de ward ziet zijn vangnet, de guardian handelt

Drie oppervlakken rondgemaakt op de beschikbaarheid die er sinds vanmorgen
server-side in zit.

Berichten (de ward): de guardians-balk toont per guardian de buddy-list-stip
met het label erbij: beschikbaar, afwezig tot een datum, offline. Het kind ziet
de echte omvang van zijn vangnet, niet alleen de namen. Owner-only omdat de
pagina dat is.

De Guardian-PWA (de guardian): in het paneel per kind staan de mede-guardians
met dezelfde stippen; op een slapende verschijnt de bewuste, zeldzame
vervolgstap "Voorstel: loslaten bij afwezigheid", die langs dezelfde
C2S-pijplijn loopt als de Shaer-apps (Offer van shaer:Lapse, lokaal ward opent
direct, remote ward krijgt het voorstel bezorgd). Lopende lapses staan als
kaart bij de aanvragen, met Eens/Oneens over de bestaande offer-draad en de zin
die het frame bewaakt. En "Even afwezig": een week of een maand, een directe
note met shaer:away naar alle wards, lokaal direct toegepast.

Shaer (beide apps): de lapse-kaart toont stemknoppen alleen aan leden van de
set. De ward kijkt mee naar wat zijn guardians beslissen; het is daar niet de
rechter, om precies de reden uit de editor's note van 3.6.3.

Onderweg gerepareerd: parseStamp kende alleen strings, waardoor een epoch-ms
endTime als lege datum rendde ("unavailable till" zonder datum).

Changed files:
src/routes/posts.js

  • /messages geeft de guardians hun beschikbaarheid mee

src/views/pages/messages.ejs

  • de stip en het label per guardian, met de opmaak erbij

src/routes/guardian.js

  • dashboardState: mede-guardians met status per lokaal kind, plus lapses
  • POST /guardian/api/away en /guardian/api/lapse
  • de nieuwe labels in uiStrings

src/assets/js/guardian.js

  • de guardians-sectie in het paneel, de lapse-kaart, de afwezig-knoppen

src/views/pages/guardian.ejs

  • de "Even afwezig"-sectie

src/assets/css/guardian.css

  • de stippen en de lapse-kaart

src/middleware/render.js

  • parseStamp accepteert epoch ms

src/services/i18n.js

  • de labels en teksten in nl, en, de

remarks: end-to-end in de browser nagelopen op de wegwerp-database: het kind
ziet oma afwezig-tot, opa offline en guard beschikbaar; de guardian opent het
paneel, stelt de lapse voor op de slapende opa (kaart verschijnt, eigen stem
geteld), drukt "A week", en bij het kind staat guard meteen op afwezig tot
5 augustus. 276 tests groen. Niet uitgerold.

-robo
Co-Authored-By: Claude Fable 5 <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/guardian.js

    r6eab7e9 r0202104  
    4848    // Releasing a ward: a deliberate two-step answer, never one click.
    4949    'release_title', 'release_effect', 'release_local', 'release_step_down',
    50     'release_last', 'release_unknown', 'release_yes', 'release_no'];
     50    'release_last', 'release_unknown', 'release_yes', 'release_no',
     51    // Availability (FEP-633c 3.6): the dots, the step-away, the lapse.
     52    'avail_available', 'avail_away', 'avail_dormant', 'panel_guards', 'panel_guards_remote',
     53    'lapse_propose', 'lapse_line', 'lapse_tally', 'lapse_note', 'lapse_agree', 'lapse_disagree', 'voted',
     54    'away_title', 'away_sub', 'away_week', 'away_month', 'away_done'];
    5155  const s = Object.fromEntries(keys.map((k) => [k, i18nT(L, `guardian.${k}`)]));
    5256  s.wave = i18nT(L, 'guardian.wave');
     
    7983    // `embeds` is null for a ward we do not host: that setting lives on the
    8084    // ward's own server, so we show it as not-adjustable rather than lying.
    81     wards: Guardianship.listWards(site.slug).map((w) => ({ ...w, embeds: wardEmbedSetting(w.other_uri) })),
     85    // `guardians` (FEP-633c 3.6): the fellow guardians of a LOCAL ward with
     86    // their availability; null for a remote ward, whose server tracks it.
     87    wards: Guardianship.listWards(site.slug).map((w) => ({
     88      ...w,
     89      embeds: wardEmbedSetting(w.other_uri),
     90      guardians: wardGuardianStatuses(w.other_uri),
     91    })),
    8292    offers: Guardianship.offersCollection(`${me}/queues/offers`, site.slug, me).orderedItems,
     93    // Running lapses (3.6.3) this guardian or its local wards are party to.
     94    lapses: Guardianship.availability.lapseQueueItems(site.slug, me, Date.now()),
    8395    help,
    8496    strings: uiStrings(L),
    8597  };
     98}
     99
     100/** The guardians of a ward WE host, with availability (3.6.1: owner-only in
     101 *  spirit; the co-guardians are among the owners of the relationship). Null
     102 *  for a remote ward: its server tracks availability, not us. */
     103function wardGuardianStatuses(wardUri) {
     104  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     105  if (!base || !String(wardUri || '').startsWith(`${base}/`)) return null;
     106  const slug = String(wardUri).trim().replace(/\/+$/, '').split('/').pop();
     107  try {
     108    const uris = Guardianship.listGuardians(slug).map((g) => ({ uri: g.other_uri, handle: g.other_handle }));
     109    const st = Object.fromEntries(
     110      Guardianship.availability.statusesFor(slug, uris.map((u) => u.uri), Date.now()).map((s) => [s.id, s]),
     111    );
     112    return uris.map((u) => ({
     113      uri: u.uri,
     114      handle: u.handle,
     115      availability: (st[u.uri] || {})['shaer:availability'] || 'active',
     116      awayUntil: (st[u.uri] || {})['shaer:awayUntil'] || null,
     117      lapse: (st[u.uri] || {})['shaer:lapse'] || null,
     118    }));
     119  } catch { return null; }
    86120}
    87121
     
    264298//    "complete"). All three are a C2S Accept/Reject on the offer id; the
    265299//    handshake module decides when it commits (§3.1).
     300// ── Step away (FEP-633c 3.6.1): the guardian declares itself unavailable ──
     301// One direct note with shaer:away and an endTime to every ward, the same
     302// path Shaer takes over C2S. Wards on this instance are applied directly (a
     303// local inbox never receives its own delivery); the rest travels S2S.
     304router.post('/api/away', requireAuth, express.json({ limit: '2kb' }), async (req, res) => {
     305  const site = siteForUser(req);
     306  if (!site) return res.status(404).json({ error: 'no_site' });
     307  const days = Math.min(365, Math.max(1, parseInt(req.body?.days, 10) || 0));
     308  if (!days) return res.status(400).json({ error: 'away_needs_an_end' });
     309  const wards = Guardianship.listWards(site.slug).map((w) => w.other_uri);
     310  if (!wards.length) return res.status(409).json({ error: 'no_wards' });
     311  const until = Date.now() + days * 24 * 3600 * 1000;
     312  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     313  const me = AP.actorId(base, site.slug);
     314  let applied = 0;
     315  for (const uri of wards) {
     316    const wslug = uri.startsWith(`${base}/`) ? uri.replace(/\/+$/, '').split('/').pop() : null;
     317    if (wslug && Guardianship.listGuardians(wslug).some((g) => g.other_uri === me)) {
     318      Guardianship.availability.declareAway(wslug, me, until);
     319      applied++;
     320    }
     321  }
     322  const L = resolveLang(req);
     323  const text = i18nT(L, 'guardian.away_msg', { date: new Date(until).toLocaleDateString('nl-NL') });
     324  const r = await AP.deliverDirectNote(site, { recipients: wards, text, awayUntil: until }).catch(() => null);
     325  if (!applied && !(r && r.id)) return res.status(502).json({ error: 'away_failed' });
     326  res.json({ ok: true, until });
     327});
     328
     329// ── Propose a lapse (FEP-633c 3.6.3) against a dormant co-guardian ────────
     330// The same C2S pipeline the Shaer apps would use: an Offer of shaer:Lapse.
     331// A local ward opens directly; a remote ward gets the proposal delivered,
     332// because the ward's server is the one that tallies and enforces.
     333router.post('/api/lapse', requireAuth, express.json({ limit: '4kb' }), async (req, res) => {
     334  const site = siteForUser(req);
     335  if (!site) return res.status(404).json({ error: 'no_site' });
     336  const ward = String(req.body?.ward || '').trim();
     337  const target = String(req.body?.target || '').trim();
     338  if (!ward || !target) return res.status(400).json({ error: 'missing_ward_or_target' });
     339  if (!Guardianship.listWards(site.slug).some((w) => w.other_uri === ward)) {
     340    return res.status(403).json({ error: 'not_my_ward' });
     341  }
     342  const r = await AP.ingestOutboxActivity(site, req.session.user, {
     343    type: 'Offer', object: { type: 'shaer:Lapse', 'shaer:ward': ward, object: target },
     344  });
     345  if (!r || r.status >= 400) return res.status(r?.status || 500).json({ error: r?.error || 'lapse_failed' });
     346  res.json({ ok: true, lapse: r.id });
     347});
     348
    266349router.post('/offer', requireAuth, express.json({ limit: '4kb' }), async (req, res) => {
    267350  const site = siteForUser(req);
Note: See TracChangeset for help on using the changeset viewer.