Changeset 742ba7e in Klonkt for src/routes


Ignore:
Timestamp:
07/28/2026 07:58:26 PM (6 weeks ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
6c152a5
Parents:
70677e96
Message:

Een ward loslaten vraagt eerst, en zegt wat het doet

Loslaten zat achter een window.confirm met een lap tekst erin. Dat is precies
het soort dialoog dat mensen wegklikken om ervan af te zijn, terwijl dit de
zwaarste knop in de app is. Nu is het een stap in het paneel: eerst de gevolgen,
dan ja of nee, met nee links zodat de uitweg de makkelijke is om te raken.

De waarschuwing wordt opgebouwd uit wat er echt aan de hand is, niet uit een
vaste zin, want loslaten betekent twee verschillende dingen. Blijven er andere
guardians over, dan stap jij op en blijft het kind een ward (FEP-633c 3.3). Ben
je de laatste, dan is het emancipatie, en 3.4 zegt uitdrukkelijk dat geen enkele
guardian daar alleen over gaat: drie instemmende volwassenen, of een meerderheid
met twee getuigen. Een nieuwe route zoekt dat op het moment van drukken op, want
voor een ward die wij niet hosten moet daarvoor die server bevraagd worden en
dat hoort niet bij elke ververs-ronde.

En het derde ding dat er hoorde te staan: het Undo federeert nog niet (fase 4 in
relations.js), dus vandaag haalt de knop de band alleen hier weg terwijl de
server van het kind je gewoon als guardian blijft noemen. Zonder die regel drukt
een guardian erop in de overtuiging dat het kind losgelaten is.

Changed files:
src/routes/guardian.js

  • GET /guardian/wards/release-check: telt de guardians van dit kind (lokaal uit de relaties, remote uit shaer:guardians van de actor) en weigert een ward die niet van jou is
  • de nieuwe labels toegevoegd aan uiStrings

src/assets/js/guardian.js

  • releaseStep: de waarschuwing plus ja/nee, in plaats van window.confirm
  • de site gaat mee in de check, anders kijkt de server naar de verkeerde

src/assets/css/guardian.css

  • --danger toegevoegd; die bestond niet, waardoor het kader en de knop onzichtbaar bleven
  • opmaak voor het waarschuwingsblok

src/services/i18n.js

  • 24 strings voor de waarschuwing in nl, en, de

test/guardian-panel.test.js

  • loslaten gaat in twee stappen en niet via window.confirm
  • elke CSS-variabele die de PWA gebruikt is er ook een die hij definieert

remarks: alle vier de gevallen in de browser nagelopen: laatste guardian,
meerdere guardians, onbereikbare server, en een ward die niet van je is. Nee
zet de knop terug zonder iets te doen, ja verwijdert alleen die ene relatie.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/guardian.js

    r70677e96 r742ba7e  
    4545    'settings_title', 'panel_open', 'panel_close', 'panel_help', 'panel_help_empty',
    4646    'panel_follow', 'panel_follow_empty', 'panel_posts', 'panel_posts_empty',
    47     'panel_actions', 'badge_help', 'badge_follow', 'badge_follow_one', 'help_empty'];
     47    'panel_actions', 'badge_help', 'badge_follow', 'badge_follow_one', 'help_empty',
     48    // Releasing a ward: a deliberate two-step answer, never one click.
     49    'release_title', 'release_effect', 'release_local', 'release_step_down',
     50    'release_last', 'release_unknown', 'release_yes', 'release_no'];
    4851  const s = Object.fromEntries(keys.map((k) => [k, i18nT(L, `guardian.${k}`)]));
    4952  s.wave = i18nT(L, 'guardian.wave');
     
    283286
    284287// ── Manage: release a committed ward (local Undo; federation is Fase 4). ──
     288/**
     289 * What actually happens if this guardian releases this ward?
     290 *
     291 * Releasing is not one action but two very different ones, and the difference
     292 * is the number of guardians the child has left (FEP-633c):
     293 *   - more than one → §3.3, you step down and the child stays a ward;
     294 *   - you are the last → §3.4, that is emancipation, and the FEP is explicit
     295 *     that no single guardian decides it alone (three consenting adults, or a
     296 *     majority plus two witnesses).
     297 * On top of that, today's release is LOCAL: the Undo is not federated yet
     298 * (relations.js, fase 4), so the ward's server keeps listing this guardian.
     299 * A guardian pressing the button would otherwise believe the child is released.
     300 *
     301 * Answered on demand rather than in the dashboard state: for a ward we do not
     302 * host this reaches out to that ward's server, and nobody should pay for that
     303 * on every refresh.
     304 */
     305router.get('/wards/release-check', requireAuth, async (req, res) => {
     306  const site = siteForUser(req);
     307  if (!site) return res.status(404).json({ error: 'no_site' });
     308  const uri = String(req.query.uri || '').trim();
     309  if (!uri) return res.status(400).json({ error: 'empty_uri' });
     310  if (!Guardianship.listWards(site.slug).some((w) => w.other_uri === uri)) {
     311    return res.status(403).json({ error: 'not_my_ward' });
     312  }
     313  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     314  const local = !!base && uri.startsWith(`${base}/`);
     315  let guardians = null;   // null = we could not find out; say so rather than guess
     316  if (local) {
     317    const slug = uri.replace(/\/+$/, '').split('/').pop();
     318    try { guardians = Guardianship.listGuardians(slug).length; } catch { /* stays null */ }
     319  } else {
     320    const doc = await AP.fetchActor(uri).catch(() => null);
     321    const g = doc && doc['shaer:guardians'];
     322    if (Array.isArray(g)) guardians = g.length;
     323    else if (typeof g === 'string') guardians = 1;
     324    else if (g && Array.isArray(g.items)) guardians = g.items.length;
     325    else if (doc) guardians = 0;   // the actor answered and names no guardians
     326  }
     327  res.json({
     328    guardians,
     329    last: guardians === null ? null : guardians <= 1,
     330    local,
     331    federates: false,   // the Undo does not travel yet (§3.2, fase 4)
     332  });
     333});
     334
    285335router.post('/wards/remove', requireAuth, express.json({ limit: '4kb' }), (req, res) => {
    286336  const site = siteForUser(req);
Note: See TracChangeset for help on using the changeset viewer.