Changeset 1e172f3 in Klonkt
- Timestamp:
- 08/07/2026 12:00:22 PM (5 weeks ago)
- Branches:
- main
- Children:
- 4c1e327
- Parents:
- ea138c0
- git-author:
- Robin <roboburr@…> (08/07/2026 12:00:20 PM)
- git-committer:
- roboburr <roboburr@…> (08/07/2026 12:00:22 PM)
- Location:
- src
- Files:
-
- 1 added
- 8 edited
-
assets/js/mod/chrome.js (added)
-
config/database.js (modified) (1 diff)
-
middleware/render.js (modified) (1 diff)
-
routes/activitypub.js (modified) (1 diff)
-
services/ActivityPubService.js (modified) (1 diff)
-
services/guardianship/follows.js (modified) (3 diffs)
-
services/guardianship/queues.js (modified) (3 diffs)
-
views/partials/bottom-tab.ejs (modified) (1 diff)
-
views/shell.ejs (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/config/database.js
rea138c0 r1e172f3 106 106 PRIMARY KEY (guardian_slug, id) 107 107 )`); 108 // Guardianship Fase 2 (shaer-jdb): een doorgestuurde follow-goedkeuring draagt 109 // een RICHTING. Bij een inkomende is de follower iemand anders en de ward het 110 // doel; bij een uitgaande is de ward zelf de follower en staat het doel in het 111 // Follow-object. Zonder deze twee kolommen werd een uitgaande opgeslagen als 112 // "deze ward wil deze ward volgen" en viel het doel weg -- dan valt er niets 113 // zinnigs te tonen, hoe je de wachtrij ook vult. 114 ensureColumn('ap_follow_reviews', 'direction', "TEXT DEFAULT 'incoming'"); 115 ensureColumn('ap_follow_reviews', 'target_uri', 'TEXT'); 116 ensureColumn('ap_follow_reviews', 'target_handle', 'TEXT'); 108 117 ensureColumn('sites', 'profile_photo', 'TEXT'); 109 118 ensureColumn('audio_tracks', 'cover_url', 'TEXT'); -
src/middleware/render.js
rea138c0 r1e172f3 286 286 ? _site.accent : '#e8b04b'; 287 287 const _navPalette = (_site && _site.palette) ? _site.palette : 'klonkt'; 288 // Welke modules de nieuwe pagina wil (shaer-bqr). De bootstrap in de shell 289 // zet dit op de body en haalt op wat er nieuw bij staat; 'chrome' hoort er 290 // altijd bij, want die komt bij elke navigatie opnieuw binnen. 291 const _navJs = ('chrome ' + (locals.pageJs || '')).trim(); 288 292 const triggerJson = JSON.stringify({ 289 pcmsNav: { bodyClass: locals.bodyClass, accent: _navAccent, palette: _navPalette },293 pcmsNav: { bodyClass: locals.bodyClass, accent: _navAccent, palette: _navPalette, js: _navJs }, 290 294 pcmsPostSwap: data.post ? { 291 295 title: data.post.title, -
src/routes/activitypub.js
rea138c0 r1e172f3 253 253 } 254 254 queueRoute('offers', (id, slug, me) => Guardianship.offersCollection(id, slug, me)); 255 queueRoute('follows', (id ) => Guardianship.followsCollection(id));255 queueRoute('follows', (id, slug, me) => Guardianship.followsCollection(id, slug, me)); 256 256 // §5.3 turned around (shaer-p729): what this ward has asked to follow, still 257 257 // waiting on its guardians. Owner-only like the rest — who a child wants to -
src/services/ActivityPubService.js
rea138c0 r1e172f3 4902 4902 const wardDoc = await fetchActor(wardUri).catch(() => null); 4903 4903 const fai = actorInfo(await fetchActor(follower).catch(() => null), follower); 4904 Guardianship.follows.recordReview(gslug, { id: followId, wardUri, wardInbox: wardDoc && wardDoc.inbox, follower, followerHandle: fai.handle, followerIcon: fai.icon, followJson: JSON.stringify(fo) }); 4904 // De RICHTING bewaren (shaer-jdb). shaer:direction wordt sinds de uitgaande 4905 // gate meegestuurd maar werd nergens gelezen, dus een uitgaande belandde 4906 // hier als "deze ward wil deze ward volgen" met het doel weggegooid. 4907 // Terugval voor oudere afzenders: is de volger de ward zelf, dan is het 4908 // uitgaand -- dat volgt uit de vorm en hoeft niet geloofd te worden. 4909 const uitgaand = act['shaer:direction'] === 'outgoing' || follower === wardUri; 4910 const doel = uitgaand ? (typeof fo.object === 'string' ? fo.object : (fo.object && fo.object.id)) : null; 4911 const dai = uitgaand ? actorInfo(await fetchActor(doel).catch(() => null), doel) : null; 4912 Guardianship.follows.recordReview(gslug, { 4913 id: followId, wardUri, wardInbox: wardDoc && wardDoc.inbox, 4914 follower, followerHandle: fai.handle, followerIcon: fai.icon, followJson: JSON.stringify(fo), 4915 direction: uitgaand ? 'outgoing' : 'incoming', 4916 target: doel || null, targetHandle: dai ? dai.handle : null, 4917 }); 4905 4918 const L = pushLang(gslug); 4906 4919 pushEvent(gslug, { type: 'guardian', title: i18nT(L, 'push.n_guard_cog_t'), body: i18nT(L, 'push.n_guard_cog_b', { who: fai.name || fai.handle || i18nT(L, 'notif.someone') }), url: `${pushPrefix(gslug)}/guardian` }); -
src/services/guardianship/follows.js
rea138c0 r1e172f3 81 81 _r = { 82 82 ins: db.prepare(`INSERT OR IGNORE INTO ap_follow_reviews 83 (id, guardian_slug, ward_uri, ward_inbox, follower_uri, follower_handle, follower_icon, follow_json, created_at) 84 VALUES (?,?,?,?,?,?,?,?, CURRENT_TIMESTAMP)`), 83 (id, guardian_slug, ward_uri, ward_inbox, follower_uri, follower_handle, follower_icon, follow_json, 84 direction, target_uri, target_handle, created_at) 85 VALUES (?,?,?,?,?,?,?,?,?,?,?, CURRENT_TIMESTAMP)`), 85 86 get: db.prepare('SELECT * FROM ap_follow_reviews WHERE guardian_slug = ? AND id = ?'), 86 87 bySlug: db.prepare("SELECT * FROM ap_follow_reviews WHERE guardian_slug = ? AND status = 'pending' ORDER BY created_at DESC"), … … 91 92 } 92 93 94 /** 95 * De guardian-zijdige kopie van een gate-verzoek op een REMOTE ward. 96 * 97 * `direction` is niet cosmetisch (shaer-jdb). Bij een INKOMENDE is de follower 98 * iemand anders en de ward het doel. Bij een UITGAANDE is de ward zelf de 99 * follower en staat het doel in het Follow-object -- die werd hiervoor 100 * opgeslagen als "deze ward wil deze ward volgen", met het doel weggegooid. 101 */ 93 102 export function recordReview(guardianSlug, r) { 94 rstmts().ins.run(r.id, guardianSlug, r.wardUri, r.wardInbox || null, r.follower, r.followerHandle || null, r.followerIcon || null, r.followJson || null); 103 const richting = r.direction === 'outgoing' ? 'outgoing' : 'incoming'; 104 rstmts().ins.run(r.id, guardianSlug, r.wardUri, r.wardInbox || null, r.follower, r.followerHandle || null, 105 r.followerIcon || null, r.followJson || null, richting, r.target || null, r.targetHandle || null); 95 106 return rstmts().get.get(guardianSlug, r.id); 107 } 108 109 /** 110 * Een openstaande review als wachtrij-item, in dezelfde vorm die de clients al 111 * lezen (offers en outgoing-follows doen het net zo). 112 */ 113 export function reviewQueueItem(r, me, guardianCount) { 114 // guardianCount blijft WEG als we hem niet kennen. Bij een remote ward wordt 115 // de guardian-set op diens eigen server bijgehouden, en 0 sturen zou lezen als 116 // "dit kind heeft geen guardians" -- het tegenovergestelde van onbekend. 117 const stemmen = (() => { 118 try { return db.prepare('SELECT guardian_uri, decision FROM ap_pending_follow_approvals WHERE follow_id = ?').all(r.id); } 119 catch { return []; } 120 })(); 121 const uitgaand = r.direction === 'outgoing'; 122 return { 123 id: r.id, 124 type: 'Follow', 125 // Bij een uitgaande is de WARD de volger; bij een inkomende is dat de vreemde. 126 actor: uitgaand ? r.ward_uri : r.follower_uri, 127 object: uitgaand ? (r.target_uri || '') : r.ward_uri, 128 'shaer:direction': uitgaand ? 'outgoing' : 'incoming', 129 'shaer:ward': r.ward_uri, 130 'shaer:target': uitgaand ? (r.target_uri || undefined) : undefined, 131 'shaer:targetHandle': uitgaand ? (r.target_handle || undefined) : undefined, 132 'shaer:follower': uitgaand ? undefined : r.follower_uri, 133 'shaer:followerHandle': uitgaand ? undefined : (r.follower_handle || undefined), 134 'shaer:quorum': 'all', 135 'shaer:approvals': stemmen.filter((x) => x.decision === 'approve').length, 136 'shaer:guardianCount': guardianCount || undefined, 137 'shaer:myVote': stemmen.some((x) => x.guardian_uri === me), 138 published: r.created_at, 139 }; 140 } 141 142 /** De openstaande reviews van een guardian, per richting. */ 143 export function listReviewsByDirection(guardianSlug, direction) { 144 return listReviews(guardianSlug).filter((r) => (r.direction === 'outgoing' ? 'outgoing' : 'incoming') === direction); 96 145 } 97 146 export function getReview(guardianSlug, id) { return rstmts().get.get(guardianSlug, id); } … … 102 151 recordPending, getPending, listForWard, decide, remove, 103 152 recordReview, getReview, listReviews, removeReview, 153 listReviewsByDirection, reviewQueueItem, 104 154 }; -
src/services/guardianship/queues.js
rea138c0 r1e172f3 6 6 * - offers: pending handshake offers where I am a party (§3), with the full 7 7 * accept tally so the client shows the right action 8 * - follows: pending gated follows for my wards (§5.3) — Fase 2, empty for now8 * - follows: pending gated follows ON my wards (§5.3), Fase 2 (shaer-jdb) 9 9 * - wards: my committed wards 10 10 */ … … 13 13 import * as availability from './availability.js'; 14 14 import * as outgoing from './outgoing.js'; 15 import * as follows from './follows.js'; 15 16 import * as handshake from './handshake.js'; 16 17 … … 35 36 } 36 37 37 /** Gated follows awaiting guardian approval — not built in Klonkt yet (Fase 2). */ 38 export function followsCollection(id) { 39 return collection(id, []); 38 /** 39 * Gate-verzoeken OP mijn wards die op mijn antwoord wachten (Guardianship Fase 2, 40 * shaer-jdb). Dit was een lege stub: de gating zelf werkt sinds shaer-hxg, maar 41 * werd nooit aan een C2S-client doorgegeven omdat de koers toen op de PWA lag. 42 * 43 * Twee bronnen, want een guardian kan wards op andere servers hebben en (nog) 44 * op deze: 45 * - ap_follow_reviews: de doorgestuurde kopie van een REMOTE ward 46 * - ap_pending_follows: een ward op deze instance 47 * Zie shaer-h6u: die tweede hoort op termijn ook over de lijn te gaan. 48 */ 49 export function followsCollection(id, slug, me) { 50 const items = follows.listReviewsByDirection(slug, 'incoming') 51 .map((r) => follows.reviewQueueItem(r, me)); 52 for (const w of relations.listWards(slug)) { 53 const wardSlug = slugOf(w.other_uri); 54 if (!wardSlug) continue; 55 for (const p of follows.listForWard(wardSlug)) { 56 items.push({ 57 id: p.id, type: 'Follow', actor: p.follower_uri, object: w.other_uri, 58 'shaer:direction': 'incoming', 'shaer:ward': w.other_uri, 59 'shaer:follower': p.follower_uri, 'shaer:followerHandle': p.follower_handle || undefined, 60 'shaer:quorum': p.quorum || 'any', published: p.created_at, 61 }); 62 } 63 } 64 return collection(id, items); 40 65 } 41 66 42 /** §5.3 outbound: this ward's own follow requests, waiting for its guardians. */ 67 /** De slug van een actor-uri op DEZE instance, of null als hij elders woont. */ 68 function slugOf(uri) { 69 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, ''); 70 if (!base || !String(uri || '').startsWith(`${base}/ap/users/`)) return null; 71 return decodeURIComponent(String(uri).slice(`${base}/ap/users/`.length).split(/[/?#]/)[0]) || null; 72 } 73 74 /** 75 * §5.3 uitgaand. Twee lezers, een wachtrij, en dat kan omdat §1 een ward en een 76 * guardian wederzijds uitsluit: je bent het een of het ander. 77 * 78 * ALS WARD wat IK wil volgen en waar mijn guardians nog over moeten 79 * ALS GUARDIAN wat mijn WARDS willen volgen en waar IK over moet (shaer-jdb) 80 * 81 * Dat tweede ontbrak. De wachtrij serveerde alleen listForWard(slug), en voor 82 * een guardian is dat per definitie leeg -- dus het scherm "Your wards want to 83 * follow" kon nooit iets tonen. 84 */ 43 85 export function outgoingFollowsCollection(id, slug, me) { 44 return collection(id, outgoing.listForWard(slug).map((o) => outgoing.queueItem(o, me))); 86 const items = outgoing.listForWard(slug).map((o) => outgoing.queueItem(o, me)); 87 for (const r of follows.listReviewsByDirection(slug, 'outgoing')) { 88 items.push(follows.reviewQueueItem(r, me)); 89 } 90 return collection(id, items); 45 91 } 46 92 -
src/views/partials/bottom-tab.ejs
rea138c0 r1e172f3 302 302 </style> 303 303 304 <script> 305 // Bottom-tab search button → opens #search-overlay. Using event delegation + looking 306 // up the overlay on EVERY click: the overlay lives in the chrome that is OOB-replaced 307 // during htmx navigation, so a once-captured reference goes stale 308 // (button did nothing afterwards). Guard against double-wiring. 309 (function() { 310 if (window.__btSearchWired) return; 311 window.__btSearchWired = true; 312 document.addEventListener('click', function(e) { 313 if (!e.target.closest || !e.target.closest('#bottom-tab-search-toggle')) return; 314 var o = document.getElementById('search-overlay'); 315 if (o) { o.hidden = false; var i = o.querySelector('input'); if (i) i.focus(); } 316 }); 317 })(); 318 </script> 304 <%# De zoekknop zit nu in de chrome-module (assets/js/mod/chrome.js). 305 Inline script hier wordt door de CSP geweigerd zodra deze partial via htmx 306 binnenkomt, en dat is bij elke navigatie -- zie shaer-0i6. %> -
src/views/shell.ejs
rea138c0 r1e172f3 339 339 </head> 340 340 341 <body class="<%= bodyClass || 'on-home' %> has-bottom-tab" data- feed-view="<%- _e(safeSite.feed_view_default || 'timeline') %>" data-grid-cols="3" data-site-base="<%- _e((typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '') %>">341 <body class="<%= bodyClass || 'on-home' %> has-bottom-tab" data-js="chrome<%= (typeof pageJs !== 'undefined' && pageJs) ? ' ' + pageJs : '' %>" data-feed-view="<%- _e(safeSite.feed_view_default || 'timeline') %>" data-grid-cols="3" data-site-base="<%- _e((typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '') %>"> 342 342 343 343 <% if (typeof isViewer !== 'undefined' && isViewer) { %> … … 449 449 } 450 450 451 // Welke modules deze pagina wil (shaer-bqr). De bootstrap hieronder leest 452 // dit zodra deze handler klaar is. 453 if (typeof d.js === 'string' && /^[a-z0-9 -]*$/.test(d.js)) { 454 document.body.setAttribute('data-js', d.js); 455 } 456 451 457 var next = d.bodyClass; 452 458 if (!next) return; … … 467 473 // de pcmsNav-trigger (accent/palette/bodyClass) correct toepast. De vroegere 468 474 // htmx:historyRestore-handler is vervallen nu htmx-history uitstaat.) 475 })(); 476 </script> 477 478 <!-- Modules laden (shaer-bqr). Inline script in gewisselde inhoud wordt door de 479 CSP geweigerd: de nonce rouleert per verzoek, dus een script dat via htmx 480 binnenkomt draagt er een die dit document niet kent (shaer-0i6). Alles wat 481 bij een pagina hoort komt daarom uit een module, en die wordt HIER geladen — 482 vanuit de shell, die alleen bij een volledige laadbeurt binnenkomt en dus 483 wél de goede nonce heeft. 484 485 Een dynamische import vanuit een vertrouwd (genonced) script is precies waar 486 'strict-dynamic' voor bedoeld is, dus de module zelf heeft geen nonce nodig. --> 487 <script> 488 (function () { 489 if (window.__modBoot) return; 490 window.__modBoot = true; 491 492 var loaded = {}; 493 function load() { 494 var names = (document.body.getAttribute('data-js') || '').trim().split(/\s+/); 495 names.forEach(function (name) { 496 // Streng: deze waarde komt uit een template en wordt een PAD. Alleen 497 // kleine letters, cijfers en streepjes; nooit een punt of een schuine 498 // streep. 499 if (!name || loaded[name] || !/^[a-z0-9-]+$/.test(name)) return; 500 loaded[name] = 1; 501 import('/assets/js/mod/' + name + '.js').catch(function (e) { 502 console.warn('[mod] ' + name + ' laadde niet:', e && e.message); 503 }); 504 }); 505 } 506 load(); 507 // Bij een htmx-navigatie wisselt de INHOUD, niet de body. De nav-trigger 508 // hieronder zet data-js opnieuw; daarna halen we op wat er nieuw bij staat. 509 // Een module die er al is wordt niet opnieuw geimporteerd -- vandaar dat elke 510 // module gedelegeerd moet werken en tegen een tweede aanroep moet kunnen. 511 document.body.addEventListener('pcmsNav', load); 469 512 })(); 470 513 </script>
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)