| [02c6231] | 1 | {"_type":"issue","id":"prutfolio-src-cke","title":"Cross-tenant lek: playlist-tak van setAudioFediOpen opende bestanden van andere sites","description":"setAudioFediOpen in src/routes/posts.js opende de [[playlist:id]]-tak zonder site-check, terwijl de track- en album-tak wel AND site_id = ? hadden. playlists.id is een globale primary key, dus een post op site A met [[playlist:x]] van site B zette de tracks van B permanent open (fedi_open is eenrichtings). Gevonden 7-8-2026 tijdens shaer-ayc. Fix: site_id-filter op de TRACKS in de subquery, plus test/fedi-open-tenancy.test.js.","notes":"BEREIKBAARHEID, en waarom we er niet verder op doorgaan (8-8, besluit Robin).\n\nNagekeken op de draaiende instances: dev = 1 site (1 god), 1 playlist, dus geen site B. beta = 2 sites met 2 verschillende eigenaren (beta/god, granbeta/member) plus site_members-rijen, maar 0 tracks en 0 playlists. Vandaag dus nergens te triggeren.\n\nBELANGRIJKER: die tweede site, de member-rol en de site_members-rijen zijn RUDIMENTEN van functionaliteit die eruit gaat. We redeneren niet langer over bereikbaarheid vanuit zulke resten in de databases -- ze zeggen niets over wat er straks nog leeft. De P1 die ik er eerst op zette was daarmee dubbel te zwaar aangezet.\n\nWat blijft staan is de fix zelf, en niet vanwege een dreiging: de playlist-tak deed iets anders dan de track- en album-tak erboven, en fedi_open is eenrichtings. Dat is een reden op zichzelf.","status":"closed","priority":1,"issue_type":"bug","owner":"roboburr@gmail.com","created_at":"2026-08-08T00:47:53Z","created_by":"Robin","updated_at":"2026-08-08T01:27:50Z","closed_at":"2026-08-08T00:48:03Z","close_reason":"Gefixt en gepusht naar main als bfe4a55. Site-filter op de tracks in de subquery; test/fedi-open-tenancy.test.js bewaakt het (faalt op onveranderde main met fedi_open=1). Volledige suite 627/627 groen op hetzner-ai.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [d11e308] | 2 | {"_type":"issue","id":"prutfolio-src-aaj","title":"GitHub-distributie: canonieke repo naar GitHub + self-host release (Docker/zip)","description":"Volgorde (Robin 2026-06-21): EERST GitHub-support + canonieke code van de VPS af, DAARNA pas Docker/zip-distributie.\n\nDOEL: muzikanten kunnen Klonkt zelf hosten (WordPress-achtig), updates via GitHub i.p.v. Robins VPS.\n\nSTAP 1 — repo naar GitHub (van VPS af):\n- Canonieke bare repo staat nu op de VPS (~/git-repos/prutfolio.git). Verplaatsen/spiegelen naar een GitHub-repo (Robin is niet GitHub-vaardig -\u003e Claude zet repo+releases op).\n- KLONKT_GIT_DIR (update-functie Beheer-\u003eUpdates / klonkt-self-update.sh) ompunten naar GitHub i.p.v. lokale bare repo.\n- Robins eigen vloot (demo's/jeffree/jasonhacky) blijft werken; deploy-loop evt. naar git pull van GitHub.\n- Secrets/branding check voor publiek maken (geen .env, geen storage, geen interne notities in de repo).\n\nSTAP 2 — self-host distributie (later, was al uitgewerkt; recon gedaan):\n- Dockerfile (multi-stage node:20-bookworm) + docker-compose + .dockerignore = de 'WordPress-zip' (1 commando).\n- Quickstart-README (Docker + kale VPS met Node+pm2+Caddy).\n- Evt. one-click templates (Railway/PikaPods).\n\nSELF-HOST READINESS (geverifieerd 2026-06-21):\n- Node-app (\u003e=20), GEEN PHP -\u003e draait op VPS/Docker/PaaS, niet op shared PHP-hosting.\n- ffmpeg = MEEGEBUNDELD via ffmpeg-static (geen system-install).\n- cwebp = OPTIONEEL (ImageWebpService valt terug op origineel als 'cwebp' ontbreekt).\n- better-sqlite3 = native (npm prebuilt binary; in Docker bookworm-base voor glibc).\n- DB migreert zichzelf bij boot (57x CREATE TABLE IF NOT EXISTS/ensureColumn) -\u003e geen los migratiescript.\n- .env.example + README.md bestaan al. Premium staat default UIT (KLONKT_PREMIUM_ENABLED) -\u003e self-hoster krijgt volledige gratis app.\n- Eerste /auth/register = god, daarna dicht.\n- server.js: PORT env, listen(PORT). LET OP: check of 'ie op 0.0.0.0 bindt (nodig in Docker) -- nog verifieren.\n- Docker 29 lokaal beschikbaar voor testen.\n\nGATING/PLAN-context: zie memory project_klonkt_monetization (self-host + Patreon $10 lifetime; security/updates gratis voor iedereen, 3 modules achter Patreon).","status":"closed","priority":1,"issue_type":"feature","owner":"roboburr@gmail.com","created_at":"2026-06-21T20:29:43Z","created_by":"roboburr","updated_at":"2026-07-01T22:02:25Z","closed_at":"2026-07-01T22:02:25Z","close_reason":"Done: public GitHub repo live 2026-06-23 (roboburr/klonkt, AGPL), install.sh + klonkt.com/install, stable release channel, Docker image build+run verified 2026-07-01. Leftovers (optional KLONKT_GIT_DIR repoint, one-click PaaS templates) intentionally deferred.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [d039264] | 3 | {"_type":"issue","id":"prutfolio-src-3fl","title":"Audio: continuous playback breekt na 3-4 tracks (browser-policy / state corruption)","description":"Na fix v1 (next() unconditional play) werkt 1-\u003e2 en 2-\u003e3, maar stopt bij 3-\u003e4. Verdacht: iOS Safari autoplay-policy revoke OF audio element state-corruption na meerdere src changes. Fix: pause()+load()+play() pattern in loadTrack, error-event auto-skip naar volgende, NotAllowedError visuele hint (pulse op play-btn).","status":"closed","priority":1,"issue_type":"bug","owner":"robin@roboburr.com","created_at":"2026-05-07T22:38:42Z","created_by":"Robin","updated_at":"2026-07-02T13:39:58Z","closed_at":"2026-07-02T13:39:58Z","close_reason":"Fixed on main (421046c). Root cause: every track change did pause + src-swap + load + play, which browsers treat as a NEW playback session - background policy pauses those, and repeated element resets corrupted state. The player now appends the whole queue into one MediaSource (audio/mpeg SourceBuffer), so auto-advance is a continuous timeline with zero pause/play events. Verified in-browser: boundary cross with no play/pause pair, per-track UI correct, gapless wrap. Blob fallback remains for iOS.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [d4c3c4e] | 4 | {"_type":"issue","id":"prutfolio-src-0tg","title":"Audio: continuous playback fix — next() altijd play() ipv conditional","description":"Bug: next() checkte 'if (isPlaying) play()' maar isPlaying was al false door de pause-event die de browser firet vóór ended. Resultaat: track stopt na 1 keer. Fix: drop de conditional, roep altijd play() aan. Zelfde voor prev(). Bestand: src/assets/js/audio-player.js regels 221-230.","status":"closed","priority":1,"issue_type":"bug","assignee":"Robin","owner":"robin@roboburr.com","created_at":"2026-05-06T22:18:40Z","created_by":"Robin","updated_at":"2026-05-06T22:28:20Z","started_at":"2026-05-06T22:26:37Z","closed_at":"2026-05-06T22:28:20Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [02c6231] | 5 | {"_type":"issue","id":"prutfolio-src-s5w","title":"PlaylistService.generateId controleert uniekheid per site, maar playlists.id is een GLOBALE primary key","description":"src/services/PlaylistService.js generateId() zoekt een vrij id met SELECT 1 FROM playlists WHERE site_id = ? AND id = ?, terwijl het schema 'id TEXT PRIMARY KEY' is -- globaal, zonder site_id erin. Gevolg: heeft site A al playlist 'ai-covers' en maakt site B er een met dezelfde titel, dan geeft generateId gewoon 'ai-covers' terug, de INSERT knalt op de globale PK, de transactie gooit, en create() vangt dat af en geeft null. De gebruiker ziet 'aanmaken mislukt' zonder reden, en de -2 suffix die de functie juist moest leveren komt er nooit. Zelfde sleutelverschil als prutfolio-src-cke (globale id, per-site query), maar dan op het SCHRIJF-pad. Praktisch waarschijnlijker dan cke: het heeft geen kwaadwillende nodig, alleen twee sites met een gelijke albumtitel. Nog niet empirisch bevestigd, alleen uit de code gelezen.","notes":"Let op bij oppakken (8-8): dit is net als prutfolio-src-cke een CROSS-SITE bug -- hij heeft twee sites op een instance nodig. Gaat multi-site eruit, dan valt hij vanzelf weg: met een site is 'uniek per site' hetzelfde als 'globaal uniek' en botst de PK nooit. Niet oppakken zonder eerst te weten of multi-site blijft.","status":"open","priority":2,"issue_type":"bug","owner":"roboburr@gmail.com","created_at":"2026-08-08T01:23:47Z","created_by":"Robin","updated_at":"2026-08-08T01:27:52Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [d11e308] | 6 | {"_type":"issue","id":"prutfolio-src-m43","title":"Music federation phase 2: standard Audio/Playlist objects","description":"Federate hosted music as the de-facto fediverse audio objects (Funkwhale Audio/Library style) so audio platforms can find tracks/playlists by URL — a Note is invisible to their search. Also answers the playlist-search report. Phase 3.","status":"open","priority":2,"issue_type":"feature","owner":"roboburr@gmail.com","created_at":"2026-07-01T22:03:00Z","created_by":"roboburr","updated_at":"2026-07-01T22:03:00Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| 7 | {"_type":"issue","id":"prutfolio-src-u2e","title":"Paid posts: per-site Patreon + passkey unlock (design first)","description":"Premium feature (like downloads). Site owner configures their own Patreon app + amount in admin; readers unlock a paid post with a passkey (no account, no cookies, token in DB not traceable to a person). One-time Patreon link + refresh on de-auth. Klonkt Premium unlock stays as-is. Needs a design round before building. Also evaluate payment alternatives (OpenCollective, Open Payments). Phase 3.","status":"open","priority":2,"issue_type":"feature","owner":"roboburr@gmail.com","created_at":"2026-07-01T22:02:59Z","created_by":"roboburr","updated_at":"2026-07-01T22:02:59Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| 8 | {"_type":"issue","id":"prutfolio-src-h1q","title":"PWA: web push notifications","description":"Push messages for the installed PWA (per-instance VAPID keys, subscribe UI, push on new post/notification). PWA id is already site-slug specific. Phase 2.","status":"open","priority":2,"issue_type":"feature","owner":"roboburr@gmail.com","created_at":"2026-07-01T22:02:58Z","created_by":"roboburr","updated_at":"2026-07-01T22:02:58Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [0877771] | 9 | {"_type":"issue","id":"prutfolio-src-k1q","title":"News/Circle: some remote video embeds do not render","description":"A Loops video in the News feed did not load like other providers; the Circle also lacks a video thumbnail for such posts. Reproduce first (regression or never supported?), then fix. Phase 1.","status":"closed","priority":2,"issue_type":"bug","owner":"roboburr@gmail.com","created_at":"2026-07-01T22:02:57Z","created_by":"roboburr","updated_at":"2026-07-01T22:38:47Z","closed_at":"2026-07-01T22:38:47Z","close_reason":"Diagnosed + fixed on main. Root cause: the image proxy refused video content, so remote-video posters 302d to the raw mp4 and tiles/feed showed a black box until play. Proxy now extracts a poster frame via a bounded 4MB range fetch; News feed videos carry a poster. Verified in-browser with the actual Loops video. Note: clips over 30s keeping controls instead of autoplaying is by design.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [7f265ce] | 10 | {"_type":"issue","id":"prutfolio-src-ovj","title":"Fediverse: verify outgoing reply mentions against remote receivers","description":"Reported case: a Klonkt reply to a remote (mastodon.xyz) post where the mention handling looked off on the receiving side. First verify against current main (the reporting instance may run older code), diff the note JSON against the expected Mention shape, then fix if real. Phase 1.","status":"closed","priority":2,"issue_type":"bug","assignee":"roboburr","owner":"roboburr@gmail.com","created_at":"2026-07-01T22:02:55Z","created_by":"roboburr","updated_at":"2026-07-01T22:22:19Z","started_at":"2026-07-01T22:16:41Z","closed_at":"2026-07-01T22:22:19Z","close_reason":"Root cause found + fixed on main: the mention/hashtag/URL prefix regexes required whitespace/\u003e before the token, so a bracketed (@user@host federated as plain text and the target was never notified (the reported reply showed exactly this). Opening brackets are now valid prefixes; quotes deliberately excluded (attribute safety). Verified live: bracketed mention beta-\u003edemocirkel resolved, linked and notified.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [2abd3d2] | 11 | {"_type":"issue","id":"prutfolio-src-xgg","title":"Fediverse: notification when mentioned in a non-reply post","description":"An inbound Create whose Mention tag targets one of our actors, but which is not a reply to our content, is currently ignored. Store + show it in the fediverse notifications. Phase 1.","status":"closed","priority":2,"issue_type":"feature","owner":"roboburr@gmail.com","created_at":"2026-07-01T22:02:53Z","created_by":"roboburr","updated_at":"2026-07-01T22:15:26Z","closed_at":"2026-07-01T22:15:26Z","close_reason":"Built on main: inbound Create with a Mention tag targeting one of our actors (non-reply) is stored (ap_mentions) and shown in fediverse notifications with a link to the original. Verified live Klonkt-to-Klonkt (democirkel -\u003e beta).","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [254939c] | 12 | {"_type":"issue","id":"prutfolio-src-412","title":"Fediverse: auto-link plain URLs in outgoing replies and posts","description":"A bare URL typed in a reply (and in post content) federates as plain text; it should become a clickable link in the outgoing note. Phase 1 bug sprint.","status":"closed","priority":2,"issue_type":"bug","assignee":"roboburr","owner":"roboburr@gmail.com","created_at":"2026-07-01T22:02:52Z","created_by":"roboburr","updated_at":"2026-07-01T22:07:43Z","started_at":"2026-07-01T22:03:23Z","closed_at":"2026-07-01T22:07:43Z","close_reason":"Fixed on main: bare URLs in outgoing posts + replies now federate as clickable links (anchor-aware, punctuation-safe). Verified live on beta.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [e1c4f3e] | 13 | {"_type":"issue","id":"prutfolio-src-byh","title":"Retry Follow delivery so follows don't get stuck on 'pending'","description":"followActor() inserts the ap_following row as 'pending' and then delivers the signed Follow once, with no retry (unlike deliverCreate, which uses the ap_delivery retry queue). A single transient delivery failure therefore leaves the follow permanently 'pending' — the remote never received it, so no Accept ever comes back.\n\nBuild later:\n- Route the Follow delivery through the existing ap_delivery retry queue (backoff), OR\n- Add a periodic re-send / re-poll for follows still 'pending' after N minutes.\n\nNote: a legitimately locked (manual-approval) account also shows 'pending' until the person approves — that is NOT this bug and must stay pending. Only retry the *delivery*, don't auto-flip status.\n\nWorkaround today: unfollow + follow again sends a fresh Follow.","status":"closed","priority":2,"issue_type":"feature","assignee":"roboburr","owner":"roboburr@gmail.com","created_at":"2026-06-26T13:36:15Z","created_by":"roboburr","updated_at":"2026-07-01T00:45:55Z","started_at":"2026-07-01T00:43:31Z","closed_at":"2026-07-01T00:45:55Z","close_reason":"Fixed: followActor now delivers the Follow via deliverWithRetry() (immediate attempt + ap_delivery backoff queue), so a failed first delivery no longer leaves the follow stuck on 'pending'.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [a404018] | 14 | {"_type":"issue","id":"prutfolio-src-5gl","title":"Self-heal van de fediverse-cache (ap_timeline) bij ingrijpende updates","description":"Robin (2026-06-26): een self-heal als vaste functie, die getriggerd wordt bij ingrijpende updates -- zodat de fediverse-cache zichzelf repareert na een release waar posts in een flux-window coverless/stale bezorgd kunnen zijn (zoals de Cirkels-op-AP-migratie: gelijktijdige vloot-update + new-follower-backfill -\u003e sommige Creates kwamen zonder cover binnen, en de timeline is een snapshot-op-bezorging die nooit auto-bijwerkt).\n\nAanpak (generaliseer het eenmalige scripts/backfill-cirkel-covers.mjs tot een ingebouwde functie):\n- selfHealTimeline() in ActivityPubService: her-fetch ap_timeline-notes (AP) en werk content + media bij (covers/edits); 404/Tombstone -\u003e rij verwijderen.\n- Versie-gate: constante SELFHEAL_VERSION + app_setting selfheal_done_v\u003cN\u003e. Draait EENMAAL bij boot wanneer N wijzigt. Je bumpt N bij een ingrijpende release.\n- Net als autoMigrateCircles: async, non-blocking, best-effort, GEBOUND (bv. laatste ~50 of rate-limited) zodat het de boot niet belast en geen fetch-storm geeft.\n- Aangeroepen in server.js naast startScheduler/startDeliveryWorker/autoMigrateCircles.\n\nComplementair aan prutfolio-src-ymj (live inkomende Update afhandelen = continue self-heal); dit is het catch-up-vangnet voor wat live gemist is. Hergebruik de coverFrom/fetchNote-helpers uit het backfill-script.","status":"closed","priority":2,"issue_type":"feature","owner":"roboburr@gmail.com","created_at":"2026-06-26T10:00:19Z","created_by":"roboburr","updated_at":"2026-06-26T10:04:54Z","closed_at":"2026-06-26T10:04:54Z","close_reason":"Gebouwd: selfHealTimeline() in ActivityPubService, gegate op selfheal_version vs SELFHEAL_VERSION (draait 1x per bump), aangeroepen in server.js bij boot. Geverifieerd op democirkel (7/20 ververst, 2e reload skipt).","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [3384e95] | 15 | {"_type":"issue","id":"prutfolio-src-8z2","title":"Cirkel-feed: boosts van cirkel-vrienden tonen ZONDER ze opnieuw auto-te-boosten (anti-feedback-loop)","description":"Toekomstige feature: ook boosts (Announce) van accounts in je cirkel tonen in de feed/Cirkel.\n\nKRITIEKE GUARD (Robin 2026-06-26): auto-boost mag NOOIT een binnengekomen boost (Announce) opnieuw boosten -- alleen ORIGINELE posts (Create, geauteurd door het gevolgde account zelf). Anders feedback-loop: A boost X -\u003e B auto-boost A-z\\\"n-boost -\u003e C auto-boost B-z\\\"n-boost -\u003e ...\n\nHuidige status: VEILIG. De auto-boost-hook in handleInbox zit alleen op de Create-tak (top-level post van een gevolgd account), niet op Announce. Announces worden nu niet in ap_timeline opgeslagen, dus boosts worden nog niet getoond.\n\nTe doen zodra we boosts tonen:\n- Inkomende Announce van een gevolgd account opslaan/tonen in de feed (los van Create).\n- Auto-boost EXPLICIET beperken tot Create-van-de-auteur; nooit een Announce her-announcen.\n- Evt. extra dedup: niet auto-boosten wat je zelf al geboost hebt, en geen Announce van een Announce.","status":"closed","priority":2,"issue_type":"feature","owner":"roboburr@gmail.com","created_at":"2026-06-26T09:39:44Z","created_by":"roboburr","updated_at":"2026-06-26T12:01:22Z","closed_at":"2026-06-26T12:01:22Z","close_reason":"Opgelost door het nieuwe model: auto-boost is volledig verwijderd (uitlichten = lokaal), dus geen auto-re-Announce → loop-vrij. Boosts tonen in cirkel = lokaal (se8), geen her-boost.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [0f2d441] | 16 | {"_type":"issue","id":"prutfolio-src-ymj","title":"Inkomende Update(Note) genegeerd -\u003e bewerkte (geboooste) post blijft origineel op Klonkt","description":"Bart: een post die hij geboost heeft en daarna op Mastodon BEWERKT, blijft op de Klonkt-site de originele versie tonen. Klonkt neemt de edit niet over.\n\nLog: [AP] inbox Update -\u003e shared (ignored)\n\nOorzaak: handleInbox (ActivityPubService) negeert inkomende Update-activiteiten. Een post-edit komt binnen als Update(Note) -\u003e de gecachte kopie wordt niet bijgewerkt.\n\nTe doen:\n- handleInbox: een Update met object.type Note/Article afhandelen i.p.v. negeren -\u003e de opgeslagen kopie updaten waar de note voorkomt: ap_timeline (content / media_json / url voor die note-id) EN ap_interactions (reply-content) als het een eerder opgeslagen reply/post is.\n- Scope op de ondertekenaar: alleen de auteur mag z\\\"n eigen note updaten (signer == object author / attributedTo).\n- updated-timestamp respecteren (alleen toepassen als nieuwer).\n\nHangt samen met prutfolio-src-b12 (quote-posts): die negeerde OOK een inkomende Update. Beide vragen om nette Update-afhandeling in handleInbox -- mogelijk samen oppakken.","status":"closed","priority":2,"issue_type":"bug","owner":"roboburr@gmail.com","created_at":"2026-06-26T09:23:48Z","created_by":"roboburr","updated_at":"2026-07-01T00:40:07Z","closed_at":"2026-07-01T00:40:07Z","close_reason":"Already implemented: inbound Update(Note/Article) handler in ActivityPubService.js handleInbox (commit bea59a1), scope-guarded on author_uri; refreshes ap_timeline content/media/nsfw/cw + cached replies.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [49b1b39] | 17 | {"_type":"issue","id":"prutfolio-src-b12","title":"Quote-posts (fediverse): zelf kunnen quoten + inkomende quote tonen/notificeren","description":"Robin wilde een post quoten vanuit Klonkt, maar dat kan nog niet (geen UI + geen uitgaande AP-quote). Andersom: WordPress quotte een Klonkt-post, maar Klonkt gaf geen melding; in de log alleen: [AP] inbox Update -\u003e shared (ignored).\n\nTe doen:\n- Inkomend: een quote herkennen in handleInbox i.p.v. de Update te negeren. Quote-velden: FEP-e232 quoteUrl / quote / _misskey_quote, of een Create/Update met een quote-link naar onze eigen post. Opslaan als interactie (ap_interactions, kind quote) en tonen op de post onder \"Vanuit de fediverse\".\n- Melding: \"X quotete je post\" in de meldingen-inbox (/meldingen).\n- Uitgaand: in de post-editor een andere post kunnen quoten, met de quote-link in de Note zodat Mastodon/WordPress het als quote rendert.\n\nLog-clue: [AP] inbox Update -\u003e shared (ignored) = de inkomende quote werd als Update genegeerd (ActivityPubService handleInbox).","status":"open","priority":2,"issue_type":"feature","owner":"roboburr@gmail.com","created_at":"2026-06-26T08:45:00Z","created_by":"roboburr","updated_at":"2026-06-26T08:45:00Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [3384e95] | 18 | {"_type":"issue","id":"prutfolio-src-se8","title":"Fediverse: boost/repost zou ook in de Cirkel-feed moeten verschijnen","description":"Een boost (Announce) van een fediverse-post vanuit de tijdlijn federeert wel als Announce, maar verschijnt NIET in de eigen Cirkel-feed (/cirkel). Bart (2026-06-25): een boost zou ook een 'repost' in je Cirkels moeten opleveren, zodat je circle-volgers zien wat je boost. Te beslissen: datamodel (verwijzing naar de geboostte post vs. gekopieerde post), en surfacen in /cirkel + de circle/AP-outbox. NB: like + reply uitgaand werken al; dit gaat specifiek om de boost↔Cirkels-koppeling.","status":"closed","priority":2,"issue_type":"feature","owner":"roboburr@gmail.com","created_at":"2026-06-25T21:13:34Z","created_by":"roboburr","updated_at":"2026-06-26T12:01:20Z","closed_at":"2026-06-26T12:01:20Z","close_reason":"Optie B gebouwd (5045c30): geboooste posts verschijnen in de Cirkel via ap_timeline.boosted, gemengd op datum, dedup structureel (1 rij/post).","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [a9900ec] | 19 | {"_type":"issue","id":"prutfolio-src-txh","title":"Fediverse: rate-limiting on public AP endpoints","description":"Hardening follow-up. Specifics are intentionally kept in the maintainer's private notes, not in this public tracker. Priority medium/low; the critical fediverse issues (SSRF, stored-XSS, cross-actor delete) are already fixed.","status":"closed","priority":2,"issue_type":"task","owner":"roboburr@gmail.com","created_at":"2026-06-25T09:53:15Z","created_by":"roboburr","updated_at":"2026-06-25T20:45:53Z","closed_at":"2026-06-25T20:45:53Z","close_reason":"rate-limiting added to /ap/* (inbox + reads), per-IP with IPv6 /64 keying","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [7bd97a6] | 20 | {"_type":"issue","id":"prutfolio-src-5qp","title":"Premium anti-bypass hardening (demotiveren) — lagen 1+2+4","description":"Doel (Robin 2026-06-21): bypassen van de premium-gate DEMOTIVEREN, niet 100% voorkomen (kan niet bij open self-host-code). Economie = $10 \u003c moeite-om-te-kraken (Duo-logica) is de hoofdlever; prijs laag houden.\n\nAKKOORD — implementeren:\n- LAAG 1 (HEB JE AL): Ed25519-getekend entitlement-token, OFFLINE geverifieerd met publieke sleutel. Niemand kan een geldig token vervalsen (geen private key). Kost 0 calls bij gebruik -\u003e schaalt gratis. Evt. pubkey bundelen i.p.v. /pubkey-fetch = nul server-contact op verify-pad.\n- LAAG 2: verspreide/inline checks i.p.v. één premiumUnlocked()-chokepoint -\u003e één 'return true'-patch unlockt niet meer alles; kraker moet ~10 plekken vinden.\n- LAAG 4: integriteits-zelfcheck (hash van gating-bestanden) + permanente 'niet-gelicenseerd'-banner/zachte degradatie bij geforceerd premium zonder geldig token.\n- (bonus) obfuscatie/minify van de gate + token-verify in de GEPUBLICEERDE build.\n- NETWERK-LEVER (al voor Cirkels): cracked instance = verkeerde proto = ongeldige handtekeningen = doet niet mee aan het echte netwerk. Geldt alleen voor netwerk-features.\n\nGESCHRAPT:\n- LAAG 5 (server-afhankelijke feature / phone-home): schaalt mee met GEBRUIK, niet met koppelingen -\u003e bij 1M users continu verkeer + 24/7-beschikbaarheid verplicht + centraal storingspunt + breekt 'no phone-home'. Niet de moeite voor een $10 massa-product.\n\nWAARSCHUWING: niet overinvesteren in DRM -\u003e raakt eerder eerlijke self-hosters (false positives/support) dan krakers, en ondermijnt de gratis-\u0026-open funnel. Volgorde: na Patreon Fase 0 + GitHub-migratie (prutfolio-src-aaj). Context: memory project_klonkt_monetization.","status":"open","priority":2,"issue_type":"feature","owner":"roboburr@gmail.com","created_at":"2026-06-21T23:01:36Z","created_by":"roboburr","updated_at":"2026-06-21T23:01:36Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [1e194e4] | 21 | {"_type":"issue","id":"prutfolio-src-da7","title":"C2S: keep the client's AS2 source on top-level posts, as replies already do","description":"A C2S client may send the authoring original beside the rendered markup:\nAS2 `source`, an object with `content` and `mediaType`. Klonkt keeps it on\none path and drops it on the other.\n\n## The asymmetry\n\n`src/services/ActivityPubService.js`, both in `ingestOutboxActivity`:\n\n // reply path, :3513 — source is PREFERRED\n const plain = (object.source \u0026\u0026 object.source.content)\n || HtmlSanitizerService.toPlainText(object.content || '');\n // ...passed to deliverReply as `text`, stored beside `html`\n\n // top-level path, c2sCreatePost :3740 — source is a FALLBACK, then sanitised\n const html = HtmlSanitizerService.sanitize(\n object.content || (object.source \u0026\u0026 object.source.content) || '');\n\nA reply keeps the client's plain text. A top-level post has nowhere to put\nit: `posts` has no plain column, `content` takes `html + mediaHtml`, and\neven `posts_fts` (:3798) is fed `toPlainText(html)` — the author's words\nround-tripped through markup rather than the words themselves.\n\nThe same client, posting the same text, is treated differently depending on\nwhether it happened to be answering someone.\n\n## What\n\n* `ensureColumn('posts', 'c2s_source', 'TEXT')` — the existing idiom\n (`src/config/database.js:1105`), same as `c2s_attachments` beside it.\n* In `c2sCreatePost`, store `object.source.content` when the client sent one.\n Nothing about how `content` is rendered needs to change.\n* Prefer it for `posts_fts` when present, so search indexes what was written.\n* Serve it back as `source` on the note representation, to the authenticated\n owner at least. Today `GET /ap/notes/:id` returns no `source` key at all,\n so a client cannot read back what it sent.\n\n## Why (Update needs it)\n\n`Update` is on the C2S roadmap, and an edit affordance needs the original.\nOffering \"edit\" over sanitised HTML means the author gets back markup they\ndid not write, and every edit round-trip degrades the text a little more.\nThe one moment the original is guaranteed available is the moment it\narrives, which is the moment it is currently discarded.\n\nAlso plain interop: Mastodon populates `source` for exactly this, and a\nclient that cannot retrieve what it posted cannot offer editing without\nkeeping its own shadow copy of everything — which is a second source of\ntruth about the same post.\n\n## Acceptance\n\n* A top-level `Create` carrying `source.content` is retrievable with that\n text intact; one without still behaves exactly as today.\n* Replies keep working as they already do — the point is to match them.\n* `posts_fts` indexes the source when there is one, the stripped HTML when\n there is not.\n* No migration: the column arrives via ensureColumn, and existing posts\n simply have NULL, which reads as \"no source was sent\".\n\n## Discovered while\n\nR9999 posted a note whose words were wrapped in `\u003ccode\u003e` with the plain\ntext sent alongside in `source`. Reading it back, `source` was absent —\nonly the HTML survived. Not lossy in that particular case, since `\u003ccode\u003e`\nstripped to text reads as intended, but nothing downstream could recover\nthe unmarked original, and the reply path in the same file would have kept\nit.\n","status":"open","priority":3,"issue_type":"feature","owner":"bart@bedrijfzondernaam.nl","created_at":"2026-08-23T18:10:06Z","created_by":"Bart","updated_at":"2026-08-23T18:10:06Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| 22 | {"_type":"issue","id":"prutfolio-src-086","title":"Following/followers: make count-only-public a per-site toggle","description":"Today `followers` and `following` are count-only for everyone except the\nauthenticated owner. That is a deliberate disclosure choice and the right\ndefault, but it is currently hard-coded, so a site owner who *wants* a public\nsocial graph cannot have one.\n\n## Current behaviour\n\n`src/routes/activitypub.js`, both handlers, identical in shape:\n\n const owner = auth \u0026\u0026 auth.site.slug === req.params.slug;\n if (owner) -\u003e full list (FEP-9876 enrich on request)\n otherwise -\u003e buildFollowers/buildFollowing(..., n, null, ...)\n\n`totalItems` is served truthfully; the members are withheld. An anonymous\nclient therefore sees a non-zero count and an empty `orderedItems`, which\nreads like a broken page unless you know the policy. It is not broken.\n\n## What\n\nA per-site setting — `public_social_graph`, or one column per collection if\nfollowers and following should be separable — that lets the owner opt into\nserving members to anonymous clients. Default stays as it is now: count only.\n\nNothing else about the handlers changes; the owner branch already produces\nexactly the payload the public branch would need.\n\n## Why\n\n* Mastodon exposes both collections publicly by default, so Klonkt currently\n looks like a dead end to anything walking the graph. For an artist site that\n wants to be found, that is a cost with no matching benefit.\n* prutfolio-src-7cz (Discovery, layer 1) proposes friend-of-a-friend over the\n Cirkel graph. Walking a social graph needs somebody to publish one. That\n layer is unbuildable across instances while every Klonkt is opaque.\n* The privacy-preserving default is the valuable part and should survive. What\n is missing is consent to the other choice, not a change of default.\n\n## Not this\n\n`gate_following` and the other `gate_*` columns are Guardianship ward gates —\nwhether a supervised account may *perform* an action. This is about what a\ncollection *discloses* to an unauthenticated reader. Two different axes; do\nnot fold them into one column.\n\n## Acceptance\n\n* Default unchanged: an existing site's collections stay count-only after\n upgrade, with no migration that silently widens disclosure.\n* With the setting on, an anonymous `GET` of both collections returns members\n and paginates the same way the owner view does.\n* With it off, the current behaviour is byte-identical to today.\n* The owner view keeps working via bearer token regardless of the setting.\n* FEP-9876 enrichment behaves the same in both branches.\n\n## Discovered while\n\nBuilding R9999, a C2S client, which read the collection anonymously and got\nan empty page. That was the client's own omission — sending the bearer token\nreturns all 43 — so this is not a bug report. The toggle is the feature that\nwas actually missing.\n","status":"open","priority":3,"issue_type":"feature","owner":"bart@bedrijfzondernaam.nl","created_at":"2026-08-23T05:58:22Z","created_by":"Bart","updated_at":"2026-08-23T05:58:22Z","dependencies":[{"issue_id":"prutfolio-src-086","depends_on_id":"prutfolio-src-7cz","type":"related","created_at":"2026-08-23T07:58:21Z","created_by":"Bart","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [4f97b70] | 23 | {"_type":"issue","id":"prutfolio-src-ls6","title":"postAudioFediOpen telt playlist-tracks van andere sites mee","description":"src/routes/posts.js regel ~299: de playlist-tak van postAudioFediOpen (die de begintoestand van het editor-vinkje bepaalt) joint playlist_tracks op audio_tracks zonder t.site_id = ?, net als het cross-tenant gat dat in prutfolio-src-cke is gedicht. Dit is een LEES-pad, geen schrijf-pad, dus geen poort die opengaat: het gevolg is een boolean die meetelt wat van een andere site is. Sinds cke is er wel een inconsistentie: bij een vreemde playlist kan het vinkje uit staan, de gebruiker vinkt aan, en er gebeurt stilzwijgend niets. Bewust buiten de scope van cke gehouden.","status":"open","priority":3,"issue_type":"task","owner":"roboburr@gmail.com","created_at":"2026-08-08T00:48:09Z","created_by":"Robin","updated_at":"2026-08-08T00:48:09Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [d11e308] | 24 | {"_type":"issue","id":"prutfolio-src-2t1","title":"Agenda: richer event types","description":"Extend shows/agenda with event types (show large/small, signing, workshop, generic) and fields like tickets/participation, location, theme, notes. Possibly external calendar integrations later.","status":"open","priority":3,"issue_type":"feature","owner":"roboburr@gmail.com","created_at":"2026-07-01T22:03:02Z","created_by":"roboburr","updated_at":"2026-07-01T22:03:02Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| 25 | {"_type":"issue","id":"prutfolio-src-wsp","title":"Header: optional store/merch link","description":"A per-site setting for a generic store link in the header (external shop). Small; fits the merch strategy.","status":"open","priority":3,"issue_type":"feature","owner":"roboburr@gmail.com","created_at":"2026-07-01T22:03:01Z","created_by":"roboburr","updated_at":"2026-07-01T22:03:01Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [5e52448] | 26 | {"_type":"issue","id":"prutfolio-src-krm","title":"SEO: og:image light/dark override setting","description":"1.2.0 picks the OG card variant from the site default theme; add an explicit light/dark override in Admin -\u003e SEO. Phase 1.","status":"closed","priority":3,"issue_type":"feature","owner":"roboburr@gmail.com","created_at":"2026-07-01T22:02:56Z","created_by":"roboburr","updated_at":"2026-07-01T22:28:28Z","closed_at":"2026-07-01T22:28:28Z","close_reason":"Built on main: Admin -\u003e SEO has an og:image variant override (Auto/Light/Dark); OgImageService honours it over the site theme. Verified: saving Light regenerates a different card.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [d11e308] | 27 | {"_type":"issue","id":"prutfolio-src-vyr","title":"Polls phase 2: create your own polls (outbound Question)","description":"Phase 1 (inbound: show + vote on remote polls in the News feed) shipped. Phase 2 = author your own polls:\n- Post editor: a poll post type (2-8 options + a duration / endTime, single vs multiple choice).\n- DB: poll_options + poll_votes (one vote per remote actor enforced).\n- buildNote -\u003e emit a Question (oneOf/anyOf of Note options each with replies.totalItems, endTime, votersCount) instead of a Note; declare votersCount/toot terms in AP_CONTEXT (AS2-validity test).\n- Inbound votes: a Create(Note) with name + inReplyTo our Question -\u003e tally.\n- Update(Question) delivery to refresh counts on remote caches; enforce endTime (closed); show results on-site.\nReuses the delivery/queue + interaction machinery. No anti-steal tension (text/interaction, no audio file).","status":"closed","priority":3,"issue_type":"feature","owner":"roboburr@gmail.com","created_at":"2026-07-01T07:43:41Z","created_by":"roboburr","updated_at":"2026-07-01T22:02:23Z","closed_at":"2026-07-01T22:02:23Z","close_reason":"Built 2026-07-01: posts can carry a poll that federates as an AS2 Question (oneOf/anyOf, endTime, votersCount), inbound ballots tallied + Update(Question) pushed, scheduler closes on endTime, voting also from the interact page. On main.","labels":["fediverse","polls"],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [4e9782c] | 28 | {"_type":"issue","id":"prutfolio-src-7cz","title":"Discovery: how Klonkt users find each other (klonkt.com hub + discoverable + FoF)","description":"Turn klonkt.com from a static salespage into the network's front door WITHOUT\nbecoming a content host. Decided direction: Form B (a map/lens over the network),\nNOT Form A (a multi-user signup server — that takes on the host/moderation/liability\nrole we deliberately avoided; that path is managed hosting, a separate business\ndecision, not this).\n\n## What\n- A `discoverable` per-site toggle (Beheer) that sets `discoverable: true/false` on\n the site's ActivityPub actor (AS2/Mastodon-standard property, already declared in\n AP_CONTEXT). Opt-in = clean GDPR consent basis. Honored locally by other instances.\n- Phase 1: opt-in artist directory on klonkt.com — pointers only (avatar, name, genre,\n link). No accounts, no hosted content.\n- Phase 2: network feed — a Klonkt \"network\" actor that FOLLOWS the opt-in artists;\n their public posts land in its ap_timeline and render as a browsable \"what's\n happening on Klonkt\" feed. Reuses existing AP machinery (inbox, follow, timeline\n cache, Delete/Tombstone handling, offline-instance handling) — little new code.\n- Optional: an AP relay so new instances can plug into the public firehose (cold-start),\n and/or a single-user showcase Klonkt instance next to the salespage as a live demo.\n\n## Why (discovery)\nSingle-user-per-server (our model) has no local timeline, so discovery is inherently\nharder than Mastodon. Layered approach: (0) plain fediverse, (1) friend-of-a-friend\nover the Cirkel graph, (2) auto-detect fellow Klonkt instances via NodeInfo, (3)\ngenre/hashtag browse, (4) this central opt-in on-ramp for cold-start. This issue = layer 4.\n\n## Invariants\n- No auto-follow / auto-boost — discovery = suggestions the user clicks, never an\n automatic outward action (the no-auto-fediverse rule).\n- Gracefully degrade — a lonely instance still works; discovery is a plus.\n- Opt-in per site (`discoverable`), honored locally; not-discoverable != private\n (an AP actor is always followable if you know the handle).\n- Scale-safe: cache NodeInfo, use the SSRF guard (safeFetch), prune.\n\n## Maintenance / GDPR (assessed light)\n- Tech: one more pm2 service in the klonkt-stats pattern; reuses the AP stack. Low.\n- GDPR: only public, self-published, opt-in content; we are a cache/lens, not the\n origin; federated Delete/Tombstone already drops cached posts (erasure largely\n automatic). Need: a delist/opt-out path, data minimization + pruning, a\n privacy-policy paragraph (klonkt.com/privacy exists), a takedown contact\n (info@robingenis.com). Materially lighter than Form A.\n- Moderation: delist a spammy instance = drop the follow (remove a pointer), reversible.\n\n## Suggested order\n1. `discoverable` toggle (Beheer -\u003e actor). Cheap, standalone.\n2. \"Discover: neighbours of your Cirkel\" strip on /cirkel (layer 1, friend-of-a-friend)\n that honors `discoverable`. Most \"aha\" per effort, fully decentralized, no klonkt.com.\n3. klonkt.com opt-in directory (pointers-only).\n4. klonkt.com network feed (network actor follows opt-in artists) + optional relay.\n","status":"open","priority":3,"issue_type":"feature","owner":"roboburr@gmail.com","created_at":"2026-07-01T00:34:33Z","created_by":"roboburr","updated_at":"2026-07-01T00:34:33Z","labels":["discovery","fediverse"],"dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [f0807cc] | 29 | {"_type":"issue","id":"prutfolio-src-4fk","title":"Fediverse: serialize pin-resync per site (parallelle /save race)","description":"Hardening-review: twee gelijktijdige /save-calls die pin-rank wijzigen draaien beide resyncFeaturedPins (Remove-all -\u003e 5s -\u003e Add) interleaved -\u003e niet-deterministische StatusPin-volgorde op Mastodon. Serialiseer per slug (in-process Map\u003cslug,Promise\u003e-chain) of snapshot de pins onder de commit-transactie.","status":"closed","priority":3,"issue_type":"task","assignee":"roboburr","owner":"roboburr@gmail.com","created_at":"2026-06-25T09:53:21Z","created_by":"roboburr","updated_at":"2026-07-01T00:57:35Z","started_at":"2026-07-01T00:55:28Z","closed_at":"2026-07-01T00:57:35Z","close_reason":"Fixed: resyncFeaturedPins() is now a per-site serializing+coalescing wrapper around doResyncFeaturedPins(); concurrent /save calls can no longer interleave the Remove/Add sequences (which scrambled StatusPin order), and rapid saves coalesce into one rerun instead of N.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [cf314b4] | 30 | {"_type":"issue","id":"prutfolio-src-4lq","title":"Fediverse: block-handling completeness follow-up","description":"Hardening follow-up. Specifics are intentionally kept in the maintainer's private notes, not in this public tracker. Priority medium/low; the critical fediverse issues (SSRF, stored-XSS, cross-actor delete) are already fixed.","status":"closed","priority":3,"issue_type":"task","assignee":"roboburr","owner":"roboburr@gmail.com","created_at":"2026-06-25T09:53:19Z","created_by":"roboburr","updated_at":"2026-07-01T01:04:20Z","started_at":"2026-07-01T01:01:49Z","closed_at":"2026-07-01T01:04:20Z","close_reason":"Fixed: Announce handler drops a boost whose original author is blocked (isBlockedAny(origUri)), and purgeBlocked matches a blocked domain by exact parsed host instead of a URL LIKE. npm test 15/15 on VPS.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| 31 | {"_type":"issue","id":"prutfolio-src-lqx","title":"Fediverse: HTTP-signature hardening follow-up","description":"Hardening follow-up. Specifics are intentionally kept in the maintainer's private notes, not in this public tracker. Priority medium/low; the critical fediverse issues (SSRF, stored-XSS, cross-actor delete) are already fixed.","status":"closed","priority":3,"issue_type":"task","assignee":"roboburr","owner":"roboburr@gmail.com","created_at":"2026-06-25T09:53:18Z","created_by":"roboburr","updated_at":"2026-07-01T01:04:18Z","started_at":"2026-07-01T01:01:47Z","closed_at":"2026-07-01T01:04:18Z","close_reason":"Fixed: verifyRequest now requires the Date header to be signed + recent (default +/-60min, env AP_SIG_MAX_SKEW_MIN) and requires a signed Digest on any body-bearing request — closes indefinite-replay and body-swap. npm test 15/15 on VPS.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|
| [00550c0] | 32 | {"_type":"issue","id":"prutfolio-src-e44","title":"Asset cache-busting via commit-hash ipv handmatig bumpen","description":"Nu staan ?v=N hardcoded in shell.ejs. Beter: middleware zet res.locals.assetVersion = git-commit-sha (build-time of runtime) zodat elke deploy auto-cache-bust krijgt. Behandel alle static script/css tags in shell.ejs (htmx.min.js, audio-player.js, install-app.js).","status":"open","priority":3,"issue_type":"task","owner":"robin@roboburr.com","created_at":"2026-05-07T20:03:17Z","created_by":"Robin","updated_at":"2026-05-07T20:03:17Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
|---|