Changeset 0202104 in Klonkt for src/routes/guardian.js
- Timestamp:
- 07/28/2026 11:40:37 PM (6 weeks ago)
- Branches:
- main
- Children:
- 439f095
- Parents:
- 6eab7e9
- File:
-
- 1 edited
-
src/routes/guardian.js (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/guardian.js
r6eab7e9 r0202104 48 48 // Releasing a ward: a deliberate two-step answer, never one click. 49 49 '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']; 51 55 const s = Object.fromEntries(keys.map((k) => [k, i18nT(L, `guardian.${k}`)])); 52 56 s.wave = i18nT(L, 'guardian.wave'); … … 79 83 // `embeds` is null for a ward we do not host: that setting lives on the 80 84 // 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 })), 82 92 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()), 83 95 help, 84 96 strings: uiStrings(L), 85 97 }; 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. */ 103 function 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; } 86 120 } 87 121 … … 264 298 // "complete"). All three are a C2S Accept/Reject on the offer id; the 265 299 // 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. 304 router.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. 333 router.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 266 349 router.post('/offer', requireAuth, express.json({ limit: '4kb' }), async (req, res) => { 267 350 const site = siteForUser(req);
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)